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
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ members = [
"lib/dsc-lib-security_context",
"resources/sshdconfig",
"resources/WindowsUpdate",
"resources/windows_service",
"tools/dsctest",
"tools/test_group_resource",
"grammars/tree-sitter-dscexpression",
"grammars/tree-sitter-ssh-server-config",
"y2j",
"xtask"
]

# This value is modified by the `Set-DefaultWorkspaceMember` helper.
# Be sure to use `Reset-DefaultWorkspaceMember` before committing to
# avoid unintentionally modifying this value.
Expand All @@ -46,6 +48,7 @@ default-members = [
"lib/dsc-lib-security_context",
"resources/sshdconfig",
"resources/WindowsUpdate",
"resources/windows_service",
"tools/dsctest",
"tools/test_group_resource",
"grammars/tree-sitter-dscexpression",
Expand Down Expand Up @@ -75,6 +78,7 @@ Windows = [
"lib/dsc-lib-security_context",
"resources/sshdconfig",
"resources/WindowsUpdate",
"resources/windows_service",
"tools/dsctest",
"tools/test_group_resource",
"grammars/tree-sitter-dscexpression",
Expand Down Expand Up @@ -246,11 +250,12 @@ urlencoding = { version = "2.1" }
which = { version = "8.0" }
# dsc-lib
ipnetwork = { version = "0.21" }
# WindowsUpdate
# WindowsUpdate, windows_service
windows = { version = "0.62", features = [
"Win32_Foundation",
"Win32_System_Com",
"Win32_System_Ole",
"Win32_System_Services",
"Win32_System_Variant",
"Win32_System_UpdateAgent"
] }
Expand Down
17 changes: 17 additions & 0 deletions data.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@
"windowspowershell.dsc.resource.json",
"windowsupdate.dsc.resource.json",
"wu_dsc.exe",
"windows_service.exe",
"windows_service.dsc.resource.json",
"wmi.dsc.resource.json",
"wmi.resource.ps1",
"wmiAdapter.psd1",
Expand Down Expand Up @@ -402,6 +404,21 @@
]
}
},
{
"Name": "windows_service",
"Kind": "Resource",
"RelativePath": "resources/windows_service",
"SupportedPlatformOS": "Windows",
"IsRust": true,
"Binaries": [
"windows_service"
],
"CopyFiles": {
"Windows": [
"windows_service.dsc.resource.json"
]
}
},
{
"Name": "dsctest",
"Kind": "Resource",
Expand Down
14 changes: 14 additions & 0 deletions resources/windows_service/.project.data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"Name": "windows_service",
"Kind": "Resource",
"IsRust": true,
"SupportedPlatformOS": "Windows",
"Binaries": [
"windows_service"
],
"CopyFiles": {
"Windows": [
"windows_service.dsc.resource.json"
]
}
}
17 changes: 17 additions & 0 deletions resources/windows_service/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "windows_service"
version = "0.1.0"
edition = "2024"

[package.metadata.i18n]
available-locales = ["en-us"]
default-locale = "en-us"
load-path = "locales"

[dependencies]
rust-i18n = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }

[target.'cfg(windows)'.dependencies]
windows = { workspace = true }
37 changes: 37 additions & 0 deletions resources/windows_service/locales/en-us.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
_version = 1

[main]
missingOperation = "Missing operation. Usage: windows_service get --input <json> | set --input <json> | export [--input <json>]"
unknownOperation = "Unknown operation: '%{operation}'. Expected: get, set, or export"
missingInput = "Missing --input argument"
missingInputValue = "Missing value for --input argument"
invalidJson = "Invalid JSON input: %{error}"
windowsOnly = "This resource is only supported on Windows"

[get]
nameOrDisplayNameRequired = "At least one of 'name' or 'displayName' must be provided"
openScmFailed = "Failed to open Service Control Manager: %{error}"
queryConfigFailed = "Failed to query service configuration: %{error}"
queryStatusFailed = "Failed to query service status: %{error}"
openServiceFailed = "Failed to open service: %{error}"
getKeyNameFailed = "Failed to resolve service name from display name: %{error}"
displayNameMismatch = "Service display name mismatch: expected '%{expected}', got '%{actual}'"

[export]
enumServicesFailed = "Failed to enumerate services: %{error}"
openServiceFailed = "Failed to open service '%{name}': %{error}"

[set]
nameRequired = "'name' is required for the set operation"
openScmFailed = "Failed to open Service Control Manager: %{error}"
openServiceFailed = "Failed to open service: %{error}"
changeConfigFailed = "Failed to change service configuration: %{error}"
changeConfig2Failed = "Failed to change service extended configuration: %{error}"
startFailed = "Failed to start service: %{error}"
stopFailed = "Failed to stop service: %{error}"
pauseFailed = "Failed to pause service: %{error}"
continueFailed = "Failed to continue service: %{error}"
unsupportedTransition = "Unsupported status transition from '%{current}' to '%{desired}'"
unsupportedLogonAccount = "Unsupported logon account '%{account}'; only built-in service accounts are supported (LocalSystem, NT AUTHORITY\\LocalService, NT AUTHORITY\\NetworkService)"
unsupportedStatus = "Cannot set service to status '%{status}'; only Running, Stopped, and Paused are supported"
statusTimeout = "Timed out waiting for service to reach status '%{expected}'; current status is '%{actual}'"
148 changes: 148 additions & 0 deletions resources/windows_service/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

mod types;

#[cfg(windows)]
mod service;

use rust_i18n::t;
use std::process::exit;

use types::WindowsService;

/// Write a JSON error object to stderr: `{"error":"<message>"}`
fn write_error(message: &str) {
eprintln!("{}", serde_json::json!({"error": message}));
}

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;

/// Deserialize the required JSON input into a `WindowsService`, or exit with an error.
fn require_input(input_json: Option<String>) -> WindowsService {
let json = match input_json {
Some(j) => j,
None => {
write_error(&t!("main.missingInput"));
exit(EXIT_INVALID_ARGS);
}
};
match serde_json::from_str(&json) {
Ok(v) => v,
Err(e) => {
write_error(&t!("main.invalidJson", error = e.to_string()));
exit(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) {
match serde_json::to_string(value) {
Ok(json) => println!("{json}"),
Err(e) => {
write_error(&t!("main.invalidJson", error = e.to_string()));
exit(EXIT_SERVICE_ERROR);
}
}
}

#[cfg(not(windows))]
fn main() {
write_error(&t!("main.windowsOnly"));
exit(EXIT_SERVICE_ERROR);
}

#[cfg(windows)]
fn main() {
let args: Vec<String> = std::env::args().collect();

if args.len() < 2 {
write_error(&t!("main.missingOperation"));
exit(EXIT_INVALID_ARGS);
}

let operation = args[1].as_str();
let input_json = parse_input_arg(&args);

match operation {
"get" => {
let input = require_input(input_json);

match service::get_service(&input) {
Ok(result) => {
print_json(&result);
exit(EXIT_SUCCESS);
}
Err(e) => {
write_error(&e.to_string());
exit(EXIT_SERVICE_ERROR);
}
}
}
"set" => {
let input = require_input(input_json);

match service::set_service(&input) {
Ok(result) => {
print_json(&result);
exit(EXIT_SUCCESS);
}
Err(e) => {
write_error(&e.to_string());
exit(EXIT_SERVICE_ERROR);
}
}
}
"export" => {
let filter: Option<WindowsService> = match input_json {
Some(json) => match serde_json::from_str(&json) {
Ok(s) => Some(s),
Err(e) => {
write_error(&t!("main.invalidJson", error = e.to_string()));
exit(EXIT_INVALID_INPUT);
}
},
None => None,
};

match service::export_services(filter.as_ref()) {
Ok(services) => {
for svc in &services {
print_json(svc);
}
exit(EXIT_SUCCESS);
}
Err(e) => {
write_error(&e.to_string());
exit(EXIT_SERVICE_ERROR);
}
}
}
_ => {
write_error(&t!("main.unknownOperation", operation = operation));
exit(EXIT_INVALID_ARGS);
}
}
}

/// Parse the `--input <json>` argument from the command-line args.
fn parse_input_arg(args: &[String]) -> Option<String> {
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());
}
write_error(&t!("main.missingInputValue"));
exit(EXIT_INVALID_ARGS);
}
i += 1;
}
None
}
Loading
Loading