A small Lua module for executing processes. It's primarily intended to be used with OpenResty, but will work in regular Lua applications as well. When used with OpenResty, it's completely non-blocking (otherwise it falls back to using LuaSocket and does block).
It's similar to (and inspired by) lua-resty-shell, the primary difference being this module uses sockexec, which doesn't spawn a shell - instead you provide an array of argument strings, which means you don't need to worry about shell escaping/quoting/parsing rules.
Additionally, as of version 2.0.0, you can use resty.exec.socket to access a lower-level interface that allows two-way communication with programs. You can read and write to running applications!
This requires your web server to have an active instance of sockexec running.
2.0.0- New
resty.exec.socketmodule for using a duplex connection resty.execno longer uses thebufsizeargumentresty.execnow accepts atimeoutargument, specify in milliseconds, defaults to 60s- This is a major revision, please test thoroughly before upgrading!
- New
- No changelog before
2.0.0
lua-resty-exec is available on luarocks as well as opm, you can install it with luarocks install lua-resty-exec or opm get jprjr/lua-resty-exec.
If you're using this outside of OpenResty, you'll also need the LuaSocket module installed, ie luarocks install luasocket.
Additionally, you'll need sockexec running, see its repo for instructions.
local exec = require'resty.exec' local prog = exec.new('/tmp/exec.sock')Creates a new prog object, using /tmp/exec.sock for its connection to sockexec.
From there, you can use prog in a couple of different ways:
local res, err = prog('uname') -- res = { stdout = "Linux\n", stderr = nil, exitcode = 0, termsig = nil } -- err = nil ngx.print(res.stdout)This will run uname, with no data on stdin.
Returns a table of output/error codes, with err set to any errors encountered.
prog.argv = { 'uname', '-a' } local res, err = prog() -- res = { stdout = "Linux localhost 3.10.18 #1 SMP Tue Aug 2 21:08:34 PDT 2016 x86_64 GNU/Linux\n", stderr = nil, exitcode = 0, termsig = nil } -- err = nil ngx.print(res.stdout)prog.stdin = 'this is neat!' local res, err = prog('cat') -- res = { stdout = "this is neat!", stderr = nil, exitcode = 0, termsig = nil } -- err = nil ngx.print(res.stdout)local res, err = prog( { argv = 'cat', stdin = 'fun!', stdout = function(data) print(data) end, stderr = function(data) print("error:", data) end } ) -- res = { stdout = nil, stderr = nil, exitcode = 0, termsig = nil } -- err = nil -- 'fun!' is printedNote: here argv is a string, which is fine if your program doesn't need any arguments.
If you set prog.stdout or prog.stderr to a function, it will be called for each chunk of stdout/stderr data received.
Please note that there's no guarantees of stdout/stderr being a complete string, or anything particularly sensible for that matter!
prog.stdout = function(data) ngx.print(data) ngx.flush(true) end local res, err = prog('some-program') By default, sockexec treats a timeout as an error. You can disable this by setting the object's timeout_fatal key to false. Examples:
-- set timeout_fatal = false on the prog objects prog.timeout_fatal = false -- or, set it at calltime: local res, err = prog({argv = {'cat'}, timeout_fatal = false})Not a problem! You can just do something like:
local res, err = prog('bash','-c','echo $PATH')Or if you want to run an entire script:
prog.stdin = script_data local res, err = prog('bash') -- this is roughly equivalent to running `bash < script` on the CLII generally recommend against daemonizing processes - I think it's far better to use some kind of message queue and/or supervision system, so you can monitor processes, take actions on failure, and so on.
That said, if you want to spin off some process, you could use start-stop-daemon, ie:
local res, err = prog('start-stop-daemon','--pidfile','/dev/null','--background','--exec','/usr/bin/sleep', '--start','--','10')will spawn sleep 10 as a detached background process.
If you don't want to deal with start-stop-daemon, I have a small utility for spawning a background program called idgaf, ie:
local res, err = prog('idgaf','sleep','10')This will basically accomplish the same thing start-stop-daemon does without requiring a billion flags.
local exec_socket = require'resty.exec.socket' -- you can specify timeout in milliseconds, optional local client = resty_socket:new({ timeout = 60000 }) -- every new program instance requires a new -- call to connect local ok, err = client:connect('/tmp/exec.sock') -- send program arguments, only accepts a table of -- arguments client:send_args({'cat'}) -- send data for stdin client:send('hello there') -- receive data local data, typ, err = client:receive() -- `typ` can be one of: -- `stdout` - data from the program's stdout -- `stderr` - data from the program's stderr -- `exitcode` - the program's exit code -- `termsig` - if terminated via signal, what signal was used -- if `err` is set, data and typ will be nil -- common `err` values are `closed` and `timeout` print(string.format('Received %s data: %s',typ,data) -- will print 'Received stdout data: hello there' client:send('hey this cat process is still running') data, typ, err = client:receive() print(string.format('Received %s data: %s',typ,data) -- will print 'Received stdout data: hey this cat process is still running' client:send_close() -- closes stdin data, typ, err = client:receive() print(string.format('Received %s data: %s',typ,data) -- will print 'Received exitcode data: 0' data, typ, err = client:receive() print(err) -- will print 'closed' ### `client` object methods: * **`ok, err = client:connect(path)`** Connects via unix socket to the path given. If this is running in nginx, the `unix:` string will be prepended automatically. * **`bytes, err = client:send_args(args)`** Sends a table of arguments to sockexec and starts the program. * **`bytes, err = client:send_data(data)`** Sends `data` to the program's standard input * **`bytes, err = client:send(data)`** Just a shortcut to `client:send_data(data)` * **`bytes, err = client:send_close()`** Closes the program's standard input. You can also send an empty string, like `client:send_data('')` * **`data, typ, err = client:receive()`** Receives data from the running process. `typ` indicates the type of data, which can be `stdout`, `stderr`, `termsig`, `exitcode` `err` is typically either `closed` or `timeout` * **`client:close()`** Forcefully closes the client connection * **`client:getfd()`** A getfd method, useful if you want to monitor the underlying socket connection in a select loop ## Some example nginx configs Assuming you're running sockexec at `/tmp/exec.sock` $ sockexec /tmp/exec.sock
Then in your nginx config: ```nginx location /uname-1 { content_by_lua_block { local prog = require'resty.exec'.new('/tmp/exec.sock') local data,err = prog('uname') if(err) then ngx.say(err) else ngx.say(data.stdout) end } } location /uname-2 { content_by_lua_block { local prog = require'resty.exec'.new('/tmp/exec.sock') prog.argv = { 'uname', '-a' } local data,err = prog() if(err) then ngx.say(err) else ngx.say(data.stdout) end } } location /cat-1 { content_by_lua_block { local prog = require'resty.exec'.new('/tmp/exec.sock') prog.stdin = 'this is neat!' local data,err = prog('cat') if(err) then ngx.say(err) else ngx.say(data.stdout) end } } location /cat-2 { content_by_lua_block { local prog = require'resty.exec'.new('/tmp/exec.sock') local data,err = prog({argv = 'cat', stdin = 'awesome'}) if(err) then ngx.say(err) else ngx.say(data.stdout) end } } location /slow-print { content_by_lua_block { local prog = require'resty.exec'.new('/tmp/exec.sock') prog.stdout = function(v) ngx.print(v) ngx.flush(true) end prog('/usr/local/bin/slow-print') } # look in `/misc` of this repo for `slow-print` } location /shell { content_by_lua_block { local prog = require'resty.exec'.new('/tmp/exec.sock') local data, err = prog('bash','-c','echo $PATH') if(err) then ngx.say(err) else ngx.say(data.stdout) end } } MIT license (see LICENSE)