From d74420b18ff5136cbd89c474a76b73c39e623366 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 30 Oct 2025 17:15:55 -0700 Subject: [PATCH 1/8] Enable resource manifests to have relative paths --- dsc/tests/dsc_discovery.tests.ps1 | 42 +++++++++++++++++++ .../src/discovery/command_discovery.rs | 34 +++++++++------ .../src/dscresources/command_resource.rs | 15 ++++++- 3 files changed, 77 insertions(+), 14 deletions(-) diff --git a/dsc/tests/dsc_discovery.tests.ps1 b/dsc/tests/dsc_discovery.tests.ps1 index 46ba91f03..3eb181c6b 100644 --- a/dsc/tests/dsc_discovery.tests.ps1 +++ b/dsc/tests/dsc_discovery.tests.ps1 @@ -245,4 +245,46 @@ Describe 'tests for resource discovery' { $env:DSC_RESOURCE_PATH = $oldPath } } + + It 'Resource manifest using relative path to exe works' { + $manifest = @' +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Microsoft.DSC.Debug/Echo", + "version": "1.0.0", + "description": "Echo resource for testing and debugging purposes", + "get": { + "executable": "../dscecho", + "args": [ + { + "jsonInputArg": "--input", + "mandatory": true + } + ] + }, + "schema": { + "command": { + "executable": "../dscecho" + } + } +} +'@ + $dscEcho = Get-Command dscecho -ErrorAction Stop + # copy to testdrive + Copy-Item -Path "$($dscEcho.Source)" -Destination $testdrive + # create manifest in subfolder + $subfolder = Join-Path $testdrive 'subfolder' + New-Item -Path $subfolder -ItemType Directory | Out-Null + Set-Content -Path (Join-Path $subfolder 'test.dsc.resource.json') -Value $manifest + + try { + $env:DSC_RESOURCE_PATH = $subfolder + $out = dsc resource get -r 'Microsoft.DSC.Debug/Echo' -i '{"output":"RelativePathTest"}' 2> "$testdrive/error.txt" | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.actualState.output | Should -BeExactly 'RelativePathTest' + } + finally { + $env:DSC_RESOURCE_PATH = $null + } + } } diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 03941127c..967d519dd 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -711,38 +711,38 @@ fn load_resource_manifest(path: &Path, manifest: &ResourceManifest) -> Result = vec![]; if let Some(get) = &manifest.get { - verify_executable(&manifest.resource_type, "get", &get.executable); + verify_executable(&manifest.resource_type, "get", &get.executable, path.parent().unwrap()); capabilities.push(Capability::Get); } if let Some(set) = &manifest.set { - verify_executable(&manifest.resource_type, "set", &set.executable); + verify_executable(&manifest.resource_type, "set", &set.executable, path.parent().unwrap()); capabilities.push(Capability::Set); if set.handles_exist == Some(true) { capabilities.push(Capability::SetHandlesExist); } } if let Some(what_if) = &manifest.what_if { - verify_executable(&manifest.resource_type, "what_if", &what_if.executable); + verify_executable(&manifest.resource_type, "what_if", &what_if.executable, path.parent().unwrap()); capabilities.push(Capability::WhatIf); } if let Some(test) = &manifest.test { - verify_executable(&manifest.resource_type, "test", &test.executable); + verify_executable(&manifest.resource_type, "test", &test.executable, path.parent().unwrap()); capabilities.push(Capability::Test); } if let Some(delete) = &manifest.delete { - verify_executable(&manifest.resource_type, "delete", &delete.executable); + verify_executable(&manifest.resource_type, "delete", &delete.executable, path.parent().unwrap()); capabilities.push(Capability::Delete); } if let Some(export) = &manifest.export { - verify_executable(&manifest.resource_type, "export", &export.executable); + verify_executable(&manifest.resource_type, "export", &export.executable, path.parent().unwrap()); capabilities.push(Capability::Export); } if let Some(resolve) = &manifest.resolve { - verify_executable(&manifest.resource_type, "resolve", &resolve.executable); + verify_executable(&manifest.resource_type, "resolve", &resolve.executable, path.parent().unwrap()); capabilities.push(Capability::Resolve); } if let Some(SchemaKind::Command(command)) = &manifest.schema { - verify_executable(&manifest.resource_type, "schema", &command.executable); + verify_executable(&manifest.resource_type, "schema", &command.executable, path.parent().unwrap()); } let resource = DscResource { @@ -768,15 +768,15 @@ fn load_extension_manifest(path: &Path, manifest: &ExtensionManifest) -> Result< let mut capabilities: Vec = vec![]; if let Some(discover) = &manifest.discover { - verify_executable(&manifest.r#type, "discover", &discover.executable); + verify_executable(&manifest.r#type, "discover", &discover.executable, path.parent().unwrap()); capabilities.push(dscextension::Capability::Discover); } if let Some(secret) = &manifest.secret { - verify_executable(&manifest.r#type, "secret", &secret.executable); + verify_executable(&manifest.r#type, "secret", &secret.executable, path.parent().unwrap()); capabilities.push(dscextension::Capability::Secret); } let import_extensions = if let Some(import) = &manifest.import { - verify_executable(&manifest.r#type, "import", &import.executable); + verify_executable(&manifest.r#type, "import", &import.executable, path.parent().unwrap()); capabilities.push(dscextension::Capability::Import); if import.file_extensions.is_empty() { warn!("{}", t!("discovery.commandDiscovery.importExtensionsEmpty", extension = manifest.r#type)); @@ -803,7 +803,17 @@ fn load_extension_manifest(path: &Path, manifest: &ExtensionManifest) -> Result< Ok(extension) } -fn verify_executable(resource: &str, operation: &str, executable: &str) { +fn verify_executable(resource: &str, operation: &str, executable: &str, directory: &Path) { + // check if executable has a relative path + if !Path::new(executable).is_absolute() { + // combine with directory and see if it exists + let exe_path = directory.join(executable); + if exe_path.exists() { + return; + } + info!("{}", t!("discovery.commandDiscovery.executableNotFound", resource = resource, operation = operation, executable = exe_path.to_string_lossy())); + return; + } if which(executable).is_err() { info!("{}", t!("discovery.commandDiscovery.executableNotFound", resource = resource, operation = operation, executable = executable)); } diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index aacf17fc1..16e48cd4e 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -6,7 +6,7 @@ use jsonschema::Validator; use rust_i18n::t; use serde::Deserialize; use serde_json::{Map, Value}; -use std::{collections::HashMap, env, process::Stdio}; +use std::{collections::HashMap, env, path::Path, process::Stdio}; use crate::configure::{config_doc::ExecutionKind, config_result::{ResourceGetResult, ResourceTestResult}}; use crate::dscerror::DscError; use super::{dscresource::{get_diff, redact}, invoke_result::{ExportResult, GetResult, ResolveResult, SetResult, TestResult, ValidateResult, ResourceGetResponse, ResourceSetResponse, ResourceTestResponse, get_in_desired_state}, resource_manifest::{ArgKind, InputKind, Kind, ResourceManifest, ReturnKind, SchemaKind}}; @@ -763,6 +763,17 @@ fn convert_hashmap_string_keys_to_i32(input: Option<&HashMap>) - #[allow(clippy::implicit_hasher)] pub fn invoke_command(executable: &str, args: Option>, input: Option<&str>, cwd: Option<&str>, env: Option>, exit_codes: Option<&HashMap>) -> Result<(i32, String, String), DscError> { let exit_codes = convert_hashmap_string_keys_to_i32(exit_codes)?; + let mut executable = executable.to_string(); + if !Path::new(&executable).is_absolute() && cwd.is_some() { + if let Some(cwd) = cwd { + let cwd_path = Path::new(cwd); + let executable_path = cwd_path.join(&executable); + if !executable_path.exists() { + return Err(DscError::CommandOperation(t!("dscresources.commandResource.executableNotFound", executable = executable_path.display()).to_string(), executable.to_string())); + } + executable = executable_path.to_string_lossy().to_string(); + } + } tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on( async { @@ -771,7 +782,7 @@ pub fn invoke_command(executable: &str, args: Option>, input: Option trace!("{}", t!("dscresources.commandResource.commandCwd", cwd = cwd)); } - match run_process_async(executable, args, input, cwd, env, exit_codes.as_ref()).await { + match run_process_async(&executable, args, input, cwd, env, exit_codes.as_ref()).await { Ok((code, stdout, stderr)) => { Ok((code, stdout, stderr)) }, From a83d5c06a18774d5632627a0cdca46f75d41d15f Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Thu, 30 Oct 2025 17:35:05 -0700 Subject: [PATCH 2/8] Fix detection of relative path --- lib/dsc-lib/locales/en-us.toml | 1 + lib/dsc-lib/src/discovery/command_discovery.rs | 18 +++++++++--------- .../src/dscresources/command_resource.rs | 3 ++- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index fd2de8f84..2c70e4fdf 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -169,6 +169,7 @@ invalidKey = "Unsupported value for key '%{key}'. Only string, bool, number, an inDesiredStateNotBool = "'_inDesiredState' is not a boolean" exportNotSupportedUsingGet = "Export is not supported by resource '%{resource}' using get operation" runProcessError = "Failed to run process '%{executable}': %{error}" +executableNotFound = "Executable '%{executable}' not found" [dscresources.dscresource] invokeGet = "Invoking get for '%{resource}'" diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 967d519dd..f86381674 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -804,17 +804,17 @@ fn load_extension_manifest(path: &Path, manifest: &ExtensionManifest) -> Result< } fn verify_executable(resource: &str, operation: &str, executable: &str, directory: &Path) { - // check if executable has a relative path - if !Path::new(executable).is_absolute() { - // combine with directory and see if it exists - let exe_path = directory.join(executable); - if exe_path.exists() { + if which(executable).is_err() { + if !Path::new(executable).is_absolute() { + // combine with directory and see if it exists + let exe_path = directory.join(executable); + if exe_path.exists() { + return; + } + info!("{}", t!("discovery.commandDiscovery.executableNotFound", resource = resource, operation = operation, executable = exe_path.to_string_lossy())); return; } - info!("{}", t!("discovery.commandDiscovery.executableNotFound", resource = resource, operation = operation, executable = exe_path.to_string_lossy())); - return; - } - if which(executable).is_err() { + info!("{}", t!("discovery.commandDiscovery.executableNotFound", resource = resource, operation = operation, executable = executable)); } } diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index 16e48cd4e..d2d713f39 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -12,6 +12,7 @@ use crate::dscerror::DscError; use super::{dscresource::{get_diff, redact}, invoke_result::{ExportResult, GetResult, ResolveResult, SetResult, TestResult, ValidateResult, ResourceGetResponse, ResourceSetResponse, ResourceTestResponse, get_in_desired_state}, resource_manifest::{ArgKind, InputKind, Kind, ResourceManifest, ReturnKind, SchemaKind}}; use tracing::{error, warn, info, debug, trace}; use tokio::{io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, process::Command}; +use which::which; pub const EXIT_PROCESS_TERMINATED: i32 = 0x102; @@ -764,7 +765,7 @@ fn convert_hashmap_string_keys_to_i32(input: Option<&HashMap>) - pub fn invoke_command(executable: &str, args: Option>, input: Option<&str>, cwd: Option<&str>, env: Option>, exit_codes: Option<&HashMap>) -> Result<(i32, String, String), DscError> { let exit_codes = convert_hashmap_string_keys_to_i32(exit_codes)?; let mut executable = executable.to_string(); - if !Path::new(&executable).is_absolute() && cwd.is_some() { + if which(&executable).is_err() && !Path::new(&executable).is_absolute() && cwd.is_some() { if let Some(cwd) = cwd { let cwd_path = Path::new(cwd); let executable_path = cwd_path.join(&executable); From 6cd4079c8aa4e6ada7fe45b4d9417014140e2077 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Fri, 31 Oct 2025 07:49:34 -0700 Subject: [PATCH 3/8] add helper --- dsc/tests/dsc_discovery.tests.ps1 | 2 +- lib/dsc-lib/locales/en-us.toml | 2 +- .../src/discovery/command_discovery.rs | 25 +++++------------- lib/dsc-lib/src/dscerror.rs | 3 +++ .../src/dscresources/command_resource.rs | 17 +++--------- lib/dsc-lib/src/util.rs | 26 ++++++++++++++++++- 6 files changed, 40 insertions(+), 35 deletions(-) diff --git a/dsc/tests/dsc_discovery.tests.ps1 b/dsc/tests/dsc_discovery.tests.ps1 index 3eb181c6b..e82fb0389 100644 --- a/dsc/tests/dsc_discovery.tests.ps1 +++ b/dsc/tests/dsc_discovery.tests.ps1 @@ -280,7 +280,7 @@ Describe 'tests for resource discovery' { try { $env:DSC_RESOURCE_PATH = $subfolder $out = dsc resource get -r 'Microsoft.DSC.Debug/Echo' -i '{"output":"RelativePathTest"}' 2> "$testdrive/error.txt" | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw -Path "$testdrive/error.txt") $out.actualState.output | Should -BeExactly 'RelativePathTest' } finally { diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 2c70e4fdf..1cccbed78 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -169,7 +169,6 @@ invalidKey = "Unsupported value for key '%{key}'. Only string, bool, number, an inDesiredStateNotBool = "'_inDesiredState' is not a boolean" exportNotSupportedUsingGet = "Export is not supported by resource '%{resource}' using get operation" runProcessError = "Failed to run process '%{executable}': %{error}" -executableNotFound = "Executable '%{executable}' not found" [dscresources.dscresource] invokeGet = "Invoking get for '%{resource}'" @@ -699,3 +698,4 @@ failedToGetExePath = "Can't get 'dsc' executable path" settingNotFound = "Setting '%{name}' not found" failedToAbsolutizePath = "Failed to absolutize path '%{path}'" invalidExitCodeKey = "Invalid exit code key '%{key}'" +executableNotFound = "Executable '%{executable}' not found with working directory '%{cwd}'" diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index f86381674..cc8101d57 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -20,14 +20,13 @@ use serde::Deserialize; use std::{collections::{BTreeMap, HashMap, HashSet}, sync::{LazyLock, RwLock}}; use std::env; use std::ffi::OsStr; -use std::fs; +use std::fs::{create_dir_all, read, read_to_string, write}; use std::path::{Path, PathBuf}; use std::str::FromStr; use tracing::{debug, info, trace, warn}; -use which::which; use crate::util::get_setting; -use crate::util::get_exe_path; +use crate::util::{canonicalize_which, get_exe_path}; const DSC_EXTENSION_EXTENSIONS: [&str; 3] = [".dsc.extension.json", ".dsc.extension.yaml", ".dsc.extension.yml"]; const DSC_MANIFEST_LIST_EXTENSIONS: [&str; 3] = [".dsc.manifests.json", ".dsc.manifests.yaml", ".dsc.manifests.yml"]; @@ -621,7 +620,7 @@ fn insert_resource(resources: &mut BTreeMap>, resource: /// /// * Returns a `DscError` if the manifest could not be loaded or parsed. pub fn load_manifest(path: &Path) -> Result, DscError> { - let contents = fs::read_to_string(path)?; + let contents = read_to_string(path)?; let file_name_lowercase = path.file_name().and_then(OsStr::to_str).unwrap_or("").to_lowercase(); let extension_is_json = path.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("json")); if DSC_RESOURCE_EXTENSIONS.iter().any(|ext| file_name_lowercase.ends_with(ext)) { @@ -804,17 +803,7 @@ fn load_extension_manifest(path: &Path, manifest: &ExtensionManifest) -> Result< } fn verify_executable(resource: &str, operation: &str, executable: &str, directory: &Path) { - if which(executable).is_err() { - if !Path::new(executable).is_absolute() { - // combine with directory and see if it exists - let exe_path = directory.join(executable); - if exe_path.exists() { - return; - } - info!("{}", t!("discovery.commandDiscovery.executableNotFound", resource = resource, operation = operation, executable = exe_path.to_string_lossy())); - return; - } - + if canonicalize_which(executable, Some(directory.to_string_lossy().as_ref())).is_err() { info!("{}", t!("discovery.commandDiscovery.executableNotFound", resource = resource, operation = operation, executable = executable)); } } @@ -849,8 +838,8 @@ fn save_adapted_resources_lookup_table(lookup_table: &HashMap) let path = std::path::Path::new(&file_path); if let Some(prefix) = path.parent() { - if fs::create_dir_all(prefix).is_ok() { - if fs::write(file_path.clone(), lookup_table_json).is_err() { + if create_dir_all(prefix).is_ok() { + if write(file_path.clone(), lookup_table_json).is_err() { info!("Unable to write lookup_table file {file_path:?}"); } } else { @@ -868,7 +857,7 @@ fn load_adapted_resources_lookup_table() -> HashMap { let file_path = get_lookup_table_file_path(); - let lookup_table: HashMap = match fs::read(file_path.clone()){ + let lookup_table: HashMap = match read(file_path.clone()){ Ok(data) => { serde_json::from_slice(&data).unwrap_or_default() }, Err(_) => { HashMap::new() } }; diff --git a/lib/dsc-lib/src/dscerror.rs b/lib/dsc-lib/src/dscerror.rs index f3ef26a1d..947531831 100644 --- a/lib/dsc-lib/src/dscerror.rs +++ b/lib/dsc-lib/src/dscerror.rs @@ -26,6 +26,9 @@ pub enum DscError { #[error("{t} '{0}' [{t2} {1}] {t3}: {2}", t = t!("dscerror.commandResource"), t2 = t!("dscerror.exitCode"), t3 = t!("dscerror.manifestDescription"))] CommandExitFromManifest(String, i32, String), + #[error("{0}")] + CommandNotFound(String), + #[error("{t} {0} {t2} '{1}'", t = t!("dscerror.commandOperation"), t2 = t!("dscerror.forExecutable"))] CommandOperation(String, String), diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index d2d713f39..67e4f104e 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -6,13 +6,12 @@ use jsonschema::Validator; use rust_i18n::t; use serde::Deserialize; use serde_json::{Map, Value}; -use std::{collections::HashMap, env, path::Path, process::Stdio}; -use crate::configure::{config_doc::ExecutionKind, config_result::{ResourceGetResult, ResourceTestResult}}; +use std::{collections::HashMap, env, process::Stdio}; +use crate::{configure::{config_doc::ExecutionKind, config_result::{ResourceGetResult, ResourceTestResult}}, util::canonicalize_which}; use crate::dscerror::DscError; use super::{dscresource::{get_diff, redact}, invoke_result::{ExportResult, GetResult, ResolveResult, SetResult, TestResult, ValidateResult, ResourceGetResponse, ResourceSetResponse, ResourceTestResponse, get_in_desired_state}, resource_manifest::{ArgKind, InputKind, Kind, ResourceManifest, ReturnKind, SchemaKind}}; use tracing::{error, warn, info, debug, trace}; use tokio::{io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, process::Command}; -use which::which; pub const EXIT_PROCESS_TERMINATED: i32 = 0x102; @@ -764,17 +763,7 @@ fn convert_hashmap_string_keys_to_i32(input: Option<&HashMap>) - #[allow(clippy::implicit_hasher)] pub fn invoke_command(executable: &str, args: Option>, input: Option<&str>, cwd: Option<&str>, env: Option>, exit_codes: Option<&HashMap>) -> Result<(i32, String, String), DscError> { let exit_codes = convert_hashmap_string_keys_to_i32(exit_codes)?; - let mut executable = executable.to_string(); - if which(&executable).is_err() && !Path::new(&executable).is_absolute() && cwd.is_some() { - if let Some(cwd) = cwd { - let cwd_path = Path::new(cwd); - let executable_path = cwd_path.join(&executable); - if !executable_path.exists() { - return Err(DscError::CommandOperation(t!("dscresources.commandResource.executableNotFound", executable = executable_path.display()).to_string(), executable.to_string())); - } - executable = executable_path.to_string_lossy().to_string(); - } - } + let executable = canonicalize_which(executable, cwd)?; tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on( async { diff --git a/lib/dsc-lib/src/util.rs b/lib/dsc-lib/src/util.rs index ea518c585..6075f8bc1 100644 --- a/lib/dsc-lib/src/util.rs +++ b/lib/dsc-lib/src/util.rs @@ -6,12 +6,13 @@ use rust_i18n::t; use serde_json::Value; use std::{ fs, - fs::File, + fs::{canonicalize, File}, io::BufReader, path::{Path, PathBuf}, env, }; use tracing::debug; +use which::which; pub struct DscSettingValue { pub setting: Value, @@ -232,6 +233,29 @@ pub fn resource_id(type_name: &str, name: &str) -> String { result } +pub fn canonicalize_which(executable: &str, cwd: Option<&str>) -> Result { + let mut executable = executable.to_string().replace("/", std::path::MAIN_SEPARATOR_STR); + if cfg!(target_os = "windows") && !executable.ends_with(".exe") { + let mut exe_path = PathBuf::from(&executable); + exe_path.set_extension("exe"); + executable = exe_path.to_string_lossy().to_string(); + } + if which(&executable).is_err() && !Path::new(&executable).is_absolute() && cwd.is_some() { + if let Some(cwd) = cwd { + let cwd_path = Path::new(cwd); + match canonicalize(cwd_path.join(&executable)) { + Err(_err) => { + return Err(DscError::CommandOperation(t!("util.executableNotFound", executable = &executable, cwd = cwd).to_string(), executable.to_string())); + }, + Ok(canonical_path) => { + executable = canonical_path.to_string_lossy().to_string(); + } + } + } + } + Ok(executable) +} + #[macro_export] macro_rules! locked_is_empty { ($lockable:expr) => {{ From 8c8010e7c61279bc7f367ef3b82bff5c6b283e30 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 31 Oct 2025 12:33:39 -0700 Subject: [PATCH 4/8] Update lib/dsc-lib/src/util.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/dsc-lib/src/util.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/dsc-lib/src/util.rs b/lib/dsc-lib/src/util.rs index 6075f8bc1..5eb08bc3c 100644 --- a/lib/dsc-lib/src/util.rs +++ b/lib/dsc-lib/src/util.rs @@ -234,12 +234,12 @@ pub fn resource_id(type_name: &str, name: &str) -> String { } pub fn canonicalize_which(executable: &str, cwd: Option<&str>) -> Result { - let mut executable = executable.to_string().replace("/", std::path::MAIN_SEPARATOR_STR); - if cfg!(target_os = "windows") && !executable.ends_with(".exe") { - let mut exe_path = PathBuf::from(&executable); - exe_path.set_extension("exe"); - executable = exe_path.to_string_lossy().to_string(); + // Use PathBuf to handle path separators robustly + let mut executable_path = PathBuf::from(executable); + if cfg!(target_os = "windows") && executable_path.extension().map_or(true, |ext| ext != "exe") { + executable_path.set_extension("exe"); } + let mut executable = executable_path.to_string_lossy().to_string(); if which(&executable).is_err() && !Path::new(&executable).is_absolute() && cwd.is_some() { if let Some(cwd) = cwd { let cwd_path = Path::new(cwd); From de4cd2cb67426ba15a91fbe84370b6d644f94cc3 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Fri, 31 Oct 2025 12:46:47 -0700 Subject: [PATCH 5/8] clean up code, add negative test --- dsc/tests/dsc_discovery.tests.ps1 | 27 ++++++++++++++++++--------- lib/dsc-lib/src/util.rs | 6 +++--- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/dsc/tests/dsc_discovery.tests.ps1 b/dsc/tests/dsc_discovery.tests.ps1 index e82fb0389..6f168e692 100644 --- a/dsc/tests/dsc_discovery.tests.ps1 +++ b/dsc/tests/dsc_discovery.tests.ps1 @@ -246,15 +246,19 @@ Describe 'tests for resource discovery' { } } - It 'Resource manifest using relative path to exe works' { - $manifest = @' + It 'Resource manifest using relative path to exe: ' -TestCases @( + @{ path = '../dscecho'; success = $true } + @{ path = '../foo/dscecho'; success = $false } + ) { + param($path, $success) + $manifest = @" { - "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "`$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", "type": "Microsoft.DSC.Debug/Echo", "version": "1.0.0", "description": "Echo resource for testing and debugging purposes", "get": { - "executable": "../dscecho", + "executable": "$path", "args": [ { "jsonInputArg": "--input", @@ -264,24 +268,29 @@ Describe 'tests for resource discovery' { }, "schema": { "command": { - "executable": "../dscecho" + "executable": "$path" } } } -'@ +"@ $dscEcho = Get-Command dscecho -ErrorAction Stop # copy to testdrive Copy-Item -Path "$($dscEcho.Source)" -Destination $testdrive # create manifest in subfolder $subfolder = Join-Path $testdrive 'subfolder' - New-Item -Path $subfolder -ItemType Directory | Out-Null + New-Item -Path $subfolder -ItemType Directory -Force | Out-Null Set-Content -Path (Join-Path $subfolder 'test.dsc.resource.json') -Value $manifest try { $env:DSC_RESOURCE_PATH = $subfolder $out = dsc resource get -r 'Microsoft.DSC.Debug/Echo' -i '{"output":"RelativePathTest"}' 2> "$testdrive/error.txt" | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw -Path "$testdrive/error.txt") - $out.actualState.output | Should -BeExactly 'RelativePathTest' + if ($success) { + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw -Path "$testdrive/error.txt") + $out.actualState.output | Should -BeExactly 'RelativePathTest' + } else { + $LASTEXITCODE | Should -Be 2 -Because (Get-Content -Raw -Path "$testdrive/error.txt") + (Get-Content -Raw -Path "$testdrive/error.txt") | Should -Match "ERROR.*?Executable '\.\./foo/dscecho(\.exe)?' not found" + } } finally { $env:DSC_RESOURCE_PATH = $null diff --git a/lib/dsc-lib/src/util.rs b/lib/dsc-lib/src/util.rs index 5eb08bc3c..19d88627e 100644 --- a/lib/dsc-lib/src/util.rs +++ b/lib/dsc-lib/src/util.rs @@ -236,16 +236,16 @@ pub fn resource_id(type_name: &str, name: &str) -> String { pub fn canonicalize_which(executable: &str, cwd: Option<&str>) -> Result { // Use PathBuf to handle path separators robustly let mut executable_path = PathBuf::from(executable); - if cfg!(target_os = "windows") && executable_path.extension().map_or(true, |ext| ext != "exe") { + if cfg!(target_os = "windows") && executable_path.extension().is_none() { executable_path.set_extension("exe"); } let mut executable = executable_path.to_string_lossy().to_string(); - if which(&executable).is_err() && !Path::new(&executable).is_absolute() && cwd.is_some() { + if which(&executable).is_err() && !Path::new(&executable).is_absolute() { if let Some(cwd) = cwd { let cwd_path = Path::new(cwd); match canonicalize(cwd_path.join(&executable)) { Err(_err) => { - return Err(DscError::CommandOperation(t!("util.executableNotFound", executable = &executable, cwd = cwd).to_string(), executable.to_string())); + return Err(DscError::CommandOperation(t!("util.executableNotFound", executable = &executable, cwd = cwd_path.to_string_lossy()).to_string(), executable.to_string())); }, Ok(canonical_path) => { executable = canonical_path.to_string_lossy().to_string(); From e2645532f67cafd5550f9ae22843b58ea12eadd6 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Fri, 31 Oct 2025 13:48:49 -0700 Subject: [PATCH 6/8] update test --- dsc/tests/dsc_extension_discover.tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc/tests/dsc_extension_discover.tests.ps1 b/dsc/tests/dsc_extension_discover.tests.ps1 index 043713536..b25d45215 100644 --- a/dsc/tests/dsc_extension_discover.tests.ps1 +++ b/dsc/tests/dsc_extension_discover.tests.ps1 @@ -138,7 +138,7 @@ Describe 'Discover extension tests' { $out = dsc -l warn resource list 2> $TestDrive/error.log | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 $out.Count | Should -BeGreaterThan 0 - (Get-Content -Path "$TestDrive/error.log" -Raw) | Should -BeLike "*WARN Extension 'Microsoft.Windows.Appx/Discover' failed to discover resources: Command: Operation program not found for executable 'powershell'*" -Because (Get-Content -Path "$TestDrive/error.log" -Raw | Out-String) + (Get-Content -Path "$TestDrive/error.log" -Raw) | Should -BeLike "*WARN Extension 'Microsoft.Windows.Appx/Discover' failed to discover resources: Command: Operation Executable 'powershell.exe' not found*" -Because (Get-Content -Path "$TestDrive/error.log" -Raw | Out-String) } finally { $env:PATH = $oldPath } From 79c27ea2723462ba90720b48e42b71879b9d0c5c Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 3 Nov 2025 13:07:14 -0800 Subject: [PATCH 7/8] code cleanup --- lib/dsc-lib/src/util.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/dsc-lib/src/util.rs b/lib/dsc-lib/src/util.rs index 19d88627e..3d5b277b4 100644 --- a/lib/dsc-lib/src/util.rs +++ b/lib/dsc-lib/src/util.rs @@ -239,21 +239,16 @@ pub fn canonicalize_which(executable: &str, cwd: Option<&str>) -> Result { - return Err(DscError::CommandOperation(t!("util.executableNotFound", executable = &executable, cwd = cwd_path.to_string_lossy()).to_string(), executable.to_string())); - }, - Ok(canonical_path) => { - executable = canonical_path.to_string_lossy().to_string(); - } + if let Ok(canonical_path) = canonicalize(cwd_path.join(executable)) { + return Ok(canonical_path.to_string_lossy().to_string()); } } + return Err(DscError::CommandOperation(t!("util.executableNotFound", executable = &executable, cwd = cwd : {:?}).to_string(), executable.to_string())); } - Ok(executable) + Ok(executable.to_string()) } #[macro_export] From 0899801993c01d951d5386efacbf9d827dddef7d Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Mon, 3 Nov 2025 14:56:38 -0800 Subject: [PATCH 8/8] fix using canonical path and test --- dsc/tests/dsc_extension_discover.tests.ps1 | 2 +- lib/dsc-lib/locales/en-us.toml | 3 ++- lib/dsc-lib/src/util.rs | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/dsc/tests/dsc_extension_discover.tests.ps1 b/dsc/tests/dsc_extension_discover.tests.ps1 index b25d45215..8e015ed92 100644 --- a/dsc/tests/dsc_extension_discover.tests.ps1 +++ b/dsc/tests/dsc_extension_discover.tests.ps1 @@ -138,7 +138,7 @@ Describe 'Discover extension tests' { $out = dsc -l warn resource list 2> $TestDrive/error.log | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 $out.Count | Should -BeGreaterThan 0 - (Get-Content -Path "$TestDrive/error.log" -Raw) | Should -BeLike "*WARN Extension 'Microsoft.Windows.Appx/Discover' failed to discover resources: Command: Operation Executable 'powershell.exe' not found*" -Because (Get-Content -Path "$TestDrive/error.log" -Raw | Out-String) + (Get-Content -Path "$TestDrive/error.log" -Raw) | Should -BeLike "*WARN Extension 'Microsoft.Windows.Appx/Discover' failed to discover resources: Command: Operation Executable 'powershell' not found*" -Because (Get-Content -Path "$TestDrive/error.log" -Raw | Out-String) } finally { $env:PATH = $oldPath } diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 1cccbed78..40e21a47d 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -698,4 +698,5 @@ failedToGetExePath = "Can't get 'dsc' executable path" settingNotFound = "Setting '%{name}' not found" failedToAbsolutizePath = "Failed to absolutize path '%{path}'" invalidExitCodeKey = "Invalid exit code key '%{key}'" -executableNotFound = "Executable '%{executable}' not found with working directory '%{cwd}'" +executableNotFoundInWorkingDirectory = "Executable '%{executable}' not found with working directory '%{cwd}'" +executableNotFound = "Executable '%{executable}' not found" diff --git a/lib/dsc-lib/src/util.rs b/lib/dsc-lib/src/util.rs index 3d5b277b4..35f010891 100644 --- a/lib/dsc-lib/src/util.rs +++ b/lib/dsc-lib/src/util.rs @@ -242,11 +242,12 @@ pub fn canonicalize_which(executable: &str, cwd: Option<&str>) -> Result