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#
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#
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#
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#
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#
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) | — |