///|
priv struct ProcessResult {
exit_code : Int
stdout : String
stderr : String
}
///|
#borrow(argv, env, cwd, stdout_path, stderr_path, exit_code_buf)
extern "C" fn process_exec_native(
argv : FixedArray[Bytes],
argc : Int,
env : FixedArray[Bytes],
envc : Int,
cwd : Bytes,
stdout_path : Bytes,
stderr_path : Bytes,
timeout_ms : Int,
exit_code_buf : Bytes,
) -> Int = "moui_cli_process_exec"
///|
fn read_int32_le_from_bytes(buf : Bytes, offset : Int) -> Int {
let b0 = buf[offset].to_int()
let b1 = buf[offset + 1].to_int()
let b2 = buf[offset + 2].to_int()
let b3 = buf[offset + 3].to_int()
b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
}
///|
fn process_tmp_dir() -> String {
@env.get_env_var("TMPDIR").unwrap_or_else(() => {
@env.get_env_var("TEMP").unwrap_or_else(() => {
@env.get_env_var("TMP").unwrap_or(".")
})
})
}
///|
fn process_capture_path(prefix : String) -> String {
join_path(process_tmp_dir(), prefix + "-" + @env.now().to_string() + ".log")
}
///|
fn process_nul_terminated_or_empty(value : String) -> Bytes {
if value == "" {
b"\x00"
} else {
nul_terminated(value)
}
}
///|
fn process_argv_to_bytes(argv : Array[String]) -> FixedArray[Bytes] {
let result = FixedArray::make(argv.length(), b"")
for index, item in argv {
result[index] = nul_terminated(item)
}
result
}
///|
fn process_env_to_bytes(env : Array[(String, String)]) -> FixedArray[Bytes] {
let result = FixedArray::make(env.length(), b"")
for index, item in env {
result[index] = nul_terminated(item.0 + "=" + item.1)
}
result
}
///|
fn process_exec_argv(
argv : Array[String],
cwd? : String = "",
env? : Array[(String, String)] = [],
timeout_ms? : Int = 0,
capture? : Bool = true,
) -> Result[ProcessResult, String] {
if argv.is_empty() {
return Err("process_exec_argv: argv must not be empty")
}
let argv_bytes = process_argv_to_bytes(argv)
let env_bytes = process_env_to_bytes(env)
let cwd_bytes = process_nul_terminated_or_empty(cwd)
let stdout_path = if capture {
process_capture_path("moui-process-stdout")
} else {
""
}
let stderr_path = if capture {
process_capture_path("moui-process-stderr")
} else {
""
}
let stdout_bytes = process_nul_terminated_or_empty(stdout_path)
let stderr_bytes = process_nul_terminated_or_empty(stderr_path)
if capture {
remove_tree(stdout_path)
remove_tree(stderr_path)
}
let exit_code_buf = Bytes::make(4, b'\x00')
let status = process_exec_native(
argv_bytes,
argv_bytes.length(),
env_bytes,
env_bytes.length(),
cwd_bytes,
stdout_bytes,
stderr_bytes,
timeout_ms,
exit_code_buf,
)
let stdout_content = if capture {
@fs.read_file_to_string(stdout_path) catch {
_ => ""
}
} else {
""
}
let stderr_content = if capture {
@fs.read_file_to_string(stderr_path) catch {
_ => ""
}
} else {
""
}
if capture {
remove_tree(stdout_path)
remove_tree(stderr_path)
}
match status {
0 =>
Ok({
exit_code: 0,
stdout: stdout_content.trim().to_owned(),
stderr: stderr_content.trim().to_owned(),
})
1 =>
Ok({
exit_code: read_int32_le_from_bytes(exit_code_buf, 0),
stdout: stdout_content.trim().to_owned(),
stderr: stderr_content.trim().to_owned(),
})
101 => Err("process setup error: " + argv.join(" "))
102 => Err("process spawn error: " + argv.join(" "))
103 =>
Err(
"process timeout after " +
timeout_ms.to_string() +
"ms: " +
argv.join(" "),
)
_ =>
Err(
"process unknown status " + status.to_string() + ": " + argv.join(" "),
)
}
}