module process

module lib/process.mu

import "process"

process is a collection of process-related functions.

Spawning needs a native build. run, spawn and wait all go through fork(2), and the interpreter refuses it: a forked child of the Go host inherits one thread and whatever runtime locks the others were holding, so it deadlocks before it can exec (#125). Compile with mu -B and they work. pid() is a plain getpid and works everywhere; kill() uses the kill syscall where the platform offers one and otherwise shells out through spawn, so on darwin it needs a native build too.

Imports

Functions

kill#

fn kill(args...)

kill sends sig (default SIGKILL) to the process.

Source lib/process.mu:389
fn kill(args...) {
    if len(args) < 1 || len(args) > 2 {
        return error("process.kill: wrong number of arguments. got=" + inspect(len(args)) + ", want=1 or 2")
    }
    proc := args[0]
    target := nil
    if typing.is_int(proc) {
        target = proc
    } else if type(proc) == "MAP" && has(proc, "pid") {
        target = proc["pid"]
    } else {
        return error("process.kill expects proc map or pid, got " + type(proc))
    }
    if !typing.is_int(target) {
        return error("process.kill expects integer pid, got " + type(target))
    }
    sig := 9
    if len(args) == 2 {
        sig = args[1]
        if !typing.is_int(sig) {
            return error("process.kill expects integer signal, got " + type(sig))
        }
    }
    platform_key := platform()
    if platform_key == "darwin/amd64" || platform_key == "darwin/arm64" {
        return _kill_via_command(target, sig)
    }
    result := syscall("kill", target, sig)
    if is_error(result) {
        return _kill_via_command(target, sig)
    }
    return nil
}

pid#

fn pid()

pid returns the process ID of the running program -- the one that spawn and kill talk about other processes by.

Source lib/process.mu:384
fn pid() {
    return syscall("getpid")
}

run#

fn run(args...)

run executes a command with optional args/opts, returning its status and output. args is an optional list of strings (default: []). opts is an optional map with keys cwd, env, and stdin. cwd and stdin are strings, whilst env is a map of strings (merged over current environment). The result is an object that contains the exit status, standard output, and standard error of the command.

Source lib/process.mu:74
fn run(args...) {
    stdout_pipe := _make_pipe()
    if is_error(stdout_pipe) {
        return stdout_pipe
    }
    stderr_pipe := _make_pipe()
    if is_error(stderr_pipe) {
        _close_pipe(stdout_pipe)
        return stderr_pipe
    }
    stdin_pipe := _make_pipe()
    if is_error(stdin_pipe) {
        _close_pipe(stdout_pipe)
        _close_pipe(stderr_pipe)
        return stdin_pipe
    }
    if len(args) < 1 || len(args) > 3 {
        _close_pipe(stdout_pipe)
        _close_pipe(stderr_pipe)
        _close_pipe(stdin_pipe)
        return error("process.run: wrong number of arguments. got=" + inspect(len(args)) + ", want=1 to 3")
    }
    cmd := args[0]
    if !typing.is_str(cmd) {
        _close_pipe(stdout_pipe)
        _close_pipe(stderr_pipe)
        _close_pipe(stdin_pipe)
        return error("process.run expects string cmd, got " + type(cmd))
    }
    argv := []
    opts := {}
    if len(args) >= 2 {
        second := args[1]
        if second == nil {
            argv = []
        } else if typing.is_list(second) {
            argv = second
        } else if type(second) == "MAP" && len(args) == 2 {
            opts = second
        } else {
            _close_pipe(stdout_pipe)
            _close_pipe(stderr_pipe)
            _close_pipe(stdin_pipe)
            return error("process.run expects list args or map opts, got " + type(second))
        }
    }
    if len(args) == 3 {
        opts = args[2]
        if opts == nil {
            opts = {}
        }
        if type(opts) != "MAP" {
            _close_pipe(stdout_pipe)
            _close_pipe(stderr_pipe)
            _close_pipe(stdin_pipe)
            return error("process.run expects map opts, got " + type(opts))
        }
    }
    i := 0
    while i < len(argv) {
        if type(argv[i]) != "STRING" {
            _close_pipe(stdout_pipe)
            _close_pipe(stderr_pipe)
            _close_pipe(stdin_pipe)
            return error("process.run expects string args, got " + type(argv[i]))
        }
        i = i + 1
    }
    stdin_text := _opts_get(opts, "stdin")
    if stdin_text != nil && type(stdin_text) != "STRING" {
        _close_pipe(stdout_pipe)
        _close_pipe(stderr_pipe)
        _close_pipe(stdin_pipe)
        return error("process.run opts.stdin must be string, got " + type(stdin_text))
    }
    if stdin_text == nil {
        _close_pipe(stdin_pipe)
        stdin_pipe = nil
    }
    close_fds := [_pipe_write_fd(stdin_pipe), _pipe_read_fd(stdout_pipe), _pipe_read_fd(stderr_pipe)]
    proc := _spawn_exec(cmd, argv, opts, _pipe_read_fd(stdin_pipe), _pipe_write_fd(stdout_pipe), _pipe_write_fd(stderr_pipe), close_fds)
    if is_error(proc) {
        _close_pipe(stdout_pipe)
        _close_pipe(stderr_pipe)
        _close_pipe(stdin_pipe)
        return proc
    }
    pid := proc["pid"]
    if !typing.is_int(pid) {
        _close_pipe(stdout_pipe)
        _close_pipe(stderr_pipe)
        _close_pipe(stdin_pipe)
        return error("process.run expected integer pid from spawn")
    }
    _close_fd(_pipe_write_fd(stdout_pipe))
    _close_fd(_pipe_write_fd(stderr_pipe))
    _close_fd(_pipe_read_fd(stdin_pipe))
    if stdin_text != nil {
        wrote := io.write(_pipe_write_fd(stdin_pipe), stdin_text)
        _close_fd(_pipe_write_fd(stdin_pipe))
        if is_error(wrote) {
            _close_fd(_pipe_read_fd(stdout_pipe))
            _close_fd(_pipe_read_fd(stderr_pipe))
            return wrote
        }
    }
    // Drain BOTH pipes to end of file before reaping the child. Waiting first
    // deadlocked on any child that produced more than one pipe buffer: the
    // child blocked in write(2) on the full pipe and never exited, and this
    // side blocked in wait(2) and never read. The reads must also be
    // concurrent with each other, or a child interleaving both streams can
    // fill whichever pipe is not currently being read.
    outs := io.read_all_each([_pipe_read_fd(stdout_pipe), _pipe_read_fd(stderr_pipe)])
    stdout := outs[0]
    stderr := outs[1]
    _close_fd(_pipe_read_fd(stdout_pipe))
    _close_fd(_pipe_read_fd(stderr_pipe))
    if is_error(stdout) {
        return stdout
    }
    if is_error(stderr) {
        return stderr
    }
    waited := nil
    if platform() == "darwin/arm64" {
        waited = wait(-1)
    } else {
        waited = wait(pid)
    }
    if is_error(waited) {
        return waited
    }
    return _result(waited["status"], stdout, stderr)
}

spawn#

fn spawn(args...)

spawn starts a command with optional args/opts, returning a proc map.

Source lib/process.mu:210
fn spawn(args...) {
    parsed := _parse_spawn_args(args)
    if is_error(parsed) {
        return parsed
    }
    return _spawn_exec(parsed["cmd"], parsed["args"], parsed["opts"], nil, nil, nil, nil)
}

wait#

fn wait(args...)

wait blocks until the process exits, returning {status: int}.

Source lib/process.mu:219
fn wait(args...) {
    if len(args) != 1 {
        return error("process.wait: wrong number of arguments. got=" + inspect(len(args)) + ", want=1")
    }
    proc := args[0]
    pid := nil
    if typing.is_int(proc) {
        pid = proc
    } else if typing.is_map(proc) {
        if has(proc, "pid") {
            pid = proc["pid"]
        } else {
            return error("process.wait expects proc map with pid")
        }
        if has(proc, "waited") && proc["waited"] == true {
            return error("process.wait called twice on proc")
        }
    } else {
        return error("process.wait expects proc map or pid, got " + type(proc))
    }
    if !typing.is_int(pid) {
        return error("process.wait expects integer pid, got " + type(pid))
    }
    platform_key := platform()
    status_buf := buf(8)
    waited := nil
    if platform_key == "darwin/arm64" {
        waited = _wait4_blocking(pid, status_buf)
    } else {
        waited = syscall("wait4", pid, status_buf, 0, 0)
    }
    if type(waited) == "ERROR" && platform_key == "darwin/arm64" && pid != -1 {
        waited = _wait4_blocking(-1, status_buf)
    }
    if is_error(waited) {
        return waited
    }
    status := _decode_uint32_le(status_buf)
    code := _exit_code_from_status(status)
    if typing.is_map(proc) {
        proc["waited"] = true
    }
    return {"status": code}
}

Internal helpers

Underscore-prefixed names are implementation detail. They are listed so the module's source reads without surprises, not as API — they may change at any time.

_append_buffer(dest, src)
_append_bytes(buffer, text)
_append_uint64_le(buffer, value)
_asm_generic_linux(platform_key)_asm_generic_linux reports whether this target has only the asm-generic syscall ABI: no fork, dup2, pipe or unlink, just clone, dup3, pipe2 and the *at forms.
_base_env_list()
_base_env_map()
_build_argv_list(cmd, argv)
_build_env_list(opts)
_build_exec_block(argv, env_list, platform_key)
_build_exec_paths(cmd, opts, env_map)
_c_string(value)
_cached_env_list
_cached_env_map
_clone_map(source)
_close_fd(fd)
_close_pipe(pipe_map)
_decode_uint32_le(data)
_decode_uint32_le_at(data, offset)
_decode_uint64_le_at(data, offset)
_dup_fd(source_fd, target_fd)
_env_entry_key(entry)
_env_map_to_list(env_map)
_exit_code_from_status(status)
_fork_process(platform_key)
_kill_via_command(target, sig)
_make_pipe()
_map_anon_flag(platform_key)
_map_fixed_flag(platform_key)
_mmap_anonymous(size, platform_key)
_opts_get(opts, key)
_parse_spawn_args(args)
_pipe_read_fd(pipe_map)
_pipe_write_fd(pipe_map)
_platform_supported(platform_key)
_read_memory(addr, size)
_read_pipe_all(fd)
_result(status, stdout, stderr)Result is an object that contains the exit status, standard output/error of an executed command.
_spawn_exec(cmd, argv, opts, stdin_fd, stdout_fd, stderr_fd, close_fds)
_temp_counter
_temp_path(prefix)
_unlink_path(path)
_wait4_blocking(pid, status_buf)
_write_bytes_at(buffer, offset, text)
_write_memory(addr, data)
_write_uint64_le_at(buffer, offset, value)