命令执行

执行外部命令和 shell 脚本,完全控制 I/O 流。

关于执行器配置,请参见 Executor

加载

local exec = require("exec")

获取 Executor

通过 ID 获取进程执行器资源:

local executor, err = exec.get("app:exec")
if err then
    return nil, err
end

-- 使用执行器
local proc = executor:exec("ls -la")
-- ...

-- 完成后释放
executor:release()
参数 类型 描述
id string 资源 ID

返回值: 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 的引号规则拆分为可执行文件和字面参数:单引号和双引号把内容归为一个词,反斜杠转义其后的字符。这里没有 shell,因此不会进行变量展开、通配符匹配、管道或重定向。未闭合的引号会返回 errors.INVALID

-- 一个包含空格的参数,按字面传递
local proc = executor:exec("grep 'hello world' notes.txt")

-- $HOME 按 $HOME 这四个字符传递,不会展开
local proc = executor:exec("echo $HOME")

要使用 shell 特性,请显式调用 shell:

local proc = executor:exec("/bin/sh -c 'ls *.log | wc -l'")

PTY 选项

分配 PTY 会给子进程一个真正的终端:行编辑、作业控制和全屏程序都能像在 shell 中一样工作。

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 的进程会把子进程的输出合并为单一终端流;请用 resizeattach_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 = "Build failed with exit code: " .. 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()

每次调用写入给定的字节后返回。当子进程需要看到 EOF 时,调用 close_stdin()

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。因信号退出时,code128 + signal。与 wait() 不同,done() 不会使句柄失效,因此流、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,当 force 为真时发送 SIGKILL,然后在后台回收它,因此该调用不会阻塞。在宽限期后仍在运行的子进程会被终止,以保证回收总能完成。未启动的句柄只是被作废,重复关闭不算错误。启用 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 接受 TTY 中描述的按键、鼠标、resize、focus 和 paste 记录。在子进程结束之后发送会返回错误。

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 Executor ID 获取执行器资源
exec.run Command 执行特定命令

exec.run 针对原始命令字符串求值,并把请求的选项作为元数据:

类型 描述
work_dir string 请求的工作目录,未设置时为空
env_names string[] 传入的环境变量名称,已排序;不暴露其取值
pty.requested boolean 是否请求了 PTY
pty.width number 解析后的 PTY 列数,请求 PTY 时存在
pty.height number 解析后的 PTY 行数,请求 PTY 时存在
pty.term string 请求的 TERM 值,请求 PTY 时存在

因此策略可以允许普通命令,同时限制那些要求终端或特定工作目录的命令。

错误

条件 类型 可重试
无效的 ID errors.INVALID
权限被拒绝 errors.INVALID
进程已关闭 errors.INVALID
进程未启动 errors.INVALID
已经启动 errors.INVALID
命令中存在未闭合的引号 errors.INVALID
进程上没有 PTY errors.INVALID
终端端口不可用 errors.UNAVAILABLE

参见 错误处理 了解如何处理错误。

另请参阅

  • Executor — 执行器配置
  • TTY — 终端事件、surface 和 viewport
  • 终端 UI — 在 viewport 中托管 PTY 子进程的 shell