跳转至

exec: 在nginx-module-lua中运行外部程序而不生成shell或阻塞

安装

如果您尚未设置RPM仓库订阅,请注册。然后您可以继续以下步骤。

CentOS/RHEL 7或Amazon Linux 2

yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install lua-resty-exec

CentOS/RHEL 8+、Fedora Linux、Amazon Linux 2023

dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-exec

要在NGINX中使用此Lua库,请确保已安装nginx-module-lua

本文档描述了lua-resty-exec v3.0.3,于2017年8月22日发布。


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

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

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

这要求您的Web服务器上必须有一个活动的sockexec实例在运行。

更新日志

  • 3.0.0
  • 新返回字段:unknown - 如果发生这种情况,请给我发送一个bug!
  • 2.0.0
  • 新的resty.exec.socket模块用于使用双工连接
  • resty.exec不再使用bufsize参数
  • resty.exec现在接受一个timeout参数,以毫秒为单位指定,默认为60秒
  • 这是一个重大修订,请在升级前进行彻底测试!
  • 2.0.0之前没有更新日志

resty.exec 用法

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

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

从这里开始,您可以以几种不同的方式使用prog

简单模式

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!' 被打印

注意:这里的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来禁用此功能。示例:

-- 在prog对象上设置timeout_fatal = false
prog.timeout_fatal = false

-- 或者,在调用时设置:
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')

-- 这大致相当于在CLI上运行`bash < script`

守护进程化进程

我一般不推荐守护进程化进程——我认为使用某种消息队列和/或监督系统要好得多,这样您可以监控进程,采取失败时的行动,等等。

也就是说,如果您想启动某个进程,您可以使用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'

-- 您可以指定超时(以毫秒为单位),可选
local client = exec_socket:new({ timeout = 60000 })

-- 每个新的程序实例都需要新的
-- 连接调用
local ok, err = client:connect('/tmp/exec.sock')

-- 发送程序参数,仅接受一个参数表
client:send_args({'cat'})

-- 发送stdin的数据
client:send('hello there')

-- 接收数据
local data, typ, err = client:receive()

-- `typ`可以是以下之一:
--    `stdout`   - 来自程序的stdout的数据
--    `stderr`   - 来自程序的stderr的数据
--    `exitcode` - 程序的退出代码
--    `termsig`  - 如果通过信号终止,使用了什么信号

-- 如果`err`被设置,data和typ将为nil
-- 常见的`err`值是`closed`和`timeout`
print(string.format('Received %s data: %s',typ,data))
-- 将打印 '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))
-- 将打印 'Received stdout data: hey this cat process is still running'

client:send_close() -- 关闭stdin
data, typ, err = client:receive()
print(string.format('Received %s data: %s',typ,data))
-- 将打印 'Received exitcode data: 0'

data, typ, err = client:receive()
print(err) -- 将打印 '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方法,如果您想在选择循环中监控底层套接字连接,这很有用。

一些示例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')
    }
    # 在此仓库的`/misc`中查找`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
    }
}

GitHub

您可以在nginx-module-exec的GitHub仓库中找到有关此模块的其他配置提示和文档。