Top.Mail.Ru
Модуль popen | Tarantool
Tarantool
Узнайте содержание релиза 2.8

Модуль popen

Модуль popen

Начиная с версии 2.4.1 в Tarantool есть встроенный модуль popen, предназначенный для выполнения внешних программ. Он работает аналогично модулю subprocess() в Python или Open3 в Ruby. Однако в popen нет вспомогательных средств, которые предоставляют эти языки; он предоставляет только базовые функции. Для создания объекта popen использует системный вызов vfork(), поэтому вызывающий поток блокируется до тех пор, пока не начинается выполнение дочернего процесса.

В модуле popen есть две функции для создания объекта popen:

  • popen.shell, аналогичная системному вызову popen из libc;
  • popen.new для создания объекта popen с более специфическими параметрами.

Обе функции возвращают дескриптор, который мы будем называть popen_handle или ph. Через дескриптор вы можете выполнять методы.

Ниже приведен перечень всех функций popen и методов дескриптора ph.

Имя Назначение
popen.shell() Выполнение shell-команды
popen.new() Выполнение дочерней программы в новом процессе
popen_handle:read() Считывание данных из дочернего процесса
popen_handle:write() Запись строки в поток stdin дочернего процесса
popen_handle:shutdown() Закрытие канала с std* со стороны родителя
popen_handle:terminate() Отправка сигнала SIGTERM дочернему процессу
popen_handle:kill() Отправка сигнала SIGKILL дочернему процессу
popen_handle:signal() Отправка сигнала дочернему процессу
popen_handle:info() Получение информации о дескрипторе popen
popen_handle:wait() Ожидание, пока дочерний процесс не завершится или не получит сигнал
popen_handle:close() Закрытие дескриптора popen
Константы модуля Константы модуля
Поля дескриптора Поля дескриптора
popen.shell(command[, mode])

Выполнение shell-команды.

Параметры:
  • command (string) – имя выполняемой команды, обязательно
  • mode (string) – режим передачи данных, необязательно
возвращает:

(при успешном выполнении) дескриптор объекта popen, который мы будем называть popen_handle или ph

(при неудачном выполнении) nil, err

Возможные ошибки: если один из параметров задан некорректно, функция возвращает IllegalParams: неправильно задан тип или значение параметра. Другие возможные ошибки смотрите в разделе popen.new().

Возможные значения режима передачи данных mode:

Several mode characters can be set together, for example 'rw', 'rRw'.

Функция shell — это сокращение для popen.new({command}, opts) с opts.shell.setsid и opts.shell.group_signal, установленными в true, и значениями opts.stdin, opts.stdout и opts.stderr, установленными на основе параметра mode.

All std* streams are inherited from the parent by default unless it is changed using mode: 'r' for stdout, 'R' for stderr, or 'w' for stdin.

Пример:

This is the equivalent of the sh -c date command. It starts a process, runs 'date', reads the output, and closes the popen object (ph).

local popen = require('popen')
-- Запуск программы и сохранение ее дескриптора.
local ph = popen.shell('date', 'r')
-- Считывание вывода программы и удаление следующей строки.
local date = ph:read():rstrip()
-- Освобождение ресурсов. Процесс принудительно завершается (но 'date'
-- все равно завершает работу).
ph:close()
print(date)

Unix defines a text file as a sequence of lines. Each line is terminated by a newline (\\n) symbol. The same convention is usually applied for text output of a command. So, when it is redirected to a file, the file will be correct.

Однако внутри приложение обычно работает со строками, которые не завершаются символом новой строки (например, строки сообщений об ошибках). Символ новой строки обычно добавляется прямо перед записью строки в stdout, консоль или лог. Поэтому в примере выше был использован метод rstrip().

popen.new(argv[, opts])

Выполнение дочерней программы в новом процессе.

Параметры:
  • argv (array) – массив, состоящий из запускаемой программы и параметров командной строки, обязательный аргумент; если поле opts.shell установлено в false (по умолчанию), то необходимо задать абсолютный путь к программе
  • mode (opts) – таблица параметров, необязательно
возвращает:

(при успешном выполнении) дескриптор объекта popen, который мы будем называть popen_handle или ph

(при неудачном выполнении) nil, err

Возможные ошибки:

  • IllegalParams: некорректный тип или значение параметра
  • IllegalParams: групповой сигнал задан, а setsid — нет

Возможные причины ошибок, когда возвращается nil, err:

  • SystemError: dup(), fcntl(), pipe(), vfork() или close() завершились с ошибкой в родительском процессе
  • SystemError: (временное ограничение) родительский процесс закрыл stdin, stdout или stderr
  • OutOfMemory: невозможно выделить память для дескриптора или временного буфера

Возможные элементы opts:

  • opts.stdin (действие над STDIN_FILENO)
  • opts.stdout (действие над STDOUT_FILENO)
  • opts.stderr (действие над STDERR_FILENO)

Возможные действия файлового дескриптора в таблице opts:

  • popen.opts.INHERIT (== 'inherit') [default] inherit the fd from the parent
  • popen.opts.DEVNULL (== 'devnull') open /dev/null on the fd
  • popen.opts.CLOSE (== 'close') close the fd
  • popen.opts.PIPE (== 'pipe') feed data from fd to parent, or from parent to fd, using a pipe

Таблица opts может содержать таблицу env переменных среды для использования внутри процесса. Каждый элемент opts.env может быть парой ключ-значение (где ключ — имя переменной, а значение — значение переменной).

  • Если opts.env не установлено, то наследуется текущая среда.
  • Если opts.env является пустой таблицей, то среда будет сброшена.
  • Если opts.env является непустой таблицей, то среда будет заменена.

Таблица opts может содержать следующие элементы типа boolean:

Имя Значение по умолчанию Назначение
opts.shell false При значении true выполняется запуск дочернего процесса через sh -c "${opts.argv}". При значении false исполняемый процесс вызывается напрямую.
opts.setsid false При значении true программа запускается в новой сессии. При значении false программа запускается в сессии и группе процессов экземпляра Tarantool.
opts.close_fds true При значении true закрываются все унаследованные родительские файловые дескрипторы. При значении false унаследованные родительские файловые дескрипторы не закрываются.
opts.restore_signals true При значении true сбрасываются все действия сигналов, измененные в родительском процессе. При значении false все действия сигналов, измененные в родительском процессе, наследуются.
opts.group_signal false При значении true отправляется сигнал в группу дочерних процессов, но только при условии, что установлено поле opts.setsid. При значении false сигнал отправляется только одному дочернему процессу.
opts.keep_child false При значении true дочернему процессу (или группе процессов, если поле opts.group_signal установлено в true) не отправляется сигнал SIGKILL. При значении false дочернему процессу (или группе процессов, если поле opts.group_signal установлено в true) отправляется сигнал SIGKILL при выполнении popen_handle:close() или когда Lua GC собирает дескрипторы.

Возвращаемый дескриптор ph дает доступ к методу popen_handle:close() для явного освобождения всех занятых ресурсов, включая сам дочерний процесс, если не установлено поле opts.keep_child. Однако, если метод close() не вызывается для дескриптора в течение его жизни, Lua GC запустит то же самое действие по освобождению ресурсов.

Tarantool рекомендует использовать opts.setsid вместе с opts.group_signal, если дочерний процесс может создать собственные дочерние процессы и их все нужно будет завершить одновременно.

Сигнал не будет отправлен, если дочерний процесс уже был завершен. В противном случае мы можем случайно завершить другой процесс, который имеет тот же идентификатор процесса после освобождения его предыдущим. Это означает, что если дочерний процесс завершается до того, как завершаются его дочерние процессы, то функция не будет отправлять сигнал группе процессов, даже когда установлены opts.setsid и opts.group_signal.

Используйте os.environ(), чтобы передать копию текущей среды с несколькими заменами (см. Пример 2 ниже).

Пример 1

В этом примере выполняется аналог команды sh -c date. Происходит запуск процесса, выполняется 'date', считывается результат и объект popen (ph) закрывается.

local popen = require('popen')

local ph = popen.new({'/bin/date'}, {
    stdout = popen.opts.PIPE,
})
local date = ph:read():rstrip()
ph:close()
print(date) -- например, Thu 16 Apr 2020 01:40:56 AM MSK

Пример 2

Example 2 is quite similar to Example 1, but sets an environment variable and uses the shell builtin 'echo' to show it.

local popen = require('popen')
local env = os.environ()
env['FOO'] = 'bar'
local ph = popen.new({'echo "${FOO}"'}, {
    stdout = popen.opts.PIPE,
    shell = true,
    env = env,
})
local res = ph:read():rstrip()
ph:close()
print(res) -- bar

Пример 3

Пример 3 показывает, как перехватить дочерний поток stderr.

local popen = require('popen')
local ph = popen.new({'echo hello >&2'}, { -- !!
    stderr = popen.opts.PIPE,              -- !!
    shell = true,
})
local res = ph:read({stderr = true}):rstrip()
ph:close()
print(res) -- hello

Пример 4

Пример 4 показывает, как запустить потоковую программу (например, grep, sed и т.д.), записать данные в ее поток stdin и считать данные из stdout.

В этом примере предполагается, что входные данные достаточно малы, чтобы поместиться в буфер канала (обычно его размер равен 64 КиБ, но это зависит от конкретной платформы и ее конфигурации).

Если процесс записывает большое количество данных, он приостановится при выполнении popen_handle:write(). Для разрешения этой проблемы вызывайте popen_handle:read() в цикле в другом файбере (запустите его перед первым вызовом :write()).

Если процесс записывает длинный текст в stderr, он может приостановиться при выполнении write(), потому что буфер канала stderr заполнился. Для решения этой проблемы считывайте из stderr в отдельном файбере.

local function call_jq(input, filter)
    -- Запуск процесса jq, соединение с stdin, stdout и stderr.
    local jq_argv = {'/usr/bin/jq', '-M', '--unbuffered', filter}
    local ph, err = popen.new(jq_argv, {
        stdin = popen.opts.PIPE,
        stdout = popen.opts.PIPE,
        stderr = popen.opts.PIPE,
    })
    if ph == nil then return nil, err end
    -- Запись входных данных в дочерний stdin и отправка EOF.
    local ok, err = ph:write(input)
    if not ok then return nil, err end
    ph:shutdown({stdin = true})
    -- Считывание всех данных до EOF.
    local chunks = {}
    while true do
        local chunk, err = ph:read()
        if chunk == nil then
            ph:close()
            return nil, err
        end
        if chunk == '' then break end -- EOF
        table.insert(chunks, chunk)
    end
    -- Считывание данных диагностики из stderr (при наличии).
    local err = ph:read({stderr = true})
    if err ~= '' then
        ph:close()
        return nil, err
    end
    -- Соединение всех частей вместе, обрезка символа конца строки.
    return table.concat(chunks):rstrip()
end

object popen_handle
popen_handle:read([opts])

Считывание данных из дочернего процесса.

Параметры:
  • ph (handle) – дескриптор дочернего процесса, созданный через popen.new() или popen.shell()
  • opts (table) – параметры

Возможные ошибки при некорректно заданных параметрах или при отмене файбера:

  • IllegalParams: некорректный тип или значение параметра
  • IllegalParams: вызов дескриптора, который уже был закрыт
  • IllegalParams: одновременно установлены opts.stdout и opts.stderr
  • IllegalParams: запрашиваемая операция ввода-вывода не поддерживается дескриптором (вывод stdout / stderr не перенаправлен)
  • IllegalParams: попытка произвести операцию над закрытым файловым дескриптором
  • FiberIsCancelled: файбер отменен во внешней программе
возвращает:

true on success, false on error

возвращаемое значение:
 

(при успешном выполнении) строка со считанным значением, пустая строка при EOF

(при неудачном выполнении) nil, err

Возможные элементы opts:

  • opts.stdout (boolean, значение по умолчанию — true, при значении true выполняется считывание из stdout)
  • opts.stderr (boolean, значение по умолчанию — false, при значении true выполняется считывание из stderr)
  • opts.timeout (число, значение по умолчанию — 100 лет, временная квота в секундах)

Другими словами: по умолчанию read() выполняет считывание из stdout, но если установить opts.stderr в true, то считывание происходит из stderr (нельзя одновременно установить opts.stdout и opts.stderr в true).

Возможные причины ошибок, когда возвращается nil, err:

  • SocketError: ошибка ввода-вывода при выполнении read()
  • TimedOut: превышение квоты opts.timeout
  • OutOfMemory: недостаточно памяти для считывания в буфер
  • LuajitError: («not enough memory»): недостаточно памяти для строки Lua
popen_handle:write(str[, opts])

Запись строки str в поток stdin дочернего процесса.

Параметры:
  • ph (handle) – дескриптор дочернего процесса, созданный через popen.new() или popen.shell()
  • str (string) – строка для записи
  • opts (table) – параметры
возвращает:

true on success, false on error

возвращаемое значение:
 

(при успешном выполнении) boolean = true

(при неудачном выполнении) nil, err

Possible opts items are: opts.timeout (number, default 100 years, time quota in seconds).

Возможные ошибки:

  • IllegalParams: некорректный тип или значение параметра
  • IllegalParams: вызов дескриптора, который уже был закрыт
  • IllegalParams: длина строки превышает SSIZE_MAX
  • IllegalParams: запрашиваемая операция ввода-вывода не поддерживается дескриптором (вывод stdin не перенаправлен)
  • IllegalParams: попытка произвести операцию над закрытым файловым дескриптором
  • FiberIsCancelled: файбер отменен во внешней программе

Возможные причины ошибок, когда возвращается nil, err:

  • SocketError: ошибка ввода-вывода при выполнении write()
  • TimedOut: превышение квоты opts.timeout

write() может передать управление (yield) и заблокировать файбер, если дочерний процесс не считывает данные из stdin и буфер канала заполнился. Размер буфера зависит от платформы. Если сомневаетесь, обратите внимание на опцию opts.timeout.

Когда опция opts.timeout не установлена, write() блокирует файбер до момента полной записи данных или возникновения ошибки записи.

popen_handle:shutdown([opts])

Закрытие канала с std* со стороны родителя.

Параметры:
  • ph (handle) – дескриптор дочернего процесса, созданный через popen.new() или popen.shell()
  • opts (table) – параметры
возвращает:

true on success, false on error

возвращаемое значение:
 

(при успешном выполнении) boolean = true

Возможные элементы opts:

  • opts.stdin (boolean) закрыть stdin со стороны родителя
  • opts.stdout (boolean) закрыть stdout со стороны родителя
  • opts.stderr (boolean) закрыть stderr со стороны родителя

Мы можем использовать термин std* для указания на любой из этих элементов.

Возможные ошибки:

  • IllegalParams: некорректный параметр дескриптора
  • IllegalParams: вызов дескриптора, который уже был закрыт
  • IllegalParams: не выбран ни один из потоков stdin, stdout или stderr
  • IllegalParams: запрашиваемая операция ввода-вывода не поддерживается дескриптором (один из потоков std* не перенаправлен)

Основная цель использования shutdown() — отправка EOF в дочерний поток stdin. Однако stdout / stderr может быть уже закрыт со стороны родительского процесса.

shutdown() does not fail on already closed fds (idempotence). However, it fails on an attempt to close the end of a pipe that never existed. In other words, only those std* options that were set to popen.opts.PIPE during handle creation may be used here (for popen.shell(): 'r' corresponds to stdout, 'R' to stderr and 'w' to stdin).

shutdown() не закрывает никакие файловые дескрипторы при завершении с ошибкой: либо закрываются все запрашиваемые дескрипторы (при успешном выполнении), либо ни один из них.

Пример:

local popen = require('popen')
local ph = popen.shell('sed s/foo/bar/', 'rw')
ph:write('lorem foo ipsum')
ph:shutdown({stdin = true})
local res = ph:read()
ph:close()
print(res) -- lorem bar ipsum
popen_handle:terminate()

Отправка сигнала SIGTERM дочернему процессу.

Параметры:
  • ph (handle) – дескриптор дочернего процесса, созданный через popen.new() или popen.shell()
возвращает:

для получения информации об ошибках и возвращаемых значениях смотрите popen_handle:signal()

terminate() просто отправляет сигнал SIGTERM. Он не освобождает никакие ресурсы (такие как память для дескрипторов popen и файловые дескрипторы).

popen_handle:kill()

Отправка сигнала SIGKILL дочернему процессу.

Параметры:
  • ph (handle) – дескриптор дочернего процесса, созданный через popen.new() или popen.shell()
возвращает:

для получения информации об ошибках и возвращаемых значениях смотрите popen_handle:signal()

kill() просто отправляет сигнал SIGKILL. Он не освобождает никакие ресурсы (такие как память для дескрипторов popen и файловые дескрипторы).

popen_handle:signal(signo)

Отправка сигнала дочернему процессу.

Параметры:
  • ph (handle) – дескриптор дочернего процесса, созданный через popen.new() или popen.shell()
  • signo (number) – отправляемый сигнал
возвращает:

(при успешном выполнении) true (сигнал отправлен)

(при неудачном выполнении) nil, err

Возможные ошибки:

  • IllegalParams: некорректный параметр дескриптора
  • IllegalParams: вызов дескриптора, который уже был закрыт

Возможные значения ошибок при возвращении nil, err:

  • SystemError: процесс больше не существует (также может возвращаться для зомби-процессов или когда все процессы в группе являются зомби-процессами (но см. примечание для Mac OS ниже)
  • SystemError: неправильный номер сигнала
  • SystemError: нет разрешения на отправку сигнала процессу или группе процессов (возвращается на Mac OS, когда сигнал отправляется группе процессов, где лидер группы является зомби-процессом (или все процессы являются зомби-процессами, детали неясны) (эта ошибка также может возникнуть по другим причинам, детали неясны)

Если для дескриптора установлены opts.setsid и opts.group_signal, сигнал отправляется группе процессов, а не отдельному процессу. Для подробной информации по групповым сигналам смотрите popen.new(). Внимание: на Mac OS процесс в группе может не получить сигнал, особенно если он только что был разветвлен (возможно это происходит из-за состояния гонки).

Примечание: Некоторые сигналы имеют разные номера на разных платформах. Поэтому в этом модуле мы предлагаем константы popen.signal.SIG*.

popen_handle:info()

Получение информации о дескрипторе popen.

Параметры:
  • ph (handle) – дескриптор дочернего процесса, созданный через popen.new() или popen.shell()
  • signo (number) – отправляемый сигнал
возвращает:

(при успешном выполнении) отформатированный результат

возвращаемое значение:
 

res

Возможные ошибки:

  • IllegalParams: некорректный параметр дескриптора
  • IllegalParams: вызов дескриптора, который уже был закрыт

Результат выводится в следующем формате:

{
    pid = <number> or <nil>,
    command = <string>,
    opts = <table>,
    status = <table>,
    stdin = one-of(
        popen.stream.OPEN   (== 'open'),
        popen.stream.CLOSED (== 'closed'),
        nil,
    ),
    stdout = one-of(
        popen.stream.OPEN   (== 'open'),
        popen.stream.CLOSED (== 'closed'),
        nil,
    ),
    stderr = one-of(
        popen.stream.OPEN   (== 'open'),
        popen.stream.CLOSED (== 'closed'),
        nil,
    ),
}

pid — это идентификатор процесса, когда тот находится в рабочем состоянии; для завершенного процесса pid имеет значение nil.

command — это конкатенация аргументов, разделенных пробелами, которые были переданы в execve(). Аргументы, состоящие из нескольких слов, заключаются в кавычки. Кавычки внутри аргументов не экранируются.

opts – это таблица параметров дескриптора, описанная в разделе opts функции popen.new(). opts.env здесь не отображается, потому что карта переменных среды не хранится в дескрипторе.

status — это таблица, отображающая состояние процесса в следующем формате:

{
    state = one-of(
        popen.state.ALIVE    (== 'alive'),
        popen.state.EXITED   (== 'exited'),
        popen.state.SIGNALED (== 'signaled'),
    )
    -- Отображается при состоянии процесса 'завершенный'.
    exit_code = <number>,
    -- Отображается при состоянии процесса 'принимающий сигнал'.
    signo = <number>,
    signame = <string>,
}

stdin, stdout, and stderr reflect the status of the parent’s end of a piped stream. If a stream is not piped, the field is not present (nil). If it is piped, the status may be either popen.stream.OPEN (== 'open') or popen.stream.CLOSED (== 'closed'). The status may be changed from 'open' to 'closed' by a popen_handle:shutdown({std… = true}) call.

Пример 1

(в консоли Tarantool)

tarantool> require('popen').new({'/usr/bin/touch', '/tmp/foo'})
---
- command: /usr/bin/touch /tmp/foo
  status:
    state: alive
  opts:
    stdout: inherit
    stdin: inherit
    group_signal: false
    keep_child: false
    close_fds: true
    restore_signals: true
    shell: false
    setsid: false
    stderr: inherit
  pid: 9499
...

Пример 2

(в консоли Tarantool)

tarantool> require('popen').shell('grep foo', 'wrR')
---
- stdout: open
  command: sh -c 'grep foo'
  stderr: open
  status:
    state: alive
  stdin: open
  opts:
    stdout: pipe
    stdin: pipe
    group_signal: true
    keep_child: false
    close_fds: true
    restore_signals: true
    shell: true
    setsid: true
    stderr: pipe
  pid: 10497
...
popen_handle:wait()

Ожидание, пока дочерний процесс не завершится или не получит сигнал.

Параметры:
  • ph (handle) – дескриптор дочернего процесса, созданный через popen.new() или popen.shell()
  • signo (number) – отправляемый сигнал
возвращает:

(при успешном выполнении) отформатированный результат

возвращаемое значение:
 

res

Возможные ошибки:

  • IllegalParams: некорректный параметр дескриптора
  • IllegalParams: вызов дескриптора, который уже был закрыт
  • FiberIsCancelled: файбер отменен во внешней программе

Отформатированный результат представляет собой таблицу состояний процесса (аналогично компоненту status таблицы, возвращаемой через popen_handle:info()).

popen_handle:close()

Закрытие дескриптора popen.

Параметры:
  • ph (handle) – дескриптор дочернего процесса, созданный через popen.new() или popen.shell()
возвращает:

(при успешном выполнении) true

(при неудачном выполнении) nil, err

Возможные ошибки:

  • IllegalParams: некорректный параметр дескриптора

Возможные результаты диагностики, когда возвращается nil, err (не рассматривайте эти случаи как ошибки):

  • SystemError: нет разрешения на отправку сигнала процессу или группе процессов (это сообщение диагностики может появиться из-за особенностей обработки зомби-процессов в Mac OS, когда установлен opts.group_signal, см. popen_handle:signal(). Оно также может появиться по другим причинам, детали неясны).

Если известно, что процесс был завершен, то в результате всегда возвращается true (например, после выполнения popen_handle:wait() не будет отправлено никакого сигнала, так что никакой ошибки не может возникнуть).

close() принудительно завершает процесс через SIGKILL и освобождает все ресурсы, связанные с дескриптором popen.

Подробная информация об отправке сигналов:

  • Сигнал отправляется только когда поле opts.keep_child не установлено.
  • Сигнал отправляется только когда процесс находится в рабочем состоянии согласно информации, доступной на текущей итерации цикла событий. (Здесь есть слабое место: сигнал может быть отправлен зомби-процессу, но это не представляет никакой угрозы).
  • Сигнал отправляется процессу или группе процессов в зависимости от opts.group_signal. (Для подробной информации о групповых сигналах смотрите popen.new()).

Ресурсы освобождаются вне зависимости от того, успешно ли отправился сигнал: дескрипторы файлов закрываются, память освобождается, а дескриптор popen помечается как закрытый.

Над закрытым дескриптором невозможно выполнять никакие операции кроме close(), которая всегда выполняется успешно над закрытым дескриптором (идемпотентность).

close() может вернуть true или nil, err, но она всегда освобождает ресурсы дескриптора. Поэтому для того, кто отправил сигнал, любое возвращаемое значение означает успешное выполнение. Возвращаемые значения только дают информацию для логирования или составления отчетов.

Поля дескриптора

popen_handle.pid
popen_handle.command
popen_handle.opts
popen_handle.status
popen_handle.stdin
popen_handle.stdout
popen_handle.stderr

За более подробной информацией обратитесь к popen_handle:info().

Константы модуля

- popen.opts
  - INHERIT (== 'inherit')
  - DEVNULL (== 'devnull')
  - CLOSE   (== 'close')
  - PIPE    (== 'pipe')

- popen.signal
  - SIGTERM (== 9)
  - SIGKILL (== 15)
  - ...

- popen.state
  - ALIVE    (== 'alive')
  - EXITED   (== 'exited')
  - SIGNALED (== 'signaled')

- popen.stream
  - OPEN    (== 'open')
  - CLOSED  (== 'closed')