From ce373b5522eca3059d08039c90628c7f0abdc14b Mon Sep 17 00:00:00 2001 From: Hongbo Zhang Date: Wed, 27 May 2026 15:17:25 +0800 Subject: [PATCH] add checked process runner --- src/process/README.mbt.md | 10 ++++++++- src/process/pkg.generated.mbti | 6 ++++++ src/process/process.mbt | 39 ++++++++++++++++++++++++++++++++++ src/process/wait_test.mbt | 19 +++++++++++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/process/README.mbt.md b/src/process/README.mbt.md index 679cf37d3..41f62b0ca 100644 --- a/src/process/README.mbt.md +++ b/src/process/README.mbt.md @@ -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 ///| @@ -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/`. diff --git a/src/process/pkg.generated.mbti b/src/process/pkg.generated.mbti index eeac56455..970c614a2 100644 --- a/src/process/pkg.generated.mbti +++ b/src/process/pkg.generated.mbti @@ -8,6 +8,7 @@ import { "moonbitlang/async/pipe", "moonbitlang/async/signal", "moonbitlang/async/stdio", + "moonbitlang/core/debug", } // Values @@ -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 @@ -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) diff --git a/src/process/process.mbt b/src/process/process.mbt index 76473bcd8..425285a61 100644 --- a/src/process/process.mbt +++ b/src/process/process.mbt @@ -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. @@ -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 { diff --git a/src/process/wait_test.mbt b/src/process/wait_test.mbt index 905804c50..7e2843a76 100644 --- a/src/process/wait_test.mbt +++ b/src/process/wait_test.mbt @@ -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()