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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,11 @@
"."
],
"azure-pipelines.1ESPipelineTemplatesSchemaFile": true,
"powershell.codeFormatting.preset": "OTBS"
"powershell.codeFormatting.preset": "OTBS",
"chat.tools.terminal.autoApprove": {
"cargo check": true,
"cargo test": true,
"cargo clippy": true,
"Invoke-Pester": true
}
}
2 changes: 1 addition & 1 deletion resources/dism_dsc/.project.data.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
"SupportedPlatformOS": "Windows",
"Binaries": ["dism_dsc"],
"CopyFiles": {
"Windows": ["optionalfeature.dsc.resource.json"]
"Windows": ["optionalfeature.dsc.resource.json", "featureondemand.dsc.resource.json"]
}
}
123 changes: 123 additions & 0 deletions resources/dism_dsc/featureondemand.dsc.resource.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
{
"$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json",
"description": "Manage Windows Features on Demand (capabilities) using the DISM API.",
"tags": [
"windows",
"dism",
"capability",
"featureondemand",
"fod"
],
"type": "Microsoft.Windows/FeatureOnDemandList",
"version": "0.1.0",
"get": {
"executable": "dism_dsc",
"args": [
"get",
"feature-on-demand"
],
"input": "stdin",
"requireSecurityContext": "elevated"
},
"export": {
"executable": "dism_dsc",
"args": [
"export",
"feature-on-demand"
],
"input": "stdin",
"requireSecurityContext": "elevated"
},
"set": {
"executable": "dism_dsc",
"args": [
"set",
"feature-on-demand"
],
"input": "stdin",
"implementsPretest": false,
"return": "state",
"requireSecurityContext": "elevated"
},
"schema": {
"embedded": {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/resources/Microsoft.Windows/FeatureOnDemandList/v0.1.0/schema.json",
"title": "Windows Feature on Demand",
"description": "Manage Windows Features on Demand (capabilities) using the DISM API. Supports get, set, and export operations.\n\nhttps://learn.microsoft.com/powershell/dsc/reference/microsoft.windows/featureondemandlist/resource\n",
"markdownDescription": "The `Microsoft.Windows/FeatureOnDemandList` resource enables you to manage Windows Features on Demand (capabilities) using the DISM API.\n\n[Online documentation][01]\n\n[01]: https://learn.microsoft.com/powershell/dsc/reference/microsoft.windows/featureondemandlist/resource\n",
"type": "object",
"additionalProperties": false,
"required": [
"capabilities"
],
"properties": {
"_restartRequired": {
"type": "array",
"title": "Restart required",
"description": "Indicates that a system restart is required to complete the state change. Returned by the set operation when DISM reports that a reboot is needed.",
"items": {
"type": "object",
"additionalProperties": true
}
},
"capabilities": {
"type": "array",
"title": "Capabilities",
"description": "An array of Feature on Demand (capability) filters or information objects.",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"title": "Capability name",
"description": "The name of the Windows capability (Feature on Demand). Required for get operation. For export operation, this is optional and wildcards (*) are supported for case-insensitive filtering."
},
"_exist": {
"type": "boolean",
"title": "Exists",
"description": "Indicates whether the capability exists on the system. Set to false when the requested capability name is not recognized by DISM."
},
"state": {
"type": "string",
"enum": [
"NotPresent",
"UninstallPending",
"Staged",
"Removed",
"Installed",
"InstallPending",
"Superseded",
"PartiallyInstalled"
],
"title": "Capability state",
"description": "The current state of the capability. For set operations, only Installed and NotPresent are accepted; other states are returned by get and export operations and are not valid inputs for set."
},
"displayName": {
"type": "string",
"title": "Display name",
"description": "The display name of the capability. Returned by get and export operations. For export, wildcards (*) are supported for case-insensitive filtering."
},
"description": {
"type": "string",
"title": "Description",
"description": "The description of the capability. Returned by get and export operations. For export, wildcards (*) are supported for case-insensitive filtering."
},
"downloadSize": {
"type": "integer",
"title": "Download size",
"description": "The download size of the capability in bytes. Returned by get and export operations."
},
"installSize": {
"type": "integer",
"title": "Install size",
"description": "The install size of the capability in bytes. Returned by get and export operations."
}
}
}
}
}
}
}
}
29 changes: 27 additions & 2 deletions resources/dism_dsc/locales/en-us.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
_version = 1

[main]
missingOperation = "Missing operation argument"
usage = "Usage: dism_dsc <get|set|export>"
missingArguments = "Missing operation and resource type arguments"
usage = "Usage: dism_dsc <get|set|export> <optional-feature|feature-on-demand>"
windowsOnly = "This resource is only supported on Windows"
unknownOperation = "Unknown operation '%{operation}'"
unknownResourceType = "Unknown resource type '%{resource_type}'. Expected 'optional-feature' or 'feature-on-demand'"
errorReadingInput = "Error reading input: %{err}"

[get]
Expand All @@ -17,6 +18,25 @@ failedSerializeOutput = "Failed to serialize output: %{err}"
failedParseInput = "Failed to parse input: %{err}"
failedSerializeOutput = "Failed to serialize output: %{err}"

[fod_get]
failedParseInput = "Failed to parse input: %{err}"
capabilitiesArrayEmpty = "Capabilities array cannot be empty for get operation"
nameRequired = "name is required for get operation"
failedSerializeOutput = "Failed to serialize output: %{err}"

[fod_export]
failedParseInput = "Failed to parse input: %{err}"
failedSerializeOutput = "Failed to serialize output: %{err}"

[fod_set]
failedParseInput = "Failed to parse input: %{err}"
capabilitiesArrayEmpty = "Capabilities array cannot be empty for set operation"
nameRequired = "name is required for set operation"
stateRequired = "state is required for set operation"
capabilityNotFound = "Capability '%{name}' was not found on this system"
unsupportedDesiredState = "Unsupported desired state '%{state}'. Supported states for set are: Installed, NotPresent"
failedSerializeOutput = "Failed to serialize output: %{err}"

[set]
failedParseInput = "Failed to parse input: %{err}"
featuresArrayEmpty = "Features array cannot be empty for set operation"
Expand All @@ -34,3 +54,8 @@ getFeatureInfoFailed = "DismGetFeatureInfo failed for '%{name}': HRESULT %{hr}"
enableFeatureFailed = "DismEnableFeature failed for '%{name}': HRESULT %{hr}"
disableFeatureFailed = "DismDisableFeature failed for '%{name}': HRESULT %{hr}"
getFeaturesFailed = "DismGetFeatures failed: HRESULT %{hr}"
capabilitiesNotSupported = "Capability operations are not supported on this system. DismGetCapabilities not found in dismapi.dll."
getCapabilitiesFailed = "DismGetCapabilities failed: HRESULT %{hr}"
getCapabilityInfoFailed = "DismGetCapabilityInfo failed for '%{name}': HRESULT %{hr}"
addCapabilityFailed = "DismAddCapability failed for '%{name}': HRESULT %{hr}"
removeCapabilityFailed = "DismRemoveCapability failed for '%{name}': HRESULT %{hr}"
9 changes: 6 additions & 3 deletions resources/dism_dsc/optionalfeature.dsc.resource.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@
"get": {
"executable": "dism_dsc",
"args": [
"get"
"get",
"optional-feature"
],
"input": "stdin",
"requireSecurityContext": "elevated"
},
"set": {
"executable": "dism_dsc",
"args": [
"set"
"set",
"optional-feature"
],
"input": "stdin",
"implementsPretest": false,
Expand All @@ -30,7 +32,8 @@
"export": {
"executable": "dism_dsc",
"args": [
"export"
"export",
"optional-feature"
],
"input": "stdin",
"requireSecurityContext": "elevated"
Expand Down
98 changes: 98 additions & 0 deletions resources/dism_dsc/src/feature_on_demand/export.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use rust_i18n::t;

use crate::optional_feature::dism::DismSessionHandle;
use crate::feature_on_demand::types::{CapabilityState, FeatureOnDemandInfo, FeatureOnDemandList};
use crate::util::{matches_wildcard, WildcardFilterable};

pub fn handle_export(input: &str) -> Result<String, String> {
let filters: Vec<FeatureOnDemandInfo> = if input.trim().is_empty() {
vec![FeatureOnDemandInfo::default()]
} else {
let list: FeatureOnDemandList = serde_json::from_str(input)
.map_err(|e| t!("fod_export.failedParseInput", err = e.to_string()).to_string())?;
list.capabilities
};

let session = DismSessionHandle::open()?;
let all_basics = session.get_all_capability_basics()?;

// Check if any filter requires full info (displayName, description, downloadSize, installSize)
let needs_full_info = filters.iter().any(|f| {
f.display_name.is_some() || f.description.is_some()
|| f.download_size.is_some() || f.install_size.is_some()
});

let mut results = Vec::new();

let (filters_with_name, filters_without_name): (Vec<&FeatureOnDemandInfo>, Vec<&FeatureOnDemandInfo>) =
if needs_full_info {
filters.iter().partition(|f| f.name.is_some())
} else {
(Vec::new(), Vec::new())
};

for (name, state_val) in &all_basics {
let state = CapabilityState::from_dism(*state_val);

if needs_full_info {
let mut should_get_full = !filters_without_name.is_empty();
if !should_get_full {
for f in &filters_with_name {
if let Some(ref filter_name) = f.name {
if matches_wildcard(name, filter_name) {
should_get_full = true;
break;
}
}
}
}
if !should_get_full {
continue;
}

let info = match session.get_capability_info(name) {
Ok(raw) if !raw.unknown => FeatureOnDemandInfo {
name: Some(raw.name),
exist: None,
state: CapabilityState::from_dism(raw.state),
display_name: Some(raw.display_name),
description: Some(raw.description),
download_size: Some(raw.download_size),
install_size: Some(raw.install_size),
},
_ => FeatureOnDemandInfo {
name: Some(name.clone()),
exist: None,
state,
display_name: None,
description: None,
download_size: None,
install_size: None,
},
};

if info.matches_any_filter(&filters) {
results.push(info);
}
} else {
// Fast path: only need name and state for filtering, skip expensive
// per-capability DismGetCapabilityInfo calls to match dism /online /get-capabilities speed.
let basic = FeatureOnDemandInfo {
name: Some(name.clone()),
state: state.clone(),
..FeatureOnDemandInfo::default()
};

if basic.matches_any_filter(&filters) {
results.push(basic);
}
}
}

let output = FeatureOnDemandList { restart_required_meta: None, capabilities: results };
serde_json::to_string(&output)
.map_err(|e| t!("fod_export.failedSerializeOutput", err = e.to_string()).to_string())
}
50 changes: 50 additions & 0 deletions resources/dism_dsc/src/feature_on_demand/get.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use rust_i18n::t;

use crate::optional_feature::dism::DismSessionHandle;
use crate::feature_on_demand::types::{CapabilityState, FeatureOnDemandInfo, FeatureOnDemandList};

pub fn handle_get(input: &str) -> Result<String, String> {
let capability_list: FeatureOnDemandList = serde_json::from_str(input)
.map_err(|e| t!("fod_get.failedParseInput", err = e.to_string()).to_string())?;

if capability_list.capabilities.is_empty() {
return Err(t!("fod_get.capabilitiesArrayEmpty").to_string());
}

let session = DismSessionHandle::open()?;
let mut results = Vec::new();

for cap_input in &capability_list.capabilities {
let name = cap_input
.name
.as_ref()
.ok_or_else(|| t!("fod_get.nameRequired").to_string())?;

let raw = session.get_capability_info(name)?;
let info = if raw.unknown {
FeatureOnDemandInfo {
name: Some(name.clone()),
exist: Some(false),
..FeatureOnDemandInfo::default()
}
} else {
FeatureOnDemandInfo {
name: Some(raw.name),
state: CapabilityState::from_dism(raw.state),
display_name: Some(raw.display_name),
description: Some(raw.description),
download_size: Some(raw.download_size),
install_size: Some(raw.install_size),
..FeatureOnDemandInfo::default()
}
};
results.push(info);
}

let output = FeatureOnDemandList { restart_required_meta: None, capabilities: results };
serde_json::to_string(&output)
.map_err(|e| t!("fod_get.failedSerializeOutput", err = e.to_string()).to_string())
}
11 changes: 11 additions & 0 deletions resources/dism_dsc/src/feature_on_demand/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

mod types;
mod get;
mod set;
mod export;

pub use get::handle_get;
pub use set::handle_set;
pub use export::handle_export;
Loading
Loading