From 251adb2fec590ed7e11a45b245190d1508010dc4 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Wed, 7 Oct 2026 19:57:07 -0700 Subject: [PATCH 1/3] Use ExitCode in Rust executables Return exit statuses through Rust main functions so normal process cleanup can flush code coverage data. Update resource authoring guidance to require the same pattern. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/skills/create-dsc-resource/SKILL.md | 38 +++++---- adapters/group_policy_template/src/main.rs | 30 +++---- resources/WindowsUpdate/src/main.rs | 29 +++---- resources/dism_dsc/src/main.rs | 23 +++--- resources/dscecho/src/main.rs | 8 +- resources/environment_variable/src/main.rs | 64 +++++++------- resources/filecontent/src/main.rs | 58 +++++++------ resources/osinfo/src/main.rs | 50 ++++++----- resources/process/src/main.rs | 12 +-- resources/runcommandonset/src/main.rs | 22 +++-- resources/runcommandonset/src/utils.rs | 32 +++---- resources/sshdconfig/src/main.rs | 14 ++-- resources/windows_firewall/src/main.rs | 84 +++++++++++-------- resources/windows_service/src/main.rs | 92 +++++++++++++-------- tools/dsctest/src/adapter.rs | 8 +- tools/dsctest/src/exit_code.rs | 2 +- tools/dsctest/src/export_schema.rs | 9 +- tools/dsctest/src/main.rs | 84 ++++++++++++------- y2j/src/main.rs | 35 ++++---- 19 files changed, 395 insertions(+), 299 deletions(-) diff --git a/.github/skills/create-dsc-resource/SKILL.md b/.github/skills/create-dsc-resource/SKILL.md index 2dd310b3d..0987120b0 100644 --- a/.github/skills/create-dsc-resource/SKILL.md +++ b/.github/skills/create-dsc-resource/SKILL.md @@ -142,30 +142,34 @@ Key fields: #### main.rs file - Initialize `rust-i18n` with `rust_i18n::i18n!("locales", fallback = "en-us");` -- Define named exit code constants at the module level: +- Import `std::process::ExitCode`, make `main` return `ExitCode`, and return normally from every code path. Never call `std::process::exit`; it bypasses normal process cleanup and prevents coverage data from being collected. +- Define named `u8` exit code constants at the module level so they can be passed directly to `ExitCode::from`: ```rust - const EXIT_SUCCESS: i32 = 0; - const EXIT_INVALID_ARGS: i32 = 1; - const EXIT_INVALID_INPUT: i32 = 2; - const EXIT_RESOURCE_ERROR: i32 = 3; + const EXIT_SUCCESS: u8 = 0; + const EXIT_INVALID_ARGS: u8 = 1; + const EXIT_INVALID_INPUT: u8 = 2; + const EXIT_RESOURCE_ERROR: u8 = 3; ``` - Implement common helper functions: - `write_error(message)` — writes `{"error": ""}` to stderr - - `require_input(input_json)` — deserializes the JSON input or exits with an error - - `print_json(value)` — serializes and prints to stdout, or exits with an error - - `parse_input_arg(args)` — parses `--input ` from command-line arguments + - `require_input(input_json)` — deserializes the JSON input and returns `Result` + - `print_json(value)` — serializes and prints to stdout, returning `Result<(), ExitCode>` + - `parse_input_arg(args)` — parses `--input ` and returns argument errors to `main` - Use `t!("key")` macro from `rust-i18n` for all user-facing strings (error messages, etc.) - For platform-specific resources, use conditional compilation: ```rust + use std::process::ExitCode; + #[cfg(not(windows))] - fn main() { + fn main() -> ExitCode { write_error(&t!("main.windowsOnly")); - exit(EXIT_RESOURCE_ERROR); + ExitCode::from(EXIT_RESOURCE_ERROR) } #[cfg(windows)] - fn main() { + fn main() -> ExitCode { // ... actual implementation + ExitCode::from(EXIT_SUCCESS) } ``` - Parse operations as the first positional argument (e.g., `get`, `set`, `export`), with `--input ` as the input argument @@ -340,7 +344,10 @@ args::ConfigSubCommand::Set { input, what_if } => { trace!("Set input: {input}, what_if: {what_if}"); let mut helper = match Helper::new_from_json(&input) { Ok(h) => h, - Err(err) => { error!("{err}"); exit(EXIT_INVALID_INPUT); } + Err(err) => { + error!("{err}"); + return ExitCode::from(EXIT_INVALID_INPUT); + } }; if what_if { helper.enable_what_if(); } @@ -352,9 +359,12 @@ args::ConfigSubCommand::Set { input, what_if } => { println!("{json}"); } Ok(None) => {} - Err(err) => { error!("{err}"); exit(EXIT_RESOURCE_ERROR); } + Err(err) => { + error!("{err}"); + return ExitCode::from(EXIT_RESOURCE_ERROR); + } } - exit(EXIT_SUCCESS); + ExitCode::from(EXIT_SUCCESS) } ``` diff --git a/adapters/group_policy_template/src/main.rs b/adapters/group_policy_template/src/main.rs index efafcdfb4..554ae86be 100644 --- a/adapters/group_policy_template/src/main.rs +++ b/adapters/group_policy_template/src/main.rs @@ -8,31 +8,31 @@ mod registry; use rust_i18n::t; use serde_json::json; -use std::process::exit; +use std::process::ExitCode; rust_i18n::i18n!("locales", fallback = "en-us"); -const EXIT_SUCCESS: i32 = 0; -const EXIT_INVALID_ARGS: i32 = 1; -const EXIT_INVALID_INPUT: i32 = 2; -const EXIT_RESOURCE_ERROR: i32 = 3; +const EXIT_SUCCESS: u8 = 0; +const EXIT_INVALID_ARGS: u8 = 1; +const EXIT_INVALID_INPUT: u8 = 2; +const EXIT_RESOURCE_ERROR: u8 = 3; fn write_error(message: &str) { eprintln!("{}", json!({ "error": message })); } #[cfg(not(windows))] -fn main() { +fn main() -> ExitCode { write_error(&t!("main.windowsOnly")); - exit(EXIT_RESOURCE_ERROR); + ExitCode::from(EXIT_RESOURCE_ERROR) } #[cfg(windows)] -fn main() { +fn main() -> ExitCode { let args: Vec = std::env::args().skip(1).collect(); let Some(operation) = args.first().map(String::as_str) else { write_error(&t!("main.missingOperation")); - exit(EXIT_INVALID_ARGS); + return ExitCode::from(EXIT_INVALID_ARGS); }; let result = match operation { @@ -40,11 +40,11 @@ fn main() { "get" | "set" => { let Some(resource_type) = argument_value(&args, "--resource-type") else { write_error(&t!("main.missingArgument", argument = "--resource-type")); - exit(EXIT_INVALID_ARGS); + return ExitCode::from(EXIT_INVALID_ARGS); }; let Some(resource_path) = argument_value(&args, "--resource-path") else { write_error(&t!("main.missingArgument", argument = "--resource-path")); - exit(EXIT_INVALID_ARGS); + return ExitCode::from(EXIT_INVALID_ARGS); }; if operation == "get" { let input = argument_value(&args, "--input").unwrap_or_default(); @@ -52,14 +52,14 @@ fn main() { } else { let Some(input) = argument_value(&args, "--input") else { write_error(&t!("main.missingArgument", argument = "--input")); - exit(EXIT_INVALID_ARGS); + return ExitCode::from(EXIT_INVALID_ARGS); }; registry::set(input, resource_type, resource_path) } } unknown => { write_error(&t!("main.unknownOperation", operation = unknown)); - exit(EXIT_INVALID_ARGS); + return ExitCode::from(EXIT_INVALID_ARGS); } }; @@ -68,7 +68,7 @@ fn main() { for line in lines { println!("{line}"); } - exit(EXIT_SUCCESS); + ExitCode::from(EXIT_SUCCESS) } Err(error) => { write_error(&error.to_string()); @@ -77,7 +77,7 @@ fn main() { } else { EXIT_RESOURCE_ERROR }; - exit(code); + ExitCode::from(code) } } } diff --git a/resources/WindowsUpdate/src/main.rs b/resources/WindowsUpdate/src/main.rs index 07e9e9c78..93f84c845 100644 --- a/resources/WindowsUpdate/src/main.rs +++ b/resources/WindowsUpdate/src/main.rs @@ -6,16 +6,17 @@ mod windows_update; use rust_i18n::t; use std::io::{self, Read, IsTerminal}; +use std::process::ExitCode; rust_i18n::i18n!("locales", fallback = "en-us"); -fn main() { +fn main() -> ExitCode { let args: Vec = std::env::args().collect(); if args.len() < 2 { eprintln!("Error: {}", t!("main.missingOperation")); eprintln!("{}", t!("main.usage")); - std::process::exit(1); + return ExitCode::FAILURE; } let operation = args[1].as_str(); @@ -32,18 +33,18 @@ fn main() { match windows_update::handle_export(&buffer) { Ok(output) => { println!("{}", output); - std::process::exit(0); + ExitCode::SUCCESS } Err(e) => { eprintln!("Error: {}", e); - std::process::exit(1); + ExitCode::FAILURE } } #[cfg(not(windows))] { eprintln!("Error: {}", t!("main.windowsUpdateOnlySupported")); - std::process::exit(1); + ExitCode::FAILURE } } "get" => { @@ -51,25 +52,25 @@ fn main() { let mut buffer = String::new(); if let Err(e) = io::stdin().read_to_string(&mut buffer) { eprintln!("{}", t!("main.errorReadingInput", err = e)); - std::process::exit(1); + return ExitCode::FAILURE; } #[cfg(windows)] match windows_update::handle_get(&buffer) { Ok(output) => { println!("{}", output); - std::process::exit(0); + ExitCode::SUCCESS } Err(e) => { eprintln!("Error: {}", e); - std::process::exit(1); + ExitCode::FAILURE } } #[cfg(not(windows))] { eprintln!("Error: {}", t!("main.windowsUpdateOnlySupported")); - std::process::exit(1); + ExitCode::FAILURE } } "set" => { @@ -77,31 +78,31 @@ fn main() { let mut buffer = String::new(); if let Err(e) = io::stdin().read_to_string(&mut buffer) { eprintln!("{}", t!("main.errorReadingInput", err = e)); - std::process::exit(1); + return ExitCode::FAILURE; } #[cfg(windows)] match windows_update::handle_set(&buffer, parse_what_if_arg(&args)) { Ok(output) => { println!("{}", output); - std::process::exit(0); + ExitCode::SUCCESS } Err(e) => { eprintln!("Error: {}", e); - std::process::exit(1); + ExitCode::FAILURE } } #[cfg(not(windows))] { eprintln!("Error: {}", t!("main.windowsUpdateOnlySupported")); - std::process::exit(1); + ExitCode::FAILURE } } _ => { eprintln!("{}", t!("main.unknownOperation", operation = operation)); eprintln!("{}", t!("main.usage")); - std::process::exit(1); + ExitCode::FAILURE } } } diff --git a/resources/dism_dsc/src/main.rs b/resources/dism_dsc/src/main.rs index 4690df64f..6b80688d4 100644 --- a/resources/dism_dsc/src/main.rs +++ b/resources/dism_dsc/src/main.rs @@ -14,6 +14,7 @@ mod windows_feature; use rust_i18n::t; use std::io::{self, IsTerminal, Read}; +use std::process::ExitCode; rust_i18n::i18n!("locales", fallback = "en-us"); @@ -27,41 +28,41 @@ fn read_stdin() -> Result { Ok(buffer) } -fn dispatch(handler: impl FnOnce(&str) -> Result) { +fn dispatch(handler: impl FnOnce(&str) -> Result) -> ExitCode { let buffer = match read_stdin() { Ok(b) => b, Err(e) => { eprintln!("{e}"); - std::process::exit(1); + return ExitCode::FAILURE; } }; match handler(&buffer) { Ok(output) => { println!("{output}"); - std::process::exit(0); + ExitCode::SUCCESS } Err(e) => { eprintln!("Error: {e}"); - std::process::exit(1); + ExitCode::FAILURE } } } #[cfg(not(windows))] -fn main() { +fn main() -> ExitCode { eprintln!("Error: {}", t!("main.windowsOnly")); - std::process::exit(1); + ExitCode::FAILURE } #[cfg(windows)] -fn main() { +fn main() -> ExitCode { let args: Vec = std::env::args().collect(); if args.len() < 3 { eprintln!("Error: {}", t!("main.missingArguments")); eprintln!("{}", t!("main.usage")); - std::process::exit(1); + return ExitCode::FAILURE; } let operation = args[1].as_str(); @@ -77,7 +78,7 @@ fn main() { ("get", "windows-feature") => dispatch(windows_feature::handle_get), ("set", "windows-feature") => { let what_if = args.iter().any(|arg| arg == "-w" || arg == "--what-if"); - dispatch(|input| windows_feature::handle_set(input, what_if)); + dispatch(|input| windows_feature::handle_set(input, what_if)) } ("export", "windows-feature") => dispatch(windows_feature::handle_export), ("get" | "set" | "export", _) => { @@ -86,12 +87,12 @@ fn main() { t!("main.unknownResourceType", resource_type = resource_type) ); eprintln!("{}", t!("main.usage")); - std::process::exit(1); + ExitCode::FAILURE } _ => { eprintln!("{}", t!("main.unknownOperation", operation = operation)); eprintln!("{}", t!("main.usage")); - std::process::exit(1); + ExitCode::FAILURE } } } diff --git a/resources/dscecho/src/main.rs b/resources/dscecho/src/main.rs index f46f14b5f..23b67bee4 100644 --- a/resources/dscecho/src/main.rs +++ b/resources/dscecho/src/main.rs @@ -10,19 +10,20 @@ use rust_i18n::{i18n, t}; use schemars::schema_for; use serde_json::{Map, Value}; use crate::echo::{Echo, Output, SecureObject, SecureString}; +use std::process::ExitCode; i18n!("locales", fallback = "en-us"); const SECURE_VALUE_REDACTED: &str = ""; -fn main() { +fn main() -> ExitCode { let args = Args::parse(); if let Some(input) = args.input { let mut echo = match serde_json::from_str::(&input) { Ok(echo) => echo, Err(err) => { eprintln!("{}: {err}", t!("main.invalidJson")); - std::process::exit(1); + return ExitCode::FAILURE; } }; match echo.output { @@ -53,12 +54,13 @@ fn main() { } let json = serde_json::to_string(&echo).unwrap(); println!("{json}"); - return; + return ExitCode::SUCCESS; } let schema = schema_for!(Echo); let json = serde_json::to_string_pretty(&schema).unwrap(); println!("{json}"); + ExitCode::SUCCESS } fn is_secure_value(value: &Value) -> bool { diff --git a/resources/environment_variable/src/main.rs b/resources/environment_variable/src/main.rs index 9406cad99..a298acfa7 100644 --- a/resources/environment_variable/src/main.rs +++ b/resources/environment_variable/src/main.rs @@ -7,7 +7,7 @@ mod types; mod environment; use rust_i18n::t; -use std::process::exit; +use std::process::ExitCode; use types::{ EnvironmentPathVariable, EnvironmentVariable, EnvironmentVariableFilterList, EnvironmentVariableList, Operation, @@ -15,11 +15,11 @@ use types::{ rust_i18n::i18n!("locales", fallback = "en-us"); -const EXIT_SUCCESS: i32 = 0; -const EXIT_INVALID_ARGS: i32 = 1; -const EXIT_INVALID_INPUT: i32 = 2; -const EXIT_RESOURCE_ERROR: i32 = 3; -const EXIT_ELEVATION_REQUIRED: i32 = 4; +const EXIT_SUCCESS: u8 = 0; +const EXIT_INVALID_ARGS: u8 = 1; +const EXIT_INVALID_INPUT: u8 = 2; +const EXIT_RESOURCE_ERROR: u8 = 3; +const EXIT_ELEVATION_REQUIRED: u8 = 4; #[derive(Clone, Copy)] enum InputKind { @@ -32,12 +32,15 @@ fn write_error(message: &str) { eprintln!("{}", serde_json::json!({ "error": message })); } -fn print_json(value: &impl serde::Serialize) { +fn print_json(value: &impl serde::Serialize) -> Result<(), ExitCode> { match serde_json::to_string(value) { - Ok(json) => println!("{json}"), + Ok(json) => { + println!("{json}"); + Ok(()) + } Err(error) => { write_error(&t!("main.serializeError", error = error.to_string())); - exit(EXIT_RESOURCE_ERROR); + Err(ExitCode::from(EXIT_RESOURCE_ERROR)) } } } @@ -46,7 +49,7 @@ fn parse_list( input_json: Option, operation: Operation, kind: InputKind, -) -> Result { +) -> Result { let json = input_json.ok_or_else(|| (t!("main.missingInput").to_string(), EXIT_INVALID_ARGS))?; @@ -73,7 +76,7 @@ fn parse_list( fn parse_export_filters( input_json: Option, -) -> Result { +) -> Result { let Some(json) = input_json else { return Ok(EnvironmentVariableFilterList::default()); }; @@ -110,32 +113,35 @@ fn serialize_result( Ok(output) } -fn print_result(value: EnvironmentVariableList, kind: InputKind) { +fn print_result(value: EnvironmentVariableList, kind: InputKind) -> Result<(), ExitCode> { match serialize_result(value, kind) { Ok(output) => print_json(&output), Err(error) => { write_error(&error); - exit(EXIT_RESOURCE_ERROR); + Err(ExitCode::from(EXIT_RESOURCE_ERROR)) } } } #[cfg(not(windows))] -fn main() { +fn main() -> ExitCode { write_error(&t!("main.windowsOnly")); - exit(EXIT_RESOURCE_ERROR); + ExitCode::from(EXIT_RESOURCE_ERROR) } #[cfg(windows)] -fn main() { +fn main() -> ExitCode { let args: Vec = std::env::args().collect(); if args.len() < 2 { write_error(&t!("main.missingOperation")); - exit(EXIT_INVALID_ARGS); + return ExitCode::from(EXIT_INVALID_ARGS); } let operation = args[1].as_str(); - let input_json = parse_input_arg(&args); + let input_json = match parse_input_arg(&args) { + Ok(input) => input, + Err(code) => return code, + }; let kind = if args.iter().any(|arg| arg == "--list") { InputKind::List } else if args.iter().any(|arg| arg == "--path") { @@ -156,46 +162,48 @@ fn main() { } _ => { write_error(&t!("main.unknownOperation", operation = operation)); - exit(EXIT_INVALID_ARGS); + return ExitCode::from(EXIT_INVALID_ARGS); } }; let result = match input { Ok(result) => result, Err((error, exit_code)) => { write_error(&error); - exit(exit_code); + return ExitCode::from(exit_code); } }; match result { Ok(value) => { - print_result(value, kind); - exit(EXIT_SUCCESS); + if let Err(code) = print_result(value, kind) { + return code; + } + ExitCode::from(EXIT_SUCCESS) } Err(error) => { write_error(&error.to_string()); - exit(if error.is_elevation_required() { + ExitCode::from(if error.is_elevation_required() { EXIT_ELEVATION_REQUIRED } else { EXIT_RESOURCE_ERROR - }); + }) } } } -fn parse_input_arg(args: &[String]) -> Option { +fn parse_input_arg(args: &[String]) -> Result, ExitCode> { let mut index = 2; while index < args.len() { if args[index] == "--input" || args[index] == "-i" { if index + 1 < args.len() { - return Some(args[index + 1].clone()); + return Ok(Some(args[index + 1].clone())); } write_error(&t!("main.missingInputValue")); - exit(EXIT_INVALID_ARGS); + return Err(ExitCode::from(EXIT_INVALID_ARGS)); } index += 1; } - None + Ok(None) } #[cfg(test)] diff --git a/resources/filecontent/src/main.rs b/resources/filecontent/src/main.rs index 1f4edef4c..a39582174 100644 --- a/resources/filecontent/src/main.rs +++ b/resources/filecontent/src/main.rs @@ -10,26 +10,38 @@ use rust_i18n::t; use serde::Serialize; use serde_json::json; use std::env; -use std::process::exit; +use std::process::ExitCode; rust_i18n::i18n!("locales", fallback = "en-us"); -const EXIT_SUCCESS: i32 = 0; -const EXIT_INVALID_ARGS: i32 = 1; -const EXIT_INVALID_INPUT: i32 = 2; -const EXIT_RESOURCE_ERROR: i32 = 3; +const EXIT_SUCCESS: u8 = 0; +const EXIT_INVALID_ARGS: u8 = 1; +const EXIT_INVALID_INPUT: u8 = 2; +const EXIT_RESOURCE_ERROR: u8 = 3; -fn main() { +fn main() -> ExitCode { let args: Vec = env::args().skip(1).collect(); let Some(operation) = args.first() else { - fail(EXIT_INVALID_ARGS, &t!("main.missingOperation")); + return fail(EXIT_INVALID_ARGS, &t!("main.missingOperation")); }; - let input = parse_input_arg(&args[1..]); + let input = match parse_input_arg(&args[1..]) { + Ok(input) => input, + Err(code) => return code, + }; match operation.as_str() { - "get" => handle_result(get(&require_input(input))), - "set" => handle_result(set(&require_input(input))), - "test" => handle_result(test(&require_input(input))), + "get" | "set" | "test" => { + let input = match require_input(input) { + Ok(input) => input, + Err(code) => return code, + }; + match operation.as_str() { + "get" => handle_result(get(&input)), + "set" => handle_result(set(&input)), + "test" => handle_result(test(&input)), + _ => unreachable!(), + } + } _ => fail( EXIT_INVALID_ARGS, &t!("main.unknownOperation", operation = operation), @@ -37,26 +49,26 @@ fn main() { } } -fn parse_input_arg(args: &[String]) -> Option<&str> { +fn parse_input_arg(args: &[String]) -> Result, ExitCode> { let mut index = 0; while index < args.len() { if args[index] == "--input" || args[index] == "-i" { let Some(input) = args.get(index + 1) else { - fail(EXIT_INVALID_ARGS, &t!("main.missingInputValue")); + return Err(fail(EXIT_INVALID_ARGS, &t!("main.missingInputValue"))); }; - return Some(input); + return Ok(Some(input)); } index += 1; } - None + Ok(None) } -fn require_input(input: Option<&str>) -> FileContent { +fn require_input(input: Option<&str>) -> Result { let Some(input) = input else { - fail(EXIT_INVALID_ARGS, &t!("main.missingInput")); + return Err(fail(EXIT_INVALID_ARGS, &t!("main.missingInput"))); }; - serde_json::from_str(input).unwrap_or_else(|error| { + serde_json::from_str(input).map_err(|error| { fail( EXIT_INVALID_INPUT, &t!("main.invalidJson", error = error.to_string()), @@ -64,16 +76,16 @@ fn require_input(input: Option<&str>) -> FileContent { }) } -fn handle_result(result: Result) -> ! { +fn handle_result(result: Result) -> ExitCode { let value = match result { Ok(value) => value, - Err(error) => fail(EXIT_RESOURCE_ERROR, &error), + Err(error) => return fail(EXIT_RESOURCE_ERROR, &error), }; match serde_json::to_string(&value) { Ok(json) => { println!("{json}"); - exit(EXIT_SUCCESS); + ExitCode::from(EXIT_SUCCESS) } Err(error) => fail( EXIT_RESOURCE_ERROR, @@ -82,7 +94,7 @@ fn handle_result(result: Result) -> ! { } } -fn fail(exit_code: i32, message: &str) -> ! { +fn fail(exit_code: u8, message: &str) -> ExitCode { eprintln!("{}", json!({ "error": message })); - exit(exit_code); + ExitCode::from(exit_code) } diff --git a/resources/osinfo/src/main.rs b/resources/osinfo/src/main.rs index ab4e4f464..e9deb1961 100644 --- a/resources/osinfo/src/main.rs +++ b/resources/osinfo/src/main.rs @@ -3,43 +3,41 @@ use dsc_lib_osinfo::{perform_test, OsInfo}; use std::io::Read; +use std::process::ExitCode; -fn main() { +fn main() -> ExitCode { + match run() { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("{error}"); + ExitCode::FAILURE + } + } +} + +fn run() -> Result<(), String> { let args: Vec = std::env::args().collect(); match args.get(1).map(String::as_str) { Some("export") => { - let json = serde_json::to_string(&OsInfo::new(true)).unwrap_or_else(|e| { - eprintln!("Failed to serialize OS info as JSON: {e}"); - std::process::exit(1); - }); + let json = serde_json::to_string(&OsInfo::new(true)) + .map_err(|e| format!("Failed to serialize OS info as JSON: {e}"))?; println!("{json}"); }, Some("test") => { let mut input = String::new(); - if let Err(e) = std::io::stdin().read_to_string(&mut input) { - eprintln!("Failed to read stdin: {e}"); - std::process::exit(1); - } - match perform_test(&input) { - Ok(result) => { - let json = serde_json::to_string(&result).unwrap_or_else(|e| { - eprintln!("Failed to serialize test result as JSON: {e}"); - std::process::exit(1); - }); - println!("{json}"); - }, - Err(e) => { - eprintln!("{e}"); - std::process::exit(1); - }, - } + std::io::stdin() + .read_to_string(&mut input) + .map_err(|e| format!("Failed to read stdin: {e}"))?; + let result = perform_test(&input)?; + let json = serde_json::to_string(&result) + .map_err(|e| format!("Failed to serialize test result as JSON: {e}"))?; + println!("{json}"); }, _ => { - let json = serde_json::to_string(&OsInfo::new(false)).unwrap_or_else(|e| { - eprintln!("Failed to serialize OS info as JSON: {e}"); - std::process::exit(1); - }); + let json = serde_json::to_string(&OsInfo::new(false)) + .map_err(|e| format!("Failed to serialize OS info as JSON: {e}"))?; println!("{json}"); }, } + Ok(()) } diff --git a/resources/process/src/main.rs b/resources/process/src/main.rs index 7c9695ede..6fb7bea62 100644 --- a/resources/process/src/main.rs +++ b/resources/process/src/main.rs @@ -3,7 +3,7 @@ mod process_info; use std::env; -use std::process::exit; +use std::process::ExitCode; use std::io::{self, Read}; use sysinfo::{ProcessesToUpdate, System}; use crate::process_info::ProcessInfo; @@ -41,7 +41,7 @@ fn print_input() { println!("{}", input.unwrap()); } -fn main() { +fn main() -> ExitCode { let args: Vec = env::args().collect(); if args.len() == 2 { // one argument passed @@ -52,20 +52,20 @@ fn main() { let json = serde_json::to_string(&p).unwrap(); println!("{json}"); } - exit(0); + ExitCode::SUCCESS }, "get" | "set" | "test" => { // used for testing only print_input(); - exit(0); + ExitCode::SUCCESS }, _ => { help(); - exit(1); + ExitCode::FAILURE }, } } else { help(); - exit(1); + ExitCode::FAILURE } } \ No newline at end of file diff --git a/resources/runcommandonset/src/main.rs b/resources/runcommandonset/src/main.rs index 6ac8b4374..e103d4736 100644 --- a/resources/runcommandonset/src/main.rs +++ b/resources/runcommandonset/src/main.rs @@ -3,7 +3,7 @@ use clap::Parser; use rust_i18n::{i18n, t}; -use std::{io::{self, Read, IsTerminal}, process::exit}; +use std::{io::{self, Read, IsTerminal}, process::ExitCode}; use tracing::{error, warn, debug, trace}; use args::{Arguments, SubCommand, TraceLevel}; @@ -16,7 +16,7 @@ pub mod utils; i18n!("locales", fallback = "en-us"); -fn main() { +fn main() -> ExitCode { let args = Arguments::parse(); let trace_level = match args.trace_level { Some(trace_level) => trace_level, @@ -53,7 +53,7 @@ fn main() { Ok(stdin) => stdin, Err(e) => { error!("{}: {e}", t!("main.invalidUtf8")); - exit(EXIT_INVALID_ARGS); + return ExitCode::from(EXIT_INVALID_ARGS); }, }; // parse_input expects at most 1 input, so wrapping Some(empty input) would throw it off @@ -70,11 +70,20 @@ fn main() { match args.subcommand { SubCommand::Get { arguments, executable, exit_code } => { - command = parse_input(arguments, executable, exit_code, stdin); + command = match parse_input(arguments, executable, exit_code, stdin) { + Ok(command) => command, + Err(code) => return ExitCode::from(code), + }; } SubCommand::Set { arguments, executable, exit_code } => { - command = parse_input(arguments, executable, exit_code, stdin); - let (exit_code, stdout, stderr) = invoke_command(command.executable.as_ref(), command.arguments.clone()); + command = match parse_input(arguments, executable, exit_code, stdin) { + Ok(command) => command, + Err(code) => return ExitCode::from(code), + }; + let (exit_code, stdout, stderr) = match invoke_command(command.executable.as_ref(), command.arguments.clone()) { + Ok(result) => result, + Err(code) => return ExitCode::from(code), + }; trace!("Stdout: {stdout}"); trace!("Stderr: {stderr}"); command.exit_code = exit_code; @@ -82,4 +91,5 @@ fn main() { } println!("{}", command.to_json()); + ExitCode::SUCCESS } diff --git a/resources/runcommandonset/src/utils.rs b/resources/runcommandonset/src/utils.rs index 54330743a..f31d001ab 100644 --- a/resources/runcommandonset/src/utils.rs +++ b/resources/runcommandonset/src/utils.rs @@ -2,17 +2,17 @@ // Licensed under the MIT License. use rust_i18n::t; -use std::{io::Read, process::{Command, exit, Stdio}}; +use std::{io::Read, process::{Command, Stdio}}; use tracing::{Level, error, debug, trace}; use tracing_subscriber::{filter::EnvFilter, layer::SubscriberExt, Layer}; use crate::args::{TraceFormat, TraceLevel}; use crate::runcommand; -pub const EXIT_INVALID_ARGS: i32 = 1; -pub const EXIT_DSC_ERROR: i32 = 2; +pub const EXIT_INVALID_ARGS: u8 = 1; +pub const EXIT_DSC_ERROR: u8 = 2; pub const EXIT_CODE_MISMATCH: i32 = 3; -pub const EXIT_INVALID_INPUT: i32 = 4; +pub const EXIT_INVALID_INPUT: u8 = 4; pub const EXIT_PROCESS_TERMINATED: i32 = 5; /// Initialize `RunCommand` struct from input provided via stdin or via CLI arguments. @@ -27,7 +27,7 @@ pub const EXIT_PROCESS_TERMINATED: i32 = 5; /// # Errors /// /// Error message then exit if the `RunCommand` struct cannot be initialized from the provided inputs. -pub fn parse_input(arguments: Option>, executable: Option, exit_code: i32, stdin: Option) -> runcommand::RunCommand { +pub fn parse_input(arguments: Option>, executable: Option, exit_code: i32, stdin: Option) -> Result { let command: runcommand::RunCommand; if let Some(input) = stdin { debug!("Input: {}", input); @@ -35,7 +35,7 @@ pub fn parse_input(arguments: Option>, executable: Option, e Ok(json) => json, Err(err) => { error!("{}: {err}", t!("utils.invalidInput")); - exit(EXIT_INVALID_INPUT); + return Err(EXIT_INVALID_INPUT); } } } else if let Some(executable) = executable { @@ -47,9 +47,9 @@ pub fn parse_input(arguments: Option>, executable: Option, e } else { error!("{}", t!("utils.executableRequired")); - exit(EXIT_INVALID_INPUT); + return Err(EXIT_INVALID_INPUT); } - command + Ok(command) } /// Setup tracing subscriber based on the provided trace level and format. @@ -119,7 +119,7 @@ pub fn enable_tracing(trace_level: &TraceLevel, trace_format: &TraceFormat) { /// # Errors /// /// Error message then exit if the command fails to execute or stdin/stdout/stderr cannot be opened. -pub fn invoke_command(executable: &str, args: Option>) -> (i32, String, String) { +pub fn invoke_command(executable: &str, args: Option>) -> Result<(i32, String, String), u8> { // originally implemented in dsc_lib/src/dscresources/command_resource.rs trace!("Invoking command {} with args {:?}", executable, args); let mut command = Command::new(executable); @@ -134,33 +134,33 @@ pub fn invoke_command(executable: &str, args: Option>) -> (i32, Stri Ok(child) => child, Err(e) => { error!("{} '{executable}': {e}", t!("utils.failedToExecute")); - exit(EXIT_DSC_ERROR); + return Err(EXIT_DSC_ERROR); } }; let Some(mut child_stdout) = child.stdout.take() else { error!("{} {executable}", t!("utils.failedOpenStdout")); - exit(EXIT_DSC_ERROR); + return Err(EXIT_DSC_ERROR); }; let mut stdout_buf = Vec::new(); match child_stdout.read_to_end(&mut stdout_buf) { Ok(_) => (), Err(e) => { error!("{} '{executable}': {e}", t!("utils.failedReadStdout")); - exit(EXIT_DSC_ERROR); + return Err(EXIT_DSC_ERROR); } } let Some(mut child_stderr) = child.stderr.take() else { error!("{} {executable}", t!("utils.failedOpenStderr")); - exit(EXIT_DSC_ERROR); + return Err(EXIT_DSC_ERROR); }; let mut stderr_buf = Vec::new(); match child_stderr.read_to_end(&mut stderr_buf) { Ok(_) => (), Err(e) => { error!("{} '{executable}': {e}", t!("utils.failedReadStderr")); - exit(EXIT_DSC_ERROR); + return Err(EXIT_DSC_ERROR); } } @@ -168,12 +168,12 @@ pub fn invoke_command(executable: &str, args: Option>) -> (i32, Stri Ok(exit_status) => exit_status, Err(e) => { error!("{} '{executable}': {e}", t!("utils.failedWait")); - exit(EXIT_DSC_ERROR); + return Err(EXIT_DSC_ERROR); } }; let exit_code = exit_status.code().unwrap_or(EXIT_PROCESS_TERMINATED); let stdout = String::from_utf8_lossy(&stdout_buf).to_string(); let stderr = String::from_utf8_lossy(&stderr_buf).to_string(); - (exit_code, stdout, stderr) + Ok((exit_code, stdout, stderr)) } diff --git a/resources/sshdconfig/src/main.rs b/resources/sshdconfig/src/main.rs index b5d1f82f2..ecd11285f 100644 --- a/resources/sshdconfig/src/main.rs +++ b/resources/sshdconfig/src/main.rs @@ -5,7 +5,7 @@ use clap::Parser; use rust_i18n::{i18n, t}; use schemars::schema_for; use serde_json::Map; -use std::process::exit; +use std::process::ExitCode; use tracing::{debug, error}; use args::{Args, Command, DefaultShell, Setting}; @@ -31,10 +31,10 @@ mod util; i18n!("locales", fallback = "en-us"); -const EXIT_SUCCESS: i32 = 0; -const EXIT_FAILURE: i32 = 1; +const EXIT_SUCCESS: u8 = 0; +const EXIT_FAILURE: u8 = 1; -fn main() { +fn main() -> ExitCode { let args = Args::parse(); enable_tracing(args.trace_level.as_ref(), &args.trace_format); @@ -81,15 +81,15 @@ fn main() { Ok(json) => println!("{json}"), Err(e) => { error!("{}", e); - exit(EXIT_FAILURE); + return ExitCode::from(EXIT_FAILURE); } } } - exit(EXIT_SUCCESS); + ExitCode::from(EXIT_SUCCESS) } Err(e) => { error!("{}", e); - exit(EXIT_FAILURE); + ExitCode::from(EXIT_FAILURE) } } } diff --git a/resources/windows_firewall/src/main.rs b/resources/windows_firewall/src/main.rs index 60d229143..4434394fb 100644 --- a/resources/windows_firewall/src/main.rs +++ b/resources/windows_firewall/src/main.rs @@ -7,126 +7,144 @@ mod types; mod firewall; use rust_i18n::t; -use std::process::exit; +use std::process::ExitCode; use types::FirewallRuleList; rust_i18n::i18n!("locales", fallback = "en-us"); -const EXIT_SUCCESS: i32 = 0; -const EXIT_INVALID_ARGS: i32 = 1; -const EXIT_INVALID_INPUT: i32 = 2; -const EXIT_FIREWALL_ERROR: i32 = 3; +const EXIT_SUCCESS: u8 = 0; +const EXIT_INVALID_ARGS: u8 = 1; +const EXIT_INVALID_INPUT: u8 = 2; +const EXIT_FIREWALL_ERROR: u8 = 3; pub(crate) fn write_error(message: &str) { eprintln!("{}", serde_json::json!({ "error": message })); } -fn print_json(value: &impl serde::Serialize) { +fn print_json(value: &impl serde::Serialize) -> Result<(), ExitCode> { match serde_json::to_string(value) { - Ok(json) => println!("{json}"), + Ok(json) => { + println!("{json}"); + Ok(()) + } Err(error) => { write_error(&t!("main.invalidJson", error = error.to_string())); - exit(EXIT_FIREWALL_ERROR); + Err(ExitCode::from(EXIT_FIREWALL_ERROR)) } } } -fn require_input(input_json: Option) -> FirewallRuleList { +fn require_input(input_json: Option) -> Result { let json = match input_json { Some(json) => json, None => { write_error(&t!("main.missingInput")); - exit(EXIT_INVALID_ARGS); + return Err(ExitCode::from(EXIT_INVALID_ARGS)); } }; match serde_json::from_str(&json) { - Ok(value) => value, + Ok(value) => Ok(value), Err(error) => { write_error(&t!("main.invalidJson", error = error.to_string())); - exit(EXIT_INVALID_INPUT); + Err(ExitCode::from(EXIT_INVALID_INPUT)) } } } #[cfg(not(windows))] -fn main() { +fn main() -> ExitCode { write_error(&t!("main.windowsOnly")); - exit(EXIT_FIREWALL_ERROR); + ExitCode::from(EXIT_FIREWALL_ERROR) } #[cfg(windows)] -fn main() { +fn main() -> ExitCode { let args: Vec = std::env::args().collect(); if args.len() < 2 { write_error(&t!("main.missingOperation")); - exit(EXIT_INVALID_ARGS); + return ExitCode::from(EXIT_INVALID_ARGS); } let operation = args[1].as_str(); - let input_json = parse_input_arg(&args); + let input_json = match parse_input_arg(&args) { + Ok(input) => input, + Err(code) => return code, + }; match operation { "get" => { - let input = require_input(input_json); + let input = match require_input(input_json) { + Ok(input) => input, + Err(code) => return code, + }; match firewall::get_rules(&input) { Ok(result) => { - print_json(&result); - exit(EXIT_SUCCESS); + if let Err(code) = print_json(&result) { + return code; + } + ExitCode::from(EXIT_SUCCESS) } Err(error) => { write_error(&error.to_string()); - exit(EXIT_FIREWALL_ERROR); + ExitCode::from(EXIT_FIREWALL_ERROR) } } } "set" => { let what_if = parse_what_if_arg(&args); - let input = require_input(input_json); + let input = match require_input(input_json) { + Ok(input) => input, + Err(code) => return code, + }; match firewall::set_rules(&input, what_if) { Ok(result) => { - print_json(&result); - exit(EXIT_SUCCESS); + if let Err(code) = print_json(&result) { + return code; + } + ExitCode::from(EXIT_SUCCESS) } Err(error) => { write_error(&error.to_string()); - exit(EXIT_FIREWALL_ERROR); + ExitCode::from(EXIT_FIREWALL_ERROR) } } } "export" => { match firewall::export_rules() { Ok(result) => { - print_json(&result); - exit(EXIT_SUCCESS); + if let Err(code) = print_json(&result) { + return code; + } + ExitCode::from(EXIT_SUCCESS) } Err(error) => { write_error(&error.to_string()); - exit(EXIT_FIREWALL_ERROR); + ExitCode::from(EXIT_FIREWALL_ERROR) } } } _ => { write_error(&t!("main.unknownOperation", operation = operation)); - exit(EXIT_INVALID_ARGS); + ExitCode::from(EXIT_INVALID_ARGS) } } } -fn parse_input_arg(args: &[String]) -> Option { +fn parse_input_arg(args: &[String]) -> Result, ExitCode> { let mut index = 2; while index < args.len() { if args[index] == "--input" || args[index] == "-i" { if index + 1 < args.len() { - return Some(args[index + 1].clone()); + return Ok(Some(args[index + 1].clone())); } write_error(&t!("main.missingInputValue")); - exit(EXIT_INVALID_ARGS); + return Err(ExitCode::from(EXIT_INVALID_ARGS)); } index += 1; } - None + Ok(None) } fn parse_what_if_arg(args: &[String]) -> bool { diff --git a/resources/windows_service/src/main.rs b/resources/windows_service/src/main.rs index 776d4307b..a014b09ee 100644 --- a/resources/windows_service/src/main.rs +++ b/resources/windows_service/src/main.rs @@ -7,7 +7,7 @@ mod types; mod service; use rust_i18n::t; -use std::process::exit; +use std::process::ExitCode; use types::WindowsService; @@ -18,100 +18,118 @@ fn write_error(message: &str) { rust_i18n::i18n!("locales", fallback = "en-us"); -const EXIT_SUCCESS: i32 = 0; -const EXIT_INVALID_ARGS: i32 = 1; -const EXIT_INVALID_INPUT: i32 = 2; -const EXIT_SERVICE_ERROR: i32 = 3; +const EXIT_SUCCESS: u8 = 0; +const EXIT_INVALID_ARGS: u8 = 1; +const EXIT_INVALID_INPUT: u8 = 2; +const EXIT_SERVICE_ERROR: u8 = 3; /// Deserialize the required JSON input into a `WindowsService`, or exit with an error. -fn require_input(input_json: Option) -> WindowsService { +fn require_input(input_json: Option) -> Result { let json = match input_json { Some(j) => j, None => { write_error(&t!("main.missingInput")); - exit(EXIT_INVALID_ARGS); + return Err(ExitCode::from(EXIT_INVALID_ARGS)); } }; match serde_json::from_str(&json) { - Ok(v) => v, + Ok(v) => Ok(v), Err(e) => { write_error(&t!("main.invalidJson", error = e.to_string())); - exit(EXIT_INVALID_INPUT); + Err(ExitCode::from(EXIT_INVALID_INPUT)) } } } /// Serialize a value to JSON and print it to stdout, or exit with an error. -fn print_json(value: &impl serde::Serialize) { +fn print_json(value: &impl serde::Serialize) -> Result<(), ExitCode> { match serde_json::to_string(value) { - Ok(json) => println!("{json}"), + Ok(json) => { + println!("{json}"); + Ok(()) + } Err(e) => { write_error(&t!("main.invalidJson", error = e.to_string())); - exit(EXIT_SERVICE_ERROR); + Err(ExitCode::from(EXIT_SERVICE_ERROR)) } } } #[cfg(not(windows))] -fn main() { +fn main() -> ExitCode { write_error(&t!("main.windowsOnly")); - exit(EXIT_SERVICE_ERROR); + ExitCode::from(EXIT_SERVICE_ERROR) } #[cfg(windows)] -fn main() { +fn main() -> ExitCode { let args: Vec = std::env::args().collect(); if args.len() < 2 { write_error(&t!("main.missingOperation")); - exit(EXIT_INVALID_ARGS); + return ExitCode::from(EXIT_INVALID_ARGS); } let operation = args[1].as_str(); - let input_json = parse_input_arg(&args); + let input_json = match parse_input_arg(&args) { + Ok(input) => input, + Err(code) => return code, + }; let what_if = parse_what_if_flag(&args); match operation { "get" => { - let input = require_input(input_json); + let input = match require_input(input_json) { + Ok(input) => input, + Err(code) => return code, + }; match service::get_service(&input) { Ok(result) => { - print_json(&result); - exit(EXIT_SUCCESS); + if let Err(code) = print_json(&result) { + return code; + } + ExitCode::from(EXIT_SUCCESS) } Err(e) => { write_error(&e.to_string()); - exit(EXIT_SERVICE_ERROR); + ExitCode::from(EXIT_SERVICE_ERROR) } } } "set" => { - let input = require_input(input_json); + let input = match require_input(input_json) { + Ok(input) => input, + Err(code) => return code, + }; // In what-if, if the desired state is _exist: false, route to delete // so the projected state and metadata describe a delete operation. if what_if && matches!(input.exist, Some(false)) { match service::what_if_delete_service(&input) { Ok(result) => { - print_json(&result); - exit(EXIT_SUCCESS); + if let Err(code) = print_json(&result) { + return code; + } + return ExitCode::from(EXIT_SUCCESS); } Err(e) => { write_error(&e.to_string()); - exit(EXIT_SERVICE_ERROR); + return ExitCode::from(EXIT_SERVICE_ERROR); } } } match service::set_service(&input, what_if) { Ok(result) => { - print_json(&result); - exit(EXIT_SUCCESS); + if let Err(code) = print_json(&result) { + return code; + } + ExitCode::from(EXIT_SUCCESS) } Err(e) => { write_error(&e.to_string()); - exit(EXIT_SERVICE_ERROR); + ExitCode::from(EXIT_SERVICE_ERROR) } } } @@ -119,37 +137,39 @@ fn main() { match service::export_services() { Ok(services) => { for svc in &services { - print_json(svc); + if let Err(code) = print_json(svc) { + return code; + } } - exit(EXIT_SUCCESS); + ExitCode::from(EXIT_SUCCESS) } Err(e) => { write_error(&e.to_string()); - exit(EXIT_SERVICE_ERROR); + ExitCode::from(EXIT_SERVICE_ERROR) } } } _ => { write_error(&t!("main.unknownOperation", operation = operation)); - exit(EXIT_INVALID_ARGS); + ExitCode::from(EXIT_INVALID_ARGS) } } } /// Parse the `--input ` argument from the command-line args. -fn parse_input_arg(args: &[String]) -> Option { +fn parse_input_arg(args: &[String]) -> Result, ExitCode> { let mut i = 2; // skip binary name and operation while i < args.len() { if args[i] == "--input" || args[i] == "-i" { if i + 1 < args.len() { - return Some(args[i + 1].clone()); + return Ok(Some(args[i + 1].clone())); } write_error(&t!("main.missingInputValue")); - exit(EXIT_INVALID_ARGS); + return Err(ExitCode::from(EXIT_INVALID_ARGS)); } i += 1; } - None + Ok(None) } /// Parse the `--what-if` / `-w` flag from the command-line args. diff --git a/tools/dsctest/src/adapter.rs b/tools/dsctest/src/adapter.rs index 74b3aebeb..2b0b7136d 100644 --- a/tools/dsctest/src/adapter.rs +++ b/tools/dsctest/src/adapter.rs @@ -83,7 +83,7 @@ pub fn adapt(resource_type: &str, input: &str, operation: &AdapterOperation, res }; println!("{}", serde_json::to_string(&resource_one).unwrap()); println!("{}", serde_json::to_string(&resource_two).unwrap()); - std::process::exit(0); + Ok(String::new()) }, AdapterOperation::Get => { match resource_type { @@ -231,7 +231,7 @@ pub fn adapt(resource_type: &str, input: &str, operation: &AdapterOperation, res path: None, }; println!("{}", serde_json::to_string(&adapted_one).unwrap()); - std::process::exit(0); + Ok(String::new()) }, "Adapted/Two" => { if let Some(version) = resource_version && version != ADAPTED_TWO_VERSION { @@ -249,7 +249,7 @@ pub fn adapt(resource_type: &str, input: &str, operation: &AdapterOperation, res path: None, }; println!("{}", serde_json::to_string(&adapted_two).unwrap()); - std::process::exit(0); + Ok(String::new()) }, "Adapted/Three" => { let adapted_three = AdaptedOne { @@ -264,7 +264,7 @@ pub fn adapt(resource_type: &str, input: &str, operation: &AdapterOperation, res path: None, }; println!("{}", serde_json::to_string(&adapted_three).unwrap()); - std::process::exit(0); + Ok(String::new()) }, "Adapted/SecurityContextElevated" => { let adapted_security_context_elevated = AdaptedOne { diff --git a/tools/dsctest/src/exit_code.rs b/tools/dsctest/src/exit_code.rs index 478f903ca..3792d9450 100644 --- a/tools/dsctest/src/exit_code.rs +++ b/tools/dsctest/src/exit_code.rs @@ -8,5 +8,5 @@ use serde::{Deserialize, Serialize}; #[serde(deny_unknown_fields)] pub struct ExitCode { #[serde(rename = "exitCode")] - pub exit_code: i32, + pub exit_code: u8, } diff --git a/tools/dsctest/src/export_schema.rs b/tools/dsctest/src/export_schema.rs index 3318dbf07..e56a41d48 100644 --- a/tools/dsctest/src/export_schema.rs +++ b/tools/dsctest/src/export_schema.rs @@ -34,7 +34,7 @@ pub struct ExportSchema { pub name: String, } -pub fn invoke_export_schema(input: &str) -> String { +pub fn invoke_export_schema(input: &str) -> Result { let instances = vec![ Schema { name: Names::Steve, @@ -49,10 +49,7 @@ pub fn invoke_export_schema(input: &str) -> String { let filter: ExportSchema = if !input.is_empty() { match serde_json::from_str(input) { Ok(filter) => filter, - Err(err) => { - eprintln!("Error JSON does not match schema: {err}"); - std::process::exit(1); - } + Err(err) => return Err(format!("Error JSON does not match schema: {err}")), } } else { ExportSchema { @@ -82,5 +79,5 @@ pub fn invoke_export_schema(input: &str) -> String { } count -= 1; } - output + Ok(output) } diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index 2363f5b2f..b195c7e6a 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -50,10 +50,19 @@ use crate::trace::Trace; use crate::version::Version; use crate::whatif::WhatIf; use crate::whatif_delete::WhatIfDelete; +use std::process::ExitCode as ProcessExitCode; use std::{thread, time::Duration}; #[allow(clippy::too_many_lines)] -fn main() { +fn main() -> ProcessExitCode { + match run() { + Ok(()) => ProcessExitCode::SUCCESS, + Err(code) => ProcessExitCode::from(code), + } +} + +#[allow(clippy::too_many_lines)] +fn run() -> Result<(), u8> { let args = Args::parse(); let json = match args.subcommand { SubCommand::Adapter { input , resource_type, resource_path, resource_version, operation } => { @@ -61,7 +70,7 @@ fn main() { Ok(result) => result, Err(err) => { eprintln!("Error adapting resource: {err}"); - std::process::exit(1); + return Err(1); } } }, @@ -70,12 +79,12 @@ fn main() { Ok(copy_resource) => copy_resource, Err(err) => { eprintln!("Error JSON does not match schema: {err}"); - std::process::exit(1); + return Err(1); } }; if let Err(err) = copy_the_resource(©_resource.source_file, ©_resource.type_name) { eprintln!("Error copying resource: {err}"); - std::process::exit(1); + return Err(1); } input }, @@ -84,7 +93,7 @@ fn main() { Ok(delete) => delete, Err(err) => { eprintln!("Error JSON does not match schema: {err}"); - std::process::exit(1); + return Err(1); } }; delete.delete_called = Some(true); @@ -95,7 +104,7 @@ fn main() { Ok(exist) => exist, Err(err) => { eprintln!("Error JSON does not match schema: {err}"); - std::process::exit(1); + return Err(1); } }; if exist.exist { @@ -111,12 +120,12 @@ fn main() { Ok(exit_code) => exit_code, Err(err) => { eprintln!("Error JSON does not match schema: {err}"); - std::process::exit(1); + return Err(1); } }; if exit_code.exit_code != 0 { eprintln!("Exiting with code: {}", exit_code.exit_code); - std::process::exit(exit_code.exit_code); + return Err(exit_code.exit_code); } input }, @@ -125,7 +134,7 @@ fn main() { Ok(export) => export, Err(err) => { eprintln!("Error JSON does not match schema: {err}"); - std::process::exit(1); + return Err(1); } }; for i in 0..export.count { @@ -140,14 +149,20 @@ fn main() { String::new() }, SubCommand::ExportSchema { input } => { - invoke_export_schema(&input) + match invoke_export_schema(&input) { + Ok(output) => output, + Err(err) => { + eprintln!("{err}"); + return Err(1); + } + } }, SubCommand::Exporter { input } => { let exporter = match serde_json::from_str::(&input) { Ok(exporter) => exporter, Err(err) => { eprintln!("Error JSON does not match schema: {err}"); - std::process::exit(1); + return Err(1); } }; for type_name in exporter.type_names { @@ -180,34 +195,38 @@ fn main() { let resource = if input.is_empty() { // If neither name nor id is provided, return the first instance - instances.into_iter().next().unwrap_or_else(|| { + let Some(resource) = instances.into_iter().next() else { eprintln!("No instances found"); - std::process::exit(1); - }) + return Err(1); + }; + resource } else { let get = match serde_json::from_str::(&input) { Ok(get) => get, Err(err) => { eprintln!("Error JSON does not match schema: {err}"); - std::process::exit(1); + return Err(1); } }; // depending on the input, return the appropriate instance whether it is name or id or both if let Some(name) = get.name { - instances.into_iter().find(|i| i.name.as_ref() == Some(&name)).unwrap_or_else(|| { + let Some(resource) = instances.into_iter().find(|i| i.name.as_ref() == Some(&name)) else { eprintln!("No instance found with name: {name}"); - std::process::exit(1); - }) + return Err(1); + }; + resource } else if let Some(id) = get.id { - instances.into_iter().find(|i| i.id == Some(id)).unwrap_or_else(|| { + let Some(resource) = instances.into_iter().find(|i| i.id == Some(id)) else { eprintln!("No instance found with id: {id}"); - std::process::exit(1); - }) + return Err(1); + }; + resource } else { - instances.into_iter().next().unwrap_or_else(|| { + let Some(resource) = instances.into_iter().next() else { eprintln!("No instances found"); - std::process::exit(1); - }) + return Err(1); + }; + resource } }; serde_json::to_string(&resource).unwrap() @@ -217,7 +236,7 @@ fn main() { Ok(in_desired_state) => in_desired_state, Err(err) => { eprintln!("Error JSON does not match schema: {err}"); - std::process::exit(1); + return Err(1); } }; in_desired_state.value_one = 1; @@ -235,7 +254,7 @@ fn main() { Ok(metadata) => metadata, Err(err) => { eprintln!("Error JSON does not match schema: {err}"); - std::process::exit(1); + return Err(1); } }; metadata.name = Some(format!("Metadata example {}", i+1)); @@ -253,7 +272,7 @@ fn main() { Ok(op) => op, Err(err) => { eprintln!("Error JSON does not match schema: {err}"); - std::process::exit(1); + return Err(1); } }; operation_result.operation = Some(operation.to_lowercase()); @@ -264,7 +283,7 @@ fn main() { Ok(re) => re, Err(err) => { eprintln!("Error JSON does not match schema: {err}"); - std::process::exit(1); + return Err(1); } }; match operation { @@ -285,7 +304,7 @@ fn main() { Ok(rr) => rr, Err(err) => { eprintln!("Error JSON does not match schema: {err}"); - std::process::exit(1); + return Err(1); } }; serde_json::to_string(&restart_required).unwrap() @@ -295,7 +314,7 @@ fn main() { Ok(sd) => sd, Err(err) => { eprintln!("Error JSON does not match schema: {err}"); - std::process::exit(1); + return Err(1); } }; let mut actual = serde_json::json!({"name": schema_default.name}); @@ -389,7 +408,7 @@ fn main() { Ok(sleep) => sleep, Err(err) => { eprintln!("Error JSON does not match schema: {err}"); - std::process::exit(1); + return Err(1); } }; thread::sleep(Duration::from_secs(sleep.seconds)); @@ -400,7 +419,7 @@ fn main() { Ok(s) => s, Err(err) => { eprintln!("Error JSON does not match schema: {err}"); - std::process::exit(1); + return Err(1); } }; // Actual state always returns valueOne=1, valueTwo=2 @@ -501,4 +520,5 @@ fn main() { if !json.is_empty() { println!("{json}"); } + Ok(()) } diff --git a/y2j/src/main.rs b/y2j/src/main.rs index d69c18bae..a3f1030a9 100644 --- a/y2j/src/main.rs +++ b/y2j/src/main.rs @@ -1,29 +1,31 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use std::{io::{self, Read, IsTerminal}, process::exit}; +use std::{io::{self, Read, IsTerminal}, process::ExitCode}; use syntect::easy::HighlightLines; use syntect::parsing::SyntaxSet; use syntect::highlighting::{ThemeSet, Style}; use syntect::util::{as_24_bit_terminal_escaped, LinesWithEndings}; -const EXIT_SUCCESS: i32 = 0; -const EXIT_INVALID_INPUT: i32 = 1; +const EXIT_INVALID_INPUT: u8 = 1; -fn main() { +fn main() -> ExitCode { + match run() { + Ok(()) => ExitCode::SUCCESS, + Err(message) => { + eprintln!("{message}"); + ExitCode::from(EXIT_INVALID_INPUT) + } + } +} + +fn run() -> Result<(), String> { let input: String = if std::io::stdin().is_terminal() { - eprintln!("Error: Input JSON/YAML via STDIN is required."); - exit(EXIT_INVALID_INPUT); + return Err("Error: Input JSON/YAML via STDIN is required.".to_string()); } else { let mut buffer: Vec = Vec::new(); io::stdin().read_to_end(&mut buffer).unwrap(); - match String::from_utf8(buffer) { - Ok(input) => input, - Err(e) => { - eprintln!("Invalid UTF-8 sequence: {e}"); - exit(EXIT_INVALID_INPUT); - } - } + String::from_utf8(buffer).map_err(|e| format!("Invalid UTF-8 sequence: {e}"))? }; let mut is_json = true; @@ -31,10 +33,7 @@ fn main() { is_json = false; match serde_yaml::from_str(&input) { Ok(yaml) => yaml, - Err(err) => { - eprintln!("Error: Input is not valid JSON or YAML: {err}"); - exit(EXIT_INVALID_INPUT); - } + Err(err) => return Err(format!("Error: Input is not valid JSON or YAML: {err}")), } }; @@ -65,5 +64,5 @@ fn main() { println!("{output}"); } - exit(EXIT_SUCCESS); + Ok(()) } From 4f79ae82135d4bc5a0130363708033c2781f7afa Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Wed, 7 Oct 2026 20:03:44 -0700 Subject: [PATCH 2/3] Address ExitCode review feedback Update environment variable tests for Result-returning argument parsing and correct helper documentation to describe propagated exit codes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- resources/environment_variable/src/main.rs | 7 ++++--- resources/runcommandonset/src/utils.rs | 4 ++-- resources/windows_service/src/main.rs | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/resources/environment_variable/src/main.rs b/resources/environment_variable/src/main.rs index a298acfa7..a053ceace 100644 --- a/resources/environment_variable/src/main.rs +++ b/resources/environment_variable/src/main.rs @@ -349,12 +349,13 @@ mod tests { fn finds_long_and_short_input_arguments() { assert_eq!( parse_input_arg(&["exe".into(), "get".into(), "--input".into(), "{}".into()]), - Some("{}".to_string()) + Ok(Some("{}".to_string())) ); assert_eq!( parse_input_arg(&["exe".into(), "get".into(), "-i".into(), "[]".into()]), - Some("[]".to_string()) + Ok(Some("[]".to_string())) ); - assert_eq!(parse_input_arg(&["exe".into(), "get".into()]), None); + assert_eq!(parse_input_arg(&["exe".into(), "get".into()]), Ok(None)); + assert!(parse_input_arg(&["exe".into(), "get".into(), "--input".into()]).is_err()); } } diff --git a/resources/runcommandonset/src/utils.rs b/resources/runcommandonset/src/utils.rs index f31d001ab..ed26a46ae 100644 --- a/resources/runcommandonset/src/utils.rs +++ b/resources/runcommandonset/src/utils.rs @@ -26,7 +26,7 @@ pub const EXIT_PROCESS_TERMINATED: i32 = 5; /// /// # Errors /// -/// Error message then exit if the `RunCommand` struct cannot be initialized from the provided inputs. +/// Returns an exit code if the `RunCommand` struct cannot be initialized from the provided inputs. pub fn parse_input(arguments: Option>, executable: Option, exit_code: i32, stdin: Option) -> Result { let command: runcommand::RunCommand; if let Some(input) = stdin { @@ -118,7 +118,7 @@ pub fn enable_tracing(trace_level: &TraceLevel, trace_format: &TraceFormat) { /// /// # Errors /// -/// Error message then exit if the command fails to execute or stdin/stdout/stderr cannot be opened. +/// Returns an exit code if the command fails to execute or stdin/stdout/stderr cannot be opened. pub fn invoke_command(executable: &str, args: Option>) -> Result<(i32, String, String), u8> { // originally implemented in dsc_lib/src/dscresources/command_resource.rs trace!("Invoking command {} with args {:?}", executable, args); diff --git a/resources/windows_service/src/main.rs b/resources/windows_service/src/main.rs index a014b09ee..7a0dd203d 100644 --- a/resources/windows_service/src/main.rs +++ b/resources/windows_service/src/main.rs @@ -23,7 +23,7 @@ const EXIT_INVALID_ARGS: u8 = 1; const EXIT_INVALID_INPUT: u8 = 2; const EXIT_SERVICE_ERROR: u8 = 3; -/// Deserialize the required JSON input into a `WindowsService`, or exit with an error. +/// Deserialize the required JSON input into a `WindowsService`, or return an exit code. fn require_input(input_json: Option) -> Result { let json = match input_json { Some(j) => j, @@ -41,7 +41,7 @@ fn require_input(input_json: Option) -> Result } } -/// Serialize a value to JSON and print it to stdout, or exit with an error. +/// Serialize a value to JSON and print it to stdout, or return an exit code. fn print_json(value: &impl serde::Serialize) -> Result<(), ExitCode> { match serde_json::to_string(value) { Ok(json) => { From b7b7014323dad4832660ba69ff7ac24d97bfc146 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Wed, 7 Oct 2026 20:28:18 -0700 Subject: [PATCH 3/3] Cover Rust executable error paths Exercise invalid input, missing instance, and command failure paths across dsctest and cross-platform Rust resources so changed-line coverage includes the new ExitCode propagation branches. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- dsc/tests/dsc_test_resource_errors.tests.ps1 | 117 +++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 dsc/tests/dsc_test_resource_errors.tests.ps1 diff --git a/dsc/tests/dsc_test_resource_errors.tests.ps1 b/dsc/tests/dsc_test_resource_errors.tests.ps1 new file mode 100644 index 000000000..e0d341b08 --- /dev/null +++ b/dsc/tests/dsc_test_resource_errors.tests.ps1 @@ -0,0 +1,117 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Test resource executable error handling' { + It 'Returns failure for invalid input' -ForEach @( + @{ Subcommand = 'copy-resource' } + @{ Subcommand = 'delete' } + @{ Subcommand = 'exist' } + @{ Subcommand = 'exit-code' } + @{ Subcommand = 'export' } + @{ Subcommand = 'export-schema' } + @{ Subcommand = 'exporter' } + @{ Subcommand = 'get' } + @{ Subcommand = 'in-desired-state' } + @{ Subcommand = 'metadata' } + @{ Subcommand = 'operation'; ExtraArguments = @('--operation', 'get') } + @{ Subcommand = 'refresh-env'; ExtraArguments = @('--operation', 'get') } + @{ Subcommand = 'restart-required' } + @{ Subcommand = 'schema-default' } + @{ Subcommand = 'sleep' } + @{ Subcommand = 'state-and-diff' } + ) { + $arguments = @($Subcommand, '--input', '{invalid') + $ExtraArguments + + $null = & dsctest @arguments 2>$TestDrive/error.log + + $LASTEXITCODE | Should -Be 1 + (Get-Content -Raw $TestDrive/error.log) | Should -Match 'Error JSON does not match schema' + } + + It 'Returns failure when an adapter receives invalid input' { + $null = dsctest adapter ` + --operation set ` + --resource-type Adapted/One ` + --input '{invalid' 2>$TestDrive/error.log + + $LASTEXITCODE | Should -Be 1 + (Get-Content -Raw $TestDrive/error.log) | Should -Match 'Error adapting resource' + } + + It 'Returns failure when a named instance does not exist' { + $null = dsctest get --input '{"name":"missing"}' 2>$TestDrive/error.log + + $LASTEXITCODE | Should -Be 1 + (Get-Content -Raw $TestDrive/error.log) | Should -Match 'No instance found with name' + } + + It 'Returns failure when an ID instance does not exist' { + $null = dsctest get --input '{"id":999}' 2>$TestDrive/error.log + + $LASTEXITCODE | Should -Be 1 + (Get-Content -Raw $TestDrive/error.log) | Should -Match 'No instance found with id' + } +} + +Describe 'Rust resource executable error handling' { + It 'Returns failure when FileContent has no operation' { + $null = filecontent 2>$TestDrive/error.log + + $LASTEXITCODE | Should -Be 1 + (Get-Content -Raw $TestDrive/error.log) | Should -Match 'Missing operation' + } + + It 'Returns failure when FileContent has no input' { + $null = filecontent get 2>$TestDrive/error.log + + $LASTEXITCODE | Should -Be 1 + (Get-Content -Raw $TestDrive/error.log) | Should -Match 'Missing --input argument' + } + + It 'Returns failure when FileContent input is invalid JSON' { + $null = filecontent get --input '{invalid' 2>$TestDrive/error.log + + $LASTEXITCODE | Should -Be 2 + (Get-Content -Raw $TestDrive/error.log) | Should -Match 'Invalid JSON input' + } + + It 'Returns failure when DSC Echo input is invalid JSON' { + $null = dscecho --input '{invalid' 2>$TestDrive/error.log + + $LASTEXITCODE | Should -Be 1 + (Get-Content -Raw $TestDrive/error.log) | Should -Match 'JSON does not match schema' + } + + It 'Returns failure when Process has no operation' { + $null = process 2>$TestDrive/error.log + + $LASTEXITCODE | Should -Be 1 + } + + It 'Returns failure when Process has an unknown operation' { + $null = process unknown 2>$TestDrive/error.log + + $LASTEXITCODE | Should -Be 1 + } + + It 'Returns piped input from the Process test operation' { + $result = 'hello' | process test 2>$TestDrive/error.log + + $LASTEXITCODE | Should -Be 0 + $result[0] | Should -BeExactly 'hello' + } + + It 'Returns failure when OSInfo test input is invalid JSON' { + $null = '{invalid' | osinfo test 2>$TestDrive/error.log + + $LASTEXITCODE | Should -Be 1 + (Get-Content -Raw $TestDrive/error.log) | Should -Not -BeNullOrEmpty + } + + It 'Returns failure when y2j input is neither JSON nor YAML' { + $null = "`tinvalid" | y2j 2>$TestDrive/error.log + + $LASTEXITCODE | Should -Be 1 + (Get-Content -Raw $TestDrive/error.log) | Should -Match 'Input is not valid JSON or YAML' + } +}