Выполнение команд
Запуск внешних команд и shell-скриптов с полным контролем над потоками ввода-вывода.
Настройка исполнителей описана в разделе Исполнитель команд.
Подключение
local exec = require("exec")
Получение исполнителя
Получите ресурс исполнителя по его идентификатору:
local executor, err = exec.get("app:exec")
if err then
return nil, err
end
-- Работа с исполнителем
local proc = executor:exec("ls -la")
-- ...
-- Освобождение ресурса
executor:release()
| Параметр | Тип | Описание |
|---|---|---|
id |
string | Идентификатор ресурса |
Возвращает: Executor, error
Создание процесса
Создайте процесс с заданной командой:
-- Простая команда
local proc, err = executor:exec("echo 'Hello, World!'")
-- С рабочей директорией
local proc = executor:exec("npm install", {
work_dir = "/app/project"
})
-- С переменными окружения
local proc = executor:exec("python script.py", {
work_dir = "/scripts",
env = {
PYTHONPATH = "/app/lib",
DEBUG = "true",
API_KEY = api_key
}
})
-- Запуск shell-скрипта
local proc = executor:exec("./deploy.sh production", {
work_dir = "/app/scripts",
env = {
DEPLOY_ENV = "production"
}
})
| Параметр | Тип | Описание |
|---|---|---|
cmd |
string | Исполняемый файл и литеральные аргументы |
options.work_dir |
string | Рабочая директория |
options.env |
table | Переменные окружения |
options.pty |
table | Выделить псевдотерминал для дочернего процесса |
options.process_group |
boolean | Запустить дочерний процесс в собственной группе процессов, чтобы сигналы также достигали потомков; не поддерживается в Windows |
Возвращает: Process, error
Процесс создан, но не запущен.
Разбор команды
cmd разбивается на исполняемый файл и литеральные аргументы по shell-подобным правилам кавычек: одинарные и двойные кавычки группируют слово, а обратный слэш экранирует следующий символ. Оболочки нет, поэтому не происходит ни подстановки переменных, ни globbing, ни каналов, ни перенаправлений. Незакрытая кавычка возвращает errors.INVALID.
-- Один аргумент с пробелом, передаётся буквально
local proc = executor:exec("grep 'hello world' notes.txt")
-- $HOME передаётся как пять символов $HOME, без подстановки
local proc = executor:exec("echo $HOME")
Чтобы воспользоваться возможностями оболочки, вызовите её явно:
local proc = executor:exec("/bin/sh -c 'ls *.log | wc -l'")
Опции PTY
Выделение PTY даёт дочернему процессу настоящий терминал: редактирование строки, управление задачами и полноэкранные программы работают так же, как в оболочке.
local proc = executor:exec("/bin/bash --noprofile --norc", {
pty = {width = 100, height = 30, term = "xterm-256color"},
})
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
width |
number | 80 | Начальное число колонок PTY, от 1 до 65535 |
height |
number | 24 | Начальное число строк PTY, от 1 до 65535 |
term |
string | нет | Значение TERM дочернего процесса |
Произведение ширины на высоту не может превышать 262 144 ячеек. Процесс с PTY сливает вывод дочернего процесса в единый терминальный поток; управляйте им через resize и attach_terminal, а не через методы каналов stdin/stdout.
start / wait
Запуск процесса и ожидание завершения:
local proc = executor:exec("./build.sh")
local ok, err = proc:start()
if err then
return nil, err
end
local exit_code, err = proc:wait()
if err then
return nil, err
end
if exit_code ~= 0 then
return nil, errors.new({ kind = errors.INTERNAL, message = "Сборка завершилась с кодом: " .. exit_code })
end
stdout_stream / stderr_stream
Получение потоков для чтения вывода процесса:
local proc = executor:exec("./process-data.sh")
local stdout = proc:stdout_stream()
local stderr = proc:stderr_stream()
proc:start()
-- Чтение всего stdout
local output = {}
while true do
local chunk = stdout:read(4096)
if not chunk then break end
table.insert(output, chunk)
end
local result = table.concat(output)
-- Проверка ошибок
local err_output = {}
while true do
local chunk = stderr:read(4096)
if not chunk then break end
table.insert(err_output, chunk)
end
local exit_code = proc:wait()
stdout:close()
stderr:close()
if exit_code ~= 0 then
return nil, errors.new({ kind = errors.INTERNAL, message = table.concat(err_output) })
end
return result
write_stdin
Запись данных в stdin процесса:
local proc = executor:exec("head -n 3")
local stdout = proc:stdout_stream()
proc:start()
proc:write_stdin("banana\napple\ncherry\n")
local lines = stdout:read()
proc:wait()
stdout:close()
Каждый вызов записывает переданные байты и возвращает управление. Вызовите close_stdin(), когда дочерний процесс должен увидеть EOF:
local proc = assert(executor:exec("sort"))
local stdout = assert(proc:stdout_stream())
assert(proc:start())
assert(proc:write_stdin("banana\napple\n"))
assert(proc:close_stdin())
local sorted = assert(stdout:read())
close_stdin() идемпотентен. Последующие записи завершаются ошибкой, поскольку сторона ввода закрыта. Процессы с PTY не предоставляют эту операцию канала.
done
Используйте done(), чтобы наблюдать за завершением, не передавая владение хэндлом процесса:
local proc = assert(executor:exec("./worker"))
assert(proc:start())
local exits = assert(proc:done())
local status, open = exits:receive()
if open then
print(status.code, status.signal, status.error)
end
Возвращаемый канал передаёт одну запись о завершении и затем закрывается. Повторные вызовы возвращают тот же канал. Запись содержит code, необязательный signal и error только когда среда выполнения не смогла наблюдать за завершением. При завершении по сигналу в качестве кода используется 128 + signal. В отличие от wait(), done() оставляет хэндл пригодным для использования, поэтому streams, signal() и close() остаются доступны. wait() после передачи записи возвращает сохранённый код.
signal / close
Отправка сигналов или освобождение процесса:
local proc = executor:exec("./long-running-server.sh")
proc:start()
-- ... позже, нужно остановить ...
-- Отправить SIGTERM и освободить хэндл
proc:close()
-- Отправить SIGKILL и освободить хэндл
proc:close(true)
-- Или отправить конкретный сигнал и сохранить хэндл
local SIGINT = 2
proc:signal(SIGINT)
close(force?) отправляет запущенному дочернему процессу SIGTERM или SIGKILL, если force истинно, а затем пожинает его в фоне, так что вызов не блокирует. Дочерний процесс, всё ещё работающий по истечении отсрочки, убивается, чтобы пожинание всегда завершалось. Незапущенный хэндл просто инвалидируется, а повторное закрытие ошибкой не является. Если включён process_group, сигналы направляются группе и по-прежнему достигают потомков после завершения лидера.
Потоки, полученные до пожинания, остаются доступными для чтения, пока не закроется их последний писатель, включая потомка, унаследовавшего канал. После close() методы процесса сообщают process closed; используйте done(), когда важен результат завершения и хэндл должен оставаться пригодным для использования.
resize
Изменяет размер PTY у процесса с PTY. Процесс на каналах возвращает ошибку.
local ok, err = proc:resize(120, 40)
| Параметр | Тип | Описание |
|---|---|---|
width |
number | Колонки, от 1 до 65535 |
height |
number | Строки, от 1 до 65535 |
Возвращает: boolean, error
Используйте это, чтобы задать начальную геометрию до передачи процесса терминальной сессии. Как только сессия владеет процессом, отправляйте ей вместо этого событие resize.
attach_terminal
Присоединяет незапущенный процесс с PTY к терминалу вызывающего процесса и возвращает TerminalSession.
local exec = require("exec")
local tty = require("tty")
local executor = assert(exec.get("app:exec"))
local proc = assert(executor:exec("/bin/bash --noprofile --norc", {
pty = {term = "xterm-256color"},
}))
local session = assert(proc:attach_terminal())
Возвращает: TerminalSession, error
Вызов потребляет процесс: сессия становится единственным владельцем его жизненного цикла, а исходный хэндл больше использовать нельзя. Сессия открывает surface на текущем терминальном порту и владеет эмуляцией PTY, кодированием ввода, изменением размера, мягким и принудительным завершением и пожинанием. Ей нужен терминальный порт — процесс на терминальном хосте или процесс, порождённый с грантом viewport, — и она завершается неудачей, если у порта нет контроллера ввода или уже открыт surface.
TerminalSession
| Метод | Возвращает | Описание |
|---|---|---|
send(event) |
boolean, error |
Переслать одно каноническое TTY-событие дочернему процессу |
done() |
channel | Канал, срабатывающий один раз по завершении дочернего процесса |
status() |
string, error |
"running" или "done", с ошибкой сбоя, если он произошёл |
close() |
boolean, error |
Запросить завершение работающего дочернего процесса |
send принимает записи key, mouse, resize, focus и paste, описанные в TTY. Отправка после завершения дочернего процесса возвращает ошибку.
local channel = require("channel")
local events = assert(tty.events())
assert(tty.start())
local done = session:done()
while true do
local selected = channel.select({
events:case_receive(),
done:case_receive(),
})
if not selected.ok or selected.channel == done then break end
if selected.value.type == "close" then break end
assert(session:send(selected.value))
end
assert(session:close())
Разрешения
Операции exec подчиняются политикам безопасности.
| Действие | Ресурс | Описание |
|---|---|---|
exec.get |
ID исполнителя | Получение ресурса исполнителя |
exec.run |
Команда | Выполнение конкретной команды |
exec.run вычисляется по сырой строке команды, а запрошенные опции передаются как метаданные:
| Ключ | Тип | Описание |
|---|---|---|
work_dir |
string | Запрошенная рабочая директория, пустая строка если не задана |
env_names |
string[] | Имена переданных переменных окружения, отсортированные; значения не раскрываются |
pty.requested |
boolean | Запрашивался ли PTY |
pty.width |
number | Итоговое число колонок PTY, присутствует при запросе |
pty.height |
number | Итоговое число строк PTY, присутствует при запросе |
pty.term |
string | Запрошенное значение TERM, присутствует при запросе |
Поэтому политика может разрешать обычные команды, ограничивая те, что запрашивают терминал или конкретную рабочую директорию.
Ошибки
| Ситуация | Тип | Повтор |
|---|---|---|
| Неверный ID | errors.INVALID |
нет |
| Доступ запрещён | errors.INVALID |
нет |
| Процесс закрыт | errors.INVALID |
нет |
| Процесс не запущен | errors.INVALID |
нет |
| Процесс уже запущен | errors.INVALID |
нет |
| Незакрытая кавычка в команде | errors.INVALID |
нет |
| У процесса нет PTY | errors.INVALID |
нет |
| Терминальный порт недоступен | errors.UNAVAILABLE |
нет |
Подробнее см. Обработка ошибок.
См. также
- Executor — конфигурация исполнителя
- TTY — терминальные события, surface и viewport
- Terminal UI — оболочка, размещающая дочерний PTY-процесс в viewport