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.stdout或prog.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指示数据的类型,可以是stdout、stderr、termsig、exitcode。
err通常是closed或timeout。
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仓库中找到有关此模块的其他配置提示和文档。