From 85fe48cf744591a7e7b03090cb55f6cfe082c504 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Tue, 24 Feb 2026 14:29:16 -0800 Subject: [PATCH 1/9] Add `_refreshEnv` resource metadata support --- lib/dsc-lib/src/configure/context.rs | 3 +++ lib/dsc-lib/src/configure/mod.rs | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/lib/dsc-lib/src/configure/context.rs b/lib/dsc-lib/src/configure/context.rs index 50641ee96..ca8d98deb 100644 --- a/lib/dsc-lib/src/configure/context.rs +++ b/lib/dsc-lib/src/configure/context.rs @@ -23,6 +23,7 @@ pub struct Context { pub copy: HashMap, pub copy_current_loop_name: String, pub dsc_version: Option, + pub environment_variables: Map, pub execution_type: ExecutionKind, pub extensions: Vec, pub outputs: Map, @@ -43,10 +44,12 @@ pub struct Context { impl Context { #[must_use] pub fn new() -> Self { + let environment_variables = std::env::vars().map(|(k, v)| (k, Value::String(v))).collect(); Self { copy: HashMap::new(), copy_current_loop_name: String::new(), dsc_version: None, + environment_variables, execution_type: ExecutionKind::Actual, extensions: Vec::new(), outputs: Map::new(), diff --git a/lib/dsc-lib/src/configure/mod.rs b/lib/dsc-lib/src/configure/mod.rs index 1605ccadf..51fd9d831 100644 --- a/lib/dsc-lib/src/configure/mod.rs +++ b/lib/dsc-lib/src/configure/mod.rs @@ -295,6 +295,21 @@ fn get_metadata_from_result(mut context: Option<&mut Context>, result: &mut Valu } } } + if key == "_refreshEnv" { + #[cfg(not(windows))] + { + warn!("{}", t!("configure.mod.metadataRefreshEnvIgnored")); + continue; + } + #[cfg(windows)] + { + if let Some(context) = context.as_mut() { + // rebuild the environment variables from Windows registry + + } + continue; + } + } metadata.other.insert(key.clone(), value.clone()); } } else { From 5cb70826d03c169e6deb0b1d95de3dfd2a0e52fd Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Thu, 26 Feb 2026 14:45:21 -0800 Subject: [PATCH 2/9] Enable `_refreshEnv` resource metadata to force retrieving updated env var on Windows --- Cargo.lock | 3 + Cargo.toml | 4 +- dsc/tests/dsc_metadata.tests.ps1 | 63 +++++++++ lib/dsc-lib/Cargo.toml | 3 + lib/dsc-lib/locales/en-us.toml | 7 + lib/dsc-lib/src/configure/context.rs | 6 +- lib/dsc-lib/src/configure/mod.rs | 54 +++++++- .../src/discovery/command_discovery.rs | 52 ++++--- lib/dsc-lib/src/dscerror.rs | 12 ++ .../src/dscresources/command_resource.rs | 31 +++-- lib/dsc-lib/src/dscresources/dscresource.rs | 21 ++- tools/dsctest/Cargo.toml | 2 + tools/dsctest/dsctest.dsc.manifests.json | 41 ++++++ tools/dsctest/src/args.rs | 15 ++ tools/dsctest/src/main.rs | 27 +++- tools/dsctest/src/refresh_env.rs | 67 +++++++++ tools/test_group_resource/src/main.rs | 131 +++++++++--------- 17 files changed, 420 insertions(+), 119 deletions(-) create mode 100644 tools/dsctest/src/refresh_env.rs diff --git a/Cargo.lock b/Cargo.lock index a3716dff0..8799b5c20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -784,6 +784,7 @@ dependencies = [ "path-absolutize", "pretty_assertions", "regex", + "registry", "rt-format", "rust-i18n", "schemars", @@ -969,9 +970,11 @@ name = "dsctest" version = "0.1.0" dependencies = [ "clap", + "registry", "schemars", "serde", "serde_json", + "utfx", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e76afff81..f78d490a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -177,7 +177,7 @@ proc-macro2 = { version = "1.0" } quote = { version = "1.0" } # dsc, dsc-lib regex = { version = "1.12" } -# registry, dsc-lib-registry +# registry, dsc-lib, dsc-lib-registry, dsctest registry = { version = "1.3" } # dsc rmcp = { version = "0.16" } @@ -229,7 +229,7 @@ tree-sitter = { version = "0.26" } tree-sitter-language = { version = "0.1" } # dsc-lib, sshdconfig, tree-sitter-dscexpression, tree-sitter-ssh-server-config tree-sitter-rust = { version = "0.24" } -# registry, dsc-lib-registry +# registry, dsc-lib-registry, dsctest utfx = { version = "0.1" } # dsc-lib uuid = { version = "1.21", features = ["v4"] } diff --git a/dsc/tests/dsc_metadata.tests.ps1 b/dsc/tests/dsc_metadata.tests.ps1 index ae9cff5f3..84460feed 100644 --- a/dsc/tests/dsc_metadata.tests.ps1 +++ b/dsc/tests/dsc_metadata.tests.ps1 @@ -223,4 +223,67 @@ Describe 'metadata tests' { (Get-Content $TestDrive/error.log) | Should -BeLike "*WARN*Resource returned '_metadata' property '_restartRequired' which contains invalid value: ``[{`"invalid`":`"item`"}]*" $out.results[0].metadata._restartRequired | Should -BeNullOrEmpty } + + It '_refreshEnv refreshes the environment variables for subsequent resources' { + if ($IsWindows) { + Remove-Item -Path "HKCU:\Environment\myTestVariable" -ErrorAction Ignore + } + + $configYaml = @' + $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: create variable + type: Test/RefreshEnv + properties: + name: myTestVariable + value: myTestValue + - name: return variable + type: Microsoft.DSC.Transitional/PowerShellScript + properties: + SetScript: | + if ($IsWindows) { + $env:myTestVariable + } +'@ + try { + $out = dsc -l trace config set -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json + $errorLogContent = Get-Content $TestDrive/error.log -Raw + $LASTEXITCODE | Should -Be 0 -Because $errorLogContent + $errorLogContent | Should -BeLike "*Resource returned '_refreshEnv' which indicates environment variable refresh is needed*" -Because $errorLogContent + if ($IsWindows) { + $out.results[1].result.afterState.output | Should -BeExactly 'myTestValue' -Because ($out | ConvertTo-Json -Depth 10) + } else { + $errorLogContent | Should -BeLike "*WARN*Resource returned '_refreshEnv' which is ignored on non-Windows platforms*" -Because $errorLogContent + } + } finally { + if ($IsWindows) { + Remove-Item -Path "HKCU:\Environment\myTestVariable" -ErrorAction Ignore + } + } + } + + It '_refreshEnv handles PATH correctly' -Skip:(!$IsWindows) { + $configYaml = @' + $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: add to path + type: Test/RefreshEnv + properties: + name: PATH + value: C:\MyTestPath + - name: return path + type: Microsoft.DSC.Transitional/PowerShellScript + properties: + SetScript: | + $env:PATH +'@ + $oldUserPath = [System.Environment]::GetEnvironmentVariable('PATH', [System.EnvironmentVariableTarget]::User) + try { + $out = dsc -l trace config set -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + $out.results[1].result.afterState.output.Split(';') | Should -Contain 'C:\MyTestPath' + } finally { + [System.Environment]::SetEnvironmentVariable('PATH', $oldUserPath, [System.EnvironmentVariableTarget]::User) + } + } } diff --git a/lib/dsc-lib/Cargo.toml b/lib/dsc-lib/Cargo.toml index 8add26ce0..b96169cb6 100644 --- a/lib/dsc-lib/Cargo.toml +++ b/lib/dsc-lib/Cargo.toml @@ -51,6 +51,9 @@ dsc-lib-security_context = { workspace = true } dsc-lib-jsonschema = { workspace = true } tree-sitter-dscexpression = { workspace = true } +[target.'cfg(windows)'.dependencies] +registry = { workspace = true } + [dev-dependencies] serde_yaml = { workspace = true } # Helps review complex comparisons, like schemas diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 52128b3c2..2f8856c55 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -88,6 +88,9 @@ skippingResourceDiscovery = "Skipping resource discovery due to 'resourceDiscove securityContextInMetadataDeprecated = "Using 'Microsoft.DSC' metadata to specify required security context is deprecated. Please use the 'securityContext' directive in the configuration document instead. See https://github.com/PowerShell/DSC/issues/1369 for more details." conflictingSecurityContext = "Conflicting security context specified in configuration document: metadata '%{metadata}' and directive '%{directive}'" versionNotSatisfied = "Configuration requires DSC version '%{required_version}', but the current version is '%{current_version}'" +invalidRegistryHive = "Invalid registry hive '%{hive}'" +metadataRefreshEnvIgnored = "Resource returned '_refreshEnv' which is ignored on non-Windows platforms" +metadataRefreshEnvFound = "Resource returned '_refreshEnv' which indicates environment variable refresh is needed" [configure.parameters] importingParametersFromComplexInput = "Importing parameters from complex input" @@ -285,6 +288,7 @@ description = "Converts a base64 representation to a string" invoked = "base64ToString function" invalidBase64 = "Invalid base64 encoding" invalidUtf8 = "Decoded bytes do not form valid UTF-8" +utf16Conversion = "Failed to convert UTF-16 bytes to string" [functions.bool] description = "Converts a string or number to a boolean" @@ -776,6 +780,9 @@ setting = "Setting" invalidRequiredVersion = "Invalid required version '%{version}' for resource '%{resource}'" resourceMissingDirectory = "Resource is missing 'directory' field." resourceMissingPath = "Resource is missing 'path' field." +registryHive = "Failed to open registry hive" +registryKey = "Failed to access registry key" +registryIterator = "Failed to iterate over registry values" [progress] failedToSerialize = "Failed to serialize progress JSON: %{json}" diff --git a/lib/dsc-lib/src/configure/context.rs b/lib/dsc-lib/src/configure/context.rs index ca8d98deb..fbcc87c08 100644 --- a/lib/dsc-lib/src/configure/context.rs +++ b/lib/dsc-lib/src/configure/context.rs @@ -18,12 +18,12 @@ pub enum ProcessMode { UserFunction, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Context { pub copy: HashMap, pub copy_current_loop_name: String, pub dsc_version: Option, - pub environment_variables: Map, + pub environment_variables: HashMap, pub execution_type: ExecutionKind, pub extensions: Vec, pub outputs: Map, @@ -44,7 +44,7 @@ pub struct Context { impl Context { #[must_use] pub fn new() -> Self { - let environment_variables = std::env::vars().map(|(k, v)| (k, Value::String(v))).collect(); + let environment_variables = std::env::vars().map(|(k, v)| (k, v)).collect(); Self { copy: HashMap::new(), copy_current_loop_name: String::new(), diff --git a/lib/dsc-lib/src/configure/mod.rs b/lib/dsc-lib/src/configure/mod.rs index 51fd9d831..3f5d12292 100644 --- a/lib/dsc-lib/src/configure/mod.rs +++ b/lib/dsc-lib/src/configure/mod.rs @@ -295,17 +295,33 @@ fn get_metadata_from_result(mut context: Option<&mut Context>, result: &mut Valu } } } - if key == "_refreshEnv" { + if key == "_refreshEnv" && value.as_bool() == Some(true) { + debug!("{}", t!("configure.mod.metadataRefreshEnvFound")); #[cfg(not(windows))] { - warn!("{}", t!("configure.mod.metadataRefreshEnvIgnored")); + info!("{}", t!("configure.mod.metadataRefreshEnvIgnored")); continue; } #[cfg(windows)] { if let Some(context) = context.as_mut() { // rebuild the environment variables from Windows registry - + // read the environment variables from HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment + // overlay with HKCU\Environment for user level variables except for PATH which is appended instead of overlayed + let mut env_vars = read_windows_registry("HKLM", r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment")?; + let user_env_vars = read_windows_registry("HKCU", r"Environment")?; + for (key, value) in user_env_vars { + if key == "PATH" { + if let Some(system_path) = env_vars.get("PATH") { + env_vars.insert("PATH".to_string(), format!("{};{}", system_path, value)); + } else { + env_vars.insert("PATH".to_string(), value.to_string()); + } + } else { + env_vars.insert(key, value.to_string()); + } + } + context.environment_variables = env_vars; } continue; } @@ -322,6 +338,30 @@ fn get_metadata_from_result(mut context: Option<&mut Context>, result: &mut Valu Ok(()) } +#[cfg(windows)] +fn read_windows_registry(hive: &str, path: &str) -> Result, DscError> { + use registry::Hive; + let hive = match hive { + "HKLM" => Hive::LocalMachine, + "HKCU" => Hive::CurrentUser, + _ => return Err(DscError::Parser(t!("configure.mod.invalidRegistryHive", hive = hive).to_string())), + }; + let reg_key = hive.open(path, registry::Security::Read)?; + let mut result = HashMap::new(); + for value in reg_key.values() { + let value = value?; + let name = value.name().to_string()?; + let data = match value.data() { + // for env var we only need to handle string and expand string types, other types will be ignored + registry::Data::String(s) => s.to_string()?, + registry::Data::ExpandString(s) => s.to_string()?, + _ => continue, + }; + result.insert(name, data); + } + Ok(result) +} + impl Configurator { /// Create a new `Configurator` instance. /// @@ -431,6 +471,8 @@ impl Configurator { }; let properties = self.get_properties(&resource, &dsc_resource.kind)?; let filter = add_metadata(&dsc_resource, properties, resource.metadata.clone())?; + let mut dsc_resource = dsc_resource.clone(); + dsc_resource.set_context(&self.context); let start_datetime = chrono::Local::now(); let mut get_result = match dsc_resource.get(&filter) { Ok(result) => result, @@ -523,6 +565,8 @@ impl Configurator { }; let properties = self.get_properties(&resource, &dsc_resource.kind)?; debug!("resource_type {}", &resource.resource_type); + let mut dsc_resource = dsc_resource.clone(); + dsc_resource.set_context(&self.context); // see if the properties contains `_exist` and is false let exist = match &properties { @@ -718,6 +762,8 @@ impl Configurator { debug!("resource_type {}", &resource.resource_type); let expected = add_metadata(&dsc_resource, properties, resource.metadata.clone())?; trace!("{}", t!("configure.mod.expectedState", state = expected)); + let mut dsc_resource = dsc_resource.clone(); + dsc_resource.set_context(&self.context); let start_datetime = chrono::Local::now(); let mut test_result = match dsc_resource.test(&expected) { Ok(result) => result, @@ -807,6 +853,8 @@ impl Configurator { }; let properties = self.get_properties(resource, &dsc_resource.kind)?; debug!("resource_type {}", &resource.resource_type); + let mut dsc_resource = dsc_resource.clone(); + dsc_resource.set_context(&self.context); let input = add_metadata(&dsc_resource, properties, resource.metadata.clone())?; trace!("{}", t!("configure.mod.exportInput", input = input)); let export_result = match add_resource_export_results_to_configuration(&dsc_resource, &mut conf, input.as_str()) { diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 484a8dae9..24d644eb1 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -791,21 +791,19 @@ fn load_adapted_resource_manifest(path: &Path, manifest: &AdaptedDscResourceMani return Err(DscError::InvalidManifest(t!("discovery.commandDiscovery.adaptedResourcePathNotFound", path = resource_path.to_string_lossy(), resource = manifest.type_name).to_string())); } - let resource = DscResource { - type_name: manifest.type_name.clone(), - kind: Kind::Resource, - implemented_as: None, - deprecation_message: manifest.deprecation_message.clone(), - description: manifest.description.clone(), - version: manifest.version.clone(), - capabilities: manifest.capabilities.clone(), - require_adapter: Some(manifest.require_adapter.clone()), - path: resource_path, - directory: directory.to_path_buf(), - manifest: None, - schema: Some(manifest.schema.clone()), - ..Default::default() - }; + let mut resource = DscResource::new(); + resource.type_name = manifest.type_name.clone(); + resource.kind = Kind::Resource; + resource.implemented_as = None; + resource.deprecation_message = manifest.deprecation_message.clone(); + resource.description = manifest.description.clone(); + resource.version = manifest.version.clone(); + resource.capabilities = manifest.capabilities.clone(); + resource.require_adapter = Some(manifest.require_adapter.clone()); + resource.path = resource_path; + resource.directory = directory.to_path_buf(); + resource.manifest = None; + resource.schema = Some(manifest.schema.clone()); Ok(resource) } @@ -855,19 +853,17 @@ fn load_resource_manifest(path: &Path, manifest: &ResourceManifest) -> Result> = None; + let mut env: HashMap = resource.get_context().environment_variables.clone(); let mut input_desired: Option<&str> = None; let (args, _) = process_set_delete_args(set.args.as_ref(), desired, &command_resource_info, execution_type); match &set.input { Some(InputKind::Env) => { - env = Some(json_to_hashmap(desired)?); + for (key, value) in json_to_hashmap(desired)? { + env.insert(key, value); + } }, Some(InputKind::Stdin) => { input_desired = Some(desired); @@ -254,7 +257,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut }, } - let (exit_code, stdout, stderr) = invoke_command(&set.executable, args, input_desired, Some(&resource.directory), env, manifest.exit_codes.as_ref())?; + let (exit_code, stdout, stderr) = invoke_command(&set.executable, args, input_desired, Some(&resource.directory), Some(env), manifest.exit_codes.as_ref())?; match set.returns { Some(ReturnKind::State) => { @@ -361,7 +364,7 @@ pub fn invoke_test(resource: &DscResource, expected: &str, target_resource: Opti path, }; let args = process_get_args(test.args.as_ref(), expected, &command_resource_info); - let command_input = get_command_input(test.input.as_ref(), expected)?; + let command_input = get_command_input(test.input.as_ref(), expected, &resource.get_context())?; info!("{}", t!("dscresources.commandResource.invokeTestUsing", resource = &resource.type_name, executable = &test.executable)); let (exit_code, stdout, stderr) = invoke_command(&test.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; @@ -520,7 +523,7 @@ pub fn invoke_delete(resource: &DscResource, filter: &str, target_resource: Opti let test_result = invoke_test(resource, filter, target_resource.clone())?; return Ok(DeleteResultKind::SyntheticWhatIf(test_result)); } - let command_input = get_command_input(delete.input.as_ref(), filter)?; + let command_input = get_command_input(delete.input.as_ref(), filter, &resource.get_context())?; info!("{}", t!("dscresources.commandResource.invokeDeleteUsing", resource = resource_type, executable = &delete.executable)); let (_exit_code, stdout, _stderr) = invoke_command(&delete.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; @@ -572,7 +575,7 @@ pub fn invoke_validate(resource: &DscResource, config: &str, target_resource: Op path, }; let args = process_get_args(validate.args.as_ref(), config, &command_resource_info); - let command_input = get_command_input(validate.input.as_ref(), config)?; + let command_input = get_command_input(validate.input.as_ref(), config, &resource.get_context())?; info!("{}", t!("dscresources.commandResource.invokeValidateUsing", resource = resource_type, executable = &validate.executable)); let (_exit_code, stdout, _stderr) = invoke_command(&validate.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; @@ -676,7 +679,7 @@ pub fn invoke_export(resource: &DscResource, input: Option<&str>, target_resourc if !input.is_empty() { verify_json_from_manifest(&resource, input, target_resource)?; - command_input = get_command_input(export.input.as_ref(), input)?; + command_input = get_command_input(export.input.as_ref(), input, &resource.get_context())?; } args = process_get_args(export.args.as_ref(), input, &command_resource_info); @@ -735,7 +738,7 @@ pub fn invoke_resolve(resource: &DscResource, input: &str) -> Result, } -fn get_command_input(input_kind: Option<&InputKind>, input: &str) -> Result { - let mut env: Option> = None; +fn get_command_input(input_kind: Option<&InputKind>, input: &str, context: &Context) -> Result { + let mut env = Some(context.environment_variables.clone()); let mut stdin: Option = None; match input_kind { Some(InputKind::Env) => { diff --git a/lib/dsc-lib/src/dscresources/dscresource.rs b/lib/dsc-lib/src/dscresources/dscresource.rs index 8c8b747b1..d58ce4197 100644 --- a/lib/dsc-lib/src/dscresources/dscresource.rs +++ b/lib/dsc-lib/src/dscresources/dscresource.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::{configure::{Configurator, config_doc::{Configuration, ExecutionKind, Resource}, context::ProcessMode, parameters::{SECURE_VALUE_REDACTED, is_secure_value}}, dscresources::resource_manifest::{AdapterInputKind, Kind}, types::FullyQualifiedTypeName}; +use crate::{configure::{Configurator, config_doc::{Configuration, ExecutionKind, Resource}, context::{Context, ProcessMode}, parameters::{SECURE_VALUE_REDACTED, is_secure_value}}, dscresources::resource_manifest::{AdapterInputKind, Kind}, types::FullyQualifiedTypeName}; use crate::discovery::discovery_trait::DiscoveryFilter; use crate::dscresources::invoke_result::{ResourceGetResponse, ResourceSetResponse}; use crate::schemas::transforms::idiomaticize_string_enum; @@ -61,6 +61,9 @@ pub struct DscResource { pub target_resource: Option>, /// The manifest of the resource. pub manifest: Option, + /// The execution context for the resource. + #[serde(skip)] + context: Option, } #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] @@ -114,6 +117,7 @@ impl DscResource { schema: None, target_resource: None, manifest: None, + context: None, } } @@ -296,6 +300,21 @@ impl DscResource { } Err(DscError::Operation(t!("dscresources.dscresource.adapterResourceNotFound", adapter = adapter).to_string())) } + + /// Get the execution context for the resource, or a default context if one is not set. + pub fn get_context(&self) -> Context { + self.context.clone().unwrap_or_else(Context::new) + } + + /// Set the execution context for the resource. + /// + /// # Arguments + /// + /// * `context` - The context to set for the resource. + /// + pub fn set_context(&mut self, context: &Context) { + self.context = Some(context.clone()); + } } impl Default for DscResource { diff --git a/tools/dsctest/Cargo.toml b/tools/dsctest/Cargo.toml index f580daa57..9a2815d20 100644 --- a/tools/dsctest/Cargo.toml +++ b/tools/dsctest/Cargo.toml @@ -7,6 +7,8 @@ edition = "2021" [dependencies] clap = { workspace = true } +registry = { workspace = true } schemars = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +utfx = { workspace = true } diff --git a/tools/dsctest/dsctest.dsc.manifests.json b/tools/dsctest/dsctest.dsc.manifests.json index dd3b5a386..93b9265b8 100644 --- a/tools/dsctest/dsctest.dsc.manifests.json +++ b/tools/dsctest/dsctest.dsc.manifests.json @@ -376,6 +376,47 @@ } } }, + { + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Test/RefreshEnv", + "version": "0.1.0", + "get": { + "executable": "dsctest", + "args": [ + "refresh-env", + "--operation", + "get", + { + "jsonInputArg": "--input", + "mandatory": true + } + ] + }, + "set": { + "executable": "dsctest", + "args": [ + "refresh-env", + "--operation", + "set", + { + "jsonInputArg": "--input", + "mandatory": true + } + ], + "implementsPretest": true, + "return": "state" + }, + "schema": { + "command": { + "executable": "dsctest", + "args": [ + "schema", + "-s", + "refresh-env" + ] + } + } + }, { "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", "type": "Test/Sleep", diff --git a/tools/dsctest/src/args.rs b/tools/dsctest/src/args.rs index fd7b7d56e..c0bd9ccbb 100644 --- a/tools/dsctest/src/args.rs +++ b/tools/dsctest/src/args.rs @@ -16,6 +16,7 @@ pub enum Schemas { InDesiredState, Metadata, Operation, + RefreshEnv, Sleep, Trace, Version, @@ -42,6 +43,12 @@ pub enum AdapterOperation { Schema, } +#[derive(Debug, Clone, PartialEq, Eq, ValueEnum)] +pub enum RefreshEnvOperation { + Get, + Set, +} + #[derive(Debug, PartialEq, Eq, Subcommand)] pub enum SubCommand { #[clap(name = "adapter", about = "Resource adapter")] @@ -123,6 +130,14 @@ pub enum SubCommand { input: String, }, + #[clap(name = "refresh-env", about = "Refresh an environment variable in the registry")] + RefreshEnv { + #[clap(name = "operation", short, long, help = "The operation to perform: get or set")] + operation: RefreshEnvOperation, + #[clap(name = "input", short, long, help = "The input to the refresh env command as JSON")] + input: String, + }, + #[clap(name = "schema", about = "Get the JSON schema for a subcommand")] Schema { #[clap(name = "subcommand", short, long, help = "The subcommand to get the schema for")] diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index 5da8dc48a..8e68f4c71 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -13,13 +13,14 @@ mod in_desired_state; mod metadata; mod operation; mod adapter; +mod refresh_env; mod sleep; mod trace; mod version; mod whatif; mod whatif_delete; -use args::{Args, Schemas, SubCommand}; +use args::{Args, RefreshEnvOperation, Schemas, SubCommand}; use clap::Parser; use schemars::schema_for; use serde_json::Map; @@ -33,6 +34,7 @@ use crate::get::Get; use crate::in_desired_state::InDesiredState; use crate::metadata::Metadata; use crate::operation::Operation; +use crate::refresh_env::RefreshEnv; use crate::sleep::Sleep; use crate::trace::Trace; use crate::version::Version; @@ -244,6 +246,26 @@ fn main() { operation_result.operation = Some(operation.to_lowercase()); serde_json::to_string(&operation_result).unwrap() }, + SubCommand::RefreshEnv { operation, input } => { + let mut refresh_env = match serde_json::from_str::(&input) { + Ok(re) => re, + Err(err) => { + eprintln!("Error JSON does not match schema: {err}"); + std::process::exit(1); + } + }; + match operation { + RefreshEnvOperation::Get => { + let result = refresh_env.get(); + serde_json::to_string(&result).unwrap() + }, + RefreshEnvOperation::Set => { + refresh_env.set(); + refresh_env.metadata.get_or_insert(Map::new()).insert("_refreshEnv".to_string(), serde_json::Value::Bool(true)); + serde_json::to_string(&refresh_env).unwrap() + } + } + }, SubCommand::Schema { subcommand } => { let schema = match subcommand { Schemas::Adapter => { @@ -279,6 +301,9 @@ fn main() { Schemas::Operation => { schema_for!(Operation) }, + Schemas::RefreshEnv => { + schema_for!(RefreshEnv) + }, Schemas::Sleep => { schema_for!(Sleep) }, diff --git a/tools/dsctest/src/refresh_env.rs b/tools/dsctest/src/refresh_env.rs new file mode 100644 index 000000000..02c342aa5 --- /dev/null +++ b/tools/dsctest/src/refresh_env.rs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use registry::{Hive, Security, value::Data}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use utfx::UCString; + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +#[allow(clippy::struct_field_names)] +pub struct RefreshEnv { + #[serde(rename="_metadata", skip_serializing_if = "Option::is_none")] + pub metadata: Option>, + pub name: String, + pub value: String, +} + +impl RefreshEnv { + pub fn set(&self) { + #[cfg(windows)] + { + // Set the environment variable in the registry for current user + let hkcu = Hive::CurrentUser.open("Environment", Security::Write).unwrap(); + let ucstring = UCString::::from_str(&self.value).unwrap(); + let data = Data::String(ucstring); + hkcu.set_value(&self.name, &data).unwrap(); + } + #[cfg(not(windows))] + { + // Do nothing on non-Windows + } + } + + pub fn get(&self) -> RefreshEnv { + #[cfg(windows)] + { + // Get the environment variable from the registry for current user + let hkcu = Hive::CurrentUser.open("Environment", Security::Read).unwrap(); + if let Ok(data) = hkcu.value(&self.name) { + if let Data::String(value) = data { + return RefreshEnv { + metadata: None, + name: self.name.clone(), + value: value.to_string_lossy(), + }; + } + } + + RefreshEnv { + metadata: None, + name: self.name.clone(), + value: String::new(), + } + } + #[cfg(not(windows))] + { + // Return the input value on non-Windows since we can't read from the registry + RefreshEnv { + metadata: None, + name: self.name.clone(), + value: self.value.clone(), + } + } + } +} diff --git a/tools/test_group_resource/src/main.rs b/tools/test_group_resource/src/main.rs index e6ed15de7..8cba3d723 100644 --- a/tools/test_group_resource/src/main.rs +++ b/tools/test_group_resource/src/main.rs @@ -14,83 +14,80 @@ fn main() { let args = Args::parse(); match args.subcommand { SubCommand::List => { - let resource1 = DscResource { - type_name: "Test/TestResource1".parse().unwrap(), - kind: Kind::Resource, - version: "1.0.0".to_string(), - capabilities: vec![Capability::Get, Capability::Set], - deprecation_message: None, + let mut resource1 = DscResource::new(); + resource1.type_name = "Test/TestResource1".parse().unwrap(); + resource1.kind = Kind::Resource; + resource1.version = "1.0.0".to_string(); + resource1.capabilities = vec![Capability::Get, Capability::Set]; + resource1.deprecation_message = None; + resource1.description = Some("This is a test resource.".to_string()); + resource1.implemented_as = Some(ImplementedAs::Custom("TestResource".to_string())); + resource1.path = PathBuf::from("test_resource1"); + resource1.directory = PathBuf::from("test_directory"); + resource1.author = Some("Microsoft".to_string()); + resource1.properties = Some(vec!["Property1".to_string(), "Property2".to_string()]); + resource1.require_adapter = Some("Test/TestGroup".parse().unwrap()); + resource1.target_resource = None; + resource1.schema = None; + resource1.manifest = Some(ResourceManifest { description: Some("This is a test resource.".to_string()), - implemented_as: Some(ImplementedAs::Custom("TestResource".to_string())), - path: PathBuf::from("test_resource1"), - directory: PathBuf::from("test_directory"), - author: Some("Microsoft".to_string()), - properties: Some(vec!["Property1".to_string(), "Property2".to_string()]), - require_adapter: Some("Test/TestGroup".parse().unwrap()), - target_resource: None, - schema: None, - manifest: Some(ResourceManifest { - description: Some("This is a test resource.".to_string()), - schema_version: dsc_lib::dscresources::resource_manifest::ResourceManifest::default_schema_id_uri(), - resource_type: "Test/TestResource1".parse().unwrap(), - kind: Some(Kind::Resource), - version: "1.0.0".to_string(), - get: Some(GetMethod { - executable: String::new(), - ..Default::default() - }), + schema_version: dsc_lib::dscresources::resource_manifest::ResourceManifest::default_schema_id_uri(), + resource_type: "Test/TestResource1".parse().unwrap(), + kind: Some(Kind::Resource), + version: "1.0.0".to_string(), + get: Some(GetMethod { + executable: String::new(), ..Default::default() }), - }; - let resource2 = DscResource { - type_name: "Test/TestResource2".parse().unwrap(), - kind: Kind::Resource, - version: "1.0.1".to_string(), - capabilities: vec![Capability::Get, Capability::Set], - deprecation_message: None, + ..Default::default() + }); + let mut resource2 = DscResource::new(); + resource2.type_name = "Test/TestResource2".parse().unwrap(); + resource2.kind = Kind::Resource; + resource2.version = "1.0.1".to_string(); + resource2.capabilities = vec![Capability::Get, Capability::Set]; + resource2.deprecation_message = None; + resource2.description = Some("This is a test resource.".to_string()); + resource2.implemented_as = Some(ImplementedAs::Custom("TestResource".to_string())); + resource2.path = PathBuf::from("test_resource2"); + resource2.directory = PathBuf::from("test_directory"); + resource2.author = Some("Microsoft".to_string()); + resource2.properties = Some(vec!["Property1".to_string(), "Property2".to_string()]); + resource2.require_adapter = Some("Test/TestGroup".parse().unwrap()); + resource2.target_resource = None; + resource2.schema = None; + resource2.manifest = Some(ResourceManifest { description: Some("This is a test resource.".to_string()), - implemented_as: Some(ImplementedAs::Custom("TestResource".to_string())), - path: PathBuf::from("test_resource2"), - directory: PathBuf::from("test_directory"), - author: Some("Microsoft".to_string()), - properties: Some(vec!["Property1".to_string(), "Property2".to_string()]), - require_adapter: Some("Test/TestGroup".parse().unwrap()), - target_resource: None, - schema: None, - manifest: Some(ResourceManifest { - description: Some("This is a test resource.".to_string()), - schema_version: dsc_lib::dscresources::resource_manifest::ResourceManifest::default_schema_id_uri(), - resource_type: "Test/TestResource2".parse().unwrap(), - kind: Some(Kind::Resource), - version: "1.0.1".to_string(), - get: Some(GetMethod { - executable: String::new(), - ..Default::default() - }), + schema_version: dsc_lib::dscresources::resource_manifest::ResourceManifest::default_schema_id_uri(), + resource_type: "Test/TestResource2".parse().unwrap(), + kind: Some(Kind::Resource), + version: "1.0.1".to_string(), + get: Some(GetMethod { + executable: String::new(), ..Default::default() }), - }; + ..Default::default() + }); println!("{}", serde_json::to_string(&resource1).unwrap()); println!("{}", serde_json::to_string(&resource2).unwrap()); }, SubCommand::ListMissingRequires => { - let resource1 = DscResource { - type_name: "Test/InvalidResource".parse().unwrap(), - kind: Kind::Resource, - version: "1.0.0".to_string(), - capabilities: vec![Capability::Get], - deprecation_message: None, - description: Some("This is a test resource.".to_string()), - implemented_as: Some(ImplementedAs::Custom("TestResource".to_string())), - path: PathBuf::from("test_resource1"), - directory: PathBuf::from("test_directory"), - author: Some("Microsoft".to_string()), - properties: Some(vec!["Property1".to_string(), "Property2".to_string()]), - require_adapter: None, - target_resource: None, - manifest: None, - schema: None, - }; + let mut resource1 = DscResource::new(); + resource1.type_name = "Test/InvalidResource".parse().unwrap(); + resource1.kind = Kind::Resource; + resource1.version = "1.0.0".to_string(); + resource1.capabilities = vec![Capability::Get]; + resource1.deprecation_message = None; + resource1.description = Some("This is a test resource.".to_string()); + resource1.implemented_as = Some(ImplementedAs::Custom("TestResource".to_string())); + resource1.path = PathBuf::from("test_resource1"); + resource1.directory = PathBuf::from("test_directory"); + resource1.author = Some("Microsoft".to_string()); + resource1.properties = Some(vec!["Property1".to_string(), "Property2".to_string()]); + resource1.require_adapter = None; + resource1.target_resource = None; + resource1.manifest = None; + resource1.schema = None; println!("{}", serde_json::to_string(&resource1).unwrap()); } } From 4ba6fe327fadaa9c45f8a4bb7e5ef37e14208b6b Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Thu, 26 Feb 2026 14:47:51 -0800 Subject: [PATCH 3/9] fix test to expect INFO --- dsc/tests/dsc_metadata.tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc/tests/dsc_metadata.tests.ps1 b/dsc/tests/dsc_metadata.tests.ps1 index 84460feed..f2c4ed9a8 100644 --- a/dsc/tests/dsc_metadata.tests.ps1 +++ b/dsc/tests/dsc_metadata.tests.ps1 @@ -253,7 +253,7 @@ Describe 'metadata tests' { if ($IsWindows) { $out.results[1].result.afterState.output | Should -BeExactly 'myTestValue' -Because ($out | ConvertTo-Json -Depth 10) } else { - $errorLogContent | Should -BeLike "*WARN*Resource returned '_refreshEnv' which is ignored on non-Windows platforms*" -Because $errorLogContent + $errorLogContent | Should -BeLike "*INFO*Resource returned '_refreshEnv' which is ignored on non-Windows platforms*" -Because $errorLogContent } } finally { if ($IsWindows) { From 9ba936fc0e5b10874a944dbade83deca78eeaa02 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Fri, 27 Feb 2026 13:55:32 -0800 Subject: [PATCH 4/9] Remove cache for env vars, set process env vars instead --- Cargo.lock | 4 +- lib/dsc-lib/src/configure/context.rs | 3 - lib/dsc-lib/src/configure/mod.rs | 49 +++---- .../src/dscresources/command_resource.rs | 31 ++--- lib/dsc-lib/src/dscresources/dscresource.rs | 21 +-- tools/test_group_resource/src/main.rs | 131 +++++++++--------- 6 files changed, 106 insertions(+), 133 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8799b5c20..a6e92f0a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3623,9 +3623,9 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicase" -version = "2.8.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-general-category" diff --git a/lib/dsc-lib/src/configure/context.rs b/lib/dsc-lib/src/configure/context.rs index fbcc87c08..41d1dca06 100644 --- a/lib/dsc-lib/src/configure/context.rs +++ b/lib/dsc-lib/src/configure/context.rs @@ -23,7 +23,6 @@ pub struct Context { pub copy: HashMap, pub copy_current_loop_name: String, pub dsc_version: Option, - pub environment_variables: HashMap, pub execution_type: ExecutionKind, pub extensions: Vec, pub outputs: Map, @@ -44,12 +43,10 @@ pub struct Context { impl Context { #[must_use] pub fn new() -> Self { - let environment_variables = std::env::vars().map(|(k, v)| (k, v)).collect(); Self { copy: HashMap::new(), copy_current_loop_name: String::new(), dsc_version: None, - environment_variables, execution_type: ExecutionKind::Actual, extensions: Vec::new(), outputs: Map::new(), diff --git a/lib/dsc-lib/src/configure/mod.rs b/lib/dsc-lib/src/configure/mod.rs index 3f5d12292..99750a8ff 100644 --- a/lib/dsc-lib/src/configure/mod.rs +++ b/lib/dsc-lib/src/configure/mod.rs @@ -296,7 +296,6 @@ fn get_metadata_from_result(mut context: Option<&mut Context>, result: &mut Valu } } if key == "_refreshEnv" && value.as_bool() == Some(true) { - debug!("{}", t!("configure.mod.metadataRefreshEnvFound")); #[cfg(not(windows))] { info!("{}", t!("configure.mod.metadataRefreshEnvIgnored")); @@ -304,26 +303,30 @@ fn get_metadata_from_result(mut context: Option<&mut Context>, result: &mut Valu } #[cfg(windows)] { - if let Some(context) = context.as_mut() { - // rebuild the environment variables from Windows registry - // read the environment variables from HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment - // overlay with HKCU\Environment for user level variables except for PATH which is appended instead of overlayed - let mut env_vars = read_windows_registry("HKLM", r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment")?; - let user_env_vars = read_windows_registry("HKCU", r"Environment")?; - for (key, value) in user_env_vars { - if key == "PATH" { - if let Some(system_path) = env_vars.get("PATH") { - env_vars.insert("PATH".to_string(), format!("{};{}", system_path, value)); - } else { - env_vars.insert("PATH".to_string(), value.to_string()); - } + info!("{}", t!("configure.mod.metadataRefreshEnvFound")); + // rebuild the environment variables from Windows registry + // read the environment variables from HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment + // overlay with HKCU\Environment for user level variables except for PATH which is appended instead of overlayed + let mut env_vars = read_windows_registry("HKLM", r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment")?; + let user_env_vars = read_windows_registry("HKCU", r"Environment")?; + for (key, value) in user_env_vars { + if key == "PATH" { + if let Some(system_path) = env_vars.get(&key) { + trace!("Appending user PATH '{value}' to system PATH '{system_path}'"); + env_vars.insert(key, format!("{};{}", system_path, value)); } else { + trace!("System PATH not found, using user PATH '{value}' as PATH"); env_vars.insert(key, value.to_string()); } + } else { + env_vars.insert(key, value.to_string()); } - context.environment_variables = env_vars; } - continue; + // set the current process env vars to the new values + for (key, value) in env_vars { + trace!("Setting environment variable {key}='{value}'"); + std::env::set_var(&key.to_string(), &value); + } } } metadata.other.insert(key.clone(), value.clone()); @@ -347,10 +350,11 @@ fn read_windows_registry(hive: &str, path: &str) -> Result return Err(DscError::Parser(t!("configure.mod.invalidRegistryHive", hive = hive).to_string())), }; let reg_key = hive.open(path, registry::Security::Read)?; - let mut result = HashMap::new(); + let mut result: HashMap = HashMap::new(); for value in reg_key.values() { let value = value?; - let name = value.name().to_string()?; + // env vars on Windows aren't case-sensitive, so we use uppercase to keep them consistent + let name = value.name().to_string()?.to_uppercase(); let data = match value.data() { // for env var we only need to handle string and expand string types, other types will be ignored registry::Data::String(s) => s.to_string()?, @@ -471,8 +475,6 @@ impl Configurator { }; let properties = self.get_properties(&resource, &dsc_resource.kind)?; let filter = add_metadata(&dsc_resource, properties, resource.metadata.clone())?; - let mut dsc_resource = dsc_resource.clone(); - dsc_resource.set_context(&self.context); let start_datetime = chrono::Local::now(); let mut get_result = match dsc_resource.get(&filter) { Ok(result) => result, @@ -565,9 +567,6 @@ impl Configurator { }; let properties = self.get_properties(&resource, &dsc_resource.kind)?; debug!("resource_type {}", &resource.resource_type); - let mut dsc_resource = dsc_resource.clone(); - dsc_resource.set_context(&self.context); - // see if the properties contains `_exist` and is false let exist = match &properties { Some(property_map) => { @@ -762,8 +761,6 @@ impl Configurator { debug!("resource_type {}", &resource.resource_type); let expected = add_metadata(&dsc_resource, properties, resource.metadata.clone())?; trace!("{}", t!("configure.mod.expectedState", state = expected)); - let mut dsc_resource = dsc_resource.clone(); - dsc_resource.set_context(&self.context); let start_datetime = chrono::Local::now(); let mut test_result = match dsc_resource.test(&expected) { Ok(result) => result, @@ -853,8 +850,6 @@ impl Configurator { }; let properties = self.get_properties(resource, &dsc_resource.kind)?; debug!("resource_type {}", &resource.resource_type); - let mut dsc_resource = dsc_resource.clone(); - dsc_resource.set_context(&self.context); let input = add_metadata(&dsc_resource, properties, resource.metadata.clone())?; trace!("{}", t!("configure.mod.exportInput", input = input)); let export_result = match add_resource_export_results_to_configuration(&dsc_resource, &mut conf, input.as_str()) { diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index 1a1e5781e..b2906c5bc 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -7,7 +7,7 @@ use rust_i18n::t; use serde::Deserialize; use serde_json::{Map, Value}; use std::{collections::HashMap, env, path::{Path, PathBuf}, process::Stdio}; -use crate::{configure::{config_doc::ExecutionKind, config_result::{ResourceGetResult, ResourceTestResult}, context::Context}, dscresources::resource_manifest::SchemaArgKind, types::{ExitCodesMap, FullyQualifiedTypeName}, util::canonicalize_which}; +use crate::{configure::{config_doc::ExecutionKind, config_result::{ResourceGetResult, ResourceTestResult}}, dscresources::resource_manifest::SchemaArgKind, types::{ExitCodesMap, FullyQualifiedTypeName}, util::canonicalize_which}; use crate::dscerror::DscError; use super::{ dscresource::{get_diff, redact, DscResource}, @@ -45,7 +45,7 @@ pub fn invoke_get(resource: &DscResource, filter: &str, target_resource: Option< let Some(manifest) = &resource.manifest else { return Err(DscError::MissingManifest(resource.type_name.to_string())); }; - let mut command_input = CommandInput { env: Some(resource.get_context().environment_variables.clone()), stdin: None }; + let mut command_input = CommandInput { env: None, stdin: None }; let Some(get) = &manifest.get else { return Err(DscError::NotImplemented("get".to_string())); }; @@ -65,7 +65,7 @@ pub fn invoke_get(resource: &DscResource, filter: &str, target_resource: Option< let args = process_get_args(get.args.as_ref(), filter, &command_resource_info); if !filter.is_empty() { verify_json_from_manifest(&resource, filter, target_resource)?; - command_input = get_command_input(get.input.as_ref(), filter, &resource.get_context())?; + command_input = get_command_input(get.input.as_ref(), filter)?; } info!("{}", t!("dscresources.commandResource.invokeGetUsing", resource = &resource.type_name, executable = &get.executable)); @@ -193,7 +193,6 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut return Err(DscError::NotImplemented(t!("dscresources.commandResource.syntheticWhatIf").to_string())); } - // `get` is required so that the pre-state can be retrieved for the set operation and returned in the final result let Some(get) = &manifest.get else { return Err(DscError::NotImplemented("get".to_string())); }; @@ -211,7 +210,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut path, }; let args = process_get_args(get.args.as_ref(), desired, &command_resource_info); - let command_input = get_command_input(get.input.as_ref(), desired, &resource.get_context())?; + let command_input = get_command_input(get.input.as_ref(), desired)?; info!("{}", t!("dscresources.commandResource.setGetCurrent", resource = &resource.type_name, executable = &get.executable)); let (exit_code, stdout, stderr) = invoke_command(&get.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; @@ -240,14 +239,12 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut pre_state_value }; - let mut env: HashMap = resource.get_context().environment_variables.clone(); + let mut env: Option> = None; let mut input_desired: Option<&str> = None; let (args, _) = process_set_delete_args(set.args.as_ref(), desired, &command_resource_info, execution_type); match &set.input { Some(InputKind::Env) => { - for (key, value) in json_to_hashmap(desired)? { - env.insert(key, value); - } + env = Some(json_to_hashmap(desired)?); }, Some(InputKind::Stdin) => { input_desired = Some(desired); @@ -257,7 +254,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut }, } - let (exit_code, stdout, stderr) = invoke_command(&set.executable, args, input_desired, Some(&resource.directory), Some(env), manifest.exit_codes.as_ref())?; + let (exit_code, stdout, stderr) = invoke_command(&set.executable, args, input_desired, Some(&resource.directory), env, manifest.exit_codes.as_ref())?; match set.returns { Some(ReturnKind::State) => { @@ -364,7 +361,7 @@ pub fn invoke_test(resource: &DscResource, expected: &str, target_resource: Opti path, }; let args = process_get_args(test.args.as_ref(), expected, &command_resource_info); - let command_input = get_command_input(test.input.as_ref(), expected, &resource.get_context())?; + let command_input = get_command_input(test.input.as_ref(), expected)?; info!("{}", t!("dscresources.commandResource.invokeTestUsing", resource = &resource.type_name, executable = &test.executable)); let (exit_code, stdout, stderr) = invoke_command(&test.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; @@ -523,7 +520,7 @@ pub fn invoke_delete(resource: &DscResource, filter: &str, target_resource: Opti let test_result = invoke_test(resource, filter, target_resource.clone())?; return Ok(DeleteResultKind::SyntheticWhatIf(test_result)); } - let command_input = get_command_input(delete.input.as_ref(), filter, &resource.get_context())?; + let command_input = get_command_input(delete.input.as_ref(), filter)?; info!("{}", t!("dscresources.commandResource.invokeDeleteUsing", resource = resource_type, executable = &delete.executable)); let (_exit_code, stdout, _stderr) = invoke_command(&delete.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; @@ -575,7 +572,7 @@ pub fn invoke_validate(resource: &DscResource, config: &str, target_resource: Op path, }; let args = process_get_args(validate.args.as_ref(), config, &command_resource_info); - let command_input = get_command_input(validate.input.as_ref(), config, &resource.get_context())?; + let command_input = get_command_input(validate.input.as_ref(), config)?; info!("{}", t!("dscresources.commandResource.invokeValidateUsing", resource = resource_type, executable = &validate.executable)); let (_exit_code, stdout, _stderr) = invoke_command(&validate.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; @@ -679,7 +676,7 @@ pub fn invoke_export(resource: &DscResource, input: Option<&str>, target_resourc if !input.is_empty() { verify_json_from_manifest(&resource, input, target_resource)?; - command_input = get_command_input(export.input.as_ref(), input, &resource.get_context())?; + command_input = get_command_input(export.input.as_ref(), input)?; } args = process_get_args(export.args.as_ref(), input, &command_resource_info); @@ -738,7 +735,7 @@ pub fn invoke_resolve(resource: &DscResource, input: &str) -> Result, } -fn get_command_input(input_kind: Option<&InputKind>, input: &str, context: &Context) -> Result { - let mut env = Some(context.environment_variables.clone()); +fn get_command_input(input_kind: Option<&InputKind>, input: &str) -> Result { + let mut env: Option> = None; let mut stdin: Option = None; match input_kind { Some(InputKind::Env) => { diff --git a/lib/dsc-lib/src/dscresources/dscresource.rs b/lib/dsc-lib/src/dscresources/dscresource.rs index d58ce4197..8c8b747b1 100644 --- a/lib/dsc-lib/src/dscresources/dscresource.rs +++ b/lib/dsc-lib/src/dscresources/dscresource.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::{configure::{Configurator, config_doc::{Configuration, ExecutionKind, Resource}, context::{Context, ProcessMode}, parameters::{SECURE_VALUE_REDACTED, is_secure_value}}, dscresources::resource_manifest::{AdapterInputKind, Kind}, types::FullyQualifiedTypeName}; +use crate::{configure::{Configurator, config_doc::{Configuration, ExecutionKind, Resource}, context::ProcessMode, parameters::{SECURE_VALUE_REDACTED, is_secure_value}}, dscresources::resource_manifest::{AdapterInputKind, Kind}, types::FullyQualifiedTypeName}; use crate::discovery::discovery_trait::DiscoveryFilter; use crate::dscresources::invoke_result::{ResourceGetResponse, ResourceSetResponse}; use crate::schemas::transforms::idiomaticize_string_enum; @@ -61,9 +61,6 @@ pub struct DscResource { pub target_resource: Option>, /// The manifest of the resource. pub manifest: Option, - /// The execution context for the resource. - #[serde(skip)] - context: Option, } #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] @@ -117,7 +114,6 @@ impl DscResource { schema: None, target_resource: None, manifest: None, - context: None, } } @@ -300,21 +296,6 @@ impl DscResource { } Err(DscError::Operation(t!("dscresources.dscresource.adapterResourceNotFound", adapter = adapter).to_string())) } - - /// Get the execution context for the resource, or a default context if one is not set. - pub fn get_context(&self) -> Context { - self.context.clone().unwrap_or_else(Context::new) - } - - /// Set the execution context for the resource. - /// - /// # Arguments - /// - /// * `context` - The context to set for the resource. - /// - pub fn set_context(&mut self, context: &Context) { - self.context = Some(context.clone()); - } } impl Default for DscResource { diff --git a/tools/test_group_resource/src/main.rs b/tools/test_group_resource/src/main.rs index 8cba3d723..e6ed15de7 100644 --- a/tools/test_group_resource/src/main.rs +++ b/tools/test_group_resource/src/main.rs @@ -14,80 +14,83 @@ fn main() { let args = Args::parse(); match args.subcommand { SubCommand::List => { - let mut resource1 = DscResource::new(); - resource1.type_name = "Test/TestResource1".parse().unwrap(); - resource1.kind = Kind::Resource; - resource1.version = "1.0.0".to_string(); - resource1.capabilities = vec![Capability::Get, Capability::Set]; - resource1.deprecation_message = None; - resource1.description = Some("This is a test resource.".to_string()); - resource1.implemented_as = Some(ImplementedAs::Custom("TestResource".to_string())); - resource1.path = PathBuf::from("test_resource1"); - resource1.directory = PathBuf::from("test_directory"); - resource1.author = Some("Microsoft".to_string()); - resource1.properties = Some(vec!["Property1".to_string(), "Property2".to_string()]); - resource1.require_adapter = Some("Test/TestGroup".parse().unwrap()); - resource1.target_resource = None; - resource1.schema = None; - resource1.manifest = Some(ResourceManifest { - description: Some("This is a test resource.".to_string()), - schema_version: dsc_lib::dscresources::resource_manifest::ResourceManifest::default_schema_id_uri(), - resource_type: "Test/TestResource1".parse().unwrap(), - kind: Some(Kind::Resource), + let resource1 = DscResource { + type_name: "Test/TestResource1".parse().unwrap(), + kind: Kind::Resource, version: "1.0.0".to_string(), - get: Some(GetMethod { - executable: String::new(), + capabilities: vec![Capability::Get, Capability::Set], + deprecation_message: None, + description: Some("This is a test resource.".to_string()), + implemented_as: Some(ImplementedAs::Custom("TestResource".to_string())), + path: PathBuf::from("test_resource1"), + directory: PathBuf::from("test_directory"), + author: Some("Microsoft".to_string()), + properties: Some(vec!["Property1".to_string(), "Property2".to_string()]), + require_adapter: Some("Test/TestGroup".parse().unwrap()), + target_resource: None, + schema: None, + manifest: Some(ResourceManifest { + description: Some("This is a test resource.".to_string()), + schema_version: dsc_lib::dscresources::resource_manifest::ResourceManifest::default_schema_id_uri(), + resource_type: "Test/TestResource1".parse().unwrap(), + kind: Some(Kind::Resource), + version: "1.0.0".to_string(), + get: Some(GetMethod { + executable: String::new(), + ..Default::default() + }), ..Default::default() }), - ..Default::default() - }); - let mut resource2 = DscResource::new(); - resource2.type_name = "Test/TestResource2".parse().unwrap(); - resource2.kind = Kind::Resource; - resource2.version = "1.0.1".to_string(); - resource2.capabilities = vec![Capability::Get, Capability::Set]; - resource2.deprecation_message = None; - resource2.description = Some("This is a test resource.".to_string()); - resource2.implemented_as = Some(ImplementedAs::Custom("TestResource".to_string())); - resource2.path = PathBuf::from("test_resource2"); - resource2.directory = PathBuf::from("test_directory"); - resource2.author = Some("Microsoft".to_string()); - resource2.properties = Some(vec!["Property1".to_string(), "Property2".to_string()]); - resource2.require_adapter = Some("Test/TestGroup".parse().unwrap()); - resource2.target_resource = None; - resource2.schema = None; - resource2.manifest = Some(ResourceManifest { - description: Some("This is a test resource.".to_string()), - schema_version: dsc_lib::dscresources::resource_manifest::ResourceManifest::default_schema_id_uri(), - resource_type: "Test/TestResource2".parse().unwrap(), - kind: Some(Kind::Resource), + }; + let resource2 = DscResource { + type_name: "Test/TestResource2".parse().unwrap(), + kind: Kind::Resource, version: "1.0.1".to_string(), - get: Some(GetMethod { - executable: String::new(), + capabilities: vec![Capability::Get, Capability::Set], + deprecation_message: None, + description: Some("This is a test resource.".to_string()), + implemented_as: Some(ImplementedAs::Custom("TestResource".to_string())), + path: PathBuf::from("test_resource2"), + directory: PathBuf::from("test_directory"), + author: Some("Microsoft".to_string()), + properties: Some(vec!["Property1".to_string(), "Property2".to_string()]), + require_adapter: Some("Test/TestGroup".parse().unwrap()), + target_resource: None, + schema: None, + manifest: Some(ResourceManifest { + description: Some("This is a test resource.".to_string()), + schema_version: dsc_lib::dscresources::resource_manifest::ResourceManifest::default_schema_id_uri(), + resource_type: "Test/TestResource2".parse().unwrap(), + kind: Some(Kind::Resource), + version: "1.0.1".to_string(), + get: Some(GetMethod { + executable: String::new(), + ..Default::default() + }), ..Default::default() }), - ..Default::default() - }); + }; println!("{}", serde_json::to_string(&resource1).unwrap()); println!("{}", serde_json::to_string(&resource2).unwrap()); }, SubCommand::ListMissingRequires => { - let mut resource1 = DscResource::new(); - resource1.type_name = "Test/InvalidResource".parse().unwrap(); - resource1.kind = Kind::Resource; - resource1.version = "1.0.0".to_string(); - resource1.capabilities = vec![Capability::Get]; - resource1.deprecation_message = None; - resource1.description = Some("This is a test resource.".to_string()); - resource1.implemented_as = Some(ImplementedAs::Custom("TestResource".to_string())); - resource1.path = PathBuf::from("test_resource1"); - resource1.directory = PathBuf::from("test_directory"); - resource1.author = Some("Microsoft".to_string()); - resource1.properties = Some(vec!["Property1".to_string(), "Property2".to_string()]); - resource1.require_adapter = None; - resource1.target_resource = None; - resource1.manifest = None; - resource1.schema = None; + let resource1 = DscResource { + type_name: "Test/InvalidResource".parse().unwrap(), + kind: Kind::Resource, + version: "1.0.0".to_string(), + capabilities: vec![Capability::Get], + deprecation_message: None, + description: Some("This is a test resource.".to_string()), + implemented_as: Some(ImplementedAs::Custom("TestResource".to_string())), + path: PathBuf::from("test_resource1"), + directory: PathBuf::from("test_directory"), + author: Some("Microsoft".to_string()), + properties: Some(vec!["Property1".to_string(), "Property2".to_string()]), + require_adapter: None, + target_resource: None, + manifest: None, + schema: None, + }; println!("{}", serde_json::to_string(&resource1).unwrap()); } } From 53032d9bc570cb839d53d11d4925f175ea7e93c2 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 27 Feb 2026 14:17:04 -0800 Subject: [PATCH 5/9] fix build on non-Windows --- lib/dsc-lib/src/dscerror.rs | 3 +++ tools/dsctest/src/refresh_env.rs | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/dsc-lib/src/dscerror.rs b/lib/dsc-lib/src/dscerror.rs index 1c99fc960..4e6136682 100644 --- a/lib/dsc-lib/src/dscerror.rs +++ b/lib/dsc-lib/src/dscerror.rs @@ -112,12 +112,15 @@ pub enum DscError { #[error("{t}: {0}", t = t!("dscerror.progress"))] Progress(#[from] TemplateError), + #[cfg(windows)] #[error("{t}: {0}", t = t!("dscerror.registryHive"))] RegistryHive(#[from] registry::Error), + #[cfg(windows)] #[error("{t}: {0}", t = t!("dscerror.registryKey"))] RegistryKey(#[from] registry::key::Error), + #[cfg(windows)] #[error("{t}: {0}", t = t!("dscerror.registryIterator"))] RegistryIterator(#[from] registry::iter::values::Error), diff --git a/tools/dsctest/src/refresh_env.rs b/tools/dsctest/src/refresh_env.rs index 02c342aa5..04817b766 100644 --- a/tools/dsctest/src/refresh_env.rs +++ b/tools/dsctest/src/refresh_env.rs @@ -1,10 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#[cfg(windows)] use registry::{Hive, Security, value::Data}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; +#[cfg(windows)] use utfx::UCString; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] @@ -47,7 +49,7 @@ impl RefreshEnv { }; } } - + RefreshEnv { metadata: None, name: self.name.clone(), From d98d8a7766c4f0de248a537999f67d0e32fe4c08 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 27 Feb 2026 14:30:13 -0800 Subject: [PATCH 6/9] fix test --- dsc/tests/dsc_metadata.tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc/tests/dsc_metadata.tests.ps1 b/dsc/tests/dsc_metadata.tests.ps1 index f2c4ed9a8..3c8d15baf 100644 --- a/dsc/tests/dsc_metadata.tests.ps1 +++ b/dsc/tests/dsc_metadata.tests.ps1 @@ -249,8 +249,8 @@ Describe 'metadata tests' { $out = dsc -l trace config set -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json $errorLogContent = Get-Content $TestDrive/error.log -Raw $LASTEXITCODE | Should -Be 0 -Because $errorLogContent - $errorLogContent | Should -BeLike "*Resource returned '_refreshEnv' which indicates environment variable refresh is needed*" -Because $errorLogContent if ($IsWindows) { + $errorLogContent | Should -BeLike "*Resource returned '_refreshEnv' which indicates environment variable refresh is needed*" -Because $errorLogContent $out.results[1].result.afterState.output | Should -BeExactly 'myTestValue' -Because ($out | ConvertTo-Json -Depth 10) } else { $errorLogContent | Should -BeLike "*INFO*Resource returned '_refreshEnv' which is ignored on non-Windows platforms*" -Because $errorLogContent From 41edf0578b1fc354c914e49ef0ab624f19555d04 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 27 Feb 2026 14:47:47 -0800 Subject: [PATCH 7/9] fix globalized string --- lib/dsc-lib/locales/en-us.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 2f8856c55..028d5e791 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -288,7 +288,6 @@ description = "Converts a base64 representation to a string" invoked = "base64ToString function" invalidBase64 = "Invalid base64 encoding" invalidUtf8 = "Decoded bytes do not form valid UTF-8" -utf16Conversion = "Failed to convert UTF-16 bytes to string" [functions.bool] description = "Converts a string or number to a boolean" @@ -783,6 +782,7 @@ resourceMissingPath = "Resource is missing 'path' field." registryHive = "Failed to open registry hive" registryKey = "Failed to access registry key" registryIterator = "Failed to iterate over registry values" +utf16Conversion = "Failed to convert UTF-16 bytes to string" [progress] failedToSerialize = "Failed to serialize progress JSON: %{json}" From c917c0518092b22613b3471cff65d4d94aff3e8f Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Fri, 27 Feb 2026 20:06:00 -0800 Subject: [PATCH 8/9] address copilot feedback --- dsc/tests/dsc_metadata.tests.ps1 | 28 ++++++++++++++++++++++++ lib/dsc-lib/locales/en-us.toml | 3 +++ lib/dsc-lib/src/configure/config_doc.rs | 12 ++++++++++ lib/dsc-lib/src/configure/context.rs | 4 +++- lib/dsc-lib/src/configure/mod.rs | 25 +++++++++++++++++---- tools/dsctest/Cargo.toml | 4 +++- tools/dsctest/dsctest.dsc.manifests.json | 12 ++++++++++ tools/dsctest/src/main.rs | 3 ++- tools/dsctest/src/refresh_env.rs | 15 ++++++++----- 9 files changed, 93 insertions(+), 13 deletions(-) diff --git a/dsc/tests/dsc_metadata.tests.ps1 b/dsc/tests/dsc_metadata.tests.ps1 index 3c8d15baf..211b28448 100644 --- a/dsc/tests/dsc_metadata.tests.ps1 +++ b/dsc/tests/dsc_metadata.tests.ps1 @@ -286,4 +286,32 @@ Describe 'metadata tests' { [System.Environment]::SetEnvironmentVariable('PATH', $oldUserPath, [System.EnvironmentVariableTarget]::User) } } + + It '_refreshEnv does not trigger for ' -Skip:(!$IsWindows) -TestCases @( + @{ operation = 'get' } + @{ operation = 'test' } + ) { + param($operation) + + $configYaml = @' + $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: test + type: Test/RefreshEnv + properties: + name: myTestVariable + value: myTestValue + - name: return variable + type: Microsoft.DSC.Transitional/PowerShellScript + properties: + SetScript: | + if ($IsWindows) { + $env:myTestVariable + } +'@ + $out = dsc -l trace config $operation -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + (Get-Content $TestDrive/error.log -Raw) | Should -BeLike "*Resource returned '_refreshEnv' which indicates environment variable refresh is needed but current operation is '$operation' which is not 'set', so ignoring*" -Because (Get-Content $TestDrive/error.log -Raw) + $out.results[0].result.afterState.output | Should -Not -Be 'myTestValue' + } } diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 028d5e791..6c2785550 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -91,6 +91,9 @@ versionNotSatisfied = "Configuration requires DSC version '%{required_version}', invalidRegistryHive = "Invalid registry hive '%{hive}'" metadataRefreshEnvIgnored = "Resource returned '_refreshEnv' which is ignored on non-Windows platforms" metadataRefreshEnvFound = "Resource returned '_refreshEnv' which indicates environment variable refresh is needed" +metadataRefreshEnvOnlyAffectsSet = "Resource returned '_refreshEnv' which indicates environment variable refresh is needed but current operation is '%{operation}' which is not 'set', so ignoring" +metadataRefreshEnvNoOperationContext = "Resource returned '_refreshEnv' which indicates environment variable refresh is needed but no operation context is available" +metadataRefreshEnvNoContext = "Resource returned '_refreshEnv' which indicates environment variable refresh is needed but no context is available" [configure.parameters] importingParametersFromComplexInput = "Importing parameters from complex input" diff --git a/lib/dsc-lib/src/configure/config_doc.rs b/lib/dsc-lib/src/configure/config_doc.rs index 05966e797..a4d2b42fb 100644 --- a/lib/dsc-lib/src/configure/config_doc.rs +++ b/lib/dsc-lib/src/configure/config_doc.rs @@ -45,6 +45,18 @@ pub enum Operation { Export, } +impl Display for Operation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let operation_str = match self { + Operation::Get => "get", + Operation::Set => "set", + Operation::Test => "test", + Operation::Export => "export", + }; + write!(f, "{operation_str}") + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] #[serde(rename_all = "camelCase")] #[schemars(transform = idiomaticize_string_enum)] diff --git a/lib/dsc-lib/src/configure/context.rs b/lib/dsc-lib/src/configure/context.rs index 41d1dca06..eca2bebe1 100644 --- a/lib/dsc-lib/src/configure/context.rs +++ b/lib/dsc-lib/src/configure/context.rs @@ -2,7 +2,7 @@ // Licensed under the MIT License. use chrono::{DateTime, Local}; -use crate::{configure::config_doc::{ExecutionKind, UserFunctionDefinition}, extensions::dscextension::DscExtension}; +use crate::{configure::config_doc::{ExecutionKind, Operation, UserFunctionDefinition}, extensions::dscextension::DscExtension}; use dsc_lib_security_context::{get_security_context, SecurityContext}; use serde_json::{Map, Value}; use std::{collections::HashMap, path::PathBuf}; @@ -25,6 +25,7 @@ pub struct Context { pub dsc_version: Option, pub execution_type: ExecutionKind, pub extensions: Vec, + pub operation: Option, pub outputs: Map, pub parameters: HashMap, pub process_expressions: bool, @@ -49,6 +50,7 @@ impl Context { dsc_version: None, execution_type: ExecutionKind::Actual, extensions: Vec::new(), + operation: None, outputs: Map::new(), parameters: HashMap::new(), process_expressions: true, diff --git a/lib/dsc-lib/src/configure/mod.rs b/lib/dsc-lib/src/configure/mod.rs index 99750a8ff..80fab4d28 100644 --- a/lib/dsc-lib/src/configure/mod.rs +++ b/lib/dsc-lib/src/configure/mod.rs @@ -296,6 +296,20 @@ fn get_metadata_from_result(mut context: Option<&mut Context>, result: &mut Valu } } if key == "_refreshEnv" && value.as_bool() == Some(true) { + if let Some(ref context) = context { + if let Some(operation) = &context.operation { + if *operation != Operation::Set { + info!("{}", t!("configure.mod.metadataRefreshEnvOnlyAffectsSet", operation = operation)); + continue; + } + } else { + debug!("{}", t!("configure.mod.metadataRefreshEnvNoOperationContext")); + continue; + } + } else { + debug!("{}", t!("configure.mod.metadataRefreshEnvNoContext")); + continue; + } #[cfg(not(windows))] { info!("{}", t!("configure.mod.metadataRefreshEnvIgnored")); @@ -306,14 +320,14 @@ fn get_metadata_from_result(mut context: Option<&mut Context>, result: &mut Valu info!("{}", t!("configure.mod.metadataRefreshEnvFound")); // rebuild the environment variables from Windows registry // read the environment variables from HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment - // overlay with HKCU\Environment for user level variables except for PATH which is appended instead of overlayed + // overlay with HKCU\Environment for user level variables except for PATH which is prefixed instead of overlayed let mut env_vars = read_windows_registry("HKLM", r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment")?; let user_env_vars = read_windows_registry("HKCU", r"Environment")?; for (key, value) in user_env_vars { if key == "PATH" { if let Some(system_path) = env_vars.get(&key) { - trace!("Appending user PATH '{value}' to system PATH '{system_path}'"); - env_vars.insert(key, format!("{};{}", system_path, value)); + trace!("Prefixing user PATH '{value}' to system PATH '{system_path}'"); + env_vars.insert(key, format!("{};{}", value, system_path)); } else { trace!("System PATH not found, using user PATH '{value}' as PATH"); env_vars.insert(key, value.to_string()); @@ -324,7 +338,6 @@ fn get_metadata_from_result(mut context: Option<&mut Context>, result: &mut Valu } // set the current process env vars to the new values for (key, value) in env_vars { - trace!("Setting environment variable {key}='{value}'"); std::env::set_var(&key.to_string(), &value); } } @@ -455,6 +468,7 @@ impl Configurator { /// This function will return an error if the underlying resource fails. pub fn invoke_get(&mut self) -> Result { let mut result = ConfigurationGetResult::new(); + self.context.operation = Some(Operation::Get); let resources = get_resource_invocation_order(&self.config, &mut self.statement_parser, &mut self.context)?; let mut progress = ProgressBar::new(resources.len() as u64, self.progress_format)?; let discovery = &mut self.discovery.clone(); @@ -547,6 +561,7 @@ impl Configurator { #[allow(clippy::too_many_lines)] pub fn invoke_set(&mut self, skip_test: bool) -> Result { let mut result = ConfigurationSetResult::new(); + self.context.operation = Some(Operation::Set); let resources = get_resource_invocation_order(&self.config, &mut self.statement_parser, &mut self.context)?; let mut progress = ProgressBar::new(resources.len() as u64, self.progress_format)?; let discovery = &mut self.discovery.clone(); @@ -739,6 +754,7 @@ impl Configurator { /// This function will return an error if the underlying resource fails. pub fn invoke_test(&mut self) -> Result { let mut result = ConfigurationTestResult::new(); + self.context.operation = Some(Operation::Test); let resources = get_resource_invocation_order(&self.config, &mut self.statement_parser, &mut self.context)?; let mut progress = ProgressBar::new(resources.len() as u64, self.progress_format)?; let discovery = &mut self.discovery.clone(); @@ -827,6 +843,7 @@ impl Configurator { /// This function will return an error if the underlying resource fails. pub fn invoke_export(&mut self) -> Result { let mut result = ConfigurationExportResult::new(); + self.context.operation = Some(Operation::Export); let mut conf = config_doc::Configuration::new(); conf.metadata.clone_from(&self.config.metadata); diff --git a/tools/dsctest/Cargo.toml b/tools/dsctest/Cargo.toml index 9a2815d20..02438e7c2 100644 --- a/tools/dsctest/Cargo.toml +++ b/tools/dsctest/Cargo.toml @@ -7,8 +7,10 @@ edition = "2021" [dependencies] clap = { workspace = true } -registry = { workspace = true } schemars = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } utfx = { workspace = true } + +[target.'cfg(windows)'.dependencies] +registry = { workspace = true } diff --git a/tools/dsctest/dsctest.dsc.manifests.json b/tools/dsctest/dsctest.dsc.manifests.json index 93b9265b8..c2c85811f 100644 --- a/tools/dsctest/dsctest.dsc.manifests.json +++ b/tools/dsctest/dsctest.dsc.manifests.json @@ -406,6 +406,18 @@ "implementsPretest": true, "return": "state" }, + "export": { + "executable": "dsctest", + "args": [ + "refresh-env", + "--operation", + "get", + { + "jsonInputArg": "--input", + "mandatory": true + } + ] + }, "schema": { "command": { "executable": "dsctest", diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index 8e68f4c71..53b2f5cc4 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -256,7 +256,8 @@ fn main() { }; match operation { RefreshEnvOperation::Get => { - let result = refresh_env.get(); + let mut result = refresh_env.get(); + result.metadata.get_or_insert(Map::new()).insert("_refreshEnv".to_string(), serde_json::Value::Bool(true)); serde_json::to_string(&result).unwrap() }, RefreshEnvOperation::Set => { diff --git a/tools/dsctest/src/refresh_env.rs b/tools/dsctest/src/refresh_env.rs index 04817b766..bfe797961 100644 --- a/tools/dsctest/src/refresh_env.rs +++ b/tools/dsctest/src/refresh_env.rs @@ -41,12 +41,15 @@ impl RefreshEnv { // Get the environment variable from the registry for current user let hkcu = Hive::CurrentUser.open("Environment", Security::Read).unwrap(); if let Ok(data) = hkcu.value(&self.name) { - if let Data::String(value) = data { - return RefreshEnv { - metadata: None, - name: self.name.clone(), - value: value.to_string_lossy(), - }; + match data { + Data::String(value) | Data::ExpandString(value) => { + return RefreshEnv { + metadata: None, + name: self.name.clone(), + value: value.to_string_lossy(), + }; + } + _ => {} } } From 5c174ee7a5aeb8d904d3c7e5d29d1354aa85072d Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 2 Mar 2026 11:03:52 -0800 Subject: [PATCH 9/9] Update lib/dsc-lib/src/dscerror.rs Co-authored-by: Tess Gauthier --- lib/dsc-lib/src/dscerror.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/dsc-lib/src/dscerror.rs b/lib/dsc-lib/src/dscerror.rs index 4e6136682..55b0ad94e 100644 --- a/lib/dsc-lib/src/dscerror.rs +++ b/lib/dsc-lib/src/dscerror.rs @@ -116,14 +116,14 @@ pub enum DscError { #[error("{t}: {0}", t = t!("dscerror.registryHive"))] RegistryHive(#[from] registry::Error), - #[cfg(windows)] - #[error("{t}: {0}", t = t!("dscerror.registryKey"))] - RegistryKey(#[from] registry::key::Error), - #[cfg(windows)] #[error("{t}: {0}", t = t!("dscerror.registryIterator"))] RegistryIterator(#[from] registry::iter::values::Error), + #[cfg(windows)] + #[error("{t}: {0}", t = t!("dscerror.registryKey"))] + RegistryKey(#[from] registry::key::Error), + #[error("{t}: {0}", t = t!("dscerror.resourceMissingDirectory"))] ResourceMissingDirectory(String),