// Native-only FFI declarations for process management.
// This file is only compiled for native/llvm backends.
///|
/// Convert a MoonBit String to null-terminated UTF-8 Bytes for C FFI.
fn str_to_cstr(s : String) -> Bytes {
let arr : Array[Byte] = []
for c in s {
let cp = c.to_int()
if cp < 0x80 {
arr.push(cp.to_byte())
} else if cp < 0x800 {
arr.push(((cp >> 6) | 0xC0).to_byte())
arr.push(((cp & 0x3F) | 0x80).to_byte())
} else if cp < 0x10000 {
arr.push(((cp >> 12) | 0xE0).to_byte())
arr.push((((cp >> 6) & 0x3F) | 0x80).to_byte())
arr.push(((cp & 0x3F) | 0x80).to_byte())
} else {
arr.push(((cp >> 18) | 0xF0).to_byte())
arr.push((((cp >> 12) & 0x3F) | 0x80).to_byte())
arr.push((((cp >> 6) & 0x3F) | 0x80).to_byte())
arr.push(((cp & 0x3F) | 0x80).to_byte())
}
}
arr.push(b'\x00')
Bytes::from_array(arr)
}
///|
#borrow(cmd, cwd, stdout_path, stderr_path)
extern "C" fn ffi_spawn(
cmd : Bytes,
cwd : Bytes,
stdout_path : Bytes,
stderr_path : Bytes,
) -> Int = "redstart_spawn"
///|
extern "C" fn ffi_kill(pid : Int, sig : Int) -> Int = "redstart_kill"
///|
extern "C" fn ffi_kill_process_group(pgid : Int, sig : Int) -> Int = "redstart_kill_process_group"
///|
extern "C" fn ffi_is_alive(pid : Int) -> Int = "redstart_is_alive"
///|
extern "C" fn ffi_is_process_group_alive(pgid : Int) -> Int = "redstart_is_process_group_alive"
///|
#borrow(output_path)
extern "C" fn ffi_write_process_group_snapshot(
pgid : Int,
output_path : Bytes,
) -> Int = "redstart_write_process_group_snapshot"
///|
extern "C" fn ffi_sleep_ms(ms : Int) -> Unit = "redstart_sleep_ms"
///|
extern "C" fn ffi_now() -> Int64 = "redstart_now"
///|
/// Get the current Unix timestamp in seconds.
pub fn now() -> Int64 {
ffi_now()
}
///|
/// Sleep for ms milliseconds.
pub fn sleep_ms(ms : Int) -> Unit {
ffi_sleep_ms(ms)
}
///|
/// Spawn a shell command as a background process.
/// Returns the PID on success, or None on failure.
pub fn spawn_process(
command : String,
cwd : String,
stdout_path : String,
stderr_path : String,
) -> Int? {
let pid = ffi_spawn(
str_to_cstr(command),
str_to_cstr(cwd),
str_to_cstr(stdout_path),
str_to_cstr(stderr_path),
)
if pid <= 0 {
None
} else {
Some(pid)
}
}
///|
/// Check if a process with the given PID is alive.
pub fn is_process_alive(pid : Int) -> Bool {
ffi_is_alive(pid) == 1
}
///|
/// Send SIGTERM (15) to a process.
pub fn kill_process(pid : Int) -> Bool {
ffi_kill(pid, 15) == 0
}
///|
fn kill_process_group(pgid : Int) -> Bool {
ffi_kill_process_group(pgid, 15) == 0
}
///|
/// Send SIGKILL (9) to a process.
pub fn force_kill_process(pid : Int) -> Bool {
ffi_kill(pid, 9) == 0
}
///|
fn force_kill_process_group(pgid : Int) -> Bool {
ffi_kill_process_group(pgid, 9) == 0
}
///|
fn is_process_group_alive(pgid : Int) -> Bool {
ffi_is_process_group_alive(pgid) == 1
}
///|
fn parse_positive_int_or(s : String, default : Int) -> Int {
let mut result = 0
let mut seen_digit = false
for c in s {
let n = c.to_int()
if n >= 48 && n <= 57 {
result = result * 10 + n - 48
seen_digit = true
} else {
return default
}
}
if seen_digit {
result
} else {
default
}
}
///|
fn parse_process_group_member(line : String) -> ProcessGroupMember? {
if line == "" {
return None
}
let parts = line.split("\t").map(StringView::to_owned).collect()
if parts.length() < 4 {
return None
}
let pid = parse_positive_int_or(parts[0], -1)
let ppid = parse_positive_int_or(parts[1], -1)
let pgid = parse_positive_int_or(parts[2], -1)
if pid <= 0 || ppid < 0 || pgid <= 0 {
return None
}
let command = parts[3:parts.length()].join("\t")
Some(new_process_group_member(pid, ppid, pgid, command))
}
///|
pub fn list_process_group_members(
proc : String,
pgid : Int,
) -> Array[ProcessGroupMember] raise RedstartError {
if pgid <= 0 {
return []
}
ensure_root_monitor_dir()
ensure_monitor_dir(proc)
let snapshot_path = process_group_snapshot_path(proc)
let rc = ffi_write_process_group_snapshot(pgid, str_to_cstr(snapshot_path))
if rc != 0 {
raise RedstartError(
"failed to inspect process group for '\{proc}' (pgid: \{pgid.to_string()})",
)
}
let content = @fs.read_file_to_string(snapshot_path) catch {
e =>
raise RedstartError(
"failed to read process-group snapshot: \{@debug.to_repr(e)}",
)
}
let lines = content.split("\n").map(StringView::to_owned).collect()
let members : Array[ProcessGroupMember] = []
for line in lines {
match parse_process_group_member(line) {
None => ()
Some(proc_info) => members.push(proc_info)
}
}
members
}
///|
pub fn inspect_process(entry : ProcessEntry) -> ProcessInspectSnapshot {
let members = match entry.pid {
Some(pid) if is_process_alive(pid) =>
list_process_group_members(entry.proc_name, pid) catch {
_ => []
}
_ => []
}
let child_processes : Array[ProcessGroupMember] = []
for proc_info in members {
if proc_info.pid != proc_info.pgid {
child_processes.push(proc_info)
}
}
{ process: entry, process_group_members: members, child_processes }
}
///|
/// Start a process with retry logic (up to max_retries attempts).
/// Returns (pid, timestamp) on success, raises RedstartError on failure.
pub fn start_with_retry(
entry : ProcessEntry,
max_retries : Int,
) -> (Int, String) raise RedstartError {
let cwd = match entry.cwd {
None => ""
Some(d) => d
}
let stdout = stdout_log_path(entry.proc_name)
let stderr = stderr_log_path(entry.proc_name)
ensure_root_monitor_dir()
ensure_monitor_dir(entry.proc_name)
let mut attempt = 0
while attempt < max_retries {
attempt = attempt + 1
match spawn_process(entry.command, cwd, stdout, stderr) {
None => if attempt < max_retries { sleep_ms(1000) }
Some(pid) => {
// Wait 500ms and check if still alive
sleep_ms(500)
if is_process_alive(pid) {
return (pid, now().to_string())
} else if attempt < max_retries {
sleep_ms(500)
}
}
}
}
raise RedstartError(
"failed to start process '\{entry.proc_name}' after \{max_retries} attempts",
)
}
///|
/// Stop a process group gracefully (SIGTERM, then SIGKILL after timeout).
pub fn stop_gracefully(pid : Int) -> Unit {
if pid <= 0 {
return
}
ignore(kill_process_group(pid))
// Poll up to 5 seconds (10 x 500ms) for the whole process group to exit.
for _ in 0..<10 {
sleep_ms(500)
if !is_process_group_alive(pid) {
return
}
}
// Force kill the whole group if any child is still alive.
if is_process_group_alive(pid) {
ignore(force_kill_process_group(pid))
for _ in 0..<10 {
sleep_ms(100)
if !is_process_group_alive(pid) {
return
}
}
}
}