Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/process/README.mbt.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,8 @@ Trait for types that can be used as process output:

## Error Handling

Process operations handle errors through exit codes:
`@process.run` returns the child process exit code. Use `@process.run_checked`
when a non-zero exit code should be treated as an error.

```moonbit check
///|
Expand All @@ -484,6 +485,13 @@ async test "exit code indicates failure" {
let is_failure = exit_code != 0
inspect(is_failure, content="true")
}

///|
#cfg(all(target="native", not(platform="windows")))
async test "checked run raises on non-zero exit" {
let result = try? @process.run_checked("sh", ["-c", "exit 1"])
assert_true(result is Err(@process.NonZeroExit(_)))
}
```

For complete examples, see the test files in `src/process/`.
6 changes: 6 additions & 0 deletions src/process/pkg.generated.mbti
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
"moonbitlang/async/pipe",
"moonbitlang/async/signal",
"moonbitlang/async/stdio",
"moonbitlang/core/debug",
}

// Values
Expand All @@ -33,6 +34,8 @@ pub async fn redirect_to_file(String, append? : Bool, create_mode? : @fs.CreateM

pub async fn run(StringView, ArrayView[String], extra_env? : Map[String, String], inherit_env? : Bool, stdin? : &ProcessInput, stdout? : &ProcessOutput, stderr? : &ProcessOutput, cwd? : StringView, cancel_handler? : CancellationHandler) -> Int

pub async fn run_checked(StringView, ArrayView[String], extra_env? : Map[String, String], inherit_env? : Bool, stdin? : &ProcessInput, stdout? : &ProcessOutput, stderr? : &ProcessOutput, cwd? : StringView, cancel_handler? : CancellationHandler) -> Unit

pub async fn[X] spawn(@async.TaskGroup[X], StringView, ArrayView[String], extra_env? : Map[String, String], inherit_env? : Bool, stdin? : &ProcessInput, stdout? : &ProcessOutput, stderr? : &ProcessOutput, cwd? : StringView, cancel_handler? : CancellationHandler, no_wait? : Bool) -> Process

pub async fn spawn_orphan(StringView, ArrayView[String], extra_env? : Map[String, String], inherit_env? : Bool, stdin? : &ProcessInput, stdout? : &ProcessOutput, stderr? : &ProcessOutput, cwd? : StringView) -> Int
Expand All @@ -42,6 +45,9 @@ pub async fn wait_pid(Int) -> Int
pub fn write_to_process() -> (&ProcessInput, WriteToProcess) raise

// Errors
pub suberror NonZeroExit {
NonZeroExit(exit_code~ : Int, cmd~ : String, args~ : Array[String])
} derive(ToJson, @debug.Debug)

// Types and methods
pub(all) struct CancellationHandler(async (Int) -> Unit)
Expand Down
39 changes: 39 additions & 0 deletions src/process/process.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ pub async fn wait_pid(pid : Int) -> Int {
process.wait_pid(context~)
}

///|
/// Error raised by `@process.run_checked` when the process exits with a
/// non-zero status code.
pub suberror NonZeroExit {
NonZeroExit(exit_code~ : Int, cmd~ : String, args~ : Array[String])
} derive(Debug, ToJson)

///|
/// Execute a system process with command `cmd`,
/// and provide `args` as extra arguments to `cmd.
Expand Down Expand Up @@ -145,6 +152,38 @@ pub async fn run(
}
}

///|
/// Execute a system process like `@process.run`, but raise `NonZeroExit`
/// if the process exits with a non-zero status code.
///
/// The meaning of parameters is the same as `@process.run`.
pub async fn run_checked(
cmd : StringView,
args : ArrayView[String],
extra_env? : Map[String, String] = {},
inherit_env? : Bool = true,
stdin? : &ProcessInput,
stdout? : &ProcessOutput,
stderr? : &ProcessOutput,
cwd? : StringView,
cancel_handler? : CancellationHandler = graceful_cancel(timeout=5000),
) -> Unit {
let exit_code = run(
cmd,
args,
extra_env~,
inherit_env~,
stdin?,
stdout?,
stderr?,
cwd?,
cancel_handler~,
)
guard exit_code == 0 else {
raise NonZeroExit(exit_code~, cmd=cmd.to_owned(), args=args.to_owned())
}
}

///|
/// A handle to a spawned process
pub struct Process {
Expand Down
19 changes: 19 additions & 0 deletions src/process/wait_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,25 @@ async test "wait exitcode" {
inspect(@process.run(shell, ["-c", "exit 42"]), content="42")
}

///|
async test "run_checked success" {
@process.run_checked(shell, ["-c", "exit 0"])
}

///|
async test "run_checked nonzero exit" {
let expected_args = ["-c", "exit 42"]
let result = try? @process.run_checked(shell, expected_args)
guard result is Err(@process.NonZeroExit(exit_code~, cmd~, args~)) else {
fail("expected NonZeroExit")
}
assert_eq(exit_code, 42)
assert_eq(cmd, shell)
assert_eq(args.length(), expected_args.length())
assert_eq(args[0], expected_args[0])
assert_eq(args[1], expected_args[1])
}

///|
async test "wait_pid" {
let log = StringBuilder::new()
Expand Down
Loading