#Process Management (@moonbitlang/core/async/process)

    Asynchronous process spawning and management for MoonBit with support for pipes, environment variables, and I/O redirection.

    #Quick Start

    #Running Simple Commands

    Execute commands and collect their output:

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "simple command execution" {
    let result = @process.run("echo", ["Hello, World!"], stdout=Collect)
    inspect(result.exit_code, content="0")
    inspect(result.stdout_text().has_prefix("Hello"), content="true")
    }

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "command with exit code" {
    let result = @process.run("sh", ["-c", "exit 42"])
    inspect(result.exit_code, content="42")
    }

    #Spawn a Process in a Task Group

    Use @process.spawn to start a process without blocking the current task. It registers the process with a task group and returns a Process handle for waiting, cancellation, and parent-side pipe access:

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "spawn command in background" {
    @async.with_task_group(group => {
    let process = @process.spawn(group, "sh", ["-c", "sleep 0.5; exit 42"])
    // do other stuff while the process is running in the background
    @async.sleep(250)
    // use `.wait()` to wait for the child process, or `.try_wait()` to peek its status
    inspect(process.wait(), content="42")
    })
    }

    Child processes follow the rules of structured concurrency:

    • by default, the task group waits for the child process before returning normally, unless no_wait=true is passed to @process.spawn
    • in any case, with_task_group will wait for the child process before exitting. If the task group decides to exit regardless of the child (for example due to fatal error or external cancellation), it will cancel the child process automatically, and wait for the child process to finish its cleanup
    • process.cancel() can be used to manually cancel the child process, and process.wait() waits for actual termination.

    #Collecting Process Output

    run() collects a stream when it is redirected with stdout=Collect or stderr=Collect. Use .stdout_binary()/.stderr_binary() on the returned struct ProcessResult for raw Bytes, or .stdout_text()/.stderr_text() to decode the corresponding stream as UTF-8. An output accessor fails if that stream was not redirected with Collect.

    #Collect Standard Output

    Capture stdout from a process:

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "collect stdout" {
    let result = @process.run("printf", ["test output"], stdout=Collect)
    inspect(result.exit_code, content="0")
    inspect(result.stdout_text(), content="test output")
    }

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "collect stdout with args" {
    let result = @process.run(
    "sh",
    ["-c", "echo 'line 1'; echo 'line 2'"],
    stdout=Collect,
    )
    let output = result.stdout_text()
    inspect(result.exit_code, content="0")
    inspect(output.contains("line 1"), content="true")
    inspect(output.contains("line 2"), content="true")
    }

    #Collect Standard Error

    Capture stderr from a process:

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "collect stderr" {
    let result = @process.run(
    "sh",
    ["-c", "printf 'error message' >&2"],
    stderr=Collect,
    )
    inspect(result.exit_code, content="0")
    inspect(result.stderr_text(), content="error message")
    }

    #Collect Both Stdout and Stderr

    Capture both output streams separately:

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "collect both streams" {
    let result = @process.run(
    "sh",
    ["-c", "printf 'out msg'; printf 'err msg' >&2"],
    stdout=Collect,
    stderr=Collect,
    )
    inspect(result.exit_code, content="0")
    inspect(result.stdout_text(), content="out msg")
    inspect(result.stderr_text(), content="err msg")
    }

    #Collect Merged Output

    Merge stdout and stderr into a single stream:

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "collect merged output" {
    let result = @process.run(
    "sh",
    ["-c", "printf 'ab'; printf 'cd' >&2; printf 'ef'"],
    stdout=Collect,
    stderr=DuplicateStdout,
    )
    inspect(result.exit_code, content="0")
    inspect(result.stdout_text(), content="abcdef")
    }

    #Process I/O with Pipes

    #Reading from Process

    Use stdout=Collect or stderr=Collect with @process.spawn to collect child output from parent. Output is available through .stdout or .stderr on the returned process handle. .stdout and .stderr also has a builtin .text field providing text view for the corresponding stream:

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "read from process with pipe" {
    @async.with_task_group(group => {
    let child = @process.spawn(
    group,
    "echo",
    ["Hello from process"],
    stdout=Collect,
    )
    defer child.stdout.close()
    let output = child.stdout.text.read_all()
    inspect(output.has_prefix("Hello"), content="true")
    })
    }

    #Writing to Process

    Use FeedFromParent with @process.spawn to create feed input to child's stdin from parent. Input can be written through .stdin on the returned process handle. Close .stdin when writing is complete so the child observes end-of-file:

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "write to process with pipe" {
    @async.with_task_group(group => {
    let child = @process.spawn(
    group,
    "cat",
    ["-"],
    stdin=FeedFromParent,
    stdout=Collect,
    )
    group.spawn_bg(() => {
    defer child.stdin.close()
    child.stdin.text.write("test input\n")
    })
    group.spawn_bg(() => {
    defer child.stdout.close()
    let output = child.stdout.text.read_all()
    inspect(output.contains("test input"), content="true")
    })
    })
    }

    Collect and FeedFromParent are the convenient choices for one child created with spawn() or spawn_orphan(). Their child-side pipe endpoints are closed automatically after process creation. The corresponding Process.stdin, Process.stdout, and Process.stderr channels are caller-owned and must be closed when no longer needed.

    #Empty Input and Discarded Output

    Use stdin=Empty when the child should receive no standard input. Reads from the child's standard input immediately reach end-of-file. Use stdout=Discard or stderr=Discard for when child's output should be discarded:

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "empty input and discarded output" {
    let result = @process.run("cat", [], stdin=Empty, stdout=Discard)
    inspect(result.exit_code, content="0")
    }

    #File Redirection

    #Redirect from File

    Use a file as process input:

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "redirect input from file" {
    @async.with_task_group(root => {
    let input_file = "_build/process_test_input.txt"
    @fs.write_text(input_file, "file content")
    root.add_defer(() => @fs.remove(input_file))
    let result = @process.run(
    "cat",
    [],
    stdin=FromFile(input_file),
    stdout=Collect,
    )
    inspect(result.exit_code, content="0")
    inspect(result.stdout_text(), content="file content")
    })
    }

    #Redirect to File

    Write process output to a file:

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "redirect output to file" {
    @async.with_task_group(root => {
    let output_file = "_build/process_test_output.txt"
    root.add_defer(() => @fs.remove(output_file))
    let result = @process.run(
    "echo",
    ["test output"],
    stdout=ToFile(output_file),
    )
    inspect(result.exit_code, content="0")
    let content = @fs.read_text(output_file)
    inspect(content.has_prefix("test output"), content="true")
    })
    }

    #File to File Redirection

    Copy file content using process redirection:

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "file to file redirection" {
    @async.with_task_group(root => {
    let input_file = "_build/process_redirect_in.txt"
    let output_file = "_build/process_redirect_out.txt"
    @fs.write_text(input_file, "redirect test")
    root.add_defer(() => @fs.remove(input_file))
    root.add_defer(() => @fs.remove(output_file))
    let _ = @process.run(
    "cat",
    [],
    stdin=FromFile(input_file),
    stdout=ToFile(output_file),
    )
    inspect(@fs.read_text(output_file), content="redirect test")
    })
    }

    FromFile(path), ToFile(path), and AppendFile(path) are automatically managed, single-process redirections. The file is opened while the process is created and the process API closes its copy of the file handle afterward.

    #Share a File Redirection

    Use redirect_to_file() when multiple children must share the same open output file, or when custom creation modes or permissions are needed. Unlike ToFile(path) and AppendFile(path), it opens the file immediately and returns a caller-owned SharedProcessOutput. Pass it as Shared(output), and close it after it has been passed to the last child or if spawning is abandoned:

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "redirect multiple processes to one file" {
    let output_file = "_build/process_shared_output.txt"
    defer @fs.remove(output_file)
    let output = @process.redirect_to_file(output_file)
    {
    defer output.close()
    @async.with_task_group(group => {
    @process.spawn(group, "printf", ["first"], stdout=Shared(output))
    |> ignore
    @process.spawn(group, "printf", ["second"], stdout=Shared(output))
    |> ignore
    })
    }
    let content = @fs.read_text(output_file)
    inspect(content.contains("first"), content="true")
    inspect(content.contains("second"), content="true")
    }

    redirect_from_file() similarly returns a caller-owned SharedProcessInput. Pass it as Shared(input). Children using it share one open file description and therefore one file offset: data consumed by one child is no longer available to the others. Always close the value after its last use.

    #Environment Variables

    #Setting Environment Variables

    Pass custom environment variables to processes:

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "set environment variable" {
    let result = @process.run(
    "sh",
    ["-c", "echo $MY_VAR"],
    extra_env={ "MY_VAR": "my_value" },
    stdout=Collect,
    )
    inspect(result.exit_code, content="0")
    inspect(result.stdout_text().trim(), content="my_value")
    }

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "multiple environment variables" {
    let result = @process.run(
    "sh",
    ["-c", "echo $VAR1-$VAR2"],
    extra_env={ "VAR1": "first", "VAR2": "second" },
    stdout=Collect,
    )
    inspect(result.exit_code, content="0")
    inspect(result.stdout_text().trim(), content="first-second")
    }

    #Isolated Environment

    Run process without inheriting parent environment:

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "isolated environment" {
    let result = @process.run(
    "env",
    [],
    extra_env={ "ONLY_VAR": "only_value" },
    inherit_env=false,
    stdout=Collect,
    )
    let output = result.stdout_text()
    inspect(result.exit_code, content="0")
    inspect(output.contains("ONLY_VAR=only_value"), content="true")
    // Parent environment variables won't be present
    inspect(output.contains("PATH="), content="false")
    }

    #Working Directory

    #Change Working Directory

    Execute processes in a specific directory:

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "set working directory" {
    let result = @process.run("pwd", [], cwd="async", stdout=Collect)
    inspect(result.exit_code, content="0")
    let text = result.stdout_text().trim()
    // Check that the path ends with "src" (works across OSes)
    inspect(text.has_suffix("async"), content="true")
    }

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "relative path in cwd" {
    let result = @process.run("ls", [], cwd="async", stdout=Collect)
    inspect(result.exit_code, content="0")
    let text = result.stdout_text()
    // Should list contents of src directory
    let has_content = text.length() > 0
    inspect(has_content, content="true")
    }

    #Asynchronous Process Management

    #Spawn and Wait

    run() waits in the current task. spawn() starts the process in a task group and returns a handle immediately:

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "spawn and wait" {
    @async.with_task_group(group => {
    let child = @process.spawn(group, "sleep", ["0.1"])
    inspect(child.pid > 0, content="true")
    inspect(child.try_wait() is None, content="true")
    inspect(child.wait(), content="0")
    })
    }

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "wait for specific exit code" {
    let result = @process.run("sh", ["-c", "exit 5"])
    inspect(result.exit_code, content="5")
    }

    The Process handle exposes:

    • pid, the operating-system process identifier
    • stdin, when stdin=FeedFromParent was requested
    • stdout and stderr, when the corresponding stream uses Collect
    • wait(), try_wait(), and cancel() for lifecycle management

    Only use the pipe fields configured during spawn(), and close each configured field when it is no longer needed.

    #Cancellation

    Cancelling the task running run(), cancelling the task group containing a spawned process, or calling Process.cancel() invokes the process cancellation handler. The default is graceful_cancel(timeout=5000): it sends SIGTERM on POSIX systems or SIGBREAK on Windows, waits up to five seconds, and then kills the process if necessary.

    Use hard_cancel() to kill immediately, or construct graceful_cancel(timeout~, signal?) to select a timeout and, on POSIX systems, a signal. Process.cancel() initiates cancellation; call Process.wait() when the code must wait until the process has actually terminated.

    #Spawn Orphan Process

    Start an orphan process with an unbounded lifetime. The orphan process will not be automatically cancelled, and may live longer than the main process. It is recommended to use @process.spawn or @process.run whenever possible, use @process.spawn_orphan only when it is absolutely necessary:

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "spawn orphan and wait later" {
    let proc = @process.spawn_orphan("sh", ["-c", "sleep 0.1; exit 7"])

    // Unlike `@process.spawn`,
    // the handle returned by `@process.spawn_orphan` must be closed manually
    defer proc.close()

    // Do other work...
    @async.sleep(50)

    // Wait for the process to complete
    let exit_code = proc.wait()
    inspect(exit_code, content="7")
    }

    #Advanced Usage

    #Merge Output Streams

    Use DuplicateStdout to redirect stderr to the same destination as stdout for a child process:

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "merge stdout and stderr" {
    @async.with_task_group(group => {
    let child = @process.spawn(
    group,
    "sh",
    ["-c", "echo 'to stdout'; echo 'to stderr' >&2"],
    stdout=Collect,
    stderr=DuplicateStdout,
    )
    defer child.stdout.close()
    let output = child.stdout.text.read_all()
    inspect(output.contains("to stdout"), content="true")
    inspect(output.contains("to stderr"), content="true")
    })
    }

    #Multiple Processes Sharing Output

    read_from_process() creates a parent reader and a SharedProcessOutput that can be passed to multiple children as Shared(writer), allowing the parent to collect their merged output. Both ends are explicit and caller-owned.

    ///|
    #cfg(all(target="native", not(platform="windows")))
    async test "multiple processes to one pipe" {
    @async.with_task_group(root => {
    let (reader, writer) = @process.read_from_process()
    root.spawn_bg(no_wait=true, () => {
    defer reader.close()
    let output = reader.text.read_all()
    inspect(output.contains("first"), content="true")
    inspect(output.contains("second"), content="true")
    })
    @async.with_task_group(group => {
    // Close the shared child-side handle after spawning the last child.
    defer writer.close()
    @process.spawn(group, "echo", ["first"], stdout=Shared(writer)) |> ignore
    @process.spawn(group, "echo", ["second"], stdout=Shared(writer)) |> ignore
    })
    })
    }

    #Other Shared Pipes

    write_to_process() returns a SharedProcessInput for children and a WriteToProcess for the parent. Pass the input endpoint as Shared(input). It is useful when the same input stream must be passed to multiple children, or when using run() without a Process.stdin handle. Children share the stream: each byte is consumed by only one reader.

    pipe() returns a (SharedProcessInput, SharedProcessOutput) pair for direct child-to-child communication. Pass the output to the producer and the input to the consumer. Both handles remain caller-owned and must be closed after the last corresponding child has been spawned, or if spawning fails.

    For every shared pipe, also close the parent-facing ReadFromProcess or WriteToProcess when reading or writing is complete. Failing to close the last writer can prevent readers from observing end-of-file.

    #Types Reference

    ProcessInput and ProcessOutput are the redirection types accepted by run(), spawn(), and spawn_orphan().

    Parent-facing process pipes are binary readers or writers. Use their .text view for UTF-8 text I/O, as in child.stdout.text.read_all() and child.stdin.text.write(...).

    API or valueRedirection roleOwnership and closing
    InheritInherit the corresponding stream from the current execution contextNo caller-owned handle
    Stdin, Stdout, StderrSelect a parent-process standard stream explicitlyNo caller-owned handle
    FeedFromParentFeed one child's stdin through Process.stdinClose Process.stdin after writing
    EmptyGive the child an empty input stream that immediately reaches EOFManaged automatically
    CollectCollect one output stream through a process handle or ProcessResultWith spawn(), close Process.stdout or .stderr after reading
    DiscardIgnore everything written to an output streamManaged automatically
    FromFile(path)Read one child's stdin from a fileManaged automatically
    ToFile(path), AppendFile(path)Write one child's output to a fileManaged automatically
    DuplicateStdoutSend a child's stderr to its stdout destinationNo separate handle; valid only as stderr
    Shared(endpoint)Use a caller-owned input or output endpointClose the endpoint after passing it to its last child
    write_to_process()SharedProcessInput plus parent writerCaller closes both ends
    read_from_process()Parent reader plus SharedProcessOutputCaller closes both ends
    pipe()SharedProcessInput plus SharedProcessOutputCaller closes both ends
    redirect_from_file(path)Shared file-backed input endpointCaller closes it after its last use
    redirect_to_file(path, ...)Shared file-backed output endpointCaller closes it after its last use

    #Best Practices

    1. Close every caller-owned channel, preferably immediately after acquiring it with defer channel.close().
    2. Use run() for simple output capture.
    3. Use spawn() for interactive pipes, because the parent endpoints are returned on the Process handle.
    4. Use shared redirections only when sharing is required, and close them after the last child has been spawned.
    5. Handle exit codes explicitly; a nonzero exit is not a spawning error.
    6. Prefer spawn() or run() and use spawn_orphan() only when the process must outlive structured concurrency.

    #Error Handling

    Failure to create a process or initialize a redirection raises an error. Once a process starts successfully, its exit status is returned in ProcessResult.exit_code; a nonzero status is not raised as an error. If a process is killed by a signal on POSIX systems, its status is the negative signal number.

    ///|
    #cfg(any(target="native", target="wasm"))
    async test "handle process errors" {
    // Non-existent command fails
    @test.assert_raise_async(() => @process.run("nonexistent_command", []))
    }

    ///|
    #cfg(any(target="native", target="wasm"))
    async test "exit code indicates failure" {
    let result = if @async.platform is Windows {
    @process.run("cmd.exe", ["/c", "exit", "1"])
    } else {
    @process.run("sh", ["-c", "exit 1"])
    }
    let is_failure = result.exit_code != 0
    inspect(is_failure, content="true")
    }

    For complete examples, see the test files in src/process/.

    CancellationHandler

    pub(all) struct CancellationHandler(async (Int) -> Unit)

    A handler function used to stop spawned process on cancellation. The function receive the PID of the process as input.

    OrphanProcess

    pub struct OrphanProcess {
    pid : Int
    stdin : WriteToProcess
    stdout : ReadFromProcess
    stderr : ReadFromProcess
    // private fields
    }

    A handle to a spawned orphan process. Must be explicitly closed.

    OrphanProcess::close

    fn OrphanProcess::close(self : OrphanProcess) -> Unit

    Close handle to an orphan process, release associated resource. Note that closing the handle will not terminate the child process.

    OrphanProcess::wait

    async fn OrphanProcess::wait(self : OrphanProcess) -> Int

    Wait for the completion of an orphan process and return its exit code. If the process is killed by signal, the result would be -signal_number.

    Process

    pub struct Process {
    pid : Int
    stdin : WriteToProcess
    stdout : ReadFromProcess
    stderr : ReadFromProcess
    // private fields
    }

    A handle to a spawned process

    Process::cancel

    fn Process::cancel(self : Process) -> Unit

    Cancel a child process. The method of cancellation is determined by the cancel_handler parameter of @process.spawn. Notice that after .cancel() is called, if may take a while before the child process actually terminates. Use .wait() to wait for actual termination of the child process.

    Process::try_wait

    fn Process::try_wait(self : Process) -> Int? raise

    If the process already terminated, return its exit code. If the process is killed by a signal, the result would be -signal_number. If the process is still running, return None.

    Process::wait

    async fn Process::wait(self : Process) -> Int

    Wait for a process to terminate, return the exit code of the process If the process is killed by a signal, the result would be -signal_number.

    ProcessInput

    pub(all) enum ProcessInput {
    Inherit
    Stdin
    FeedFromParent
    Empty
    FromFile(StringView)
    Shared(SharedProcessInput)
    }

    Describes how a child process's standard input should be redirected.

    ProcessOutput

    pub(all) enum ProcessOutput {
    Inherit
    Stdout
    Stderr
    Collect
    Discard
    ToFile(StringView)
    AppendFile(StringView)
    DuplicateStdout
    Shared(SharedProcessOutput)
    }

    Describes how a child process's standard output or standard error should be redirected.

    ProcessResult

    pub struct ProcessResult {
    exit_code : Int
    // private fields
    }

    The result of a completed process. Exit code of the process can be obtained via .exit_code.

    If a process's stdout or stderr is redirected with Collect, the content of that stream can be obtained via .stdout_binary() or .stdout_text()/.stderr_binary() or .stderr_text().

    ProcessResult::stderr_binary

    fn ProcessResult::stderr_binary(self : ProcessResult) -> Bytes

    Obtain the collected content of a child process's stderr. The child process's stderr must be redirected with Collect.

    ProcessResult::stderr_text

    fn ProcessResult::stderr_text(self : ProcessResult) -> String raise

    Obtain the collected content of a child process's stderr, as UTF-8 encoded text. The child process's stderr must be redirected with Collect.

    ProcessResult::stdout_binary

    fn ProcessResult::stdout_binary(self : ProcessResult) -> Bytes

    Obtain the collected content of a child process's stdout. The child process's stdout must be redirected with Collect.

    ProcessResult::stdout_text

    fn ProcessResult::stdout_text(self : ProcessResult) -> String raise

    Obtain the collected content of a child process's stdout, as UTF-8 encoded text. The child process's stdout must be redirected with Collect.

    ReadFromProcess

    pub struct ReadFromProcess {
    text :
    Utf8Reader

    // private fields
    }

    A pipe for reading the output of child process. Must be closed when it is no longer used.

    ReadFromProcess::close

    fn ReadFromProcess::close(self : ReadFromProcess) -> Unit

    Close a process output reading pipe, release associated resource

    ReadFromProcess::drop_all

    async fn ReadFromProcess::drop_all(self : ReadFromProcess) -> Int64

    ReadFromProcess::drop_exactly

    async fn ReadFromProcess::drop_exactly(self : ReadFromProcess, len : Int) -> Unit

    ReadFromProcess::peek

    async fn ReadFromProcess::peek(self : ReadFromProcess) -> BytesView?

    ReadFromProcess::peek_at_least

    async fn ReadFromProcess::peek_at_least(self : ReadFromProcess, min_len : Int) -> BytesView

    ReadFromProcess::peek_exactly

    async fn ReadFromProcess::peek_exactly(self : ReadFromProcess, len : Int) -> BytesView

    ReadFromProcess::peek_until

    async fn ReadFromProcess::peek_until(self : ReadFromProcess, pattern : BytesView, include_delimiter? : Bool) -> BytesView?

    ReadFromProcess::read

    async fn ReadFromProcess::read(self : ReadFromProcess) -> Bytes?

    ReadFromProcess::read_all

    async fn ReadFromProcess::read_all(self : ReadFromProcess) -> Bytes

    ReadFromProcess::read_at_least

    async fn ReadFromProcess::read_at_least(self : ReadFromProcess, min_len : Int) -> Bytes

    ReadFromProcess::read_exactly

    async fn ReadFromProcess::read_exactly(self : ReadFromProcess, len : Int) -> Bytes

    ReadFromProcess::read_into

    async fn ReadFromProcess::read_into(self : ReadFromProcess, dst : FixedArray[Byte], offset? : Int, max_len? : Int) -> Int

    ReadFromProcess::read_until

    async fn ReadFromProcess::read_until(self : ReadFromProcess, pattern : BytesView, include_delimiter? : Bool) -> Bytes?

    SharedProcessInput

    type SharedProcessInput

    A caller-owned input endpoint that can be shared by multiple child processes. Pass it as stdin=Shared(input), then close it after it has been passed to the last child, or if spawning is abandoned.

    SharedProcessInput::close

    fn SharedProcessInput::close(self : SharedProcessInput) -> Unit

    Closes a shared process input channel and releases its resources.

    SharedProcessOutput

    type SharedProcessOutput

    A caller-owned output endpoint that can be shared by multiple child processes. Pass it as stdout=Shared(output) or stderr=Shared(output), then close it after it has been passed to the last child, or if spawning is abandoned.

    SharedProcessOutput::close

    fn SharedProcessOutput::close(self : SharedProcessOutput) -> Unit

    Closes a shared process output channel and releases its resources.

    WriteToProcess

    pub struct WriteToProcess {
    text :
    Utf8Writer

    // private fields
    }

    A pipe for writing to the input of child process. Must be closed when it is no longer used.

    WriteToProcess::close

    fn WriteToProcess::close(self : WriteToProcess) -> Unit

    Close a process input writing pipe, release associated resource

    WriteToProcess::write

    async fn WriteToProcess::write(self : WriteToProcess, data : BytesView) -> Unit

    WriteToProcess::write_reader

    graceful_cancel

    fn graceful_cancel(timeout~ : Int, signal? :
    Signal
    ) -> CancellationHandler

    A process cancellation handler that first try to gracefully stop the process, and if the process is still running after timeout milliseconds, forcefully stop the process.

    Graceful process termination is implemented by sendig signal to the process. The default signal is SIGTERM on POSIX-like systems and SIGBREAK (aka CTRL_BREAK_EVENT) on Windows.

    Note that on Windows, SIGBREAK is the only signal allowed to be be sent.

    hard_cancel

    fn hard_cancel() -> CancellationHandler

    A process cancellation handler that forcefully stop the process. Implemented via SIGKILL on POSIX-like systems and TerminateProcess on Windows.

    pipe

    Creates a pipe for direct communication between child processes.

    Returns (input, output). Pass Shared(output) to producer processes and Shared(input) to consumer processes. Data written by producers can be read by consumers.

    Both endpoints are caller-owned and can be shared by multiple children. Close each endpoint after passing it to its last child, or if spawning is abandoned.

    read_from_process

    fn read_from_process() -> (ReadFromProcess, SharedProcessOutput) raise

    Creates a pipe for collecting output from one or more child processes.

    Returns (reader, output). Read the merged output through reader, and pass Shared(output) as a child's stdout or stderr redirection.

    For one child created with spawn() or spawn_orphan(), prefer Collect.

    Both returned values are caller-owned. Close output after passing it to the last child, and close reader after reading is complete.

    redirect_from_file

    async fn redirect_from_file(path : String) -> SharedProcessInput

    Opens a file and returns a caller-owned input endpoint that can be shared by multiple child processes.

    Pass the result as stdin=Shared(input). Unlike FromFile(path), this function opens the file immediately. Children share one open file description and therefore one file offset. Close the endpoint after passing it to the last child, or if spawning is abandoned.

    redirect_to_file

    async fn redirect_to_file(path : String, append? : Bool, create_mode? :
    CreateMode
    , permission? : Int) -> SharedProcessOutput

    Opens a file and returns a caller-owned output endpoint that can be shared by multiple child processes.

    Pass the result as stdout=Shared(output) or stderr=Shared(output). Unlike ToFile(path) and AppendFile(path), this function opens the file immediately and supports custom creation modes and permissions. Close the endpoint after passing it to the last child, or if spawning is abandoned.

    run

    async fn run(cmd : StringView, args : ArrayView[String], extra_env? : Map[String, String], inherit_env? : Bool, stdin? : ProcessInput, stdout? : ProcessOutput, stderr? : ProcessOutput, cwd? : StringView, no_console_window? : Bool, cancel_handler? : CancellationHandler) -> ProcessResult

    Execute a system process with command cmd, and provide args as extra arguments to cmd.

    Instead of spawning the child process in a task group, run will simply block and wait for completion of the child process in current task. If @process.run() is cancelled after the child process started, the child process will be cancelled automatically as well, and @process.run() will return only after the child completes its cleanup and actually terminates.

    @process.run() does not expose a Process object for the child, so the caller cannot retrieve the PID or .stdin channel of the child process. As a consequence, FeedFromParent cannot be used to interact with @process.run(). When the current process needs to interact with the child's stdin, use @process.spawn with FeedFromParent, or create a caller-owned input channel with @process.write_to_process() and pass it as stdin=Shared(input).

    When stdout or stderr is redirected with Collect, @process.run() collects the corresponding output automatically. The result can be obtained by calling .stdout_binary()/.stderr_binary()/.stdout_text()/.stderr_text() on the returned struct ProcessResult.

    All remaining arguments of @process.run have the same meaning as @process.spawn, see @process.spawn for more details.

    spawn

    async fn spawn(group :
    TaskGroup
    , cmd : StringView, args : ArrayView[String], extra_env? : Map[String, String], inherit_env? : Bool, stdin? : ProcessInput, stdout? : ProcessOutput, stderr? : ProcessOutput, cwd? : StringView, no_console_window? : Bool, cancel_handler? : CancellationHandler, no_wait? : Bool) -> Process

    Spawn a child process inside a task group. The child process runs the program on path cmd, and args will be provided to the program as command line arguments. On Linux/MacOS, both cmd and args are encoded using UTF-8. On Windows, both cmd and args are passed via native unicode string, and args will be converted to Windows-style command line string via C argv splitting rule.

    If no_wait is false (false by default), the task group will wait for termination of the child process before returning normally. When the task group is about to exit, for example due to fatal error, external cancellation, the child process will be terminated automatically.

    A Process object will be returned, users can retrieve the PID of the spawned process via .pid or wait for the process to terminate via .wait()/.try_wait().

    The new process's standard streams are configured by stdin, stdout, and stderr. They default to Inherit, which inherits the corresponding stream from the current execution context.

    Use FeedFromParent to write through Process.stdin, and Collect to read through Process.stdout or Process.stderr. FromFile, ToFile, and AppendFile provide automatically managed file redirection. DuplicateStdout redirects standard error to the standard-output destination. Stdin, Stdout, and Stderr select a parent-process standard stream explicitly.

    Use Shared(input) or Shared(output) with caller-owned endpoints returned by write_to_process(), read_from_process(), pipe(), redirect_from_file(), or redirect_to_file() when an endpoint must be shared by multiple children or used independently of a Process handle. Caller-owned endpoints must be closed after they have been passed to their last child, or if spawning is abandoned.

    If inherit_env is true (true by default), the new process will inherit environment variables of current process.

    extra_env, if present, will set extra environment variables for the new process (in addition to and overriding those inherited ones, if inherit_env is true). Keys and values of extra_env are encoded using UTF8.

    If cwd is present, the spawned command will be executed in the directory specified by cwd.

    When no_console_window is true (false by default), creation of a new console window will be disabled on Windows. By default a new console window may be created when the calling process is a GUI application and the child process is a conlose application. no_console_window has no effect on non-Windows platforms.

    The child process can be cancelled manually via .cancel() on the returned Process object. The child may also get cancelled automatically by the task group. When the child process is cancelled, cancel_handler will be used to automatically stop the process. The default value of cancel_handler is graceful_cancel(timeout=5000) (First try to gracefully terminate the process, and if the process is still running after five seconds, terminate it forcefully). Note that the task group will not exit until the child process actually terminates (i.e. after the child process completes its cleanup process). So the child process will never leak outside its task group.

    If cancel_handler is still running after the process terminates, it will be cancelled.

    spawn_orphan

    async fn spawn_orphan(cmd : StringView, args : ArrayView[String], extra_env? : Map[String, String], inherit_env? : Bool, stdin? : ProcessInput, stdout? : ProcessOutput, stderr? : ProcessOutput, cwd? : StringView, no_console_window? : Bool) -> OrphanProcess

    Execute a system process with command cmd, and provide args as extra arguments to cmd. Both cmd and elements of args are encoded using UTF8. A handle similar to Process is returned, see @process.spawn for more details.

    The spawned process would be orphan, meaning that it will keep running until completion or explicitly terminated, even if the task that calls spawn_orphan is cancelled. Users are recommended to use @process.run whenever possible, because @process.spawn/@process.run has better structured concurrency integration. spawn_orphan should only be used when the process is intended to be orphan (i.e. keep running after parent process terminates).

    The meaning of the arguments is the same as @process.spawn, see @process.spawn for more details.

    wait_pid

    async fn wait_pid(pid : Int) -> Int

    Wait for the process with specfic ID to terminate, and return the exit code of the process. If the process is killed by a signal, the result would be -signal_number.

    write_to_process

    fn write_to_process() -> (SharedProcessInput, WriteToProcess) raise

    Creates a pipe for feeding input to one or more child processes.

    Returns (input, writer). Pass Shared(input) as a child's stdin redirection, and feed data through writer.

    For one child created with spawn() or spawn_orphan(), prefer FeedFromParent.

    Both returned values are caller-owned. Close input after passing it to the last child, and close writer after writing is complete so readers observe end-of-file.