subproc

    Synchronous process management for MoonBit: spawn with env/cwd/redirection, waitpid and pidfd waits, kill, procfs sampling and platform probes

    process
    spawn
    pidfd
    procfs
    signal
    Download zip
    Author
    Version
    0.3.0
    License
    MIT
    Last updated
    7 hours ago
    Downloads
    9

    Dependencies

    #chensuiyi/subproc

    Synchronous process management for MoonBit: spawn (argv / env / cwd / log redirection / own process group), waitpid and pidfd waits, process-group signals, procfs sampling (rss / cpu), and platform probes (uname / SO_REUSEPORT / self_exe). Linux native.

    #Install

    moon add chensuiyi/subproc

    #Features

    • pidfd (Linux ≥ 5.3): a kernel handle pinned to one process — pid reuse never fools liveness/exit checks, adopted (non-child) processes supported
    • wait_child_timeout: hard deadline; on expiry SIGKILLs the group and reaps with a bounded retry — a stuck helper can never freeze the caller
    • instance_env_blob: whitelisted env plus reserved vars as a NUL blob
    • procfs sampling: VmRSS and utime+stime per process
    • Platform probes: uname, SO_REUSEPORT bind test, /proc/self/exe, uid
    • Own process group per child (setsid), stdout/stderr redirection to log files

    #Scenarios

    • Process supervisors: spawn, reap, crash budgets, memory sampling
    • Build tools and dev CLIs: run tests / compilers with hard timeouts
    • Adopted-process tracking: exit detection that survives pid reuse

    #API

    FunctionDescription
    spawn(spec)Start a child, return pid
    wait_child(pid, nohang?)Reap via waitpid
    wait_child_timeout(pid, timeout_ms)Deadline-bounded reap with group SIGKILL
    wait_pidfd(fd, nohang?)Reap via waitid(P_PIDFD)
    drain_child(pid)Bounded reap of a just-killed child
    pidfd_open / pidfd_alive / pidfd_exited / wait_pidfdpidfd lifecycle
    kill_group(pgid, sig) / pid_alive(pid)Signals
    read_rss_kb(pid) / read_cpu_ticks(pid) / clk_tck()procfs sampling
    hostname / uname / get_uid / self_exe / probe_reuseportPlatform probes
    instance_env_blob / base_env_blobEnvironment blobs
    exit / eprintln / sleep_ms / closeRuntime helpers

    // Spawn with log redirection and own process group
    let pid = @subproc.spawn({
    bun_path: "bun",
    script: "app.js",
    cwd: "/srv/app",
    out_log: "/var/log/app.out.log",
    err_log: "/var/log/app.err.log",
    env_blob: @subproc.instance_env_blob("app", 0, 3000),
    })

    // Graceful stop with hard deadline
    @subproc.kill_group(pid, @subproc.SIGTERM)
    let reaped = @subproc.wait_child_timeout(pid, 5000)

    // Sample
    let rss = @subproc.read_rss_kb(pid)

    #Author

    #License

    MIT

    #chensuiyi/subproc

    MoonBit 同步进程管理:spawn(参数 / env / cwd / 日志重定向 / 独立进程组)、waitpid 与 pidfd 等待、进程组信号、procfs 采样(rss / cpu)、平台探针(uname / SO_REUSEPORT / self_exe)。Linux native。

    #安装

    moon add chensuiyi/subproc

    #功能

    • pidfd(Linux ≥ 5.3):内核句柄钉住原始进程,pid 复用不干扰存活/退出判定,支持收养非子进程
    • wait_child_timeout:硬超时,到期 SIGKILL 整组并有界回收——卡死的辅助进程永远不会冻结调用方
    • instance_env_blob:白名单环境 + 保留变量的 NUL blob
    • procfs 采样:每进程 VmRSS 与 utime+stime
    • 平台探针:uname、SO_REUSEPORT 绑定测试、/proc/self/exe、uid
    • 每个子进程独立进程组(setsid),stdout/stderr 重定向到日志文件

    #场景

    • 进程监督器:拉起、回收、崩溃预算、内存采样
    • 构建工具与开发 CLI:带硬超时地运行测试 / 编译器
    • 收养进程跟踪:pid 复用下仍可靠的退出判定

    #API

    函数说明
    spawn(spec)启动子进程,返回 pid
    wait_child(pid, nohang?)waitpid 回收
    wait_child_timeout(pid, timeout_ms)截限回收,超时 SIGKILL 整组
    wait_pidfd(fd, nohang?)waitid(P_PIDFD) 回收
    drain_child(pid)有界回收刚杀掉的子进程
    pidfd_open / pidfd_alive / pidfd_exited / wait_pidfdpidfd 生命周期
    kill_group(pgid, sig) / pid_alive(pid)信号
    read_rss_kb(pid) / read_cpu_ticks(pid) / clk_tck()procfs 采样
    hostname / uname / get_uid / self_exe / probe_reuseport平台探针
    instance_env_blob / base_env_blob环境 blob
    exit / eprintln / sleep_ms / close运行辅助

    #用法示例

    // 带日志重定向与独立进程组启动
    let pid = @subproc.spawn({
    bun_path: "bun",
    script: "app.js",
    cwd: "/srv/app",
    out_log: "/var/log/app.out.log",
    err_log: "/var/log/app.err.log",
    env_blob: @subproc.instance_env_blob("app", 0, 3000),
    })

    // 优雅停止(硬截止)
    @subproc.kill_group(pid, @subproc.SIGTERM)
    let reaped = @subproc.wait_child_timeout(pid, 5000)

    // 采样
    let rss = @subproc.read_rss_kb(pid)

    #作者

    陈随易 (@chenbimo)

    #协议

    MIT

    ProcessError

    pub(all) suberror ProcessError {
    Failed(op~ : String, errno~ : Int)
    }

    ProcessError::output

    fn ProcessError::output(self : ProcessError, logger : &Logger) -> Unit

    ProcessError::to_string

    fn ProcessError::to_string(self : ProcessError) -> String

    ChildExit

    pub(all) enum ChildExit {
    Exited(Int)
    Signaled(Int)
    } derive(Eq,
    Debug
    )

    ChildExit::equal

    fn ChildExit::equal(ChildExit, ChildExit) -> Bool

    ChildExit::not_equal

    fn ChildExit::not_equal(x : ChildExit, y : ChildExit) -> Bool

    SpawnSpec

    pub(all) struct SpawnSpec {
    bun_path : String
    script : String
    cwd : String
    out_log : String
    err_log : String
    env_blob : Bytes
    } derive(
    Debug
    )

    SIGKILL

    let SIGKILL : Int

    SIGTERM

    let SIGTERM : Int

    base_env_blob

    fn base_env_blob() -> Bytes

    Whitelisted environment only; used to launch helper processes. BM2_* passes through so notification credentials exported in the caller's shell reach the daemon's ${VAR} expansion.

    clk_tck

    fn clk_tck() -> Int

    Clock ticks per second (sysconf(_SC_CLK_TCK)), the divisor for cpu ticks.

    close

    fn close(fd : Int) -> Unit

    Close a pidfd or lock fd. Owned by the supervision layer.

    disk_free_kb

    fn disk_free_kb(path : String) -> Int64?

    Free space in KiB on the filesystem holding path, None on error.

    drain_child

    fn drain_child(pid : Int) -> Unit

    Reap a just-killed child until it is gone (bounded), so a failed spawn attempt never leaves a zombie behind. Returns immediately when the child is not reapable (already reaped, adopted, or not ours).

    eprintln

    fn eprintln(text : String) -> Unit

    Write a line to stderr (for CLI diagnostics; stdout stays machine-usable).

    exit

    fn exit(code : Int) -> Unit

    Exit the process immediately with the given code.

    get_pid

    fn get_pid() -> Int

    get_uid

    fn get_uid() -> Int

    hostname

    fn hostname() -> String

    Host name from the kernel, or "unknown" when it cannot be read. Used to say which machine a notification came from.

    instance_env_blob

    fn instance_env_blob(app_name : String, instance_id : Int, port : Int, extra? : Array[(String, String)]) -> Bytes

    Minimal instance environment: whitelisted vars from the daemon's own environment plus the reserved app vars, encoded as a NUL-separated "KEY=VALUE" blob with a trailing extra NUL for spawn. Reserved vars are written last, so nothing can override them.

    kill_group

    fn kill_group(pgid : Int, sig : Int) -> Unit raise ProcessError

    load_1m

    fn load_1m() -> Double?

    One-minute load average, None when unavailable.

    local_parts

    fn local_parts(epoch_ms : UInt64) -> (Int, Int, Int, Int, Int, Int)?

    Local-time components of epoch_ms in the system timezone: (year, month 1-12, day, weekday 0=Sunday..6, hour 0-23, minute 0-59). None when the conversion fails.

    pid_alive

    fn pid_alive(pid : Int) -> Bool

    kill(pid, 0): true when the pid exists and we may signal it.

    pidfd_alive

    fn pidfd_alive(fd : Int) -> Bool

    True while the pidfd's process is alive; false once it has exited.

    pidfd_exited

    fn pidfd_exited(fd : Int, pid : Int) -> Bool

    True when the process behind the pidfd has really exited. The pidfd becomes readable on exit, but also on stop events; confirming with pid_alive (ESRCH) rules those out. waitid(P_PIDFD) itself only works for child processes, so this poll-based check is the only reliable exit signal for adopted (reparented) instances.

    pidfd_open

    fn pidfd_open(pid : Int) -> Int raise ProcessError

    Open a pidfd pinned to pid (Linux >= 5.3). The fd always refers to that original process, never to a pid-reused successor.

    platform_error

    fn platform_error() -> String?

    The library is Linux-only. Host binaries refuse to run elsewhere with a clear reason instead of failing later on the first POSIX-specific syscall. Returns None when the platform is supported.

    probe_reuseport

    fn probe_reuseport(port : Int) -> Int

    Ask the kernel whether port accepts a SO_REUSEPORT join: 0 = free (nothing listening yet), 1 = joinable (listener enables reusePort), 2 = held exclusively (reusePort missing), negative = errno.

    read_cpu_ticks

    fn read_cpu_ticks(pid : Int) -> UInt64?

    utime + stime of a process in clock ticks, None when it is gone. A single sample is meaningless on its own; CPU percent comes from two samples taken clk_tck() apart.

    read_rss_kb

    fn read_rss_kb(pid : Int) -> Int?

    VmRSS in kB from /proc//status, None when the process is gone.

    self_exe

    fn self_exe() -> String raise ProcessError

    Absolute path of the running executable (/proc/self/exe).

    sleep_ms

    fn sleep_ms(ms : Int) -> Unit

    spawn

    fn spawn(spec : SpawnSpec) -> Int raise ProcessError

    uname

    fn uname() -> String raise ProcessError

    Kernel name from uname(2), e.g. "Linux".

    wait_child

    fn wait_child(pid : Int, nohang? : Bool) -> (Int, ChildExit)? raise ProcessError

    Reap a child with waitpid. pid = -1 reaps any child. Returns None when nohang is set and no child has exited.

    wait_child_timeout

    fn wait_child_timeout(pid : Int, timeout_ms : Int) -> (Int, ChildExit)?

    Wait for a child with a hard deadline, through a pidfd so the wait cannot be fooled by pid reuse. When the deadline passes the whole process group is SIGKILLed and the reap is retried for a short bounded window, so a helper that hangs (a stuck mount, a wrapper script) can never freeze the single-threaded daemon forever. Returns None when no exit could be observed in time.

    wait_pidfd

    fn wait_pidfd(fd : Int, nohang? : Bool) -> (Int, ChildExit)? raise ProcessError

    Reap a process through a pidfd (waitid P_PIDFD). Works for adopted (non-child) processes too. ECHILD means the exit was already reaped, which is reported as None rather than an error.