Перейти к содержанию

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 больше не использует аргумент 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 несколькими различными способами:

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.