명령 실행
I/O 스트림에 대한 완전한 제어와 함께 외부 명령과 셸 스크립트를 실행합니다.
실행기 설정은 실행기를 참조하세요.
로딩
local exec = require("exec")
실행기 획득
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
}
})
-- 셸 스크립트 실행
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는 셸과 유사한 인용 규칙으로 실행 파일과 리터럴 인자로 분리됩니다: 작은따옴표와 큰따옴표는 하나의 단어를 묶고, 백슬래시는 다음 문자를 이스케이프합니다. 셸이 없으므로 변수 확장, 글로빙, 파이프, 리다이렉션은 일어나지 않습니다. 닫히지 않은 인용부호는 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 기반 프로세스는 자식의 출력을 단일 터미널 스트림으로 병합합니다; stdin/stdout 파이프 메서드 대신 resize와 attach_terminal로 제어하세요.
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가 포함됩니다. 시그널로 종료되면 코드는 128 + 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가 true이면 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
이 호출은 프로세스를 소비합니다: 세션이 프로세스의 유일한 수명 주기 소유자가 되고 원래 핸들은 더 이상 사용할 수 없습니다. 세션은 현재 터미널 포트에 서피스를 열고 PTY 에뮬레이션, 입력 인코딩, 크기 조정, 정상 및 강제 종료, 회수를 소유합니다. 터미널 포트가 필요하며 — 터미널 호스트 프로세스, 또는 뷰포트 grant로 스폰된 프로세스 — 포트에 입력 컨트롤러가 없거나 이미 열린 서피스가 있으면 실패합니다.
TerminalSession
| 메서드 | 반환 | 설명 |
|---|---|---|
send(event) |
boolean, error |
정규 TTY 이벤트 하나를 자식에 전달 |
done() |
channel | 자식이 종료되면 한 번 발화하는 채널 |
status() |
string, error |
"running" 또는 "done", 실패 시 실패 에러 포함 |
close() |
boolean, error |
실행 중인 자식의 종료 요청 |
send는 TTY에 설명된 key, mouse, 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.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 |
아니오 |
에러 처리는 에러 처리를 참조하세요.