exec: Запуск внешних программ в nginx-module-lua без создания оболочки или блокировки
Установка
Если вы еще не настроили подписку на репозиторий 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
Чтобы использовать эту библиотеку Lua с NGINX, убедитесь, что nginx-module-lua установлен.
Этот документ описывает lua-resty-exec v3.0.3, выпущенный 22 августа 2017 года.
Небольшой модуль Lua для выполнения процессов. Он в первую очередь предназначен для использования с OpenResty, но также будет работать и в обычных приложениях Lua. При использовании с OpenResty он полностью неблокирующий (в противном случае он возвращается к использованию LuaSocket и блокирует).
Он похож на (и вдохновлен) lua-resty-shell, основное отличие заключается в том, что этот модуль использует sockexec, который не создает оболочку - вместо этого вы предоставляете массив строк аргументов, что означает, что вам не нужно беспокоиться о правилах экранирования/цитирования/разбора оболочки.
Кроме того, начиная с версии 2.0.0, вы можете использовать resty.exec.socket для доступа к интерфейсу более низкого уровня, который позволяет двустороннюю связь с программами. Вы можете читать и записывать в работающие приложения!
Это требует, чтобы ваш веб-сервер имел активный экземпляр sockexec.
Журнал изменений
3.0.0- новое поле, возвращаемое:
unknown- если это произойдет, пожалуйста, сообщите мне об ошибке! 2.0.0- Новый модуль
resty.exec.socketдля использования дуплексного соединения resty.execбольше не использует аргументbufsizeresty.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 несколькими различными способами:
ez-mode
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. Примеры:
-- установить timeout_fatal = false для объектов prog
prog.timeout_fatal = false
-- или установить это во время вызова:
local res, err = prog({argv = {'cat'}, timeout_fatal = false})
Но я на самом деле хочу оболочку!
Не проблема! Вы можете просто сделать что-то вроде:
local res, err = prog('bash','-c','echo $PATH')
Или, если вы хотите запустить целый скрипт:
prog.stdin = script_data
local res, err = prog('bash')
-- это примерно эквивалентно выполнению `bash < script` в 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'
-- вы можете указать тайм-аут в миллисекундах, необязательно
local client = exec_socket:new({ timeout = 60000 })
-- каждый новый экземпляр программы требует нового
-- вызова connect
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('Получены %s данные: %s',typ,data))
-- напечатает 'Получены stdout данные: hello there'
client:send('hey this cat process is still running')
data, typ, err = client:receive()
print(string.format('Получены %s данные: %s',typ,data))
-- напечатает 'Получены stdout данные: hey this cat process is still running'
client:send_close() -- закрывает stdin
data, typ, err = client:receive()
print(string.format('Получены %s данные: %s',typ,data))
-- напечатает 'Получены exitcode данные: 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
Предполагая, что вы запускаете sockexec по адресу /tmp/exec.sock
$ 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
Вы можете найти дополнительные советы по конфигурации и документацию для этого модуля в репозитории GitHub для nginx-module-exec.