// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// 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()`.
pub struct ProcessResult {
exit_code : Int32
priv stdout : Bytes?
priv stderr : Bytes?
}
///|
/// Obtain the collected content of a child process's stdout.
/// The child process's `stdout` must be redirected with `Collect`.
pub fn ProcessResult::stdout_binary(self : ProcessResult) -> Bytes {
guard self.stdout is Some(stdout) else {
abort(
"cannot read output of a process whose stdout is not redirected with `Collect`",
)
}
stdout
}
///|
/// 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`.
pub fn ProcessResult::stdout_text(self : ProcessResult) -> String raise {
@utf8.decode(self.stdout_binary())
}
///|
/// Obtain the collected content of a child process's stderr.
/// The child process's `stderr` must be redirected with `Collect`.
pub fn ProcessResult::stderr_binary(self : ProcessResult) -> Bytes {
guard self.stderr is Some(stderr) else {
abort(
"cannot read output of a process whose stderr is not redirected with `Collect`",
)
}
stderr
}
///|
/// 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`.
pub fn ProcessResult::stderr_text(self : ProcessResult) -> String raise {
@utf8.decode(self.stderr_binary())
}
///|
/// 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.
pub async fn run(
cmd : StringView,
args : ArrayView[String],
extra_env~ : Map[String, String] = Map([]),
inherit_env~ : Bool = true,
stdin~ : ProcessInput = Inherit,
stdout~ : ProcessOutput = Inherit,
stderr~ : ProcessOutput = Inherit,
cwd? : StringView,
no_console_window~ : Bool = false,
cancel_handler~ : CancellationHandler = graceful_cancel(timeout=5000),
) -> ProcessResult {
let context = "@process.run()"
let (process, stdin, stdout, stderr) = raw_spawn(
cmd,
args,
extra_env~,
inherit_env~,
stdin~,
stdout~,
stderr~,
cwd~,
no_console_window~,
is_orphan=false,
context~,
)
defer process.close()
// The caller of `run` has no way to obtain this channel,
// so close it immediately to avoid leak & hang up.
stdin.close()
let pid = process.pid
let mut exit_code = 0
let mut stdout_result = None
let mut stderr_result = None
@async.with_task_group <| group => {
group.spawn_bg() <| () => {
defer stdout.close()
if stdout.io is Some(_) {
stdout_result = Some(stdout.read_all())
}
}
group.spawn_bg() <| () => {
defer stderr.close()
if stderr.io is Some(_) {
stderr_result = Some(stderr.read_all())
}
}
match @async.handle_cancellation(() => process.wait_pid(context~)) {
None => {
@async.protect_from_cancel() <| () => {
@async.with_task_group() <| group => {
group.spawn_bg(no_wait=true, () => cancel_handler(pid))
ignore <| process.wait_pid(context~)
}
}
@coroutine.raise_cancellation_signal()
}
Some(ret) => exit_code = ret
}
}
{ exit_code, stdout: stdout_result, stderr: stderr_result, }
}
///|
/// A handle to a spawned process
pub struct Process {
pid : Int32
/// Redirected `stdin` channel of the process.
/// This field is only meaningful if the process is spawned
/// with `stdin=FeedFromParent`.
/// In this case, it can be used to write to stdin of the child process.
stdin : WriteToProcess
/// Redirected `stdout` channel of the process.
/// This field is only meaningful if the process is spawned
/// with `stdout=Collect`.
/// In this case, it can be used to read from the stdout of the child process.
stdout : ReadFromProcess
/// Redirected `stderr` channel of the process.
/// This field is only meaningful if the process is spawned
/// with `stderr=Collect`.
/// In this case, it can be used to read from the stderr of the child process.
stderr : ReadFromProcess
priv result : @async.Task[Int32]
}
///|
/// 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`.
pub async fn Process::wait(self : Process) -> Int32 {
self.result.wait()
}
///|
/// 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`.
pub fn Process::try_wait(self : Process) -> Int32? raise {
self.result.try_wait()
}
///|
/// 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.
pub fn Process::cancel(self : Process) -> Unit {
self.result.cancel()
}
///|
/// 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.
pub async fn spawn(
group : @async.TaskGroup,
cmd : StringView,
args : ArrayView[String],
extra_env~ : Map[String, String] = Map([]),
inherit_env~ : Bool = true,
stdin~ : ProcessInput = Inherit,
stdout~ : ProcessOutput = Inherit,
stderr~ : ProcessOutput = Inherit,
cwd? : StringView,
no_console_window~ : Bool = false,
cancel_handler~ : CancellationHandler = graceful_cancel(timeout=5000),
no_wait? : Bool,
) -> Process {
let context = "@process.spawn()"
let (process, stdin, stdout, stderr) = raw_spawn(
cmd,
args,
extra_env~,
inherit_env~,
stdin~,
stdout~,
stderr~,
cwd~,
no_console_window~,
is_orphan=false,
context~,
)
let pid = process.pid
let result = group.spawn(no_wait?, () => {
defer process.close()
match @async.handle_cancellation(() => process.wait_pid(context~)) {
None => {
let mut result = 0
@async.protect_from_cancel() <| () => {
@async.with_task_group() <| group => {
group.spawn_bg(no_wait=true, () => cancel_handler(pid))
result = process.wait_pid(context~)
}
}
result
}
Some(ret) => ret
}
})
{ pid, stdin, stdout, stderr, result, }
}