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

Запуск внешних команд и 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