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/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' + } +} 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/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..a053ceace 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)] @@ -341,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/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..ed26a46ae 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. @@ -26,8 +26,8 @@ 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 { +/// 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 { 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. @@ -118,8 +118,8 @@ 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) { +/// 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); 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..7a0dd203d 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 { +/// 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, 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) { +/// 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) => 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(()) }