Модуль popen
Начиная с: 2.4.1
В Tarantool есть встроенный модуль popen, предназначенный для
выполнения внешних программ. Он работает аналогично модулю
subprocess() в
Python или Open3 в
Ruby. Однако в popen нет вспомогательных средств, которые
предоставляют эти языки; он предоставляет только базовые функции. Для
создания объекта popen использует системный вызов
vfork(),
поэтому вызывающий поток блокируется до тех пор, пока не начинается
выполнение дочернего процесса.
В модуле popen есть две функции для создания объекта popen:
- popen.shell — похожа на системный вызов libc popen
- popen.new — для создания объекта popen с более специфичными параметрами
Обе функции возвращают дескриптор, который мы будем называть
popen_handle или ph. Через дескриптор вы можете выполнять методы.
Ниже приведен перечень всех функций popen и методов дескриптора ph.
Имя | Назначение |
|---|---|
Выполнение shell-команды | |
Запуск дочерней программы в новом процессе | |
Считывание данных из дочернего процесса | |
Запись строки в поток stdin дочернего процесса | |
Закрытие концов std*-дескрипторов со стороны родителя | |
Отправка сигнала SIGTERM дочернему процессу | |
Отправка сигнала SIGKILL дочернему процессу | |
Отправка сигнала дочернему процессу | |
Получение информации о дескрипторе popen | |
Ожидание завершения или получения сигнала дочерним процессом | |
Закрытие дескриптора popen | |
Константы модуля | |
Поля дескриптора |
Выполнение shell-команды.
Параметры:
command(string) — выполняемая команда, обязательный параметрmode(string) — режим обмена данными, необязательный параметр
Возвращает
(при успешном выполнении) дескриптор popen, который мы будем называть
popen_handle или ph
(при неудачном выполнении) nil, err
Возможные ошибки: если один из параметров задан некорректно, функция возвращает IllegalParams: неправильно задан тип или значение параметра. Другие возможные ошибки смотрите в разделе popen.new().
Возможные значения режима передачи данных mode:
'w'— включает popen_handle:write()'r'— включает popen_handle:read()'R'— включает popen_handle:read({stderr = true})'nil'— означает наследование std*-файловых дескрипторов родителя
Несколько символов режима можно задать вместе, например 'rw', 'rRw'.
Функция shell — это сокращение для
popen.new({command}, opts) с opts.shell.setsid и
opts.shell.group_signal, установленными в true, и значениями
opts.stdin, opts.stdout и opts.stderr, установленными на основе
параметра mode.
По умолчанию все потоки std* наследуются от родителя, если это не
изменено с помощью режима: 'r' для stdout, 'R' для stderr или 'w'
для stdin.
Пример:
Этот пример эквивалентен команде sh -c date. Он запускает процесс,
выполняет 'date', считывает вывод и закрывает объект popen (ph).
local popen = require('popen')-- Запуск программы и сохранение ее дескриптора.local ph = popen.shell('date', 'r')-- Считывание вывода программы и удаление следующей строки.local date = ph:read():rstrip()-- Освобождение ресурсов. Процесс принудительно завершается (но 'date'-- все равно завершает работу).ph:close()print(date)
В Unix текстовый файл определяется как последовательность строк. Каждая
строка завершается символом новой строки (\n). Это же соглашение
обычно применяется к текстовому выводу команды. Поэтому при
перенаправлении в файл файл будет корректным.
Однако внутри приложение обычно работает со строками, которые не
завершаются символом новой строки (например, строки сообщений об
ошибках). Символ новой строки обычно добавляется прямо перед записью
строки в stdout, консоль или лог. Поэтому в примере выше был использован
метод rstrip().
Выполнение дочерней программы в новом процессе.
Параметры:
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
Возможные элементы opts:
opts.stdin(действие над STDIN_FILENO)opts.stdout(действие над STDOUT_FILENO)opts.stderr(действие над STDERR_FILENO)
Возможные действия файлового дескриптора в таблице opts:
popen.opts.INHERIT(=='inherit') [по умолчанию] — наследовать файловый дескриптор от родителяpopen.opts.DEVNULL(=='devnull') — открыть /dev/null на месте файлового дескриптораpopen.opts.CLOSE(=='close') — закрыть файловый дескрипторpopen.opts.PIPE(=='pipe') — передавать данные от файлового дескриптора родителю или от родителя к файловому дескриптору через канал (pipe)
Таблица opts может содержать таблицу env переменных среды для
использования внутри процесса. Каждый элемент opts.env может быть
парой ключ-значение (где ключ — имя переменной, а значение —
значение переменной).
- Если
opts.envне задан, наследуется текущее окружение. - Если
opts.env— пустая таблица, окружение будет отброшено. - Если
opts.env— непустая таблица, окружение будет заменено.
Таблица opts может содержать следующие элементы типа boolean:
Имя | По умолчанию | Назначение |
|---|---|---|
opts.shell | false | Если true, дочерний процесс запускается через |
opts.setsid | false | Если true, программа запускается в новой сессии. Если false, программа запускается в сессии и группе процессов экземпляра Tarantool. |
opts.close_fds | true | Если true, все унаследованные от родителя файловые дескрипторы закрываются. Если false, они не закрываются. |
opts.restore_signals | true | Если true, все обработчики сигналов, измененные в родительском процессе, сбрасываются. Если false, они наследуются. |
opts.group_signal | false | Если true, сигнал отправляется группе дочерних процессов, но только если включен |
opts.keep_child | false | Если true, SIGKILL не отправляется дочернему процессу (или группе процессов при включенном |
Возвращаемый дескриптор ph дает доступ к методу
popen_handle:close() для явного освобождения всех занятых
ресурсов, включая сам дочерний процесс, если не установлено поле
opts.keep_child. Однако, если метод close() не вызывается для
дескриптора в течение его жизни, Lua GC запустит то же самое действие по
освобождению ресурсов.
Начиная с версии 3.2.0 в таблицу opts добавлена опция inherit_fds.
Она позволяет задать номера файловых дескрипторов, которые должны
остаться открытыми в дочернем процессе, если установлен флаг
close_fds.
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) -- e.g. Thu 16 Apr 2020 01:40:56 AM MSK
Пример 2:
Пример 2 похож на пример 1, но задает переменную среды и использует
встроенную shell-команду 'echo', чтобы показать ее.
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 endph:shutdown({stdin = true})-- Считывание всех данных до EOF.local chunks = {}while true dolocal chunk, err = ph:read()if chunk == nil thenph:close()return nil, errendif chunk == '' then break end -- EOFtable.insert(chunks, chunk)end-- Считывание данных диагностики из stderr (при наличии).local err = ph:read({stderr = true})if err ~= '' thenph:close()return nil, errend-- Соединение всех частей вместе, обрезка символа конца строки.return table.concat(chunks):rstrip()end
Считывание данных из дочернего процесса.
Параметры:
ph(handle) — дескриптор дочернего процесса, созданного с помощью popen.new() или popen.shell()opts(table) — параметры
Возможные элементы opts:
opts.stdout(boolean, по умолчаниюtrue; еслиtrue, чтение ведется из stdout)opts.stderr(boolean, по умолчаниюfalse; еслиtrue, чтение ведется из stderr)opts.timeout(number, по умолчанию 100 лет; временная квота в секундах)
Иными словами: по умолчанию read() читает из stdout, но читает из
stderr, если установить opts.stderr в true. Одновременно установить
opts.stdout и opts.stderr в true нельзя.
Возвращает
(при успешном выполнении) строку со считанным значением, пустую строку при EOF
(при неудачном выполнении) nil, err
Возможные ошибки:
Эти ошибки возникают при некорректных параметрах или при отмене файбера:
IllegalParams: некорректный тип или значение параметраIllegalParams: вызов на закрытом дескриптореIllegalParams: установлены одновременно opts.stdout и opts.stderrIllegalParams: запрошенная операция ввода-вывода не поддерживается дескриптором (stdout / stderr не соединен каналом)IllegalParams: попытка операции над закрытым файловым дескрипторомFiberIsCancelled: отменено внешним кодом
nil, err возвращается в следующих случаях сбоя:
- SocketError: ошибка ввода-вывода при read()
- TimedOut: превышена квота opts.timeout
- LuajitError: ("not enough memory"): нет памяти для Lua-строки
Запись строки str в поток stdin дочернего процесса.
Параметры:
ph(handle) — дескриптор дочернего процесса, созданного с помощью popen.new() или popen.shell()str(string) — записываемая строкаopts(table) — параметры
Возвращает
(при успешном выполнении) true
(при неудачном выполнении) nil, err
Тип возвращаемого значения
boolean
Возможные элементы opts: opts.timeout (number, по умолчанию 100
лет, временная квота в секундах).
Возможные ошибки:
- IllegalParams: некорректный тип или значение параметра
- IllegalParams: вызов на закрытом дескрипторе
- IllegalParams: длина строки больше SSIZE_MAX
- IllegalParams: запрошенная операция ввода-вывода не поддерживается дескриптором (stdin не соединен каналом)
- IllegalParams: попытка операции над закрытым файловым дескриптором
- FiberIsCancelled: отменено внешним кодом
Возможные причины ошибок, когда возвращается nil, err:
- SocketError: ошибка ввода-вывода при write()
- TimedOut: превышена квота opts.timeout
write() может передать управление (yield) и заблокировать файбер, если
дочерний процесс не считывает данные из stdin и буфер канала заполнился.
Размер буфера зависит от платформы. Если сомневаетесь, обратите внимание
на опцию opts.timeout.
Когда опция opts.timeout не установлена, write() блокирует файбер до
момента полной записи данных или возникновения ошибки записи.
Закрытие канала с std* со стороны родителя.
Параметры:
ph(handle) — дескриптор дочернего процесса, созданного с помощью popen.new() или popen.shell()opts(table) — параметры
Возвращает
(при успешном выполнении) true
Тип возвращаемого значения
boolean
Возможные элементы opts:
opts.stdin(boolean) — закрыть со стороны родителя конец stdinopts.stdout(boolean) — закрыть со стороны родителя конец stdoutopts.stderr(boolean) — закрыть со стороны родителя конец stderr
Мы можем использовать термин std* для указания на любой из этих элементов.
Возможные ошибки:
- IllegalParams: некорректный параметр дескриптора
- IllegalParams: вызов на закрытом дескрипторе
- IllegalParams: не выбран ни stdin, ни stdout, ни stderr
- IllegalParams: запрошенная операция ввода-вывода не поддерживается дескриптором (один из std* не соединен каналом)
Основная цель использования shutdown() — отправка EOF в дочерний
поток stdin. Однако stdout / stderr может быть уже закрыт со стороны
родительского процесса.
shutdown() не завершается ошибкой на уже закрытых файловых
дескрипторах (идемпотентность). Однако она завершается ошибкой при
попытке закрыть конец канала, которого никогда не существовало. Иными
словами, здесь можно использовать только те опции std*, которые были
установлены в popen.opts.PIPE при создании дескриптора (для
popen.shell(): 'r' соответствует stdout, 'R' —
stderr, 'w' — 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
Отправка сигнала SIGTERM дочернему процессу.
Параметры:
ph(handle) — дескриптор дочернего процесса, созданного с помощью popen.new() или popen.shell()
Возвращает
описание ошибок и возвращаемых значений см. в разделе popen_handle:signal()
terminate() просто отправляет сигнал SIGTERM. Он не освобождает
никакие ресурсы (такие как память для дескрипторов popen и файловые
дескрипторы).
Отправка сигнала SIGKILL дочернему процессу.
Параметры:
ph(handle) — дескриптор дочернего процесса, созданного с помощью popen.new() или popen.shell()
Возвращает
описание ошибок и возвращаемых значений см. в разделе popen_handle:signal()
kill() просто отправляет сигнал SIGKILL. Он не освобождает никакие
ресурсы (такие как память для дескрипторов popen и файловые
дескрипторы).
Отправка сигнала дочернему процессу.
Параметры:
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.
Параметры:
ph(handle) — дескриптор дочернего процесса, созданного с помощью popen.new() или popen.shell()
Возвращает
(при успешном выполнении) отформатированный результат
Тип возвращаемого значения
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 и stderr отражают состояние конца канала со
стороны родителя. Если поток не соединен каналом, поле отсутствует
(nil). Если соединен, состояние может быть popen.stream.OPEN
(== 'open') или popen.stream.CLOSED (== 'closed'). Состояние
можно изменить с 'open' на 'closed' вызовом
popen_handle:shutdown({std... = true}).
Пример 1:
(в консоли Tarantool)
tarantool> require('popen').new({'/usr/bin/touch', '/tmp/foo'})---- command: /usr/bin/touch /tmp/fooopts:close_fds: truegroup_signal: falsekeep_child: falserestore_signals: truesetsid: falseshell: falsestderr: inheritstdin: inheritstdout: inheritpid: 9499status:state: alive...
Пример 2:
(в консоли Tarantool)
tarantool> require('popen').shell('grep foo', 'wrR')---- command: sh -c 'grep foo'opts:close_fds: truegroup_signal: truekeep_child: falserestore_signals: truesetsid: trueshell: truestderr: pipestdin: pipestdout: pipepid: 10497status:state: alivestderr: openstdin: openstdout: open...
Ожидание, пока дочерний процесс не завершится или не получит сигнал.
Параметры:
ph(handle) — дескриптор дочернего процесса, созданного с помощью popen.new() или popen.shell()timeout(number) — начиная с версии 3.2.0. Параметр определяет период в секундах, в течение которого метод ожидает завершения. Значение по умолчанию — "infinity".
Возвращает
(при успешном выполнении) отформатированный результат
(при неудачном выполнении) nil, err
Тип возвращаемого значения
res
Возможные ошибки:
IllegalParams: некорректный параметр дескриптораIllegalParams: вызов на закрытом дескриптореFiberIsCancelled: отменено внешним кодом
Возможные причины ошибок при возвращении nil, err:
TimedOut: начиная с версии 3.2.0. Ошибка означает, что метод не достиг положительного результата, но превысил заданный timeout.ChannelIsClosed: начиная с версии 3.2.0. Ошибка возвращается, когда целевой дескриптор popen закрывается во время выполнения операции :wait().
Отформатированный результат представляет собой таблицу состояний
процесса (аналогично компоненту status таблицы, возвращаемой через
popen_handle:info()).
Пример использования параметра timeout:
local ph = popen.new(<...>)local res, err = ph:wait({timeout = 1})if res == nil then-- Timeout is reached.assert(err.type == 'TimedOut')<...>end
Закрытие дескриптора popen.
Параметры:
ph(handle) — дескриптор дочернего процесса, созданного с помощью popen.new() или popen.shell()
Возвращает
(при успешном выполнении) true
(при неудачном выполнении) nil, err
Тип возвращаемого значения
res
Возможные ошибки:
- 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.pidpopen_handle.commandpopen_handle.optspopen_handle.statuspopen_handle.stdinpopen_handle.stdoutpopen_handle.stderr
За более подробной информацией обратитесь к popen_handle:info().
- popen.opts- INHERIT (== 'inherit')- DEVNULL (== 'devnull')- CLOSE (== 'close')- PIPE (== 'pipe')- popen.signal- SIGKILL (== 9)- SIGTERM (== 15)- ...- popen.state- ALIVE (== 'alive')- EXITED (== 'exited')- SIGNALED (== 'signaled')- popen.stream- OPEN (== 'open')- CLOSED (== 'closed')