lua-resty-exec

在 OpenResty 中运行外部程序,无需启动 shell 或阻塞

$ opm get jprjr/lua-resty-exec

lua-resty-exec

一个用于执行进程的小型 Lua 模块。它主要用于 OpenResty,但也适用于常规 Lua 应用程序。当与 OpenResty 一起使用时,它完全是非阻塞的(否则它将回退到使用 LuaSocket 并阻塞)。

它类似于(并受到启发)lua-resty-shell,主要区别在于此模块使用 sockexec,它不会启动 shell - 相反,您提供一个参数字符串数组,这意味着您不需要担心 shell 转义/引用/解析规则。

此外,从 2.0.0 版开始,您可以使用 resty.exec.socket 访问更低级的接口,该接口允许与程序进行双向通信。您可以读取和写入正在运行的应用程序!

这需要您的 Web 服务器运行 sockexec 的活动实例。

变更日志

  • 3.0.0

    • 返回的新字段:unknown - 如果发生这种情况,请告诉我错误!

  • 2.0.0

    • 新的 resty.exec.socket 模块用于使用双工连接

    • resty.exec 不再使用 bufsize 参数

    • resty.exec 现在接受 timeout 参数,以毫秒为单位指定,默认为 60 秒

    • 这是一个重大修订,请在升级之前彻底测试!

  • 2.0.0 之前没有变更日志

安装

lua-resty-execluarocksopm 上可用,您可以使用 luarocks install lua-resty-execopm get jprjr/lua-resty-exec 安装它。

如果您在 OpenResty 之外使用它,您还需要安装 LuaSocket 模块,即 luarocks install luasocket

此外,您需要运行 sockexec,请参阅 其仓库 获取说明。

resty.exec 用法

    local exec = require'resty.exec'
    local prog = exec.new('/tmp/exec.sock')

创建一个新的 prog 对象,使用 /tmp/exec.sock 与 sockexec 连接。

从那里,您可以通过两种不同的方式使用 prog

ez-模式

    local res, err = prog('uname')
    
    -- res = { stdout = "Linux\n", stderr = nil, exitcode = 0, termsig = nil }
    -- err = nil
    
    ngx.print(res.stdout)

这将运行 uname,并且在 stdin 上没有数据。

返回一个输出/错误代码表,其中 err 设置为遇到的任何错误。

事先设置 argv

    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)

事先设置 stdin

    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)

使用显式 argv、stdin 数据、stdout/stderr 回调调用

    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 printed

注意:这里 argv 是一个字符串,如果您的程序不需要任何参数,这很好。

设置 stdout/stderr 回调

如果您将 prog.stdoutprog.stderr 设置为函数,它将为收到的每个 stdout/stderr 数据块调用。

请注意,没有保证 stdout/stderr 是一个完整的字符串,或者任何特别合理的东西!

    prog.stdout = function(data)
        ngx.print(data)
        ngx.flush(true)
    end
    
    local res, err = prog('some-program')
    

将超时视为非错误

默认情况下,sockexec 将超时视为错误。您可以通过将对象的 timeout_fatal 键设置为 false 来禁用此功能。示例

    -- 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})

但我实际上想要一个 shell!

没问题!您可以这样做

    local res, err = prog('bash','-c','echo $PATH')

或者,如果您想运行整个脚本

    prog.stdin = script_data
    local res, err = prog('bash')
    
    -- this is roughly equivalent to running `bash < script` on the CLI

守护进程

我通常建议不要守护进程 - 我认为使用某种消息队列和/或监控系统要好得多,这样您就可以监控进程,在失败时采取行动,等等。

也就是说,如果您想启动一些进程,您可以使用 start-stop-daemon,即

    local res, err = prog('start-stop-daemon','--pidfile','/dev/null','--background','--exec','/usr/bin/sleep', '--start','--','10')

将以分离的后台进程启动 sleep 10

如果您不想处理 start-stop-daemon,我有一个用于启动后台程序的小型实用程序,称为 idgaf,即

    local res, err = prog('idgaf','sleep','10')

这基本上会完成 start-stop-daemon 所做的事情,而无需使用数十亿个标志。

resty.exec.socket 用法

    local exec_socket = require'resty.exec.socket'
    
    -- you can specify timeout in milliseconds, optional
    local client = exec_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 对象方法

  • ok, err = client:connect(path)**

通过 unix 套接字连接到给定的路径。如果这在 nginx 中运行,unix: 字符串将自动添加在前面。

  • bytes, err = client:send_args(args)**

将参数表发送到 sockexec 并启动程序。

  • bytes, err = client:send_data(data)**

data 发送到程序的标准输入

  • bytes, err = client:send(data)**

只是 client:send_data(data) 的快捷方式

  • bytes, err = client:send_close()**

关闭程序的标准输入。您也可以发送一个空字符串,例如 client:send_data('')

  • data, typ, err = client:receive()**

从正在运行的进程接收数据。typ 表示数据类型,可以是 stdoutstderrtermsigexitcode

err 通常是 closedtimeout

  • client:close()**

强制关闭客户端连接

  • client:getfd()**

一个 getfd 方法,如果您想在 select 循环中监控底层套接字连接,这很有用

一些示例 nginx 配置

假设您在 /tmp/exec.sock 处运行 sockexec

    $ sockexec /tmp/exec.sock

然后在您的 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

POD 错误

嘿!上面的文档有一些编码错误,解释如下:

大约在第 300 行

未终止的 B<...> 序列

大约在第 314 行

未终止的 B<...> 序列

大约在第 327 行

未终止的 B<...> 序列

大约在第 340 行

未终止的 B<...> 序列

大约在第 353 行

未终止的 B<...> 序列

大约在第 367 行

未终止的 B<...> 序列

大约在第 383 行

未终止的 B<...> 序列

大约在第 396 行

未终止的 B<...> 序列

作者

John Regan

许可证

mit

依赖关系

luajit, jprjr/netstring >= 1.0.6

版本