New feature: Lua hooks

This commit is contained in:
Jan-Piet Mens
2015-09-19 16:11:53 +02:00
parent 5e1fd52f92
commit 6dd03bb0e6
10 changed files with 401 additions and 39 deletions
+9 -1
View File
@@ -19,6 +19,13 @@ CFLAGS += -DGHASHPREC=$(GHASHPREC)
ifeq ($(HAVE_PING),yes)
CFLAGS += -DHAVE_PING=1
endif
ifeq ($(WITH_LUA),yes)
CFLAGS += -DWITH_LUA=1 $(LUA_CFLAGS)
LIBS += $(LUA_LIBS)
OTR_OBJS += hooks.o
endif
ifeq ($(HAVE_KILL),yes)
CFLAGS += -DHAVE_KILL=1
endif
@@ -60,7 +67,7 @@ ocat: ocat.o $(OTR_OBJS)
$(CC) $(CFLAGS) -o ocat ocat.o $(OTR_OBJS) $(LIBS)
ot-recorder.o: ot-recorder.c storage.h util.h Makefile geo.h udata.h json.h http.h gcache.h config.mk
ot-recorder.o: ot-recorder.c storage.h util.h Makefile geo.h udata.h json.h http.h gcache.h config.mk hooks.h
geo.o: geo.h geo.c udata.h Makefile config.mk
geohash.o: geohash.h geohash.c udata.h Makefile config.mk
base64.o: base64.h base64.c
@@ -71,6 +78,7 @@ util.o: util.c util.h Makefile config.mk
mongoose.o: mongoose.c mongoose.h
ocat.o: ocat.c storage.h util.h config.mk version.h
storage.o: storage.c storage.h util.h gcache.h config.mk
hooks.o: hooks.c udata.h hooks.h util.h version.h
clean:
+68
View File
@@ -224,6 +224,7 @@ You will require:
* [libmosquitto](http://mosquitto.org)
* [libCurl](http://curl.haxx.se/libcurl/)
* [lmdb](http://symas.com/mdb) included
* Optionally [Lua](http://lua.org)
Obtain and download the software, either [as a package, if available](https://packagecloud.io/owntracks/ot-recorder), via [our Homebrew Tap](https://github.com/owntracks/homebrew-recorder) on Mac OS X, directly as a clone of the repository, or as a [tar ball](https://github.com/owntracks/recorder/releases) which you unpack. Copy the included `config.mk.in` file to `config.mk` and edit that. You specify the features or tweaks you need. (The file is commented.) Pay particular attention to the installation directory and the value of the _store_ (`STORAGEDEFAULT`): that is where the recorder will store its files. `DOCROOT` is the root of the directory from which the _recorder_'s HTTP server will serve files.
@@ -417,6 +418,73 @@ curl 'http://127.0.0.1:8083/api/0/kill?user=ngin&device=ojo'
```
The response contains a list of removed `.rec` files, and file system operations are logged to syslog.
## Lua hook
If _recorder_ is compiled with Lua support, a Lua script you provide is launched at startup. Lua is _a powerful, fast, lightweight, embeddable scripting language_. You can use this to process location publishes in any way you desire: your imagination (and Lua-scripting knowhow) set the limits. Some examples:
* insert publishes into a database of your choice
* switch on the coffee machine when your OwnTracks device reports you're entering home (but see also [mqttwarn](http://jpmens.net/2014/02/17/introducing-mqttwarn-a-pluggable-mqtt-notifier/)
* write a file with data in a format of your choice (see `etc/example.lua`)
Run the _recorder_ with the path to your Lua script specified in its `--lua-script` option (there is no default). If the script cannot be loaded (e.g. because it cannot be read or contains syntax errors), the _recorder_ unloads Lua and continues *without* your script.
Your Lua program is automatically provided with a table which contains the following members:
* `otr.version` is a read-only string with the _recorder_ version (example: `"0.3.2"`)
* `otr.log()` is a function which takes a string which is logged to syslog at the _recorder_'s facility and log level INFO.
* `otr.strftime(fmt, t)` is a function which takes a format string (see `strftime(3)`) and an integer number of seconds and returns a string with the formatted UTC time. If `t` is 0 or negative, the current system time is used.
Your Lua script must provide the following functions:
### `otr_init`
This is invoked at start of _recorder_. If the function returns a non-zero value, _recorder_ unloads Lua and disables its processing; i.e. the `hook()` will *not* be invoked on location publishes.
### `otr_exit`
This is invoked when the _recorder_ stops, which it doesn't really do unless you CTRL-C it.
### `otr_hook`
This function is invoked at every location publish processed by the _recorder_. Your function is passed three arguments:
1. _topic_ is the topic published to (e.g. `owntracks/jane/phone`)
2. _type_ is the type of MQTT message. This is the `_type` in our JSON messages (e.g. `location`) or `"unknown"`.
3. _location_ is a [Lua table](http://www.lua.org/pil/2.5.html) (associative array) with all the elements obtained in the JSON message. In the case of _type_ being `location`, we also add country code (`cc`) and the location's address (`addr`) unless reverse-geo lookups have been disabled in _recorder_.
Assume the following small example Lua script in `example.lua`:
```lua
local file
function otr_init()
otr.log("example.lua starting; writing to /tmp/lua.out")
file = io.open("/tmp/lua.out", "a")
file:write("written by OwnTracks Recorder version " .. otr.version .. "\n")
end
function otr_hook(topic, _type, data)
local timestr = otr.strftime("It is %T in the year %Y", 0)
print("L: " .. topic .. " -> " .. _type)
file:write(timestr .. " " .. topic .. " lat=" .. data['lat'] .. data['addr'] .. "\n")
end
```
When _recorder_ is launched with `--lua-script example.lua` it invokes `otr_init()` which opens a file. Then, for each location received, it calls `otr_hook()` which updates the file.
Assuming an OwnTracks device publishes this payload
```json
{"cog":-1,"batt":-1,"lon":2.29513,"acc":5,"vel":-1,"vac":-1,"lat":48.85833,"t":"u","tst":1441984413,"alt":0,"_type":"location","tid":"JJ"}
```
the file `/tmp/lua.out` would contain
```txt
written by OwnTracks Recorder version 0.3.0
It is 14:10:01 in the year 2015 owntracks/jane/phone lat=48.858339 Avenue Anatole France, 75007 Paris, France
```
#### Environment
The following environment variables control _ocat_'s behaviour:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 41 KiB

+6
View File
@@ -8,6 +8,9 @@ HAVE_HTTP ?= yes
# Do you want to use reverse-geo caching? (Highly recommended)
HAVE_LMDB ?= yes
# Do you have Lua libraries installed and want the Lua hook integration?
WITH_LUA ?= no
# Do you want support for the `pingping' monitoring feature?
HAVE_PING ?= yes
@@ -51,3 +54,6 @@ APIKEY ?=
MOSQUITTO_INC = -I/usr/include
MOSQUITTO_LIB = -L/usr/lib
MORELIBS = # -lssl
LUA_CFLAGS =
LUA_LIBS = -llua
+13
View File
@@ -0,0 +1,13 @@
local file
function otr_init()
otr.log("example.lua starting; writing to /tmp/lua.out")
file = io.open("/tmp/lua.out", "a")
file:write("written by OwnTracks Recorder version " .. otr.version .. "\n")
end
function otr_hook(topic, _type, data)
local timestr = otr.strftime("It is %T in the year %Y", 0)
print("L: " .. topic .. " -> " .. _type)
file:write(timestr .. " " .. topic .. " lat=" .. data['lat'] .. data['addr'] .. "\n")
end
+201
View File
@@ -0,0 +1,201 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef WITH_LUA
# include <stdarg.h>
# include <stdint.h>
# include "util.h"
# include "udata.h"
# include "hooks.h"
# include "lua.h"
# include "lualib.h"
# include "lauxlib.h"
# include "json.h"
# include "version.h"
static int otr_log(lua_State *lua);
static int otr_strftime(lua_State *lua);
/*
* Invoke the function `name' in the Lua script, which _may_ return
* an integer that we, in turn, return to caller.
*/
static int l_function(lua_State *L, char *name)
{
int rc = 0;
lua_getglobal(L, name);
if (lua_type(L, -1) != LUA_TFUNCTION) {
olog(LOG_ERR, "Cannot invoke Lua function %s: %s\n", name, lua_tostring(L, -1));
rc = 1;
} else {
lua_call(L, 0, 1);
rc = (int)lua_tonumber(L, -1);
}
lua_settop(L, 0);
return (rc);
}
struct luadata *hooks_init(char *script)
{
struct luadata *ld;
int rc;
if ((ld = malloc(sizeof(struct luadata))) == NULL)
return (NULL);
ld->script = strdup(script);
// ld->L = lua_open();
ld->L = luaL_newstate();
luaL_openlibs(ld->L);
/*
* Set up a global with some values.
*/
lua_newtable(ld->L);
lua_pushstring(ld->L, VERSION);
lua_setfield(ld->L, -2, "version");
// lua_pushstring(ld->L, "/Users/jpm/Auto/projects/on-github/owntracks/recorder/lua");
// lua_setfield(ld->L, -2, "luapath");
lua_pushcfunction(ld->L, otr_log);
lua_setfield(ld->L, -2, "log");
lua_pushcfunction(ld->L, otr_strftime);
lua_setfield(ld->L, -2, "strftime");
lua_setglobal(ld->L, "otr");
olog(LOG_DEBUG, "initializing Lua hooks at %s", script);
/* Load the Lua script */
if (luaL_dofile(ld->L, ld->script)) {
olog(LOG_ERR, "Cannot load Lua from %s: %s", ld->script, lua_tostring(ld->L, -1));
hooks_exit(ld, "failed to load script");
return (NULL);
}
rc = l_function(ld->L, "otr_init");
if (rc != 0) {
/*
* After all this work (sigh), the Script has decided we shouldn't use
* hooks, so unload the whole Lua stuff and NULL back.
*/
hooks_exit(ld, "otr_init() returned non-zero");
ld = NULL;
}
return (ld);
}
void hooks_exit(struct luadata *ld, char *reason)
{
if (ld) {
olog(LOG_NOTICE, "unloading Lua: %s", reason);
free(ld->script);
lua_close(ld->L);
free(ld);
}
}
void hooks_hook(struct udata *ud, char *topic, JsonNode *fullo)
{
struct luadata *ld = ud->luadata;
char *_type = "unknown";
JsonNode *j;
lua_getglobal(ld->L, "otr_hook");
if (lua_type(ld->L, -1) != LUA_TFUNCTION) {
olog(LOG_NOTICE, "cannot invoke otr_hook in Lua script");
return;
}
lua_pushstring(ld->L, topic); /* arg1: topic */
if ((j = json_find_member(fullo, "_type")) != NULL) {
if (j->tag == JSON_STRING)
_type = j->string_;
}
lua_pushstring(ld->L, _type); /* arg2: record type */
lua_newtable(ld->L); /* arg3: table */
json_foreach(j, fullo) {
lua_pushstring(ld->L, j->key); /* table key */
if (j->tag == JSON_STRING) {
lua_pushstring(ld->L, j->string_);
} else if (j->tag == JSON_NUMBER) {
lua_pushnumber(ld->L, j->number_);
} else if (j->tag == JSON_NULL) {
lua_pushnil(ld->L);
} else if (j->tag == JSON_BOOL) {
lua_pushboolean(ld->L, j->bool_);
}
lua_rawset(ld->L, -3);
}
/* Invoke `hook' function in Lua with our args */
if (lua_pcall(ld->L, 3, 1, 0)) {
olog(LOG_ERR, "Failed to run script: %s", lua_tostring(ld->L, -1));
exit(1);
}
// rc = (int)lua_tonumber(ld->L, -1);
// printf("C: FILTER returns %d\n", rc);
}
/*
* --- Here come the functions we provide to Lua scripts.
*/
static int otr_log(lua_State *lua)
{
const char *str;
if (lua_gettop(lua) >= 1) {
str = lua_tostring(lua, 1);
olog(LOG_INFO, "%s", str);
}
return 0;
}
/*
* otr.strftime(format, seconds)
* Perform a strtime(3) for Lua with the specified format and
* seconds, and return the string result to Lua. As a special
* case, if `seconds' is negative, use current time.
*/
static int otr_strftime(lua_State *lua)
{
const char *fmt;
long secs;
struct tm *tm;
char buf[BUFSIZ];
if (lua_gettop(lua) >= 1) {
fmt = lua_tostring(lua, 1);
if ((secs = lua_tonumber(lua, 2)) < 1)
secs = time(0);
if ((tm = gmtime(&secs)) != NULL) {
strftime(buf, sizeof(buf), fmt, tm);
lua_pushstring(lua, buf);
return (1);
}
}
return (0);
}
#endif /* WITH_LUA */
+19
View File
@@ -0,0 +1,19 @@
#ifndef HOOKS_H_INCLUDED
# define HOOKS_H_INCLUDED
#ifdef WITH_LUA
# include "lua.h"
struct luadata {
char *script; /* Path to Lua script in --lua-script */
lua_State *L; /* The Lua machine */
};
struct luadata *hooks_init(char *luascript);
void hooks_exit(struct luadata *, char *reason);
void hooks_hook(struct udata *ud, char *topic, JsonNode *obj);
#endif /* WITH_LUA */
#endif
+3
View File
@@ -173,6 +173,9 @@ void print_versioninfo()
#ifdef HAVE_LMDB
printf("\tHAVE_LMDB = yes\n");
#endif
#ifdef WITH_LUA
printf("\tWITH_LUA = yes\n");
#endif
#ifdef HAVE_HTTP
printf("\tHAVE_HTTP = yes\n");
#endif
+79 -38
View File
@@ -45,6 +45,9 @@
#ifdef HAVE_HTTP
# include "http.h"
#endif
#ifdef WITH_LUA
# include "hooks.h"
#endif
#define SSL_VERIFY_PEER (1)
@@ -357,7 +360,7 @@ JsonNode *csv(char *payload, char *tid, char *t, double *lat, double *lon, long
void on_message(struct mosquitto *mosq, void *userdata, const struct mosquitto_message *m)
{
JsonNode *json;
JsonNode *json, *fullo, *geo = NULL;
char tid[BUFSIZ], t[BUFSIZ], *p;
double lat, lon;
long tst;
@@ -510,7 +513,7 @@ void on_message(struct mosquitto *mosq, void *userdata, const struct mosquitto_m
cached = FALSE;
if (ud->revgeo == TRUE) {
JsonNode *geo, *j;
JsonNode *j;
if ((geo = gcache_json_get(ud->gc, utstring_body(ghash))) != NULL) {
/* Habemus cached data */
@@ -523,11 +526,9 @@ void on_message(struct mosquitto *mosq, void *userdata, const struct mosquitto_m
if ((j = json_find_member(geo, "addr")) != NULL) {
utstring_printf(addr, "%s", j->string_);
}
json_delete(geo);
} else {
if ((geo = revgeo(lat, lon, addr, cc)) != NULL) {
gcache_json_put(ud->gc, utstring_body(ghash), geo);
json_delete(geo);
} else {
/* We didn't obtain reverse Geo, maybe because of over
* quota; make a note of the missing geohash */
@@ -551,45 +552,51 @@ void on_message(struct mosquitto *mosq, void *userdata, const struct mosquitto_m
* We have exactly three topic parts (owntracks/user/device), and valid JSON.
*/
/*
* Create a new location object containing all the bits and
* pieces we need and push that into connected Websockets.
* and/or into Lua hooks.
*/
fullo = json_mkobject();
json_copy_to_object(fullo, json, TRUE);
if (geo != NULL) {
json_copy_to_object(fullo, geo, FALSE);
json_delete(geo);
}
/*
* I need a unique "key" in the Websocket clients to keep track
* of which device is being updated; use topic.
*/
json_append_member(fullo, "topic", json_mkstring(m->topic));
/*
* We have to know which user/device this is for in order to
* determine whether a connected Websocket client is authorized
* to see this. Add user/device
*/
json_append_member(fullo, "user", json_mkstring(utstring_body(username)));
json_append_member(fullo, "device", json_mkstring(utstring_body(device)));
#ifdef HAVE_HTTP
if (ud->mgserver && !pingping) {
/*
* Create a new location object containing all the bits and
* pieces we need and push that into connected Websockets.
* TODO: clean up
*/
JsonNode *geo, *wso = json_mkobject();
json_copy_to_object(wso, json, TRUE);
if ((geo = gcache_json_get(ud->gc, utstring_body(ghash))) != NULL) {
json_copy_to_object(wso, geo, FALSE);
json_delete(geo);
}
/*
* I need a unique "key" in the Websocket clients to keep track
* of which device is being updated; use topic.
*/
json_append_member(wso, "topic", json_mkstring(m->topic));
/*
* We have to know which user/device this is for in order to
* determine whether a connected Websocket client is authorized
* to see this. Add user/device
*/
json_append_member(wso, "user", json_mkstring(utstring_body(username)));
json_append_member(wso, "device", json_mkstring(utstring_body(device)));
http_ws_push_json(ud->mgserver, wso);
json_delete(wso);
http_ws_push_json(ud->mgserver, fullo);
}
#endif
#ifdef WITH_LUA
if (ud->luadata && !pingping) {
hooks_hook(ud, m->topic, fullo);
}
#endif
json_delete(fullo);
if ((jsonstring = json_stringify(json, NULL)) != NULL) {
char *js;
@@ -685,8 +692,7 @@ void on_disconnect(struct mosquitto *mosq, void *userdata, int reason)
static void catcher(int sig)
{
fprintf(stderr, "Going down on signal %d\n", sig);
exit(1);
run = 0;
}
void usage(char *prog)
@@ -707,6 +713,9 @@ void usage(char *prog)
printf(" --http-host <host> HTTP addr to bind to (localhost)\n");
printf(" --http-port <port> -A HTTP port (8083); 0 to disable HTTP\n");
printf(" --doc-root <directory> document root (%s)\n", DOCROOT);
#endif
#ifdef WITH_LUA
printf(" --lua-script <script.lua> path to Lua script. If unset, no Lua hooks\n");
#endif
printf(" --precision ghash precision (dflt: %d)\n", GHASHPREC);
printf(" --hosted use OwnTracks Hosted\n");
@@ -732,6 +741,9 @@ int main(int argc, char **argv)
struct mosquitto *mosq = NULL;
char err[1024], *p, *username, *password, *cafile, *device;
char *hostname = "localhost", *logfacility = "local0";
#ifdef WITH_LUA
char *luascript = NULL;
#endif
int port = 1883;
int rc, i, ch, hosted = FALSE;
static struct udata udata, *ud = &udata;
@@ -755,6 +767,9 @@ int main(int argc, char **argv)
#ifdef HAVE_HTTP
udata.mgserver = NULL;
#endif
#ifdef WITH_LUA
udata.luadata = NULL;
#endif
if ((p = getenv("OTR_HOST")) != NULL) {
hostname = strdup(p);
@@ -790,6 +805,9 @@ int main(int argc, char **argv)
{ "logfacility", required_argument, 0, 4},
{ "precision", required_argument, 0, 5},
{ "hosted", no_argument, 0, 6},
#ifdef WITH_LUA
{ "lua-script", required_argument, 0, 7},
#endif
#ifdef HAVE_HTTP
{ "http-host", required_argument, 0, 3},
{ "http-port", required_argument, 0, 'A'},
@@ -804,6 +822,12 @@ int main(int argc, char **argv)
break;
switch (ch) {
#ifdef WITH_LUA
case 7:
/* FIXME: check existence of script file */
luascript = strdup(optarg);
break;
#endif
case 6:
hosted = TRUE;
break;
@@ -911,6 +935,18 @@ int main(int argc, char **argv)
openlog("ot-recorder", LOG_PID | LOG_PERROR, syslog_facility_code(logfacility));
#ifdef WITH_LUA
/*
* If option for lua-script has not been given, ignore all hooks.
*/
if (luascript) {
if ((udata.luadata = hooks_init(luascript)) == NULL)
olog(LOG_NOTICE, "proceeding sans Lua");
free(luascript);
}
#endif
#ifdef HAVE_HTTP
if (http_port) {
if (!is_directory(doc_root)) {
@@ -946,6 +982,7 @@ int main(int argc, char **argv)
signal(SIGINT, catcher);
signal(SIGTERM, catcher);
mosq = mosquitto_new(utstring_body(clientid), CLEAN_SESSION, (void *)&udata);
if (!mosq) {
@@ -1059,6 +1096,10 @@ int main(int argc, char **argv)
#ifdef HAVE_HTTP
mg_destroy_server(&udata.mgserver);
#endif
#ifdef WITH_LUA
hooks_exit(ud->luadata, "recorder stops");
#endif
mosquitto_disconnect(mosq);
mosquitto_destroy(mosq);
+3
View File
@@ -24,6 +24,9 @@ struct udata {
#ifdef HAVE_HTTP
struct mg_server *mgserver; /* Mongoose */
#endif
#ifdef WITH_LUA
struct luadata *luadata; /* Lua stuff */
#endif
};
#endif