Модуль 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-команды.
Параметры: возвращает: (при успешном выполнении) дескриптор объекта popen, который мы будем называть
popen_handle
илиph
(при неудачном выполнении)
nil, err
Возможные ошибки: если один из параметров задан некорректно, функция возвращает IllegalParams: неправильно задан тип или значение параметра. Другие возможные ошибки смотрите в разделе popen.new().
Возможные значения режима передачи данных
mode
:'w'
which enables popen_handle:write()'r'
which enables popen_handle:read()'R'
which enables popen_handle:read({stderr = true})'nil'
which means inherit parent’s std* file descriptors
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 (по умолчанию), то необходимо задать абсолютный путь к программе - opts (
table
) – таблица параметров, необязательно
возвращает: (при успешном выполнении) дескриптор объекта 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 parentpopen.opts.DEVNULL
(=='devnull'
) open /dev/null on the fdpopen.opts.CLOSE
(=='close'
) close the fdpopen.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
- argv (
-
object
popen_handle
¶ -
popen_handle:
read
([opts])¶ Считывание данных из дочернего процесса.
Параметры: - ph (
handle
) – дескриптор дочернего процесса, созданный через popen.new() или popen.shell() - opts (
table
) – параметры
Возможные элементы
opts
:opts.stdout
(boolean, значение по умолчанию —true
, при значенииtrue
выполняется считывание из stdout)opts.stderr
(boolean, значение по умолчанию —false
, при значенииtrue
выполняется считывание из stderr)opts.timeout
(число, значение по умолчанию — 100 лет, временная квота в секундах)
In other words: by default
read()
reads from stdout, but reads from stderr if one setsopts.stderr
totrue
. It is not legal to set bothopts.stdout
andopts.stderr
totrue
.возвращает: (при успешном выполнении) строка со считанным значением, пустая строка при EOF
(при неудачном выполнении)
nil, err
Possible errors
These errors are raised on incorrect parameters or when the fiber is cancelled:
- IllegalParams: некорректный тип или значение параметра
- IllegalParams: вызов дескриптора, который уже был закрыт
- IllegalParams: одновременно установлены opts.stdout и opts.stderr
- IllegalParams: запрашиваемая операция ввода-вывода не поддерживается дескриптором (вывод stdout / stderr не перенаправлен)
- IllegalParams: попытка произвести операцию над закрытым файловым дескриптором
- FiberIsCancelled: cancelled by external code
nil, err
is returned on following failures:- SocketError: ошибка ввода-вывода при выполнении read()
- TimedOut: превышение квоты opts.timeout
- OutOfMemory: недостаточно памяти для считывания в буфер
- LuajitError: («not enough memory»): недостаточно памяти для строки Lua
- ph (
-
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()
блокирует файбер до момента полной записи данных или возникновения ошибки записи.- ph (
-
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 thosestd*
options that were set topopen.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
- ph (
-
popen_handle:
terminate
()¶ Отправка сигнала SIGTERM дочернему процессу.
Параметры: - ph (
handle
) – дескриптор дочернего процесса, созданный через popen.new() или popen.shell()
возвращает: для получения информации об ошибках и возвращаемых значениях смотрите popen_handle:signal()
terminate()
просто отправляет сигнал SIGTERM. Он не освобождает никакие ресурсы (такие как память для дескрипторов popen и файловые дескрипторы).- ph (
-
popen_handle:
kill
()¶ Отправка сигнала SIGKILL дочернему процессу.
Параметры: - ph (
handle
) – дескриптор дочернего процесса, созданный через popen.new() или popen.shell()
возвращает: для получения информации об ошибках и возвращаемых значениях смотрите popen_handle:signal()
kill()
просто отправляет сигнал SIGKILL. Он не освобождает никакие ресурсы (такие как память для дескрипторов popen и файловые дескрипторы).- ph (
-
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*
.- ph (
-
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
, andstderr
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 eitherpopen.stream.OPEN
(=='open'
) orpopen.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 ...
- ph (
-
popen_handle:
wait
()¶ Ожидание, пока дочерний процесс не завершится или не получит сигнал.
Параметры: - ph (
handle
) – дескриптор дочернего процесса, созданный через popen.new() или popen.shell() - signo (
number
) – отправляемый сигнал
возвращает: (при успешном выполнении) отформатированный результат
возвращаемое значение: res
Возможные ошибки:
- IllegalParams: некорректный параметр дескриптора
- IllegalParams: вызов дескриптора, который уже был закрыт
- FiberIsCancelled: файбер отменен во внешней программе
Отформатированный результат представляет собой таблицу состояний процесса (аналогично компоненту
status
таблицы, возвращаемой через popen_handle:info()).- ph (
-
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
, но она всегда освобождает ресурсы дескриптора. Поэтому для того, кто отправил сигнал, любое возвращаемое значение означает успешное выполнение. Возвращаемые значения только дают информацию для логирования или составления отчетов.- ph (
Поля дескриптора
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')
-