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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 24 additions & 14 deletions .github/skills/create-dsc-resource/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<message>"}` 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 <json>` from command-line arguments
- `require_input(input_json)` — deserializes the JSON input and returns `Result<T, ExitCode>`
- `print_json(value)` — serializes and prints to stdout, returning `Result<(), ExitCode>`
- `parse_input_arg(args)` — parses `--input <json>` 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 <json>` as the input argument
Expand Down Expand Up @@ -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(); }

Expand All @@ -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)
}
```

Expand Down
30 changes: 15 additions & 15 deletions adapters/group_policy_template/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,58 +8,58 @@ 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<String> = 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 {
"list" => admx::list_resources(),
"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();
registry::get(input, resource_type, resource_path)
} 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);
}
};

Expand All @@ -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());
Expand All @@ -77,7 +77,7 @@ fn main() {
} else {
EXIT_RESOURCE_ERROR
};
exit(code);
ExitCode::from(code)
}
}
}
Expand Down
117 changes: 117 additions & 0 deletions dsc/tests/dsc_test_resource_errors.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Describe 'Test resource executable error handling' {
It 'Returns failure for invalid <Subcommand> 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'
}
}
29 changes: 15 additions & 14 deletions resources/WindowsUpdate/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = 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();
Expand All @@ -32,76 +33,76 @@ 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" => {
// Read input from stdin
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" => {
// Read input from stdin
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
}
}
}
Expand Down
Loading
Loading