Compare commits

..

80 Commits

Author SHA1 Message Date
rbalsleyMSFT ceeabd1ebc Add changelog for 2512.1 UI Preview release
Documents new features including shared cleanup module, Windows Security
Platform install delay, persistent KB folder for updates, and CU download
skipping when ESD is current.

Fixes WingetWin32Apps.json creation bug for pre-downloaded applications.
2026-01-05 12:34:58 -08:00
rbalsleyMSFT 15149ffa0b Bumps version to 2512.1Preview
Updates version string in BuildFFUVM.ps1 and ApplyFFU.ps1
from 2511.2Preview to 2512.1Preview for the new release.
2026-01-05 12:33:34 -08:00
rbalsleyMSFT 2f180747b7 Bumps version to 2511.2Preview
Updates version string in BuildFFUVM.ps1 and ApplyFFU.ps1
to reflect the new preview release.
2026-01-05 12:10:35 -08:00
rbalsleyMSFT 25fe90253c Regenerate Win32 app JSON for pre-downloaded content
Ensures CLI builds properly register silent install commands even when
app content already exists and download is skipped.
2026-01-05 12:07:16 -08:00
rbalsleyMSFT 86d122aacf Skips CU downloads when ESD version is current or newer
Extracts ESD metadata resolution into a separate function to enable
version comparison before downloading cumulative updates.

Parses Windows version from both ESD filenames and KB article search
results to determine if the ESD already contains the latest updates,
avoiding redundant downloads and installations.

Improves VHDX cache matching by tracking update names that were skipped
due to version matching, ensuring cached images are correctly reused
when updates are already integrated in the base image.

Adds check to skip downloading updates that already exist locally.

Removes prior behavior of always removing the KB folder. The `$RemoveUpdates` parameter now controls whether the KB folder is removed or not. This change was made due to the size of the Windows 11 CU being > 3-4GB. This will reduce bandwidth, however will require setting `$RemoveUpdates` to true to cleanup old update files.
2025-12-20 15:52:28 -08:00
rbalsleyMSFT 9737d5c930 Centralizes KB path cleanup into common cleanup module
Removes duplicated KB path cleanup logic scattered across multiple locations in the build script and consolidates it into the shared cleanup module.

Adds KBPath parameter to the cleanup function and handles removal of Windows/.NET cumulative update downloads when RemoveUpdates flag is set.

Improves maintainability by eliminating redundant cleanup code and ensures consistent cleanup behavior across different build scenarios including standard builds, VHDX caching, and restore defaults operations.
2025-12-16 21:18:42 -08:00
rbalsleyMSFT c6088d91fa Add 30 second delay to allow for Windows Security Platform to install in Update-Defender.ps1 2025-12-15 16:21:27 -08:00
rbalsleyMSFT 15fdf77ce4 Refactors cleanup logic into shared module
Consolidates duplicated cleanup code by moving logic into a shared function, eliminating redundant implementations across multiple locations.

Removes standalone cleanup functions (Remove-FFU, Remove-Apps, Remove-Updates) and replaces scattered cleanup calls with a single invocation of Invoke-FFUPostBuildCleanup.

Enhances driver cleanup to preserve configuration files (Drivers.json and DriverMapping.json) while removing other contents, preventing loss of driver mapping data.

Improves maintainability by centralizing cleanup operations and reducing code duplication, making future updates easier to implement consistently.
2025-12-15 16:20:01 -08:00
rbalsleyMSFT f7f001ac2e Update ChangeLog for version 2511.1
Added detailed changelog for version 2511.1, including major updates to driver handling, new hardware support, and various fixes.
2025-12-09 18:00:10 -08:00
rbalsleyMSFT 3524d02047 Adds interactive disk selection for multi-disk systems
Improves disk selection logic to handle systems with multiple fixed disks by prompting the user to choose the target disk when more than one candidate is detected.

Refactors Get-HardDrive to return an array of disk candidates instead of a single disk object with extracted properties, enabling the main script to present options and validate user selection.

Displays disk information in a formatted table showing disk number, size, sector size, bus type, and model to help users make informed decisions.

Validates user input to ensure only available disk numbers can be selected, preventing deployment errors on multi-disk configurations.

Maintains single-disk auto-selection behavior for systems with only one fixed disk, preserving the original user experience for common scenarios.
2025-12-08 18:24:20 -08:00
rbalsleyMSFT 7948201e18 Replace web request with BITS transfer for ESD downloads
Switches from using Invoke-WebRequest to Start-BitsTransferWithRetry for downloading Windows ESD files. This provides better reliability and automatic retry handling for large file downloads.

Also removes trailing blank lines for cleaner formatting.
2025-11-29 18:44:34 -08:00
rbalsleyMSFT 8d84137a27 Refactor app download logic for code reuse between UI and CLI
Moves the `Start-WingetAppDownloadTask` function from the UI module to the common module to enable parallel app downloads in both CLI and UI build paths. This eliminates code duplication and ensures consistent download behavior across build modes.

Updates the `Get-Apps` function to leverage parallel processing instead of sequential iteration, improving performance when downloading multiple applications. Adds support for `LogFilePath` and `ThrottleLimit` parameters to control logging and concurrency.

Introduces a `SkipWin32Json` parameter to differentiate behavior between UI mode (skips JSON generation) and CLI mode (creates JSON for installation). This allows the same download task to work correctly in both contexts.

Updates all callers of `Get-Apps` to pass the new parameters, ensuring proper logging and parallel execution configuration across the build pipeline.
2025-11-29 16:29:52 -08:00
rbalsleyMSFT 2273cffbc2 Add Threads parameter to control parallel driver download throttling
Introduces a new `Threads` parameter that allows users to configure the concurrency level for parallel driver downloads within the script. This parameter defaults to 5, matching the existing UI behavior, and accepts values between 1 and 64 to provide flexible control over resource utilization.

The parameter is now passed to the parallel processing function via the `ThrottleLimit` argument, enabling users to optimize performance based on their system capabilities and network conditions.
2025-11-29 13:57:54 -08:00
rbalsleyMSFT 63ef35a005 Fix USB drive detection logic for empty arrays
Replaces null comparison with count check to properly handle empty arrays. The previous condition would incorrectly pass when the array exists but contains no elements, potentially causing the function to proceed without a valid USB drive.
2025-11-24 17:41:36 -08:00
rbalsleyMSFT 417be73b23 Replace SerialNumber with UniqueId for USB drive identification
Switches USB drive matching logic from relying on SerialNumber to using UniqueId, which provides more reliable and consistent device identification across different systems.

Updates the Get-USBDrive function to retrieve UniqueId via Get-Disk and trims the machine name suffix (characters after colon) for consistent matching. The new approach first filters candidates by model and media type, then validates each candidate against the configured UniqueId.

Reflects this change across the UI layer by updating column headers, configuration handling, and drive enumeration functions to use UniqueId instead of SerialNumber for saving and loading USB drive selections.
2025-11-24 16:46:57 -08:00
rbalsleyMSFT 18367219c8 Fix xcopy command to handle PPKG filenames with spaces
Adds proper quoting around file paths in the xcopy invocation to support PPKG filenames containing spaces. Without quoted paths, xcopy fails when filenames include whitespace characters.
2025-11-24 15:32:02 -08:00
rbalsleyMSFT 2a77cf1a02 Add MSI path quoting to handle spaces in msiexec arguments
Introduces a new function to detect and properly quote unquoted MSI file paths in msiexec command arguments. This prevents installation failures when MSI paths contain spaces, as the Windows installer requires quoted paths to correctly parse arguments with whitespace.

The solution uses pattern matching to identify `/i` arguments followed by unquoted `.msi` file paths and automatically wraps them in double quotes. This runs automatically during application installation when msiexec is detected, ensuring reliable installations regardless of path formatting in the configuration.
2025-11-24 14:58:00 -08:00
rbalsleyMSFT 41b0f7d742 Escape quotes in MSI installation example tooltip
Updates the tooltip text for application arguments to properly escape quotes around the MSI file path in the example command. This ensures the example is syntactically correct and prevents issues when users copy the suggested format for MSI installations.
2025-11-24 14:03:11 -08:00
rbalsleyMSFT 24c81c234f Remove redundant Images directory creation logic 2025-11-21 22:59:46 -08:00
rbalsleyMSFT 4833d9f00d Merge branch 'UI_2510' of https://github.com/rbalsleyMSFT/FFU into UI_2510 2025-11-21 17:26:52 -08:00
rbalsleyMSFT 37e3497522 Bump version to 2511.1Preview
Please read - potential breaking change with Drivers you'll want to be aware of - https://github.com/rbalsleyMSFT/FFU/discussions/350
2025-11-21 17:25:23 -08:00
rbalsleyMSFT 8229aa73fe Add BITS transfer priority configuration support
Introduces a new parameter to control BITS download priority across the build system and UI, allowing users to optimize transfer speeds when needed.

The feature adds a priority selector to the UI with four options (Foreground, High, Normal, Low) and propagates the selection through the build script and common modules. Priority can be set via UI, command-line parameter, or environment variable, with Normal as the default.

Updates the BITS transfer retry logic to respect the configured priority instead of hardcoding Normal priority, and fixes minor code formatting inconsistencies.
2025-11-21 14:04:52 -08:00
rbalsleyMSFT e67590d0a1 Add HTTP fallback for BITS transfer network authentication errors
Enhances the BITS transfer retry logic to detect when the current session lacks network authentication and automatically falls back to Invoke-WebRequest for completion.

Detects the "not logged on to the network" error condition by checking both the HResult code (0x800704DD) and error message patterns, then triggers an HTTP-based fallback mechanism to retry the download with remaining attempts.

This ensures downloads can succeed even when BITS fails due to authentication issues, improving reliability of the transfer function in environments with network authentication requirements.
2025-11-20 18:02:59 -08:00
rbalsleyMSFT 33f0608d84 Remove redundant progress status update in driver package processing
Eliminates an unnecessary progress queue update that occurs immediately before downloading package XML, as this status message duplicates information already provided in the subsequent log message.

This simplifies the progress reporting flow and reduces redundant communication to the progress queue without impacting user-facing functionality.
2025-11-20 17:19:20 -08:00
rbalsleyMSFT 3d1a586c73 Refactor driver cleanup logic into reusable helper function
Extracts duplicate folder removal code across multiple driver modules (Dell, HP, Lenovo, Microsoft) into a centralized `Remove-DriverModelFolder` helper function. This new utility includes safety checks to prevent accidental deletion of the drivers root directory or paths outside the drivers folder hierarchy.

Also moves model path construction in the Microsoft driver module earlier in the function to ensure consistent path handling before any operations that might fail.

Benefits improved maintainability, reduces code duplication, and adds protective safeguards for destructive operations during error handling.
2025-11-20 17:05:31 -08:00
rbalsleyMSFT 7d36253668 Standardize driver compression status reporting across vendors
Updates compression workflow to use consistent status messaging and better error handling across all driver vendor modules (Dell, HP, Lenovo, Microsoft).

Changes improve status tracking by:
- Standardizing compression success status to "Compression successful" instead of vendor-specific messages
- Introducing relative path variables to reduce code duplication and improve maintainability
- Suppressing command output by piping to `$null` for cleaner execution
- Adding explicit failure state in exception handlers to ensure success property reflects actual outcome
- Updating parallel processing logic to recognize the new standardized compression status

These modifications ensure consistent behavior across vendors and make the parallel processing coordinator aware of all compression completion states.
2025-11-20 14:32:54 -08:00
rbalsleyMSFT e076e9f4ca Add option to skip driver installation during deployment
Allows users to bypass driver installation by entering 0 at the selection prompt, providing flexibility for deployments that don't require driver updates.

Introduces a skip flag and restructures the selection validation logic to accept either a valid driver selection or a skip command. When skipped, driver-related variables are set to null and appropriate logging messages are generated.
2025-11-19 23:58:29 -08:00
rbalsleyMSFT 44aa4d3a32 Add SUBST drive mapping for driver injection to handle long paths
Introduces helper functions to create and remove SUBST drive mappings for driver folders, addressing potential issues with long file paths during driver injection.

The new implementation:
- Adds `Get-AvailableDriveLetter` to find an unused drive letter
- Adds `New-DriverSubstMapping` to create a virtual drive mapping to the source folder
- Adds `Remove-DriverSubstMapping` to clean up the mapping after use
- Updates the driver injection workflow to use the mapped drive path instead of the original path
- Wraps the operation in try-catch-finally to ensure cleanup occurs even on errors

This approach mitigates issues with excessively long driver folder paths that could exceed command-line limits or cause path-related failures during DISM operations.
2025-11-19 23:08:39 -08:00
rbalsleyMSFT a1d08b6fa4 Simplifies driver folder discovery logic
Removes hardcoded manufacturer name list and treats all immediate child folders under the Drivers path as potential manufacturer containers. This supports both known vendors (Dell, HP, Lenovo, Microsoft) and unknown/custom manufacturers without requiring code changes.

Adds support for manufacturer folders that directly contain installable driver content (no model subfolders), enabling flatter folder structures when appropriate.

Improves code readability by consolidating the folder processing logic into a single loop that checks for model subfolders first, then falls back to treating the manufacturer folder itself as a driver source if it contains installable content.
2025-11-18 23:48:07 -08:00
rbalsleyMSFT fc4a71f7e1 Restricts manufacturer folder detection to known OEMs
Improves driver folder discovery logic by explicitly defining recognized manufacturer folder names (Dell, HP, Lenovo, Microsoft) instead of treating any root-level folder with children as a manufacturer folder.

Prevents incorrect categorization of non-manufacturer folders at the root level and ensures only legitimate OEM folders are processed for model-specific driver packages.

Refactors control flow to eliminate else block, improving code clarity and maintainability.
2025-11-18 20:03:22 -08:00
rbalsleyMSFT 9a59b9fea4 Removes Path column from driver source table display
Simplifies the driver source selection table by removing the full Path column from the formatted output.

The RelativePath column already provides sufficient information for users to identify driver sources, making the full path redundant and cluttering the display.
2025-11-18 18:59:38 -08:00
rbalsleyMSFT 19081a2e1f Skips empty driver folders during deployment
Adds validation to filter out driver folders containing only WIM files or no installable content before attempting driver injection. This prevents unnecessary processing and potential errors when scanning driver repositories.

Introduces a helper function to recursively check folders for installable driver content (non-WIM files). The validation runs before adding folders to the driver sources list, with appropriate logging when folders are skipped.

Also includes minor code formatting improvements for consistency.
2025-11-18 18:47:01 -08:00
rbalsleyMSFT 3cb4003bcd Improves driver source selection UI clarity
Enhances the driver source selection menu by displaying relative paths instead of full paths, making it easier to identify manufacturer and model folders at a glance.

Adds logic to surface Manufacturer\Model folder structures by expanding top-level folders that contain subdirectories, while preserving simple folder listings when no nested structure exists.

Includes a relative path resolver that normalizes paths and calculates display-friendly names relative to the drivers root directory.

Updates logging and console output to show relative paths for better readability while maintaining full path information internally for file operations.
2025-11-18 15:41:19 -08:00
rbalsleyMSFT beb48e500e Adds generic fallback for driver matching
Implements a generic model-based matching strategy for manufacturers without explicit handling rules in the driver mapping logic.

Previously, unsupported manufacturers would immediately return null without attempting to match driver rules. Now falls back to comparing normalized model names against available rules, improving driver detection coverage for manufacturers that don't require specialized matching logic.

Updates logging to distinguish between successful generic matches and complete failures to find matching drivers.
2025-11-17 18:24:49 -08:00
rbalsleyMSFT 93c4679c46 Removes Surface-specific validation for Microsoft devices
Allows driver mapping to support all Microsoft-branded devices, not just Surface products.

Previously, the code rejected any Microsoft device that didn't match "Surface" in the model name. This restriction prevented proper driver mapping for other Microsoft hardware.

Updates log messages to use generic "Microsoft model" terminology instead of "Surface" to reflect the broader device support.
2025-11-17 17:53:51 -08:00
rbalsleyMSFT d6688def9d Adds support for 8 new hardware manufacturers
Extends hardware detection and driver mapping capabilities to support Panasonic, Viglen, AZW, Fujitsu, Getac, ByteSpeed, and Intel devices.

Implements manufacturer-specific identification logic using baseboard SKU and product information where appropriate. Adds special handling for ByteSpeed devices that may be rebranded Intel NUCs.

Improves model name trimming to ensure consistent string comparisons across all manufacturers.
2025-11-17 17:33:13 -08:00
rbalsleyMSFT 489d53f55c Refactors system identity logic into dedicated function
Extracts manufacturer-specific identifier resolution into a new Get-SystemIdentityMetadata function to improve code organization and reusability.

Consolidates normalization logic so UI display and driver mapping share consistent system identifiers, eliminating duplicate manufacturer detection calls. Stores normalized values alongside original data for better traceability.

Improves maintainability by centralizing model name handling for Lenovo devices and identifier resolution logic for Dell, HP, and Lenovo systems into a single reusable component.
2025-11-17 15:44:15 -08:00
rbalsleyMSFT 3deb8fb8d2 Refactors manufacturer metadata handling
Consolidates manufacturer-specific identifier logic into a single metadata object to reduce code duplication and improve maintainability.

Previously, manufacturer identifiers were captured and assigned using separate variables with duplicated switch-case logic. Now captures all metadata (SystemSku, FallbackSku, MachineType, Label, IdentifierValue) upfront in a structured object, allowing downstream logic to reference a single source of truth.

Improves readability by centralizing the identifier selection logic within each manufacturer case rather than scattering it across multiple switch statements.
2025-11-17 12:59:54 -08:00
rbalsleyMSFT 1af3a0f092 Improves driver mapping with vendor-specific identifiers
Implements manufacturer-specific device identification for automatic driver selection using System SKU, Machine Type, and other vendor identifiers instead of relying solely on model name pattern matching.

Adds normalized manufacturer detection to handle vendor name variations consistently across Dell, HP, Lenovo, and Microsoft Surface devices.

Extracts comprehensive system information from WMI including baseboard details, BIOS metadata, and firmware versions to support accurate device identification and troubleshooting.

Refactors system information gathering into reusable functions that separate data collection from display logic, enabling the driver mapping feature to leverage device identifiers.

Enhances logging by capturing vendor-specific identifiers while hiding internal matching fields from user-facing output to reduce confusion.

Fixes minor log message wording for clarity when driver installation encounters expected failures.
2025-11-15 19:14:45 -08:00
rbalsleyMSFT de80ac551b Adds SystemId support for HP driver management
Enhances HP driver handling to properly track and display SystemId alongside ProductName throughout the driver workflow.

Parses SystemId from HP PlatformList.xml and creates unique entries per ProductName/SystemId combination to avoid conflicts when multiple models share the same product name but different system identifiers.

Updates driver lookup and metadata preservation to include SystemId, MachineType, and ProductName fields across download tasks and result processing, ensuring this information persists through JSON import/export operations.

Improves display name generation to show "ProductName (SystemId)" format for HP models when both values are available, providing clearer model identification in the UI and configuration files.

Standardizes driver metadata handling by replacing simple Make lookups with comprehensive driver metadata lookups that preserve all relevant fields for Dell, HP, and Lenovo vendors.
2025-11-14 19:48:22 -08:00
rbalsleyMSFT 89601efde0 Adds SystemId/MachineType extraction and tracking
Enhances driver mapping functionality to automatically extract and store manufacturer-specific identifiers alongside driver paths.

Implements SystemId lookup for HP devices using PlatformList.xml with caching and normalization for improved matching accuracy. Extracts SystemId from Dell model names and MachineType from Lenovo model names using regex pattern matching.

Updates existing mapping entries when identifiers are discovered and adds them to new entries during driver registration. Improves driver matching capabilities by providing additional metadata beyond manufacturer and model name.
2025-11-14 16:37:12 -08:00
rbalsleyMSFT 235065322c Standardizes driver model name handling across vendors
Improves consistency in how driver model names and identifiers are processed for different manufacturers (Microsoft, Dell, HP, Lenovo) throughout the codebase.

Introduces helper functions to extract base names from display names and construct standardized display names with identifiers in parentheses format.

Centralizes the logic for converting driver items to JSON model objects, eliminating code duplication across save and download operations.

Enhances validation during driver import and processing to skip invalid entries with missing required fields like MachineType for Lenovo or empty model names.

Ensures proper parsing of model names containing parenthetical identifiers (e.g., "Product Name (Type)") and extracts components correctly for each vendor's requirements.

Removes obsolete CabRelativePath property from Dell driver handling.
2025-11-14 15:23:52 -08:00
rbalsleyMSFT 11b3e120e2 Enhances driver package representation in Get-DellLatestDriverPackages function by adding a human-readable driver name. This change captures the display name from the XML, sanitizes it for display purposes, and ensures proper formatting by collapsing multiple spaces and replacing illegal characters. This improves clarity and usability when displaying driver information. 2025-11-05 17:30:53 -08:00
rbalsleyMSFT 667edf3724 Normalizes model display in Get-DellClientModels function by utilizing GroupManifest Display CDATA when available. Implements fallback logic to construct model names from brand and model nodes, improving clarity and consistency in model representation. 2025-11-05 15:33:15 -08:00
rbalsleyMSFT 4a10e27ddf Enhances driver download error handling and logging across Dell and Lenovo driver tasks.
- Implements detailed failure tracking for driver downloads, capturing model names and statuses for failed attempts.
- Updates logging to provide clearer messages for both successful and failed downloads, improving traceability.
- Modifies the user interface to display comprehensive error messages when downloads fail, including a summary of failed models.
- Ensures that all exceptions during download and extraction processes are logged and thrown, preventing silent failures.
2025-10-30 18:26:05 -07:00
rbalsleyMSFT b4305a1edb Fixed a UI issue where sorting after filtering Drivers was causing the filter to break and all items were sorted instead of the filtered items. 2025-10-27 16:40:29 -07:00
rbalsleyMSFT 7598ee96da Clears existing items in cmbMake dropdown during UI initialization to prevent duplication on re-initialization. Ensures a fresh list of manufacturers is displayed each time defaults are restored. 2025-10-27 16:04:22 -07:00
rbalsleyMSFT 6de7c861ed Adds logging for catalog downloads in Save-DellDriversTask function. Includes messages for downloading Dell model and server catalogs to improve traceability during driver retrieval operations. 2025-10-27 15:58:48 -07:00
rbalsleyMSFT 658c57e22c Updates Dell driver catalog handling to use CatalogIndexPC for Windows releases 11 and below. Modifies catalog file paths and URLs accordingly to ensure accurate driver retrieval. 2025-10-24 22:42:57 -07:00
rbalsleyMSFT 60cf1dab18 Enhances progress status messages in driver download and extraction functions for Dell, HP, Lenovo, and Microsoft drivers. Updates messages to include driver names and versions for better clarity during operations. 2025-10-24 22:28:44 -07:00
rbalsleyMSFT 4ce9183bd3 Enhances Dell driver retrieval by resolving missing CabUrl through CatalogIndexPC. Introduces Resolve-DellCabUrlFromModel function for improved reliability. Updates Save-DriversJson and Import-DriversJson functions to handle additional properties (SystemId, CabUrl, CabRelativePath) for Dell models. Refines Get-DellDrivers function to ensure consistent model handling and logging. 2025-10-24 15:19:56 -07:00
rbalsleyMSFT 7dd002396f Normalizes model display in Get-DellClientModels to prevent duplicate brand names in output. 2025-10-23 12:27:46 -07:00
rbalsleyMSFT 1130a830c7 Merge pull request #325 from arwidmark/UI_2510
Update FFU.Common.Core.psm1 to run BITS downloads in Normal Priority
2025-10-22 17:42:27 -07:00
rbalsleyMSFT 66a9026b8f Completely refactored Dell driver downloads
- Client OSes will now use CatalogIndexPC.xml to identify which ProductLine_SystemID.xml to use to identify which drivers to download. This is inline with how DCU works.
- In the UI, Dell Model names now show the full product line, model number, and system ID in the model column.
- There are many more models now shown due to breaking each model out by systemID (one model will have many systemIDs).
- Downloads per model should be much smaller as prior code was downloading drivers for models that Dell had reused their model number (e.g. Precision/Inspiron/Latitude/Vostro 3520 would result in a very large driver download)
- Dell driver downloads are best effort based on the data from the XML files. In some cases the Dell support website may show a newer driver than what is downloaded. This is rare, but in testing I've seen one or two drivers per model where the XML doesn't have what's listed on Dell's website. Again, rare, but not unexpected.
2025-10-22 13:24:29 -07:00
arwidmark 458f1e517c Update BuildFFUVM_UI.ps1 2025-10-20 23:06:18 -05:00
arwidmark a13f9b481a Update FFU.Common.Core.psm1 2025-10-20 22:53:21 -05:00
rbalsleyMSFT de70a22c42 Removes unnecessary properties and excluded applications from DownloadFFU.xml for cleaner configuration for Office downloads. 2025-10-15 12:53:48 -07:00
rbalsleyMSFT f3d3506e02 Improves Lenovo PSREF token retrieval reliability
Enhances the Edge automation process for retrieving authentication tokens with multiple reliability improvements:

- Implements dynamic port allocation to prevent port conflicts
- Adds retry logic with configurable attempts for token retrieval
- Implements proper WebSocket message polling to handle asynchronous responses
- Creates isolated temporary Edge profiles to avoid state interference
- Adds fallback mechanism to extract tokens from cookies when localStorage is unavailable
- Improves error handling and diagnostic logging throughout the workflow
- Ensures comprehensive cleanup of resources including WebSocket connections, browser processes, and temporary profiles

These changes address intermittent failures when the token is not immediately available in localStorage and improve overall robustness of the headless browser automation.
2025-10-07 15:07:30 -07:00
rbalsleyMSFT 1daa14584a Updates default Windows version to 25H2
Changes the default Windows version from 24H2 to 25H2 across the FFU development tool to reflect the latest Windows 11 release.

Adds dynamic products.cab download functionality for Windows 11 using Windows Update service API instead of static MCT links. This is due to a change in how the MCT pulls the products.cab file.
2025-10-02 17:03:09 -07:00
rbalsleyMSFT 08c9214976 Adds 2509.1 release notes
Documents new preview release and bumps displayed version. Notes broadened artifact cleanup, larger default disk size, auto load/restore of environment, centralized cleanup, dynamic PE driver sourcing, driver model normalization, deferred driver folder cleanup, safer name sanitization, improved config import & JSON determinism, per‑app exit code controls (incl. winget), multi‑FFU USB inclusion, removal of obsolete ESD workaround, and added Windows 11 25H2 support to improve UX, reliability, and maintainability.
2025-10-01 17:40:29 -07:00
rbalsleyMSFT c110dcd40e Updates preview version to 2509.1
Increments internal preview version for build and deployment scripts to reflect next preview cycle, ensuring logs and artifacts show the correct release identifier for traceability.
2025-10-01 17:19:39 -07:00
rbalsleyMSFT eaa3e1e6af Adds Windows 11 25H2 mapping
Extends supported Windows 11 releases to include 25H2 to keep version resolution current for upcoming media and configuration scenarios.
2025-10-01 13:23:46 -07:00
rbalsleyMSFT 6562d16ce5 Standardizes JSON output: depth, UTF-8, key order
- Sorts top-level config keys before serialization for deterministic files and cleaner diffs.
- Increases JSON depth to 10 to retain nested settings.
- Writes JSON as UTF-8 via Set-Content for consistent encoding.
- Applies across config export and UI save flows.
2025-09-20 13:30:41 -07:00
rbalsleyMSFT 15a5b16b39 Adds UI/CLI to copy additional FFUs to USB build
- Enables selecting multiple existing FFU images to include on the deployment USB for easier distribution and testing.
- Adds a UI option with selectable, sortable list from the capture folder, refresh support, and persisted selections.
- Validates that selections exist when the option is enabled to prevent empty runs.
- Supports unattended/CLI flows by prompting early or accepting a preselected list for USB creation; deduplicates and logs chosen files.
- Always includes the just-built (or latest available) FFU as a base.
- Improves no-FFU handling and streamlines multi-FFU selection workflow.
2025-09-18 18:17:58 -07:00
rbalsleyMSFT d9c0c9c68e Adds exit-code overrides and UI for winget apps
Adds per-app control for additional accepted exit codes and ignoring non‑zero exit codes to improve handling of installers with nonstandard returns.

Exposes editable fields in the app list UI, persists them across search defaults, import/export, and pre-download save, and applies overrides during app resolution to honor configured behavior.
2025-09-18 15:15:00 -07:00
rbalsleyMSFT d1ca123104 Sanitizes app names for storage and paths
Applies name sanitization when persisting the app list and when building/checking Win32 and Store download directories.
Prevents invalid characters in folder names, aligns persisted names with on-disk structure, and improves detection of existing content to avoid redundant downloads and errors.
2025-09-17 13:22:17 -07:00
rbalsleyMSFT f37647599a Includes exit code fields when using Copy Apps button
Adds persistence of AdditionalExitCodes and IgnoreNonZeroExitCodes when exporting the UI list to prevent losing custom exit handling settings and maintain parity with the primary save routine.
2025-09-16 17:05:34 -07:00
rbalsleyMSFT cb14e84a26 Add robust sanitization for names used in paths
Introduces a new common function, `ConvertTo-SafeName`, to sanitize strings by removing characters that are invalid in Windows file paths.

This function is now used consistently when creating directory and file names for drivers (Dell, HP, Lenovo, Microsoft) and applications to prevent path-related errors. It replaces several ad-hoc sanitization methods with a single, more robust implementation.
2025-09-16 16:43:43 -07:00
rbalsleyMSFT 8d7e4d1066 Refactor config loading and improve error handling
Extracts the logic for importing supplemental assets (Winget, BYO, Drivers) into a new reusable function. This function is now called by both the manual and automatic configuration loaders, reducing code duplication.

Enhances the manual configuration loading process with more robust error handling. It now provides specific user-facing error messages for file read failures, empty files, and invalid JSON, improving the user experience when loading a malformed configuration.

When loading a configuration, if optional supplemental files like AppList.json are referenced but not found, an informational message is now displayed to the user instead of failing silently.
2025-09-15 18:10:39 -07:00
rbalsleyMSFT c30ed923b6 feat: Defer cleanup of compressed driver source folders
Implements a deferred cleanup mechanism for driver source folders when they are compressed to a WIM and also used for WinPE.

When drivers are compressed, the original source folders are now preserved if they are also needed for WinPE driver injection. A marker file is created in these preserved folders.

A new cleanup step is added after the WinPE media creation to remove these preserved folders, ensuring they are available when needed but not left behind permanently.
2025-09-12 15:08:48 -07:00
rbalsleyMSFT 50713188bf Refactor: Improve model name normalization for driver mapping
Enhances the model name normalization function to better handle variations in hardware model strings. This change introduces specific rules to canonicalize "All-in-One" and screen size variants (e.g., "-in" or "inch") for more reliable matching against driver mapping rules.

Additionally, optimizes performance by normalizing the system model once before the comparison loop. Logging is also added to show the original and normalized model strings for easier debugging.
2025-09-11 18:15:51 -07:00
rbalsleyMSFT e2ccd11f07 feat: Add option to dynamically build PE drivers
Introduces a new feature, `UseDriversAsPEDrivers`, that allows WinPE drivers to be sourced directly from the main driver repository.

When enabled, the script scans all available drivers, parses their INF files, and copies only the essential driver types (e.g., storage, mouse, keyboard, touchpad, system devices) needed for WinPE. This eliminates the need to maintain a separate, manually curated `PEDrivers` folder.

The UI is updated with a new checkbox that becomes visible when "Copy PE Drivers" is selected, making this a sub-option. Parameter validation is also adjusted to support this new workflow.
2025-09-11 12:13:06 -07:00
rbalsleyMSFT f3316a017b feat: Add restore defaults and centralize cleanup logic
Introduces a "Restore Defaults" feature in the UI to reset the environment. This action removes generated configuration files, ISOs, downloaded apps, updates, drivers, and FFUs.

The post-build cleanup logic is refactored from the main build script into a new common function. This new function is used by both the standard build process and the new restore defaults feature, promoting code reuse and simplifying maintenance.
2025-09-10 11:31:53 -07:00
rbalsleyMSFT bdf1b63833 Improves UI state after environment autoload
Updates the visibility of UI panels for Winget and drivers when a previous environment is automatically loaded.

This ensures that if Winget apps or driver models are present, their corresponding UI sections are made visible. Additionally, it updates the "select all" checkbox state for Winget results and attempts to pre-select the hardware make for loaded drivers.
2025-09-09 18:00:52 -07:00
rbalsleyMSFT 3ef26f2918 Adds auto-loading of previous configuration on startup
Implements a new feature to automatically load the previously saved environment when the UI is launched.

This improves user experience by restoring the last saved configuration, including selected applications and drivers, eliminating the need to manually reload them on each run.

The process loads the main `FFUConfig.json` and then proceeds to load associated Winget, BYO App, and Driver lists if they are defined. UI elements and checkboxes are updated accordingly to reflect the loaded state.
2025-09-09 17:49:36 -07:00
rbalsleyMSFT 372360d739 Update default disk size to 50GB in FFU scripts and UI
- Changed the default disk size parameter from 30GB to 50GB in BuildFFUVM.ps1 and FFUUI.Core.psm1 to accommodate larger virtual machines.
- Updated tooltip and default value in the UI XAML file to reflect the new disk size.
2025-09-03 12:06:11 -07:00
rbalsleyMSFT dc5877f398 Removes the VM workaround for MCT ESD builds
Comments out the logic that forces app installation when building from a downloaded ESD file. This workaround was implemented to prevent an OOBE reboot loop but is no longer required. This should speed up scenarios where you want to download the ESD media, install the latest CU and .NET CU, and capture the FFU.
2025-08-28 13:56:54 -07:00
rbalsleyMSFT 49b2113fe1 Merge pull request #262 from jrollmann/UI_2508
Update USBImagingToolCreator.ps1
2025-08-27 15:38:12 -07:00
jrollmann 556cfa1ee3 Update USBImagingToolCreator.ps1 2025-08-27 17:07:52 -04:00
rbalsleyMSFT 1ab4093d54 Refactor: Enhance artifact cleanup for disabled features
Renames `Remove-DisabledUpdates` to `Remove-DisabledArtifacts` to better reflect its expanded scope.

This function now also removes Office installation scripts and downloaded content if the Office installation is disabled via the `$InstallOffice` flag.

The function call is moved to run before app installations to ensure artifacts are removed prior to the installation phase.
2025-08-27 12:40:11 -07:00
30 changed files with 5613 additions and 2251 deletions
+215
View File
@@ -1,5 +1,220 @@
# Change Log # Change Log
# 2512.1 UI Preview
## What's Changed
### Refactored Cleanup logic into a shared module
Consolidates duplicated cleanup code by moving logic into a shared function, eliminating redundant implementations across multiple locations.
Removes standalone cleanup functions (Remove-FFU, Remove-Apps, Remove-Updates) and replaces scattered cleanup calls with a single invocation of Invoke-FFUPostBuildCleanup.
### Add 30 second delay to allow for Windows Security Platform to install
There was an issue where the Windows Security Platform would attempt to install in the VM during the build via `Update-Defender.ps1` however the install didn't always happen and on deployment of the FFU, Windows Update would show that the Windows Security Platform needed an update. I suspect this is related to the AppxSVC not being ready during Audit Mode. Adding a 30 second delay appears to work more reliably.
### Windows and .NET CU's now persist across builds
Content in the FFUDevelopment\KB folder was always deleted once it was used. Since the Windows CU is so large now, it doesn't make sense to delete it if a user wants it again and may not be using cached VHDX files.
Deletion of the KB folder is now correctly handled via the **Remove Downloaded Update Files** option on the Build tab.
### Skip CU downloads if the Windows ESD version is current or newer
Now that the Windows ESD media is kept up to date, there rarely will be a need to download the latest CU. There will always be a slight gap when the latest CU comes out and the updated media is available, but that's generally just a few days to a week.
The script will now do some parsing of the windows version of the ESD file and the latest CU and if the ESD is newer, the CU will not be downloaded.
### Fixes an issue with WingetWin32Apps.json file not being created if applications were pre-downloaded via the UI
Fixed a bug due to some code consolidation that broke scenarios where applications that were downloaded via the UI, but were not installing in the VM.
**Full Changelog**: https://github.com/rbalsleyMSFT/FFU/compare/v2511.1preview...v2511.2
# 2511.1 UI Preview
## What's Changed
### Major changes to drivers
A few weeks ago I wrote a [lengthy post](https://github.com/rbalsleyMSFT/FFU/discussions/350) asking for some help testing some changes that were added.
The summary of that post is that there have been significant changes for both Dell and HP driver downloads to leverage the SystemID for each model. This increases the total number of driver models that are exposed in the UI. This also requires the `DriverMapping.json` to be modified to require the SystemID and query the SystemID from WMI when doing automatic matching.
#### Driver folder structure changes on the USB drive - breaking change
Driver folder structure on the USB drive has also changed. The new structure is `Drivers\Make\Model` (e.g. `D:\Drivers\Lenovo\Lenovo 300w`). This structure is consistent with how the UI and `BuildFFUVM.ps1` script download and store drivers and automatically copy them. So if you've been following that, then no changes are required.
Please read [the post](https://github.com/rbalsleyMSFT/FFU/discussions/350) for more details on these changes to drivers.
### Windows 11 25H2 is now the default option for MCT/ESD downloads
For MCT/ESD downloads: Adds dynamic products.cab download functionality for Windows 11 using Windows Update service API instead of static MCT links. This is due to a change in how the MCT pulls the products.cab file. In other words, the Windows 11 25H2 ESD media is now updated each month (usually shortly after patch Tuesday)
### Added 8 new hardware manufactures for automatic driver matching during deployment
Extends hardware detection and driver mapping capabilities to support Panasonic, Viglen, AZW, Fujitsu, Getac, ByteSpeed, and Intel devices when applying the FFU to a device. This does not mean FFU Builder supports downloading drivers from these manufacturers. You'll still need to download the drivers for them manually. You can now create your own `DriverMapping.json` file to include these manufacturers.
Thanks to @arwidmark and the [Modern Driver Management](https://msendpointmgr.com/modern-driver-management/) team for the WMI queries.
### Fixed an issue with long paths when applying drivers from USB
Implemented SUBST drive mappings to shorten driver file paths within WinPE as some paths were causing dism to error when servicing drivers. You should see a Z:\ drive when applying drivers from the USB drive.
### Added an option to skip driver selection when multiple driver models are detected during deployment
Allows users to bypass driver installation by entering 0 at the selection prompt, providing flexibility for deployments that don't require driver updates.
### Add HTTP fallback for BITS transfer network authentication errors
Fixes an issue with standard users elevating PowerShell as Admin and getting BITS errors when trying to download content.
### Add -BitsPriority script parameter
Introduces a new parameter `-BitsPriority` with options `(Foreground, High, Normal, Low)` to control BITS download priority across the build system and UI, allowing users to optimize transfer speeds when needed.
The feature adds a priority selector to the UI with four options (Foreground, High, Normal, Low) and propagates the selection through the build script and common modules. Priority can be set via UI or command-line parameter with Normal as the default.
### BYO Apps: Add MSI path quoting to handle spaces in msiexec arguments
When specifying Build Your Own Apps msiexec arguments, if there were spaces in the argument list that weren't quoted properly, you'd get an error. This should now automatically add missing spaces in case you forget to add them or there are spaces in your application name.
### Misc Fixes
* Fixed some reliability issues when trying to download Lenovo drivers
* Fixed an issue with PPKG files with spaces
* Replaced SerialNumber with UniqueID for USB drive identification when building USB drives. USB drive manufacturers may use the same serial number for different drives, potentially causing data loss if the wrong drive is chosen.
* `-Threads` parameter has been added to `BuildFFUVM.ps1` which defaults to 5, matching the UI behavior. This value can be 1-64.
* ESD media downloads now use BITS by default
* Fixed an issue with multi-disk devices. Prior, if multiple disks were detected, ApplyFFU.ps1 would fail. Now a menu pops up asking the end user to select the disk they want to deploy the FFU to
## New Contributors
* @arwidmark made their first contribution in https://github.com/rbalsleyMSFT/FFU/pull/325
**Full Changelog**: https://github.com/rbalsleyMSFT/FFU/compare/v2509.1preview...v2511.1preview
# 2509.1 UI Preview
## What's Changed
### [Refactor: Enhance artifact cleanup for disabled features](https://github.com/rbalsleyMSFT/FFU/commit/1ab4093d54b7d9bda9f47d7819694e66ae8de357)
Renames `Remove-DisabledUpdates` to `Remove-DisabledArtifacts` to better reflect its expanded scope.
This function now also removes Office installation scripts and downloaded content if the Office installation is disabled via the `$InstallOffice` flag.
The function call is moved to run before app installations to ensure artifacts are removed prior to the installation phase.
### [Removes the VM workaround for MCT ESD builds](https://github.com/rbalsleyMSFT/FFU/commit/dc5877f398316969299ee03800f3d07c7d98a9ab)
Comments out the logic that forces app installation when building from a downloaded ESD file. This workaround was implemented to prevent an OOBE reboot loop but is no longer required. This should speed up scenarios where you want to download the ESD media, install the latest CU and .NET CU, and capture the FFU.
### [Update default disk size to 50GB in FFU scripts and UI](https://github.com/rbalsleyMSFT/FFU/commit/372360d7392ad945be0db889a68e1fff0ed3b5d6)
Changed the default disk size parameter from 30GB to 50GB in BuildFFUVM.ps1 and FFUUI.Core.psm1 to accommodate larger virtual machines.
Updated tooltip and default value in the UI XAML file to reflect the new disk size.
### [Adds auto-loading of previous configuration on startup](https://github.com/rbalsleyMSFT/FFU/commit/3ef26f2918977906ebe14e328f015ce4f1941dc3)
Implements a new feature to automatically load the previously saved environment when the UI is launched.
This improves user experience by restoring the last saved configuration, including selected applications and drivers, eliminating the need to manually reload them on each run.
The process loads the main `FFUConfig.json` and then proceeds to load associated Winget, BYO App, and Driver lists if they are defined. UI elements and checkboxes are updated accordingly to reflect the loaded state.
### [Improves UI state after environment autoload](https://github.com/rbalsleyMSFT/FFU/commit/bdf1b63833c83171aed63e8fc16702078ccd577b)
Updates the visibility of UI panels for Winget and drivers when a previous environment is automatically loaded.
This ensures that if Winget apps or driver models are present, their corresponding UI sections are made visible. Additionally, it updates the "select all" checkbox state for Winget results and attempts to pre-select the hardware make for loaded drivers.
### [Add restore defaults and centralize cleanup logic](https://github.com/rbalsleyMSFT/FFU/commit/f3316a017b73bf12cf1a66e3d03a63e29c437cb1)
Introduces a "Restore Defaults" feature in the UI to reset the environment. This action removes generated configuration files, ISOs, downloaded apps, updates, drivers, and FFUs.
The post-build cleanup logic is refactored from the main build script into a new common function. This new function is used by both the standard build process and the new restore defaults feature, promoting code reuse and simplifying maintenance.
### [Add option to dynamically build PE drivers](https://github.com/rbalsleyMSFT/FFU/commit/e2ccd11f07217b389f1622a69794224412e046e1)
Thanks to @JonasKloseBW for the original code for this in https://github.com/rbalsleyMSFT/FFU/pull/115
Introduces a new parameter, `UseDriversAsPEDrivers`, that allows WinPE drivers to be sourced directly from the main driver repository.
When enabled, the script scans all available drivers, parses their INF files, and copies only the essential driver types (e.g., storage, mouse, keyboard, touchpad, system devices) needed for WinPE. This eliminates the need to maintain a separate, manually curated `PEDrivers` folder.
The UI is updated with a new checkbox that becomes visible when "Copy PE Drivers" is selected, making this a sub-option. Parameter validation is also adjusted to support this new workflow.
### [Improve model name normalization for driver mapping](https://github.com/rbalsleyMSFT/FFU/commit/50713188bffcb64f1b0c1f9eb89e02a300e3de98)
Enhances the model name normalization function to better handle variations in hardware model strings. This change introduces specific rules to canonicalize "All-in-One" and screen size variants (e.g., "-in" or "inch") for more reliable matching against driver mapping rules.
Additionally, optimizes performance by normalizing the system model once before the comparison loop. Logging is also added to show the original and normalized model strings for easier debugging.
### [Defer cleanup of compressed driver source folders](https://github.com/rbalsleyMSFT/FFU/commit/c30ed923b68b933f719b9a2941043b813bf4fd3f)
Implements a deferred cleanup mechanism for driver source folders when they are compressed to a WIM and also used for WinPE.
When drivers are compressed, the original source folders are now preserved if they are also needed for WinPE driver injection. A marker file is created in these preserved folders.
A new cleanup step is added after the WinPE media creation to remove these preserved folders, ensuring they are available when needed but not left behind permanently.
### [Refactor config loading and improve error handling](https://github.com/rbalsleyMSFT/FFU/commit/8d7e4d106620761d0ae1a5133f6d6ba301131471)
Extracts the logic for importing supplemental assets (Winget, BYO, Drivers) into a new reusable function. This function is now called by both the manual and automatic configuration loaders, reducing code duplication.
Enhances the manual configuration loading process with more robust error handling. It now provides specific user-facing error messages for file read failures, empty files, and invalid JSON, improving the user experience when loading a malformed configuration.
When loading a configuration, if optional supplemental files like AppList.json are referenced but not found, an informational message is now displayed to the user instead of failing silently.
### [Add robust sanitization for names used in paths](https://github.com/rbalsleyMSFT/FFU/commit/cb14e84a26acaf5863aa3bb094dbf18424798875)
Introduces a new common function, `ConvertTo-SafeName`, to sanitize strings by removing characters that are invalid in Windows file paths.
This function is now used consistently when creating directory and file names for drivers (Dell, HP, Lenovo, Microsoft) and applications to prevent path-related errors. It replaces several ad-hoc sanitization methods with a single, more robust implementation.
### [Includes exit code fields when using Copy Apps button](https://github.com/rbalsleyMSFT/FFU/commit/f37647599a318da29b62154bebff8c8a857d3002)
Adds persistence of AdditionalExitCodes and IgnoreNonZeroExitCodes when exporting the UI list to prevent losing custom exit handling settings and maintain parity with the primary save routine.
### [Sanitizes app names for storage and paths](https://github.com/rbalsleyMSFT/FFU/commit/d1ca1231045e38316733495e1fdb8590a225be67)
Applies name sanitization when persisting the app list and when building/checking Win32 and Store download directories.
Prevents invalid characters in folder names, aligns persisted names with on-disk structure, and improves detection of existing content to avoid redundant downloads and errors.
### [Adds exit-code overrides and UI for winget apps](https://github.com/rbalsleyMSFT/FFU/commit/d9c0c9c68ee1769230c9789b5c7cb84bcff4d642)
Adds per-app control for additional accepted exit codes and ignoring nonzero exit codes to improve handling of installers with nonstandard returns.
Exposes editable fields in the app list UI, persists them across search defaults, import/export, and pre-download save, and applies overrides during app resolution to honor configured behavior.
### [Adds UI/CLI to copy additional FFUs to USB build](https://github.com/rbalsleyMSFT/FFU/commit/15a5b16b39887b71ae545c638d57183c97bdf629)
- Enables selecting multiple existing FFU images to include on the deployment USB for easier distribution and testing.
- Adds a UI option with selectable, sortable list from the capture folder, refresh support, and persisted selections.
- Validates that selections exist when the option is enabled to prevent empty runs.
- Supports unattended/CLI flows by prompting early or accepting a preselected list for USB creation; deduplicates and logs chosen files.
- Always includes the just-built (or latest available) FFU as a base.
- Improves no-FFU handling and streamlines multi-FFU selection workflow.
### [Standardizes JSON output: depth, UTF-8, key order](https://github.com/rbalsleyMSFT/FFU/commit/6562d16ce500197b428b51915332c6649df302df)
- Sorts top-level config keys before serialization for deterministic files and cleaner diffs.
- Increases JSON depth to 10 to retain nested settings.
- Writes JSON as UTF-8 via Set-Content for consistent encoding.
- Applies across config export and UI save flows.
### [Adds Windows 11 25H2 mapping](https://github.com/rbalsleyMSFT/FFU/commit/eaa3e1e6af5c25e0f8b185f8107e017782b0f00f)
Extends supported Windows 11 releases to include 25H2. Default is still 24H2.
* Update USBImagingToolCreator.ps1 by @jrollmann in https://github.com/rbalsleyMSFT/FFU/pull/262
## New Contributors
* @jrollmann made their first contribution in https://github.com/rbalsleyMSFT/FFU/pull/262
# 2507.1 UI Preview # 2507.1 UI Preview
Waaay too many to list. Just watch the Youtube video in the Readme :) Waaay too many to list. Just watch the Youtube video in the Readme :)
@@ -2,16 +2,6 @@
<Add SourcePath="C:\FFUDevelopment\Apps\Office" OfficeClientEdition="64" Channel="Current"> <Add SourcePath="C:\FFUDevelopment\Apps\Office" OfficeClientEdition="64" Channel="Current">
<Product ID="O365ProPlusRetail"> <Product ID="O365ProPlusRetail">
<Language ID="MatchOS" /> <Language ID="MatchOS" />
<ExcludeApp ID="Access" />
<ExcludeApp ID="Lync" />
<ExcludeApp ID="Publisher" />
<ExcludeApp ID="Bing" />
</Product> </Product>
</Add> </Add>
<Property Name="SharedComputerLicensing" Value="0" />
<Property Name="FORCEAPPSHUTDOWN" Value="FALSE" />
<Property Name="DeviceBasedLicensing" Value="0" />
<Property Name="SCLCacheOverride" Value="0" />
<Updates Enabled="TRUE" />
<Display Level="None" AcceptEULA="TRUE" />
</Configuration> </Configuration>
@@ -92,6 +92,49 @@ function Invoke-Process {
} }
} }
function Format-MsiArguments {
<#
.SYNOPSIS
Ensures MSI file paths in msiexec arguments are properly quoted.
.DESCRIPTION
Detects /i arguments followed by an unquoted path ending in .msi
and wraps the path in double quotes to handle paths with spaces.
#>
param(
[Parameter(Mandatory)]
[string]$CommandLine,
[Parameter(Mandatory)]
[string]$Arguments
)
# Only process if the command is msiexec
if ($CommandLine -notmatch '^msiexec(\.exe)?$') {
return $Arguments
}
# Regex pattern explanation:
# (?i) - Case-insensitive matching
# (/i)\s+ - Match /i followed by whitespace
# (?!") - Negative lookahead: not already quoted
# (.+?\.msi) - Capture path ending in .msi (lazy match to stop at first .msi)
# (?=\s+/|\s*$) - Followed by another switch or end of string
# Pattern to match /i followed by an unquoted MSI path
$pattern = '(?i)(/i)\s+(?!")(.+?\.msi)(?=\s+/|\s*$)'
if ($Arguments -match $pattern) {
$originalArgs = $Arguments
# Replace with quoted path
$Arguments = $Arguments -replace $pattern, '$1 "$2"'
Write-Host "Detected unquoted MSI path in msiexec arguments. Adjusted arguments:"
Write-Host "Original: $originalArgs"
Write-Host "Modified: $Arguments"
}
return $Arguments
}
function Install-Applications { function Install-Applications {
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -177,6 +220,15 @@ function Install-Applications {
$ignoreNonZeroExitCodes = $app.IgnoreNonZeroExitCodes $ignoreNonZeroExitCodes = $app.IgnoreNonZeroExitCodes
} }
# Auto-quote MSI paths if using msiexec and path contains spaces but no quotes
if ($null -ne $argumentsToPass -and $argumentsToPass.Count -gt 0) {
$joinedArgs = $argumentsToPass -join ' '
$formattedArgs = Format-MsiArguments -CommandLine $app.CommandLine -Arguments $joinedArgs
if ($formattedArgs -ne $joinedArgs) {
$argumentsToPass = @($formattedArgs)
}
}
if ($null -eq $argumentsToPass -or $argumentsToPass.Count -eq 0) { if ($null -eq $argumentsToPass -or $argumentsToPass.Count -eq 0) {
Write-Host "Running command: $($app.CommandLine) (no arguments)" Write-Host "Running command: $($app.CommandLine) (no arguments)"
$result = Invoke-Process -FilePath $app.CommandLine -AdditionalSuccessCodes $additionalSuccessCodes -IgnoreNonZeroExitCodes $ignoreNonZeroExitCodes $result = Invoke-Process -FilePath $app.CommandLine -AdditionalSuccessCodes $additionalSuccessCodes -IgnoreNonZeroExitCodes $ignoreNonZeroExitCodes
File diff suppressed because it is too large Load Diff
+22 -1
View File
@@ -15,6 +15,7 @@
This script acts as the primary host for the UI, connecting the user interface with the underlying build and logic modules. This script acts as the primary host for the UI, connecting the user interface with the underlying build and logic modules.
#> #>
#Requires -RunAsAdministrator
[CmdletBinding()] [CmdletBinding()]
[System.STAThread()] [System.STAThread()]
@@ -126,6 +127,14 @@ $window.Add_Loaded({
Initialize-UIDefaults -State $script:uiState Initialize-UIDefaults -State $script:uiState
Initialize-DynamicUIElements -State $script:uiState Initialize-DynamicUIElements -State $script:uiState
Register-EventHandlers -State $script:uiState Register-EventHandlers -State $script:uiState
# Attempt automatic load of previous environment (silent)
try {
Invoke-AutoLoadPreviousEnvironment -State $script:uiState
}
catch {
WriteLog "Auto-load previous environment failed: $($_.Exception.Message)"
}
}) })
@@ -392,8 +401,20 @@ $script:uiState.Controls.btnRun.Add_Click({
# Gather config on the UI thread before starting the job # Gather config on the UI thread before starting the job
$config = Get-UIConfig -State $script:uiState $config = Get-UIConfig -State $script:uiState
# Validate Additional FFU selection if enabled
if ($config.BuildUSBDrive -and $config.CopyAdditionalFFUFiles -and (($null -eq $config.AdditionalFFUFiles) -or ($config.AdditionalFFUFiles.Count -eq 0))) {
[System.Windows.MessageBox]::Show("Please select at least one additional FFU file to copy, or uncheck 'Copy Additional FFU Files'.", "Selection Required", "OK", "Warning") | Out-Null
$btnRun.IsEnabled = $true
$script:uiState.Controls.txtStatus.Text = "Build canceled: Additional FFU selection required."
return
}
$configFilePath = Join-Path $config.FFUDevelopmentPath "\config\FFUConfig.json" $configFilePath = Join-Path $config.FFUDevelopmentPath "\config\FFUConfig.json"
$config | ConvertTo-Json -Depth 10 | Set-Content -Path $configFilePath -Encoding UTF8 # Sort top-level keys alphabetically for consistent output
$sortedConfig = [ordered]@{}
foreach ($k in ($config.Keys | Sort-Object)) { $sortedConfig[$k] = $config[$k] }
$sortedConfig | ConvertTo-Json -Depth 10 | Set-Content -Path $configFilePath -Encoding UTF8
$script:uiState.Data.lastConfigFilePath = $configFilePath $script:uiState.Data.lastConfigFilePath = $configFilePath
if ($config.InstallOffice -and $config.OfficeConfigXMLFile) { if ($config.InstallOffice -and $config.OfficeConfigXMLFile) {
+61 -20
View File
@@ -115,9 +115,9 @@
<TextBox x:Name="txtVMHostIPAddress" Grid.Row="2" Grid.Column="1" Margin="5" VerticalAlignment="Center" HorizontalAlignment="Stretch"/> <TextBox x:Name="txtVMHostIPAddress" Grid.Row="2" Grid.Column="1" Margin="5" VerticalAlignment="Center" HorizontalAlignment="Stretch"/>
<!-- Row 3: Disk Size (GB) --> <!-- Row 3: Disk Size (GB) -->
<StackPanel Grid.Row="3" Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,5"> <StackPanel Grid.Row="3" Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,5">
<TextBlock Text="Disk Size (GB)" ToolTip="Size of the virtual hard disk for the virtual machine. Default is a 30GB dynamic disk."/> <TextBlock Text="Disk Size (GB)" ToolTip="Size of the virtual hard disk for the virtual machine. Default is a 50GB dynamic disk."/>
</StackPanel> </StackPanel>
<TextBox x:Name="txtDiskSize" Grid.Row="3" Grid.Column="1" Margin="5" VerticalAlignment="Center" HorizontalAlignment="Stretch" Text="30" ToolTip="Size of the virtual hard disk for the virtual machine. Default is a 30GB dynamic disk."/> <TextBox x:Name="txtDiskSize" Grid.Row="3" Grid.Column="1" Margin="5" VerticalAlignment="Center" HorizontalAlignment="Stretch" Text="50" ToolTip="Size of the virtual hard disk for the virtual machine. Default is a 50GB dynamic disk."/>
<!-- Row 4: Memory (GB) --> <!-- Row 4: Memory (GB) -->
<StackPanel Grid.Row="4" Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,5"> <StackPanel Grid.Row="4" Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,5">
<TextBlock Text="Memory (GB)" ToolTip="Amount of memory to allocate for the virtual machine. Recommended to use 8GB if possible, especially for Windows 11. Default is 4GB."/> <TextBlock Text="Memory (GB)" ToolTip="Amount of memory to allocate for the virtual machine. Recommended to use 8GB if possible, especially for Windows 11. Default is 4GB."/>
@@ -359,7 +359,7 @@
<!-- Arguments --> <!-- Arguments -->
<TextBlock Text="Arguments:" Margin="0,0,0,5"/> <TextBlock Text="Arguments:" Margin="0,0,0,5"/>
<TextBox x:Name="txtAppArguments" Margin="0,0,0,10" ToolTip="Enter the arguments for the command line. If the application is an msi, the command line should only contain msiexec and the rest of the command line arguments would go here (e.g. /i D:\Win32\Mozilla firefox\setup.msi /qn /norestart)."/> <TextBox x:Name="txtAppArguments" Margin="0,0,0,10" ToolTip="Enter the arguments for the command line. If the application is an msi, the command line should only contain msiexec and the rest of the command line arguments would go here (e.g. /i &quot;D:\Win32\Mozilla firefox\setup.msi&quot; /qn /norestart)."/>
<!-- Source --> <!-- Source -->
<TextBlock Text="Source:" Margin="0,0,0,5"/> <TextBlock Text="Source:" Margin="0,0,0,5"/>
@@ -628,9 +628,10 @@
<CheckBox x:Name="chkCompressDriversToWIM" Content="Compress Driver Model Folder to WIM" Margin="0,0,5,0" ToolTip="When set to $true, will compress each downloaded driver model folder into a separate WIM file within the Drivers folder. This is useful with Copy Drivers to USB drive."/> <CheckBox x:Name="chkCompressDriversToWIM" Content="Compress Driver Model Folder to WIM" Margin="0,0,5,0" ToolTip="When set to $true, will compress each downloaded driver model folder into a separate WIM file within the Drivers folder. This is useful with Copy Drivers to USB drive."/>
</StackPanel> </StackPanel>
<!-- Row 12: Copy PE Drivers Checkbox --> <!-- Row 12: PE Driver Options (UseDriversAsPEDrivers is a dependent sub-option) -->
<StackPanel Grid.Row="12" Orientation="Horizontal" Margin="5"> <StackPanel Grid.Row="12" Margin="5">
<CheckBox x:Name="chkCopyPEDrivers" Content="Copy PE Drivers" Margin="0,0,5,0" ToolTip="When set to $true, will copy the drivers from the $FFUDevelopmentPath\PEDrivers folder to the WinPE deployment media. Default is $false."/> <CheckBox x:Name="chkCopyPEDrivers" Content="Copy PE Drivers" Margin="0,0,0,5" ToolTip="When set to $true, will copy the drivers from the $FFUDevelopmentPath\PEDrivers folder to the WinPE deployment media. Default is $false."/>
<CheckBox x:Name="chkUseDriversAsPEDrivers" Content="Use Drivers Folder as PE Drivers Source" Margin="25,0,0,0" Visibility="Collapsed" ToolTip="When set to $true (and Copy PE Drivers is also checked), bypasses the PE Drivers Folder path and instead scans the Drivers folder to gather only required WinPE drivers. Hidden unless Copy PE Drivers is checked."/>
</StackPanel> </StackPanel>
</Grid> </Grid>
</ScrollViewer> </ScrollViewer>
@@ -640,7 +641,7 @@
<TabItem Header="Build" Padding="20"> <TabItem Header="Build" Padding="20">
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<Grid Margin="10"> <Grid Margin="10">
<!-- Define 10 rows for the Build tab --> <!-- Define 12 rows for the Build tab -->
<Grid.RowDefinitions> <Grid.RowDefinitions>
<!-- Row 0: Header --> <!-- Row 0: Header -->
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
@@ -656,13 +657,15 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<!-- Row 6: Threads --> <!-- Row 6: Threads -->
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<!-- Row 7: General Build Options Header --> <!-- Row 7: BITS Priority -->
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<!-- Row 8: General Build Options Checkboxes --> <!-- Row 8: General Build Options Header -->
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<!-- Row 9: Build USB Drive Section --> <!-- Row 9: General Build Options Checkboxes -->
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<!-- Row 10: Post-Build Cleanup --> <!-- Row 10: Build USB Drive Section -->
<RowDefinition Height="Auto"/>
<!-- Row 11: Post-Build Cleanup -->
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@@ -728,11 +731,25 @@
<TextBlock Grid.Column="0" Text="Threads" VerticalAlignment="Center" ToolTip="Controls the number of parallel threads used by ForEach-Object -Parallel and sets the value of the -ThrottleLimit parameter. Default is 5. Used in Winget, Application Copy, and driver downloads"/> <TextBlock Grid.Column="0" Text="Threads" VerticalAlignment="Center" ToolTip="Controls the number of parallel threads used by ForEach-Object -Parallel and sets the value of the -ThrottleLimit parameter. Default is 5. Used in Winget, Application Copy, and driver downloads"/>
<TextBox x:Name="txtThreads" Grid.Column="1" Margin="5" VerticalAlignment="Center" Width="50" HorizontalAlignment="Left" Text="5" ToolTip="Controls the number of parallel threads used by ForEach-Object -Parallel and sets the value of the -ThrottleLimit parameter. Default is 5. Used in Winget, Application Copy, and driver downloads"/> <TextBox x:Name="txtThreads" Grid.Column="1" Margin="5" VerticalAlignment="Center" Width="50" HorizontalAlignment="Left" Text="5" ToolTip="Controls the number of parallel threads used by ForEach-Object -Parallel and sets the value of the -ThrottleLimit parameter. Default is 5. Used in Winget, Application Copy, and driver downloads"/>
</Grid> </Grid>
<!-- Row 7: General Build Options Header --> <!-- Row 7: BITS Priority -->
<TextBlock Grid.Row="7" Text="General Build Options" FontWeight="Bold" FontSize="16" Margin="0,10,0,5"/> <Grid Grid.Row="7" Margin="0,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="BITS Priority" VerticalAlignment="Center" ToolTip="Controls the BITS download priority used by the UI and BuildFFUVM.ps1. Switch to Foreground to maximize download speed if needed."/>
<ComboBox x:Name="cmbBitsPriority" Grid.Column="1" Margin="5" VerticalAlignment="Center" Width="150" HorizontalAlignment="Left" ToolTip="Controls the BITS download priority used by the UI and BuildFFUVM.ps1. Switch to Foreground to maximize download speed if needed.">
<sys:String>Foreground</sys:String>
<sys:String>High</sys:String>
<sys:String>Normal</sys:String>
<sys:String>Low</sys:String>
</ComboBox>
</Grid>
<!-- Row 8: General Build Options Header -->
<TextBlock Grid.Row="8" Text="General Build Options" FontWeight="Bold" FontSize="16" Margin="0,10,0,5"/>
<!-- Row 8: General Build Options Checkboxes --> <!-- Row 9: General Build Options Checkboxes -->
<WrapPanel Grid.Row="8" Margin="0,5"> <WrapPanel Grid.Row="9" Margin="0,5">
<CheckBox x:Name="chkBuildUSBDriveEnable" Content="Build USB Drive" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will partition and format a USB drive and copy the captured FFU to the drive."/> <CheckBox x:Name="chkBuildUSBDriveEnable" Content="Build USB Drive" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will partition and format a USB drive and copy the captured FFU to the drive."/>
<CheckBox x:Name="chkCompactOS" Content="Compact OS" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will compact the OS when building the FFU."/> <CheckBox x:Name="chkCompactOS" Content="Compact OS" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will compact the OS when building the FFU."/>
<CheckBox x:Name="chkUpdateADK" Content="Update ADK" Margin="5" VerticalAlignment="Center" Tag="When set to $true, the script will check for and install/update to the latest Windows ADK and WinPE add-on."/> <CheckBox x:Name="chkUpdateADK" Content="Update ADK" Margin="5" VerticalAlignment="Center" Tag="When set to $true, the script will check for and install/update to the latest Windows ADK and WinPE add-on."/>
@@ -744,8 +761,8 @@
<CheckBox x:Name="chkVerbose" Content="Verbose" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will enable write-verbose output to the console for the build script."/> <CheckBox x:Name="chkVerbose" Content="Verbose" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will enable write-verbose output to the console for the build script."/>
</WrapPanel> </WrapPanel>
<!-- Row 9: Build USB Drive Section --> <!-- Row 10: Build USB Drive Section -->
<StackPanel Grid.Row="9" Margin="0,10,0,5" x:Name="usbDriveSection" Visibility="Collapsed"> <StackPanel Grid.Row="10" Margin="0,10,0,5" x:Name="usbDriveSection" Visibility="Collapsed">
<TextBlock Text="Build USB Drive Settings" FontWeight="Bold" FontSize="16" Margin="0,0,0,10"/> <TextBlock Text="Build USB Drive Settings" FontWeight="Bold" FontSize="16" Margin="0,0,0,10"/>
<StackPanel Margin="5,0,0,10"> <StackPanel Margin="5,0,0,10">
<CheckBox x:Name="chkAllowExternalHardDiskMedia" Content="Allow External Hard Disk Media" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will allow the use of external hard disk media."/> <CheckBox x:Name="chkAllowExternalHardDiskMedia" Content="Allow External Hard Disk Media" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will allow the use of external hard disk media."/>
@@ -755,6 +772,29 @@
<CheckBox x:Name="chkCopyAutopilot" Content="Copy Autopilot Profile" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will copy the Autopilot profile to the USB drive."/> <CheckBox x:Name="chkCopyAutopilot" Content="Copy Autopilot Profile" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will copy the Autopilot profile to the USB drive."/>
<CheckBox x:Name="chkCopyUnattend" Content="Copy Unattend.xml" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will copy the Unattend.xml file to the USB drive."/> <CheckBox x:Name="chkCopyUnattend" Content="Copy Unattend.xml" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will copy the Unattend.xml file to the USB drive."/>
<CheckBox x:Name="chkCopyPPKG" Content="Copy Provisioning Package" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will copy the provisioning package to the USB drive."/> <CheckBox x:Name="chkCopyPPKG" Content="Copy Provisioning Package" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will copy the provisioning package to the USB drive."/>
<CheckBox x:Name="chkCopyAdditionalFFUFiles" Content="Copy Additional FFU Files" Margin="5" VerticalAlignment="Center" Tag="When set to $true, allows selecting existing FFU files in the capture folder to also copy to the USB drive."/>
<!-- Additional FFU Selection Section -->
<Grid x:Name="additionalFFUPanel" Margin="5,0,0,10" Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Header row -->
<DockPanel Grid.Row="0" Margin="0,5" LastChildFill="False">
<TextBlock Text="Additional FFU Files" DockPanel.Dock="Left" FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,10,0"/>
<Button x:Name="btnRefreshAdditionalFFUs" Content="Refresh" DockPanel.Dock="Left" Padding="10,5" ToolTip="Refresh the list of FFU files from the capture folder"/>
</DockPanel>
<!-- ListView row -->
<ListView x:Name="lstAdditionalFFUs" Grid.Row="1" Margin="0,5" Height="150">
<ListView.View>
<GridView>
<GridViewColumn Header="FFU Name" DisplayMemberBinding="{Binding Name}" Width="300"/>
<GridViewColumn Header="Last Modified" DisplayMemberBinding="{Binding LastModified}" Width="200"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
<!-- Max USB Drives --> <!-- Max USB Drives -->
<StackPanel Orientation="Horizontal" Margin="5"> <StackPanel Orientation="Horizontal" Margin="5">
@@ -778,7 +818,7 @@
<GridView> <GridView>
<GridViewColumn Header="Model" DisplayMemberBinding="{Binding Model}" Width="200"/> <GridViewColumn Header="Model" DisplayMemberBinding="{Binding Model}" Width="200"/>
<GridViewColumn Header="Serial Number" DisplayMemberBinding="{Binding SerialNumber}" Width="150"/> <GridViewColumn Header="Unique ID" DisplayMemberBinding="{Binding UniqueId}" Width="300"/>
<GridViewColumn Header="Size (GB)" DisplayMemberBinding="{Binding Size}" Width="80"/> <GridViewColumn Header="Size (GB)" DisplayMemberBinding="{Binding Size}" Width="80"/>
</GridView> </GridView>
</ListView.View> </ListView.View>
@@ -787,8 +827,8 @@
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<!-- Row 10: Post-Build Cleanup --> <!-- Row 11: Post-Build Cleanup -->
<StackPanel Grid.Row="10" Margin="0,10,0,5"> <StackPanel Grid.Row="11" Margin="0,10,0,5">
<TextBlock Text="Post-Build Cleanup" FontWeight="Bold" FontSize="16" Margin="0,0,0,5"/> <TextBlock Text="Post-Build Cleanup" FontWeight="Bold" FontSize="16" Margin="0,0,0,5"/>
<CheckBox x:Name="chkCleanupAppsISO" Content="Cleanup Apps ISO" Margin="5" VerticalAlignment="Center" Tag="Remove Apps ISO after FFU capture."/> <CheckBox x:Name="chkCleanupAppsISO" Content="Cleanup Apps ISO" Margin="5" VerticalAlignment="Center" Tag="Remove Apps ISO after FFU capture."/>
<CheckBox x:Name="chkCleanupCaptureISO" Content="Cleanup Capture ISO" Margin="5" VerticalAlignment="Center" Tag="Remove WinPE capture ISO after FFU capture."/> <CheckBox x:Name="chkCleanupCaptureISO" Content="Cleanup Capture ISO" Margin="5" VerticalAlignment="Center" Tag="Remove WinPE capture ISO after FFU capture."/>
@@ -816,6 +856,7 @@
<TextBlock x:Name="txtStatus" Grid.Row="2" Margin="0,5,0,0"/> <TextBlock x:Name="txtStatus" Grid.Row="2" Margin="0,5,0,0"/>
<!-- Buttons (Build Config File / Load Config File / Build FFU) --> <!-- Buttons (Build Config File / Load Config File / Build FFU) -->
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,20,20"> <StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,20,20">
<Button x:Name="btnRestoreDefaults" Content="Restore Defaults" Width="150" Margin="0,0,10,0" FontSize="14" Padding="10,5"/>
<Button x:Name="btnBuildConfig" Content="Save Config File" Width="150" Margin="0,0,10,0" FontSize="14" Padding="10,5"/> <Button x:Name="btnBuildConfig" Content="Save Config File" Width="150" Margin="0,0,10,0" FontSize="14" Padding="10,5"/>
<Button x:Name="btnLoadConfig" Content="Load Config File" Width="150" Margin="0,0,10,0" FontSize="14" Padding="10,5"/> <Button x:Name="btnLoadConfig" Content="Load Config File" Width="150" Margin="0,0,10,0" FontSize="14" Padding="10,5"/>
<Button x:Name="btnRun" Content="Build FFU" Width="120" FontSize="14" Padding="10,5"/> <Button x:Name="btnRun" Content="Build FFU" Width="120" FontSize="14" Padding="10,5"/>
@@ -0,0 +1,127 @@
# Provides shared cleanup functionality for both UI and build script.
function Invoke-FFUPostBuildCleanup {
param(
[string]$RootPath,
[string]$AppsPath,
[string]$DriversPath,
[string]$FFUCapturePath,
[string]$CaptureISOPath,
[string]$DeployISOPath,
[string]$AppsISOPath,
[string]$KBPath,
[bool]$RemoveCaptureISO = $false,
[bool]$RemoveDeployISO = $false,
[bool]$RemoveAppsISO = $false,
[bool]$RemoveDrivers = $false,
[bool]$RemoveFFU = $false,
[bool]$RemoveApps = $false,
[bool]$RemoveUpdates = $false
)
$originalProgressPreference = $ProgressPreference
$ProgressPreference = 'SilentlyContinue'
try {
WriteLog "CommonCleanup: Starting cleanup (CaptureISO=$RemoveCaptureISO DeployISO=$RemoveDeployISO AppsISO=$RemoveAppsISO Drivers=$RemoveDrivers FFU=$RemoveFFU Apps=$RemoveApps Updates=$RemoveUpdates KBPath=$KBPath)."
# Primary ISO paths (new naming/location)
if ($RemoveCaptureISO -and -not [string]::IsNullOrWhiteSpace($CaptureISOPath) -and (Test-Path -LiteralPath $CaptureISOPath)) {
WriteLog "CommonCleanup: Removing $CaptureISOPath"
try { Remove-Item -LiteralPath $CaptureISOPath -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $CaptureISOPath : $($_.Exception.Message)" }
}
if ($RemoveDeployISO -and -not [string]::IsNullOrWhiteSpace($DeployISOPath) -and (Test-Path -LiteralPath $DeployISOPath)) {
WriteLog "CommonCleanup: Removing $DeployISOPath"
try { Remove-Item -LiteralPath $DeployISOPath -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $DeployISOPath : $($_.Exception.Message)" }
}
if ($RemoveAppsISO -and -not [string]::IsNullOrWhiteSpace($AppsISOPath) -and (Test-Path -LiteralPath $AppsISOPath)) {
WriteLog "CommonCleanup: Removing $AppsISOPath"
try { Remove-Item -LiteralPath $AppsISOPath -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $AppsISOPath : $($_.Exception.Message)" }
}
# Legacy / root-level WinPE ISOs (pattern-based)
if ($RemoveCaptureISO) {
Get-ChildItem -LiteralPath $RootPath -Filter 'WinPE_FFU_Capture*.iso' -ErrorAction SilentlyContinue | ForEach-Object {
try { WriteLog "CommonCleanup: Removing legacy capture ISO $($_.FullName)"; Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing legacy capture ISO $($_.FullName) : $($_.Exception.Message)" }
}
}
if ($RemoveDeployISO) {
Get-ChildItem -LiteralPath $RootPath -Filter 'WinPE_FFU_Deploy*.iso' -ErrorAction SilentlyContinue | ForEach-Object {
try { WriteLog "CommonCleanup: Removing legacy deploy ISO $($_.FullName)"; Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing legacy deploy ISO $($_.FullName) : $($_.Exception.Message)" }
}
}
if ($RemoveDrivers -and -not [string]::IsNullOrWhiteSpace($DriversPath) -and (Test-Path -LiteralPath $DriversPath -PathType Container)) {
WriteLog "CommonCleanup: Removing contents of $DriversPath (preserving Drivers.json and DriverMapping.json)"
try {
# Preserve drivers json files
$driverItems = Get-ChildItem -LiteralPath $DriversPath -Force -ErrorAction SilentlyContinue | Where-Object { @('Drivers.json', 'DriverMapping.json') -notcontains $_.Name }
if ($driverItems) {
$driverItems | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue
}
}
catch { WriteLog "CommonCleanup: Driver content cleanup issue: $($_.Exception.Message)" }
}
if ($RemoveFFU -and -not [string]::IsNullOrWhiteSpace($FFUCapturePath) -and (Test-Path -LiteralPath $FFUCapturePath -PathType Container)) {
WriteLog "CommonCleanup: Removing FFU files in $FFUCapturePath"
Get-ChildItem -LiteralPath $FFUCapturePath -Filter *.ffu -ErrorAction SilentlyContinue | ForEach-Object {
try { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing FFU $($_.FullName) : $($_.Exception.Message)" }
}
}
if ($RemoveApps -and -not [string]::IsNullOrWhiteSpace($AppsPath) -and (Test-Path -LiteralPath $AppsPath -PathType Container)) {
$win32 = Join-Path $AppsPath 'Win32'
$store = Join-Path $AppsPath 'MSStore'
if (Test-Path -LiteralPath $win32) {
WriteLog "CommonCleanup: Removing $win32"
try { Remove-Item -LiteralPath $win32 -Recurse -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $win32 : $($_.Exception.Message)" }
}
if (Test-Path -LiteralPath $store) {
WriteLog "CommonCleanup: Removing $store"
try { Remove-Item -LiteralPath $store -Recurse -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $store : $($_.Exception.Message)" }
}
$office = Join-Path $AppsPath 'Office'
if ((Test-Path -LiteralPath $office) -and $InstallOffice) {
WriteLog "CommonCleanup: Checking for Office artifacts in $office"
$officeSub = Join-Path $office 'Office'
if (Test-Path -LiteralPath $officeSub) {
WriteLog "CommonCleanup: Removing $officeSub"
try { Remove-Item -LiteralPath $officeSub -Recurse -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $officeSub : $($_.Exception.Message)" }
}
$setupExe = Join-Path $office 'setup.exe'
if (Test-Path -LiteralPath $setupExe) {
WriteLog "CommonCleanup: Removing $setupExe"
try { Remove-Item -LiteralPath $setupExe -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $setupExe : $($_.Exception.Message)" }
}
}
}
if ($RemoveUpdates) {
if (-not [string]::IsNullOrWhiteSpace($AppsPath) -and (Test-Path -LiteralPath $AppsPath)) {
# Remove per-run app update payloads stored under Apps
$appUpdateDirs = @('Defender', 'Edge', 'MSRT', 'OneDrive')
foreach ($d in $appUpdateDirs) {
$target = Join-Path $AppsPath $d
if (Test-Path -LiteralPath $target) {
WriteLog "CommonCleanup: Removing update folder $target"
try { Remove-Item -LiteralPath $target -Recurse -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $target : $($_.Exception.Message)" }
}
}
}
if (-not [string]::IsNullOrWhiteSpace($KBPath) -and (Test-Path -LiteralPath $KBPath)) {
# Remove Windows/.NET CU downloads stored under KB
WriteLog "CommonCleanup: Removing downloaded updates in $KBPath"
try { Remove-Item -LiteralPath $KBPath -Recurse -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $KBPath : $($_.Exception.Message)" }
}
}
WriteLog "CommonCleanup: Completed."
}
catch {
WriteLog "CommonCleanup: Fatal cleanup error $($_.Exception.Message)"
}
finally {
$ProgressPreference = $originalProgressPreference
}
}
Export-ModuleMember -Function Invoke-FFUPostBuildCleanup
+111 -4
View File
@@ -12,6 +12,10 @@ $script:CommonCoreLogFilePath = $null
# Mutex for log file access # Mutex for log file access
$script:commonCoreLogMutexName = "Global\FFUCommonCoreLogMutex" # Unique name $script:commonCoreLogMutexName = "Global\FFUCommonCoreLogMutex" # Unique name
$script:commonCoreLogMutex = New-Object System.Threading.Mutex($false, $script:commonCoreLogMutexName) $script:commonCoreLogMutex = New-Object System.Threading.Mutex($false, $script:commonCoreLogMutexName)
$script:BitsTransferPriority = 'Normal'
if (-not [string]::IsNullOrWhiteSpace($env:FFU_BITS_PRIORITY)) {
$script:BitsTransferPriority = $env:FFU_BITS_PRIORITY
}
# Function to set the log file path for this module # Function to set the log file path for this module
function Set-CommonCoreLogPath { function Set-CommonCoreLogPath {
@@ -31,6 +35,23 @@ function Set-CommonCoreLogPath {
} }
} }
function Set-BitsTransferPriority {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Foreground', 'High', 'Normal', 'Low')]
[string]$Priority
)
$script:BitsTransferPriority = $Priority
try {
Set-Item -Path Env:FFU_BITS_PRIORITY -Value $Priority -ErrorAction Stop
}
catch {
WriteLog "Failed to set FFU_BITS_PRIORITY environment variable: $($_.Exception.Message)"
}
WriteLog "BITS transfer priority set to $Priority."
}
# Centralized WriteLog function # Centralized WriteLog function
function WriteLog { function WriteLog {
[CmdletBinding()] [CmdletBinding()]
@@ -143,20 +164,36 @@ function Start-BitsTransferWithRetry {
[string]$Source, [string]$Source,
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$Destination, [string]$Destination,
[int]$Retries = 3 [int]$Retries = 3,
[ValidateSet('Foreground','High','Normal','Low')]
[string]$Priority
) )
if ([string]::IsNullOrWhiteSpace($Priority)) {
if (-not [string]::IsNullOrWhiteSpace($env:FFU_BITS_PRIORITY)) {
$Priority = $env:FFU_BITS_PRIORITY
}
elseif (-not [string]::IsNullOrWhiteSpace($script:BitsTransferPriority)) {
$Priority = $script:BitsTransferPriority
}
else {
$Priority = 'Normal'
}
}
$attempt = 0 $attempt = 0
$lastError = $null $lastError = $null
$notLoggedOnHResult = [int]0x800704dd
$fallbackTriggered = $false
while ($attempt -lt $Retries) { while ($attempt -lt $Retries -and -not $fallbackTriggered) {
$OriginalVerbosePreference = $VerbosePreference $OriginalVerbosePreference = $VerbosePreference
$OriginalProgressPreference = $ProgressPreference $OriginalProgressPreference = $ProgressPreference
try { try {
$VerbosePreference = 'SilentlyContinue' $VerbosePreference = 'SilentlyContinue'
$ProgressPreference = 'SilentlyContinue' $ProgressPreference = 'SilentlyContinue'
Start-BitsTransfer -Source $Source -Destination $Destination -ErrorAction Stop Start-BitsTransfer -Source $Source -Destination $Destination -Priority $Priority -ErrorAction Stop
$ProgressPreference = $OriginalProgressPreference $ProgressPreference = $OriginalProgressPreference
$VerbosePreference = $OriginalVerbosePreference $VerbosePreference = $OriginalVerbosePreference
@@ -166,7 +203,24 @@ function Start-BitsTransferWithRetry {
catch { catch {
$lastError = $_ $lastError = $_
$attempt++ $attempt++
WriteLog "Attempt $attempt of $Retries failed to download $Source. Error: $($lastError.Exception.Message)." $errorMessage = $lastError.Exception.Message
WriteLog "Attempt $attempt of $Retries failed to download $Source. Error: $errorMessage."
$hResult = $null
if ($null -ne $lastError.Exception) {
$hResult = $lastError.Exception.HResult
}
$needsHttpFallback = $false
if ($hResult -eq $notLoggedOnHResult) {
$needsHttpFallback = $true
}
elseif ($errorMessage -match '0x800704DD' -or $errorMessage -match 'not.*logged on to the network') {
$needsHttpFallback = $true
}
if ($needsHttpFallback) {
WriteLog "BITS cannot download $Source because the current session is not logged on to the network. Falling back to Invoke-WebRequest."
$fallbackTriggered = $true
break
}
Start-Sleep -Seconds (1 * $attempt) Start-Sleep -Seconds (1 * $attempt)
} }
finally { finally {
@@ -179,6 +233,41 @@ function Start-BitsTransferWithRetry {
} }
} }
if ($fallbackTriggered) {
$remainingAttempts = $Retries - $attempt
if ($remainingAttempts -lt 1) {
$remainingAttempts = 1
}
$httpAttempt = 0
while ($httpAttempt -lt $remainingAttempts) {
$httpAttempt++
$OriginalVerbosePreference = $VerbosePreference
$OriginalProgressPreference = $ProgressPreference
try {
$VerbosePreference = 'SilentlyContinue'
$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -Uri $Source -OutFile $Destination -ErrorAction Stop
$ProgressPreference = $OriginalProgressPreference
$VerbosePreference = $OriginalVerbosePreference
WriteLog "Successfully transferred $Source to $Destination via HTTP fallback."
return
}
catch {
$lastError = $_
WriteLog "HTTP fallback attempt $httpAttempt of $remainingAttempts failed to download $Source. Error: $($lastError.Exception.Message)."
Start-Sleep -Seconds (1 * $httpAttempt)
}
finally {
if (Get-Variable -Name 'OriginalProgressPreference' -ErrorAction SilentlyContinue) {
$ProgressPreference = $OriginalProgressPreference
}
if (Get-Variable -Name 'OriginalVerbosePreference' -ErrorAction SilentlyContinue) {
$VerbosePreference = $OriginalVerbosePreference
}
}
}
}
WriteLog "Failed to download $Source after $Retries attempts. Last Error: $($lastError.Exception.Message)" WriteLog "Failed to download $Source after $Retries attempts. Last Error: $($lastError.Exception.Message)"
throw $lastError throw $lastError
} }
@@ -194,4 +283,22 @@ function Set-Progress {
WriteLog "[PROGRESS] $Percentage | $Message" WriteLog "[PROGRESS] $Percentage | $Message"
} }
function ConvertTo-SafeName {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Name
)
# Replace invalid Windows filename characters (<>:"/\|?* and control chars) with a dash
$sanitized = $Name -replace '[<>:\"/\\|?*\x00-\x1F]', '-'
# Collapse multiple consecutive dashes
$sanitized = $sanitized -replace '-{2,}', '-'
# Trim leading/trailing spaces, periods, and dashes
$sanitized = $sanitized.Trim(' ', '.', '-')
if ([string]::IsNullOrWhiteSpace($sanitized)) {
$sanitized = 'Unnamed'
}
return $sanitized
}
Export-ModuleMember -Function * Export-ModuleMember -Function *
@@ -0,0 +1,300 @@
<#
.SYNOPSIS
Common Dell driver helpers (catalog index, model listing, latest package selection).
#>
function Convert-DellVendorVersion {
param([Parameter(Mandatory=$true)][string]$VendorVersion)
$segments = $VendorVersion.Split('.') | ForEach-Object {
if ($_ -match '^\d+$') { [int]$_ } else { 0 }
}
return ,$segments
}
function Compare-DellVendorVersion {
param(
[int[]]$Left,
[int[]]$Right
)
$len = [Math]::Max($Left.Length,$Right.Length)
for ($i=0; $i -lt $len; $i++) {
$l = if ($i -lt $Left.Length) { $Left[$i] } else { 0 }
$r = if ($i -lt $Right.Length) { $Right[$i] } else { 0 }
if ($l -gt $r) { return 1 }
if ($l -lt $r) { return -1 }
}
return 0
}
function Get-DellCatalogIndex {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)][string]$DriversFolder
)
$dellFolder = Join-Path $DriversFolder 'Dell'
if (-not (Test-Path $dellFolder)) { New-Item -Path $dellFolder -ItemType Directory -Force | Out-Null }
$cabPath = Join-Path $dellFolder 'CatalogIndexPC.cab'
$xmlPath = Join-Path $dellFolder 'CatalogIndexPC.xml'
$url = 'https://downloads.dell.com/catalog/CatalogIndexPC.cab'
$need = $true
if (Test-Path $xmlPath) {
$ageDays = ((Get-Date) - (Get-Item $xmlPath).CreationTime).TotalDays
if ($ageDays -lt 7) { $need = $false }
}
if ($need) {
if (Test-Path $cabPath) { Remove-Item $cabPath -Force -ErrorAction SilentlyContinue }
if (Test-Path $xmlPath) { Remove-Item $xmlPath -Force -ErrorAction SilentlyContinue }
Start-BitsTransferWithRetry -Source $url -Destination $cabPath
Invoke-Process -FilePath Expand.exe -ArgumentList """$cabPath"" ""$xmlPath""" | Out-Null
Remove-Item $cabPath -Force -ErrorAction SilentlyContinue
}
if (-not (Test-Path $xmlPath)) { throw "Dell CatalogIndexPC XML missing: $xmlPath" }
return $xmlPath
}
function Get-DellClientModels {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)][string]$CatalogIndexXmlPath
)
$settings = New-Object System.Xml.XmlReaderSettings
$settings.IgnoreWhitespace = $true
$settings.IgnoreComments = $true
$reader = [System.Xml.XmlReader]::Create($CatalogIndexXmlPath,$settings)
$models = [System.Collections.Generic.List[pscustomobject]]::new()
try {
while ($reader.Read()) {
if ($reader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $reader.Name -eq 'GroupManifest') {
# Read subtree to pick out brand/model/systemID + path
$sub = $reader.ReadSubtree()
$doc = New-Object System.Xml.XmlDocument
$doc.Load($sub)
$sub.Dispose()
# Use local-name() to ignore namespaces
$brandNode = $doc.SelectSingleNode("//*[local-name()='SupportedSystems']/*[local-name()='Brand']")
if (-not $brandNode) { continue }
$brandDisplay = ($brandNode.SelectSingleNode("*[local-name()='Display']")?.InnerText).Trim()
$modelNode = $brandNode.SelectSingleNode("*[local-name()='Model']")
if (-not $modelNode) { continue }
$modelNumber = ($modelNode.SelectSingleNode("*[local-name()='Display']")?.InnerText).Trim()
$systemId = $modelNode.GetAttribute('systemID')
$manifestInfo = $doc.SelectSingleNode("//*[local-name()='ManifestInformation']")
if (-not $manifestInfo) { continue }
$pathAttr = $manifestInfo.GetAttribute('path')
if (-not $pathAttr) { continue }
$cabUrl = 'https://downloads.dell.com/' + $pathAttr
# Normalize model display using GroupManifest Display CDATA if available (strip 'PDK Catalog for')
$gmDisplayNode = $doc.SelectSingleNode("/*[local-name()='GroupManifest']/*[local-name()='Display']")
$modelFull = $null
if ($gmDisplayNode -and $gmDisplayNode.InnerText) {
$rawDisplay = $gmDisplayNode.InnerText.Trim()
$modelFull = ($rawDisplay -replace '^\s*PDK Catalog for\s+','').Trim()
}
if ([string]::IsNullOrWhiteSpace($modelFull)) {
# Fallback: assemble from brand/model nodes (legacy heuristic)
$prefixedModelNumber = $modelNumber
if ($modelNumber -and $brandDisplay) {
if ($modelNumber.StartsWith($brandDisplay,[System.StringComparison]::OrdinalIgnoreCase)) {
$prefixedModelNumber = $modelNumber
}
else {
$prefixedModelNumber = "$brandDisplay $modelNumber"
}
}
elseif ($brandDisplay -and -not $modelNumber) {
$prefixedModelNumber = $brandDisplay
}
$modelFull = $prefixedModelNumber
}
$modelDisplay = "$modelFull ($systemId)"
$models.Add([pscustomobject]@{
Brand = $brandDisplay
ModelNumber = $modelNumber
SystemId = $systemId
CabRelativePath = $pathAttr
CabUrl = $cabUrl
ModelDisplay = $modelDisplay
})
}
}
}
finally {
$reader.Dispose()
}
return $models
}
function Get-DellLatestDriverPackages {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)][string]$ModelXmlPath,
[Parameter(Mandatory=$true)][string]$WindowsArch,
[Parameter(Mandatory=$true)][int]$WindowsRelease
)
if (-not (Test-Path $ModelXmlPath)) { throw "Model XML not found: $ModelXmlPath" }
$xml = [xml](Get-Content -Path $ModelXmlPath -Raw)
# Collect all SoftwareComponent nodes
$components = $xml.SelectNodes("//*[local-name()='SoftwareComponent']")
if (-not $components) { return @() }
$rawPackages = [System.Collections.Generic.List[pscustomobject]]::new()
foreach ($comp in $components) {
$ctype = $comp.SelectSingleNode("*[local-name()='ComponentType']")
if (-not $ctype) { continue }
if ($ctype.GetAttribute('value') -ne 'DRVR') { continue }
# OS filtering (arch only release filtering intentionally minimal for now)
$osNodes = @($comp.SelectNodes("*[local-name()='SupportedOperatingSystems']/*[local-name()='OperatingSystem']"))
if (-not $osNodes) { continue }
$validOS = $osNodes | Where-Object { $_.GetAttribute('osArch') -eq $WindowsArch } | Select-Object -First 1
if (-not $validOS) { continue }
$path = $comp.GetAttribute('path')
if (-not $path) { continue }
$downloadUrl = "https://downloads.dell.com/$path"
$fileName = [IO.Path]::GetFileName($path)
$vendorVersion = $comp.GetAttribute('vendorVersion')
$versionArr = if ($vendorVersion) { Convert-DellVendorVersion $vendorVersion } else { @(0) }
$dateTimeAttr = $comp.GetAttribute('dateTime')
$dt = Get-Date
if ($dateTimeAttr) {
try { $dt = [DateTime]::Parse($dateTimeAttr) } catch { }
}
$categoryNode = $comp.SelectSingleNode("*[local-name()='Category']/*[local-name()='Display']")
$category = if ($categoryNode) { $categoryNode.InnerText.Trim() } else { 'Uncategorized' }
# Collect componentIDs (SupportedDevices + SupportedDCHDevices)
$compIds = [System.Collections.Generic.List[string]]::new()
$devNodes = @($comp.SelectNodes(".//*[local-name()='Device']"))
foreach ($dn in $devNodes) {
$id = $dn.GetAttribute('componentID')
if ($id) { [void]$compIds.Add($id) }
}
if ($compIds.Count -eq 0) { continue }
# Build a deterministic sortable key: zero-pad each numeric segment to 6 digits
$versionSortable = ($versionArr | ForEach-Object { $_.ToString('D6') }) -join '-'
# Capture a humanreadable driver name (preserve spaces like HP/Lenovo; remove only illegal path chars and extra whitespace)
$displayNode = $comp.SelectSingleNode("*[local-name()='Name']/*[local-name()='Display']")
$nameRaw = if ($displayNode) { $displayNode.InnerText.Trim() } else { $fileName }
# Remove characters not suitable for display (and disallowed in file names) but keep spaces
$nameDisplay = $nameRaw -replace '[\\\/:\*\?\"\<\>\|]', ' ' -replace '[,]', '-'
# Collapse multiple spaces to single
$nameDisplay = ($nameDisplay -replace '\s+', ' ').Trim()
$rawPackages.Add([pscustomobject]@{
Path = $path
DownloadUrl = $downloadUrl
FileName = $fileName
Name = $nameDisplay
Category = $category
VendorVersion = $vendorVersion
VersionArray = $versionArr
VersionSortable = $versionSortable
DateTime = $dt
ComponentIds = $compIds
})
}
if ($rawPackages.Count -eq 0) { return @() }
# Sort newest first by VersionSortable (lexicographic works due to zero padding) then DateTime
$sorted = $rawPackages | Sort-Object -Property @{ Expression = { $_.VersionSortable }; Descending = $true }, @{ Expression = { $_.DateTime }; Descending = $true }
$chosen = [System.Collections.Generic.List[pscustomobject]]::new()
$assignedIds = [System.Collections.Generic.HashSet[string]]::new()
foreach ($pkg in $sorted) {
$hasOverlap = $false
foreach ($cid in $pkg.ComponentIds) {
if ($assignedIds.Contains($cid)) { $hasOverlap = $true; break }
}
if ($hasOverlap) {
WriteLog "Get-DellLatestDriverPackages: Skipping superseded package $($pkg.FileName) (shared componentID with newer package)."
continue
}
foreach ($cid in $pkg.ComponentIds) { [void]$assignedIds.Add($cid) }
$chosen.Add([pscustomobject]@{
Path = $pkg.Path
DownloadUrl = $pkg.DownloadUrl
DriverFileName = $pkg.FileName
Name = $pkg.Name
Category = $pkg.Category
VendorVersion = $pkg.VendorVersion
DateTime = $pkg.DateTime
ComponentIds = $pkg.ComponentIds
})
}
if ($chosen.Count -eq 0) {
WriteLog "Get-DellLatestDriverPackages: No qualifying driver packages after supersedence."
return @()
}
WriteLog ("Get-DellLatestDriverPackages: Selected {0} package(s) after supersedence." -f $chosen.Count)
return $chosen
}
# Resolve a Dell permodel CabUrl when missing by inspecting CatalogIndexPC
function Resolve-DellCabUrlFromModel {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$DriversFolder,
[Parameter()][string]$ModelDisplay,
[Parameter()][string]$SystemId
)
if ([string]::IsNullOrWhiteSpace($SystemId) -and -not [string]::IsNullOrWhiteSpace($ModelDisplay)) {
# Try to parse the trailing (XXXX) token (SystemId)
if ($ModelDisplay -match '\(([0-9A-Fa-f]{4})\)\s*$') {
$SystemId = $matches[1].ToUpperInvariant()
}
}
if ([string]::IsNullOrWhiteSpace($SystemId)) {
WriteLog "Resolve-DellCabUrlFromModel: No SystemId could be determined from '$ModelDisplay'."
return $null
}
try {
$indexXml = Get-DellCatalogIndex -DriversFolder $DriversFolder
# Reuse existing model parsing to avoid duplicating streaming logic
$allModels = Get-DellClientModels -CatalogIndexXmlPath $indexXml
$match = $allModels | Where-Object { $_.SystemId -eq $SystemId } | Select-Object -First 1
if ($null -eq $match) {
WriteLog "Resolve-DellCabUrlFromModel: SystemId '$SystemId' not found in CatalogIndexPC.xml."
return $null
}
WriteLog "Resolve-DellCabUrlFromModel: Resolved CabUrl for '$($match.ModelDisplay)' -> $($match.CabUrl)"
return [pscustomobject]@{
Brand = $match.Brand
ModelNumber = $match.ModelNumber
SystemId = $match.SystemId
CabRelativePath = $match.CabRelativePath
CabUrl = $match.CabUrl
ModelDisplay = $match.ModelDisplay
}
}
catch {
WriteLog "Resolve-DellCabUrlFromModel: Failure resolving CabUrl for '$ModelDisplay' / SystemId '$SystemId' : $($_.Exception.Message)"
return $null
}
}
Export-ModuleMember -Function Convert-DellVendorVersion,Compare-DellVendorVersion,Get-DellCatalogIndex,Get-DellClientModels,Get-DellLatestDriverPackages,Resolve-DellCabUrlFromModel
+475 -86
View File
@@ -22,7 +22,10 @@ function Compress-DriverFolderToWim {
[string]$WimName, # Optional, defaults to folder name [string]$WimName, # Optional, defaults to folder name
[Parameter()] [Parameter()]
[string]$WimDescription # Optional, defaults to folder name [string]$WimDescription, # Optional, defaults to folder name
[Parameter()]
[bool]$PreserveSource = $false # When $true, do not delete source folder; create marker for deferred cleanup
) )
WriteLog "Starting compression of folder '$SourceFolderPath' to '$DestinationWimPath'." WriteLog "Starting compression of folder '$SourceFolderPath' to '$DestinationWimPath'."
@@ -66,6 +69,20 @@ function Compress-DriverFolderToWim {
WriteLog "Successfully compressed '$SourceFolderPath' to '$DestinationWimPath' using dism.exe." WriteLog "Successfully compressed '$SourceFolderPath' to '$DestinationWimPath' using dism.exe."
# Remove the source folder after successful compression # Remove the source folder after successful compression
if ($PreserveSource) {
WriteLog "Preserving source driver folder for deferred WinPE driver harvesting: $SourceFolderPath"
try {
$markerFile = Join-Path -Path $SourceFolderPath -ChildPath '__PreservedForPEDrivers.txt'
if (-not (Test-Path -Path $markerFile -PathType Leaf)) {
New-Item -Path $markerFile -ItemType File -Force | Out-Null
WriteLog "Created preservation marker file: $markerFile"
}
}
catch {
WriteLog "Warning: Failed to create preservation marker in $SourceFolderPath. Error: $($_.Exception.Message)"
}
}
else {
WriteLog "Removing source driver folder: $SourceFolderPath" WriteLog "Removing source driver folder: $SourceFolderPath"
try { try {
Remove-Item -Path $SourceFolderPath -Recurse -Force -ErrorAction Stop Remove-Item -Path $SourceFolderPath -Recurse -Force -ErrorAction Stop
@@ -75,6 +92,7 @@ function Compress-DriverFolderToWim {
WriteLog "Warning: Failed to remove source folder '$SourceFolderPath'. Error: $($_.Exception.Message)" WriteLog "Warning: Failed to remove source folder '$SourceFolderPath'. Error: $($_.Exception.Message)"
# Do not fail the whole operation, just log a warning. # Do not fail the whole operation, just log a warning.
} }
}
return $true # Indicate success return $true # Indicate success
} }
@@ -137,31 +155,185 @@ function Update-DriverMappingJson {
$updatedCount = 0 $updatedCount = 0
$addedCount = 0 $addedCount = 0
$hpSystemIdCache = @{}
$normalizeHpName = {
param([string]$text)
if ([string]::IsNullOrWhiteSpace($text)) {
return $null
}
return ([regex]::Replace($text.ToLowerInvariant(), '[^a-z0-9]', ''))
}
$getHpSystemId = {
param([string]$modelName)
if ([string]::IsNullOrWhiteSpace($modelName)) {
return $null
}
if ($hpSystemIdCache.ContainsKey($modelName)) {
return $hpSystemIdCache[$modelName]
}
$hpFolder = Join-Path -Path $DriversFolder -ChildPath 'HP'
if (-not (Test-Path -Path $hpFolder -PathType Container)) {
$hpSystemIdCache[$modelName] = $null
return $null
}
$platformListXml = Join-Path -Path $hpFolder -ChildPath 'PlatformList.xml'
$platformListCab = Join-Path -Path $hpFolder -ChildPath 'platformList.cab'
if (-not (Test-Path -Path $platformListXml -PathType Leaf)) {
try {
WriteLog "Attempting to refresh HP PlatformList.xml for SystemID lookup."
Start-BitsTransferWithRetry -Source 'https://hpia.hpcloud.hp.com/ref/platformList.cab' -Destination $platformListCab -ErrorAction Stop
if (Test-Path -Path $platformListXml) { Remove-Item -Path $platformListXml -Force -ErrorAction SilentlyContinue }
Invoke-Process -FilePath "expand.exe" -ArgumentList @("`"$platformListCab`"", "`"$platformListXml`"") -ErrorAction Stop | Out-Null
if (Test-Path -Path $platformListCab) { Remove-Item -Path $platformListCab -Force -ErrorAction SilentlyContinue }
}
catch {
WriteLog "Failed to refresh HP PlatformList.xml: $($_.Exception.Message)"
$hpSystemIdCache[$modelName] = $null
return $null
}
}
try {
[xml]$platformListContent = Get-Content -Path $platformListXml -Raw -Encoding UTF8 -ErrorAction Stop
$targetName = $modelName.Trim()
$normalizedTarget = & $normalizeHpName $targetName
$modelMatch = $platformListContent.ImagePal.Platform | Where-Object {
[string]::Equals($_.ProductName.'#text'.Trim(), $targetName, [System.StringComparison]::OrdinalIgnoreCase)
} | Select-Object -First 1
if (-not $modelMatch -and $normalizedTarget) {
$modelMatch = $platformListContent.ImagePal.Platform | Where-Object {
$candidateName = $_.ProductName.'#text'
$normalizedCandidate = & $normalizeHpName $candidateName
$normalizedCandidate -eq $normalizedTarget
} | Select-Object -First 1
}
if (-not $modelMatch -and $normalizedTarget) {
$modelMatch = $platformListContent.ImagePal.Platform | Where-Object {
$candidateName = $_.ProductName.'#text'
$normalizedCandidate = & $normalizeHpName $candidateName
($normalizedCandidate -like "*$normalizedTarget*") -or ($normalizedTarget -like "*$normalizedCandidate*")
} | Select-Object -First 1
}
if ($modelMatch -and -not [string]::IsNullOrWhiteSpace($modelMatch.SystemID)) {
$resolvedId = $modelMatch.SystemID.Trim().ToUpperInvariant()
$hpSystemIdCache[$modelName] = $resolvedId
return $resolvedId
}
else {
WriteLog "HP SystemId lookup: no match found in PlatformList.xml for model '$modelName'."
}
}
catch {
WriteLog "Failed to parse HP PlatformList.xml for model '$modelName': $($_.Exception.Message)"
}
$hpSystemIdCache[$modelName] = $null
return $null
}
foreach ($driver in $DownloadedDrivers) { foreach ($driver in $DownloadedDrivers) {
# Skip if any required property is missing or null
if (-not $driver.PSObject.Properties['Make'] -or -not $driver.PSObject.Properties['Model'] -or -not $driver.PSObject.Properties['DriverPath'] -or [string]::IsNullOrWhiteSpace($driver.DriverPath)) { if (-not $driver.PSObject.Properties['Make'] -or -not $driver.PSObject.Properties['Model'] -or -not $driver.PSObject.Properties['DriverPath'] -or [string]::IsNullOrWhiteSpace($driver.DriverPath)) {
WriteLog "Skipping driver entry due to missing or empty Make, Model, or DriverPath. Details: $(($driver | ConvertTo-Json -Compress -Depth 3))" WriteLog "Skipping driver entry due to missing or empty Make, Model, or DriverPath. Details: $(($driver | ConvertTo-Json -Compress -Depth 3))"
continue continue
} }
# Find existing entry $systemIdValue = $null
$machineTypeValue = $null
if ($driver.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($driver.SystemId)) {
$systemIdValue = $driver.SystemId.Trim().ToUpperInvariant()
}
if ($driver.PSObject.Properties['MachineType'] -and -not [string]::IsNullOrWhiteSpace($driver.MachineType)) {
$machineTypeValue = $driver.MachineType.Trim()
}
switch ($driver.Make) {
'Dell' {
if (-not $systemIdValue -and $driver.Model -match '\(([^)]+)\)\s*$') {
$systemIdValue = $matches[1].Trim().ToUpperInvariant()
}
}
'HP' {
if (-not $systemIdValue) {
$systemIdValue = & $getHpSystemId $driver.Model
}
}
'Lenovo' {
if (-not $machineTypeValue -and $driver.Model -match '\(([^)]+)\)\s*$') {
$machineTypeValue = $matches[1].Trim()
}
}
}
$existingEntry = $mappingList | Where-Object { $_.Manufacturer -eq $driver.Make -and $_.Model -eq $driver.Model } | Select-Object -First 1 $existingEntry = $mappingList | Where-Object { $_.Manufacturer -eq $driver.Make -and $_.Model -eq $driver.Model } | Select-Object -First 1
if ($null -ne $existingEntry) { if ($null -ne $existingEntry) {
# Update existing entry if the path is different $entryUpdated = $false
if ($existingEntry.DriverPath -ne $driver.DriverPath) { if ($existingEntry.DriverPath -ne $driver.DriverPath) {
WriteLog "Updating driver path for '$($driver.Make) - $($driver.Model)' from '$($existingEntry.DriverPath)' to '$($driver.DriverPath)'." WriteLog "Updating driver path for '$($driver.Make) - $($driver.Model)' from '$($existingEntry.DriverPath)' to '$($driver.DriverPath)'."
$existingEntry.DriverPath = $driver.DriverPath $existingEntry.DriverPath = $driver.DriverPath
$entryUpdated = $true
}
if ($driver.Make -in @('HP', 'Dell') -and -not [string]::IsNullOrWhiteSpace($systemIdValue)) {
if ($existingEntry.PSObject.Properties['SystemId']) {
if ($existingEntry.SystemId -ne $systemIdValue) {
WriteLog "Updating SystemId for '$($driver.Make) - $($driver.Model)' to '$systemIdValue'."
$existingEntry.SystemId = $systemIdValue
$entryUpdated = $true
}
}
else {
WriteLog "Adding SystemId '$systemIdValue' for '$($driver.Make) - $($driver.Model)'."
$existingEntry | Add-Member -NotePropertyName SystemId -NotePropertyValue $systemIdValue
$entryUpdated = $true
}
}
if ($driver.Make -eq 'Lenovo' -and -not [string]::IsNullOrWhiteSpace($machineTypeValue)) {
if ($existingEntry.PSObject.Properties['MachineType']) {
if ($existingEntry.MachineType -ne $machineTypeValue) {
WriteLog "Updating MachineType for '$($driver.Make) - $($driver.Model)' to '$machineTypeValue'."
$existingEntry.MachineType = $machineTypeValue
$entryUpdated = $true
}
}
else {
WriteLog "Adding MachineType '$machineTypeValue' for '$($driver.Make) - $($driver.Model)'."
$existingEntry | Add-Member -NotePropertyName MachineType -NotePropertyValue $machineTypeValue
$entryUpdated = $true
}
}
if ($entryUpdated) {
$updatedCount++ $updatedCount++
} }
} }
else { else {
# Add new entry
$newEntry = [PSCustomObject]@{ $newEntry = [PSCustomObject]@{
Manufacturer = $driver.Make Manufacturer = $driver.Make
Model = $driver.Model Model = $driver.Model
DriverPath = $driver.DriverPath DriverPath = $driver.DriverPath
} }
if ($driver.Make -in @('HP', 'Dell') -and -not [string]::IsNullOrWhiteSpace($systemIdValue)) {
$newEntry | Add-Member -NotePropertyName SystemId -NotePropertyValue $systemIdValue
}
if ($driver.Make -eq 'Lenovo' -and -not [string]::IsNullOrWhiteSpace($machineTypeValue)) {
$newEntry | Add-Member -NotePropertyName MachineType -NotePropertyValue $machineTypeValue
}
$mappingList.Add($newEntry) $mappingList.Add($newEntry)
WriteLog "Adding new mapping for '$($driver.Make) - $($driver.Model)' with path '$($driver.DriverPath)'." WriteLog "Adding new mapping for '$($driver.Make) - $($driver.Model)' with path '$($driver.DriverPath)'."
$addedCount++ $addedCount++
@@ -274,113 +446,330 @@ function Get-LenovoPSREFToken {
if your alternative works is to see if you can retrieve 100e, 300w, 500w, etc. These don't show up in catalogv2.xml, but they do in PSREF. if your alternative works is to see if you can retrieve 100e, 300w, 500w, etc. These don't show up in catalogv2.xml, but they do in PSREF.
#> #>
# Path to Edge $token = $null
$edgeExe = "$Env:ProgramFiles (x86)\Microsoft\Edge\Application\msedge.exe" $socket = $null
$edgeProcess = $null
$tempProfile = $null
$port = $null
# Any free port works. 9222 is common. function Get-FreeLocalTcpPort {
$port = 9222 $listener = $null
$uri = 'https://psref.lenovo.com'
# Headless run with remote debugging.
$flags = "--headless=new --disable-gpu --remote-debugging-port=$port $uri"
$edge = Start-Process -FilePath $edgeExe -ArgumentList $flags -PassThru
Writelog "Edge process started with PID: $($edge.Id)."
# Wait a short moment so the target appears.
Start-Sleep -Seconds 3
# Find the first page target.
$targets = Invoke-RestMethod "http://localhost:$port/json"
$wsUrl = ($targets | Where-Object type -eq 'page')[0].webSocketDebuggerUrl
# Connect to that WebSocket.
$socket = [System.Net.WebSockets.ClientWebSocket]::new()
$socket.ConnectAsync($wsUrl, [Threading.CancellationToken]::None).Wait()
# Helper to send a DevTools command.
function Send-DevToolsCommand {
param([int]$id, [string]$method, [hashtable]$params = @{})
$cmd = @{ id = $id; method = $method; params = $params } |
ConvertTo-Json -Compress
$data = [Text.Encoding]::UTF8.GetBytes($cmd)
$socket.SendAsync([ArraySegment[byte]]$data, 'Text', $true,
[Threading.CancellationToken]::None).Wait()
}
# Ask the page to return localStorage['asut'].
Send-DevToolsCommand -id 1 -method 'Runtime.evaluate' -params @{
expression = "localStorage.getItem('asut')"
}
# Receive frames until the whole message arrives.
$ms = New-Object System.IO.MemoryStream
$buf = New-Object byte[] 8192
do {
$seg = [ArraySegment[byte]]::new($buf)
$res = $socket.ReceiveAsync($seg,
[Threading.CancellationToken]::None).Result
$ms.Write($buf, 0, $res.Count)
} until ($res.EndOfMessage)
$ms.Position = 0
$json = ([System.IO.StreamReader]::new($ms, [Text.Encoding]::UTF8)).ReadToEnd() |
ConvertFrom-Json
$token = $json.result.result.value
# Concatenate the token value with X-PSREF-USER-TOKEN=
$token = "X-PSREF-USER-TOKEN=$token"
WriteLog "Retrieved Lenovo PSREF token: $token"
# Clean up.
$socket.Dispose()
if ($null -ne $socket) {
$socket.Dispose()
}
# Find the PID listening on the debugging port for reliable termination.
$listeningPid = $null
try { try {
# Find the process listening on the specific port. The regex now looks for the local address and port, followed by anything, then LISTENING. $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
# Dots are escaped for literal matching. $listener.Start()
$netstatOutput = netstat -ano -p TCP | Where-Object { $_ -match "127\.0\.0\.1:$port.*LISTENING" } $endpoint = [System.Net.IPEndPoint]$listener.LocalEndpoint
if ($netstatOutput) { return $endpoint.Port
# The last number in the line is the PID }
$listeningPid = ($netstatOutput -split '\s+')[-1] finally {
WriteLog "Found Edge process PID $listeningPid listening on port $port. This is the process we will terminate." if ($null -ne $listener) {
$listener.Stop()
}
}
}
function Get-EdgeDevToolsPageTarget {
param(
[Parameter(Mandatory = $true)][int]$Port,
[int]$MaxAttempts = 20,
[int]$DelayMilliseconds = 500,
[string]$UrlContains
)
for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
try {
$targets = Invoke-RestMethod -Uri "http://localhost:$Port/json" -ErrorAction Stop
if ($null -ne $targets) {
if ($targets -isnot [System.Array]) { $targets = @($targets) }
$pageTargets = $targets | Where-Object { $_.type -eq 'page' }
if (-not [string]::IsNullOrWhiteSpace($UrlContains)) {
$pageTargets = $pageTargets | Where-Object {
-not [string]::IsNullOrWhiteSpace($_.url) -and $_.url -like "*$UrlContains*"
}
}
$target = $pageTargets | Select-Object -First 1
if ($null -ne $target) {
return $target
}
WriteLog "DevTools endpoint on port $Port returned targets but no page matched the criteria (attempt $attempt of $MaxAttempts)."
} }
else { else {
WriteLog "Could not find any process listening on port $port." WriteLog "DevTools endpoint on port $Port returned no targets (attempt $attempt of $MaxAttempts)."
} }
} }
catch { catch {
WriteLog "Could not run netstat to find listening PID. Error: $($_.Exception.Message)" WriteLog "DevTools endpoint on port $Port not ready (attempt $attempt of $MaxAttempts). Error: $($_.Exception.Message)"
}
Start-Sleep -Milliseconds $DelayMilliseconds
}
throw "Edge DevTools endpoint on port $Port did not expose a matching page target after $MaxAttempts attempts."
}
try {
$ffuDevelopmentRoot = Split-Path -Path $PSScriptRoot -Parent
WriteLog "Derived FFUDevelopmentPath from module path: $ffuDevelopmentRoot"
if ([string]::IsNullOrWhiteSpace($ffuDevelopmentRoot)) {
throw "FFUDevelopmentPath could not be resolved. Unable to create Edge profile."
}
if (-not (Test-Path -Path $ffuDevelopmentRoot -PathType Container)) {
throw "Resolved FFUDevelopmentPath '$ffuDevelopmentRoot' does not exist."
}
$tempProfile = Join-Path -Path $ffuDevelopmentRoot -ChildPath ("edge-psref-" + [guid]::NewGuid())
WriteLog "Creating temporary Edge profile at $tempProfile."
New-Item -ItemType Directory -Path $tempProfile -Force | Out-Null
$edgeExe = "$Env:ProgramFiles (x86)\Microsoft\Edge\Application\msedge.exe"
$uri = 'https://psref.lenovo.com'
$port = Get-FreeLocalTcpPort
WriteLog "Using Edge DevTools port $port for Lenovo PSREF token retrieval."
$flags = "--headless=new --disable-gpu --remote-debugging-port=$port $uri --user-data-dir=`"$tempProfile`""
$edgeProcess = Start-Process -FilePath $edgeExe -ArgumentList $flags -PassThru
WriteLog "Edge process started with PID: $($edgeProcess.Id)."
$pageTarget = Get-EdgeDevToolsPageTarget -Port $port -MaxAttempts 40 -DelayMilliseconds 500 -UrlContains 'psref.lenovo.com'
if (-not [string]::IsNullOrWhiteSpace($pageTarget.url)) {
WriteLog "Selected DevTools target URL: $($pageTarget.url)"
}
$wsUrl = $pageTarget.webSocketDebuggerUrl
if ([string]::IsNullOrWhiteSpace($wsUrl)) {
throw "Edge DevTools page target on port $port did not provide a WebSocket URL."
}
$socket = [System.Net.WebSockets.ClientWebSocket]::new()
$socket.ConnectAsync($wsUrl, [Threading.CancellationToken]::None).Wait()
function Send-DevToolsCommand {
param([int]$id, [string]$method, [hashtable]$params = @{})
$cmd = @{ id = $id; method = $method; params = $params } | ConvertTo-Json -Compress
$data = [Text.Encoding]::UTF8.GetBytes($cmd)
$socket.SendAsync([ArraySegment[byte]]$data, 'Text', $true, [Threading.CancellationToken]::None).Wait()
}
$buffer = New-Object byte[] 8192
function Invoke-DevToolsValue {
param(
[Parameter(Mandatory = $true)][int]$CommandId,
[Parameter(Mandatory = $true)][string]$Expression,
[int]$MaxPolls = 25
)
Send-DevToolsCommand -id $CommandId -method 'Runtime.evaluate' -params @{
expression = $Expression
returnByValue = $true
awaitPromise = $true
}
for ($poll = 1; $poll -le $MaxPolls; $poll++) {
$localStream = $null
try {
$localStream = New-Object System.IO.MemoryStream
do {
$segment = [ArraySegment[byte]]::new($buffer)
$result = $socket.ReceiveAsync($segment, [Threading.CancellationToken]::None).Result
$localStream.Write($buffer, 0, $result.Count)
} until ($result.EndOfMessage)
$jsonBytes = $localStream.ToArray()
$jsonText = [Text.Encoding]::UTF8.GetString($jsonBytes)
$previewPayload = $jsonText
if (-not [string]::IsNullOrEmpty($previewPayload) -and $previewPayload.Length -gt 500) {
$previewPayload = $previewPayload.Substring(0, 500) + '...'
}
WriteLog "DevTools eval payload (cmd $CommandId, poll $poll): $previewPayload"
$message = $null
try {
$message = $jsonText | ConvertFrom-Json
}
catch {
WriteLog "Failed to parse DevTools eval payload for command id $CommandId (poll $poll): $($_.Exception.Message)"
continue
}
if ($message.PSObject.Properties['id'] -and $message.id -eq $CommandId) {
if ($message.PSObject.Properties['error']) {
$errorMessage = $message.error.message
throw "Edge DevTools reported an error for expression '$Expression': $errorMessage"
}
if ($message.PSObject.Properties['result'] -and $message.result.PSObject.Properties['result']) {
$innerResult = $message.result.result
return [PSCustomObject]@{
Value = $innerResult.value
Type = $innerResult.type
Subtype = $innerResult.subtype
}
}
$serializedMessage = $message | ConvertTo-Json -Compress -Depth 5
WriteLog "DevTools response for command id $CommandId lacked result data. Message: $serializedMessage"
return $null
}
if ($message.PSObject.Properties['method']) {
WriteLog "Received DevTools event '$($message.method)' while waiting for command id $CommandId."
}
else {
WriteLog "Received DevTools message without id or method while waiting for command id $CommandId."
}
}
finally {
if ($null -ne $localStream) {
$localStream.Dispose()
}
}
}
throw "No DevTools response received for command id $CommandId after $MaxPolls polls."
}
WriteLog "Waiting for PSREF page to initialize local storage context."
Start-Sleep -Seconds 2
$commandCounter = 1000
$rawToken = $null
$maxTokenAttempts = 12
for ($attempt = 1; $attempt -le $maxTokenAttempts -and [string]::IsNullOrWhiteSpace($rawToken); $attempt++) {
$commandCounter++
$tokenResponse = Invoke-DevToolsValue -CommandId $commandCounter -Expression "window.localStorage?.getItem('asut')" -MaxPolls 25
if ($null -ne $tokenResponse -and -not [string]::IsNullOrWhiteSpace($tokenResponse.Value)) {
$rawToken = $tokenResponse.Value
WriteLog "DevTools response for command id $commandCounter returned token length $($rawToken.Length)."
break
}
WriteLog "Lenovo PSREF token not yet available (attempt $attempt of $maxTokenAttempts)."
$commandCounter++
$keysResponse = Invoke-DevToolsValue -CommandId $commandCounter -Expression "JSON.stringify(Object.keys(window.localStorage || {}))" -MaxPolls 10
if ($null -ne $keysResponse -and -not [string]::IsNullOrWhiteSpace($keysResponse.Value)) {
WriteLog "Current localStorage keys: $($keysResponse.Value)"
}
$commandCounter++
$cookieResponse = Invoke-DevToolsValue -CommandId $commandCounter -Expression "document.cookie" -MaxPolls 10
if ($null -ne $cookieResponse -and -not [string]::IsNullOrWhiteSpace($cookieResponse.Value)) {
WriteLog "document.cookie contents: $($cookieResponse.Value)"
$cookieEntry = ($cookieResponse.Value -split ';') | ForEach-Object { $_.Trim() } | Where-Object { $_ -like 'asut=*' } | Select-Object -First 1
if ($cookieEntry) {
$rawToken = $cookieEntry.Substring($cookieEntry.IndexOf('=') + 1)
WriteLog "Extracted Lenovo PSREF token from cookies with length $($rawToken.Length)."
break
}
}
Start-Sleep -Milliseconds 750
}
if ([string]::IsNullOrWhiteSpace($rawToken)) {
throw "Received empty Lenovo PSREF token from Edge DevTools after $maxTokenAttempts attempts."
}
$token = "X-PSREF-USER-TOKEN=$rawToken"
WriteLog "Retrieved Lenovo PSREF token: $token"
}
catch {
WriteLog "Failed to retrieve Lenovo PSREF token. Error: $($_.Exception.Message)"
throw
}
finally {
if ($null -ne $socket) {
try {
$socket.Dispose()
WriteLog "Edge DevTools WebSocket disposed."
}
catch {
WriteLog "Error disposing Edge DevTools WebSocket: $($_.Exception.Message)"
}
}
$listeningPid = $null
if ($null -ne $port) {
try {
$netstatOutput = netstat -ano -p TCP | Where-Object { $_ -match "127\.0\.0\.1:$port.*LISTENING" }
if ($netstatOutput) {
$listeningPid = ($netstatOutput -split '\s+')[-1]
WriteLog "Found Edge process PID $listeningPid listening on port $port."
}
else {
WriteLog "No process reported as listening on port $port."
}
}
catch {
WriteLog "Could not run netstat to find listening PID for port $port. Error: $($_.Exception.Message)"
}
} }
# Determine the correct PID to kill. Prioritize the one found via netstat.
$pidToKill = $null $pidToKill = $null
if ($listeningPid) { if ($null -ne $listeningPid) {
$pidToKill = $listeningPid $pidToKill = $listeningPid
} }
elseif ($null -ne $edgeProcess -and -not $edgeProcess.HasExited) { elseif ($null -ne $edgeProcess -and -not $edgeProcess.HasExited) {
$pidToKill = $edgeProcess.Id $pidToKill = $edgeProcess.Id
WriteLog "Could not find listening process via netstat. Falling back to initial Edge process PID $($pidToKill) for termination." WriteLog "Falling back to initial Edge process PID $pidToKill for termination."
} }
if ($pidToKill) { if ($null -ne $pidToKill) {
WriteLog "Attempting to terminate Edge process tree with PID: $pidToKill"
try { try {
taskkill /PID $pidToKill /T /F | Out-Null taskkill /PID $pidToKill /T /F | Out-Null
WriteLog "Successfully issued termination command for Edge process tree with PID: $pidToKill." WriteLog "Issued termination command for Edge process tree with PID: $pidToKill."
} }
catch { catch {
WriteLog "Failed to terminate Edge process tree with PID: $pidToKill. It may have already closed. Error: $($_.Exception.Message)" WriteLog "Failed to terminate Edge process tree with PID: $pidToKill. Error: $($_.Exception.Message)"
} }
} }
else { else {
WriteLog "No active Edge process found to terminate." WriteLog "No active Edge process found to terminate."
} }
if ($null -ne $edgeProcess) {
try {
$edgeProcess.WaitForExit(3000) | Out-Null
}
catch {
WriteLog "Error while waiting for Edge process PID $($edgeProcess.Id) to exit: $($_.Exception.Message)"
}
}
Start-Sleep -Milliseconds 250
if (-not [string]::IsNullOrWhiteSpace($tempProfile) -and (Test-Path -Path $tempProfile -PathType Container)) {
$maxRemoveAttempts = 5
$originalProgressPreference = $ProgressPreference
try {
$ProgressPreference = 'SilentlyContinue'
for ($removeAttempt = 1; $removeAttempt -le $maxRemoveAttempts; $removeAttempt++) {
try {
Remove-Item -Path $tempProfile -Recurse -Force -ErrorAction Stop
WriteLog "Removed temporary Edge profile at $tempProfile."
break
}
catch {
if ($removeAttempt -eq $maxRemoveAttempts) {
WriteLog "Failed to remove temporary Edge profile at $tempProfile after $maxRemoveAttempts attempts. Error: $($_.Exception.Message)"
}
else {
WriteLog "Temporary Edge profile still locked (attempt $removeAttempt of $maxRemoveAttempts). Retrying..."
Start-Sleep -Milliseconds 500
}
}
}
}
finally {
$ProgressPreference = $originalProgressPreference
}
}
}
return $token return $token
} }
@@ -156,7 +156,7 @@ function Invoke-ParallelProcessing {
# Execute the appropriate background task based on $localTaskType # Execute the appropriate background task based on $localTaskType
switch ($localTaskType) { switch ($localTaskType) {
'WingetDownload' { 'WingetDownload' {
# Pass the progress queue to the task function # Pass the progress queue and SkipWin32Json to the task function
$wingetTaskArgs = @{ $wingetTaskArgs = @{
ApplicationItemData = $currentItem ApplicationItemData = $currentItem
AppListJsonPath = $localJobArgs['AppListJsonPath'] AppListJsonPath = $localJobArgs['AppListJsonPath']
@@ -164,6 +164,7 @@ function Invoke-ParallelProcessing {
OrchestrationPath = $localJobArgs['OrchestrationPath'] OrchestrationPath = $localJobArgs['OrchestrationPath']
ProgressQueue = $localProgressQueue ProgressQueue = $localProgressQueue
WindowsArch = $localJobArgs['WindowsArch'] WindowsArch = $localJobArgs['WindowsArch']
SkipWin32Json = [bool]$localJobArgs['SkipWin32Json']
} }
$taskResult = Start-WingetAppDownloadTask @wingetTaskArgs $taskResult = Start-WingetAppDownloadTask @wingetTaskArgs
if ($null -ne $taskResult) { if ($null -ne $taskResult) {
@@ -209,7 +210,8 @@ function Invoke-ParallelProcessing {
-Headers $localJobArgs['Headers'] ` -Headers $localJobArgs['Headers'] `
-UserAgent $localJobArgs['UserAgent'] ` -UserAgent $localJobArgs['UserAgent'] `
-ProgressQueue $localProgressQueue ` -ProgressQueue $localProgressQueue `
-CompressToWim $localJobArgs['CompressToWim'] -CompressToWim $localJobArgs['CompressToWim'] `
-PreserveSourceOnCompress $localJobArgs['PreserveSourceOnCompress']
} }
'Dell' { 'Dell' {
$taskResult = Save-DellDriversTask -DriverItemData $currentItem ` $taskResult = Save-DellDriversTask -DriverItemData $currentItem `
@@ -217,7 +219,8 @@ function Invoke-ParallelProcessing {
-WindowsArch $localJobArgs['WindowsArch'] ` -WindowsArch $localJobArgs['WindowsArch'] `
-WindowsRelease $localJobArgs['WindowsRelease'] ` -WindowsRelease $localJobArgs['WindowsRelease'] `
-ProgressQueue $localProgressQueue ` -ProgressQueue $localProgressQueue `
-CompressToWim $localJobArgs['CompressToWim'] -CompressToWim $localJobArgs['CompressToWim'] `
-PreserveSourceOnCompress $localJobArgs['PreserveSourceOnCompress']
} }
'HP' { 'HP' {
$taskResult = Save-HPDriversTask -DriverItemData $currentItem ` $taskResult = Save-HPDriversTask -DriverItemData $currentItem `
@@ -226,7 +229,8 @@ function Invoke-ParallelProcessing {
-WindowsRelease $localJobArgs['WindowsRelease'] ` -WindowsRelease $localJobArgs['WindowsRelease'] `
-WindowsVersion $localJobArgs['WindowsVersion'] ` -WindowsVersion $localJobArgs['WindowsVersion'] `
-ProgressQueue $localProgressQueue ` -ProgressQueue $localProgressQueue `
-CompressToWim $localJobArgs['CompressToWim'] -CompressToWim $localJobArgs['CompressToWim'] `
-PreserveSourceOnCompress $localJobArgs['PreserveSourceOnCompress']
} }
'Lenovo' { 'Lenovo' {
$taskResult = Save-LenovoDriversTask -DriverItemData $currentItem ` $taskResult = Save-LenovoDriversTask -DriverItemData $currentItem `
@@ -235,7 +239,8 @@ function Invoke-ParallelProcessing {
-Headers $localJobArgs['Headers'] ` -Headers $localJobArgs['Headers'] `
-UserAgent $localJobArgs['UserAgent'] ` -UserAgent $localJobArgs['UserAgent'] `
-ProgressQueue $localProgressQueue ` -ProgressQueue $localProgressQueue `
-CompressToWim $localJobArgs['CompressToWim'] -CompressToWim $localJobArgs['CompressToWim'] `
-PreserveSourceOnCompress $localJobArgs['PreserveSourceOnCompress']
} }
default { default {
$unsupportedMakeMessage = "Error: Unsupported Make '$make' for driver download." $unsupportedMakeMessage = "Error: Unsupported Make '$make' for driver download."
@@ -265,7 +270,7 @@ function Invoke-ParallelProcessing {
else { else {
# Fallback for any task that *still* doesn't return 'Success'. This is now the exceptional case. # Fallback for any task that *still* doesn't return 'Success'. This is now the exceptional case.
WriteLog "Warning: Task for '$taskSpecificIdentifier' did not return a 'Success' property. Inferring from status: '$($taskResult.Status)'" WriteLog "Warning: Task for '$taskSpecificIdentifier' did not return a 'Success' property. Inferring from status: '$($taskResult.Status)'"
if ($taskResult.Status -like 'Completed*' -or $taskResult.Status -like 'Already downloaded*') { if ($taskResult.Status -like 'Completed*' -or $taskResult.Status -like 'Already downloaded*' -or $taskResult.Status -like 'Compression successful*') {
$resultCode = 0 # Treat as success $resultCode = 0 # Treat as success
} }
else { else {
+401 -37
View File
@@ -108,11 +108,13 @@ function Get-Application {
# Determine app type and folder path # Determine app type and folder path
$appIsWin32 = ($Source -eq 'msstore' -and $AppId.StartsWith("XP")) $appIsWin32 = ($Source -eq 'msstore' -and $AppId.StartsWith("XP"))
$sanitizedAppName = ConvertTo-SafeName -Name $AppName
if ($sanitizedAppName -ne $AppName) { WriteLog "Sanitized app name: '$AppName' -> '$sanitizedAppName'" }
if ($Source -eq 'winget' -or $appIsWin32) { if ($Source -eq 'winget' -or $appIsWin32) {
$appBaseFolderPath = Join-Path -Path "$AppsPath\Win32" -ChildPath $AppName $appBaseFolderPath = Join-Path -Path "$AppsPath\Win32" -ChildPath $sanitizedAppName
} }
else { else {
$appBaseFolderPath = Join-Path -Path "$AppsPath\MSStore" -ChildPath $AppName $appBaseFolderPath = Join-Path -Path "$AppsPath\MSStore" -ChildPath $sanitizedAppName
} }
# If downloading multiple archs for a Win32 app, create a subfolder # If downloading multiple archs for a Win32 app, create a subfolder
@@ -337,6 +339,344 @@ function Get-Application {
return $overallResult return $overallResult
} }
# Function to handle downloading a winget application in parallel
# This function is called by Invoke-ParallelProcessing for each app
function Start-WingetAppDownloadTask {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[PSCustomObject]$ApplicationItemData,
[Parameter(Mandatory = $true)]
[string]$AppListJsonPath,
[Parameter(Mandatory = $true)]
[string]$AppsPath,
[Parameter(Mandatory = $true)]
[string]$OrchestrationPath,
[Parameter(Mandatory = $true)]
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue,
[string]$WindowsArch,
[switch]$SkipWin32Json
)
$appName = $ApplicationItemData.Name
$appId = $ApplicationItemData.Id
$source = $ApplicationItemData.Source
$status = "Checking..."
$resultCode = -1
$sanitizedAppName = ConvertTo-SafeName -Name $appName
# Initial status update
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
WriteLog "Starting download task for $($appName) with ID $($appId) from source $($source)."
try {
# Define paths
$userAppListPath = Join-Path -Path $AppsPath -ChildPath "UserAppList.json"
$appFound = $false
# 1. Check UserAppList.json and content
if (Test-Path -Path $userAppListPath) {
try {
$userAppListContent = Get-Content -Path $userAppListPath -Raw | ConvertFrom-Json
$userAppEntry = $userAppListContent | Where-Object { $_.Name -eq $appName }
if ($userAppEntry) {
$appFolder = Join-Path -Path "$AppsPath\Win32" -ChildPath $sanitizedAppName
if (Test-Path -Path $appFolder -PathType Container) {
$folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($folderSize -gt 1MB) {
$appFound = $true
$status = "Not Downloaded: App in $userAppListPath and found in $appFolder"
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
WriteLog "Found '$appName' in $userAppListPath and content exists in '$appFolder'."
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 }
}
else {
$appFound = $true
$status = "App in '$userAppListPath' but content missing/small in '$appFolder'. Copy content or remove from UserAppList.json."
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
WriteLog $status
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 1 }
}
}
else {
$appFound = $true
$status = "App in '$userAppListPath' but content folder '$appFolder' not found. Copy content or remove from UserAppList.json."
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
WriteLog $status
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 1 }
}
}
}
catch {
WriteLog "Warning: Could not read or parse '$userAppListPath'. Error: $($_.Exception.Message)"
}
}
# 2. Check existing downloaded Win32 content (folder-based)
if (-not $appFound -and $source -eq 'winget') {
$appFolder = Join-Path -Path "$AppsPath\Win32" -ChildPath $sanitizedAppName
if (Test-Path -Path $appFolder -PathType Container) {
$contentFound = $false
if ($ApplicationItemData.Architecture -eq 'x86 x64') {
$x86Folder = Join-Path -Path $appFolder -ChildPath "x86"
$x64Folder = Join-Path -Path $appFolder -ChildPath "x64"
if ((Test-Path -Path $x86Folder -PathType Container) -and (Test-Path -Path $x64Folder -PathType Container)) {
$x86Size = (Get-ChildItem -Path $x86Folder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
$x64Size = (Get-ChildItem -Path $x64Folder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($x86Size -gt 1MB -and $x64Size -gt 1MB) {
$contentFound = $true
}
}
}
else {
$folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($folderSize -gt 1MB) {
$contentFound = $true
}
}
if ($contentFound) {
$appFound = $true
$status = "Not Downloaded: Existing content found in $appFolder"
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
WriteLog "Found existing content for '$appName' in '$appFolder'. Skipping download to prevent duplicate entry."
# Regenerate WinGetWin32Apps.json for CLI builds when content already exists
# UI mode pre-downloads should not generate this file (SkipWin32Json)
if (-not $SkipWin32Json) {
$archFolders = Get-ChildItem -Path $appFolder -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -in @('x86', 'x64', 'arm64') }
if ($archFolders) {
foreach ($archFolder in $archFolders) {
WriteLog "Adding silent install command for pre-downloaded $sanitizedAppName ($($archFolder.Name)) to $OrchestrationPath\WinGetWin32Apps.json"
Add-Win32SilentInstallCommand -AppFolder $sanitizedAppName -AppFolderPath $archFolder.FullName -OrchestrationPath $OrchestrationPath -SubFolder $archFolder.Name | Out-Null
}
}
else {
WriteLog "Adding silent install command for pre-downloaded $sanitizedAppName to $OrchestrationPath\WinGetWin32Apps.json"
Add-Win32SilentInstallCommand -AppFolder $sanitizedAppName -AppFolderPath $appFolder -OrchestrationPath $OrchestrationPath | Out-Null
}
}
else {
WriteLog "Skipping WinGetWin32Apps.json regeneration for pre-downloaded $sanitizedAppName (UI mode)."
}
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 }
}
}
}
# Check MSStore folder
if (-not $appFound -and (Test-Path -Path "$AppsPath\MSStore" -PathType Container)) {
$appFolder = Join-Path -Path "$AppsPath\MSStore" -ChildPath $sanitizedAppName
if (Test-Path -Path $appFolder -PathType Container) {
$folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($folderSize -gt 1MB) {
$appFound = $true
$status = "Already downloaded (MSStore)"
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
WriteLog "Found '$appName' content in '$appFolder'."
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 }
}
}
}
# 3. If not found locally, add to AppList.json and download
if (-not $appFound) {
# Add to AppList.json with mutex lock for thread safety
$appListContent = $null
$appListDir = Split-Path -Path $AppListJsonPath -Parent
if (-not (Test-Path -Path $appListDir -PathType Container)) {
New-Item -Path $appListDir -ItemType Directory -Force | Out-Null
}
if (Test-Path -Path $AppListJsonPath) {
try {
$appListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json
if (-not $appListContent.PSObject.Properties['apps']) {
$appListContent = @{ apps = @() }
}
}
catch {
WriteLog "Warning: Could not read or parse '$AppListJsonPath'. Creating new structure. Error: $($_.Exception.Message)"
$appListContent = @{ apps = @() }
}
}
else {
$appListContent = @{ apps = @() }
}
$appExistsInAppList = $false
if ($appListContent.apps) {
foreach ($app in $appListContent.apps) {
if ($app.id -eq $appId) {
$appExistsInAppList = $true
break
}
}
}
if (-not $appExistsInAppList) {
$newApp = @{ name = $sanitizedAppName; id = $appId; source = $source }
if (-not ($appListContent.apps -is [array])) { $appListContent.apps = @() }
$appListContent.apps += $newApp
try {
# Use a mutex lock to prevent race conditions when writing to the same file
$lockName = "AppListJsonLock"
$lock = New-Object System.Threading.Mutex($false, $lockName)
try {
$lock.WaitOne() | Out-Null
# Re-read content inside lock to ensure latest version
if (Test-Path -Path $AppListJsonPath) {
$currentAppListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json
if (-not ($currentAppListContent.apps | Where-Object { $_.id -eq $appId })) {
$currentAppListContent.apps += $newApp
$currentAppListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8
WriteLog "Added '$appName' to '$AppListJsonPath'."
}
else {
WriteLog "'$appName' already exists in '$AppListJsonPath' (checked inside lock)."
}
}
else {
# File doesn't exist, write the initial content
$appListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8
WriteLog "Created '$AppListJsonPath' and added '$appName'."
}
}
finally {
$lock.ReleaseMutex()
$lock.Dispose()
}
}
catch {
WriteLog "Error saving '$AppListJsonPath'. Error: $($_.Exception.Message)"
$status = "Failed to save AppList.json: $($_.Exception.Message)"
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 1 }
}
}
else {
WriteLog "'$appName' already exists in '$AppListJsonPath'."
}
# Proceed with download
$status = "Downloading..."
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
# Ensure necessary folders exist
WriteLog "Orchestration Path: $($OrchestrationPath)"
if (-not (Test-Path -Path $OrchestrationPath -PathType Container)) {
New-Item -Path $OrchestrationPath -ItemType Directory -Force | Out-Null
}
$win32Folder = Join-Path -Path $AppsPath -ChildPath "Win32"
if ($source -eq "winget" -and -not (Test-Path -Path $win32Folder -PathType Container)) {
New-Item -Path $win32Folder -ItemType Directory -Force | Out-Null
}
$storeAppsFolder = Join-Path -Path $AppsPath -ChildPath "MSStore"
if ($source -eq "msstore" -and -not (Test-Path -Path $storeAppsFolder -PathType Container)) {
New-Item -Path $storeAppsFolder -ItemType Directory -Force | Out-Null
}
try {
# Call Get-Application to perform the actual download
# Pass SkipWin32Json based on caller context (UI mode skips, CLI mode creates)
$getAppParams = @{
AppName = $appName
AppId = $appId
Source = $source
AppsPath = $AppsPath
ApplicationArch = $ApplicationItemData.Architecture
WindowsArch = $WindowsArch
OrchestrationPath = $OrchestrationPath
ErrorAction = 'Stop'
}
if ($SkipWin32Json) {
$getAppParams['SkipWin32Json'] = $true
}
$resultCode = Get-Application @getAppParams
# Determine status based on result code
switch ($resultCode) {
0 { $status = "Downloaded successfully" }
1 { $status = "Error: No app installers were found" }
2 { $status = "Silent install switch could not be found. Did not download." }
3 { $status = "Error: Publisher does not support download" }
4 { $status = "Skipped: Use 'msstore' source instead." }
default { $status = "Downloaded with status: $resultCode" }
}
# Remove app from AppList.json if silent install switch could not be found (resultCode 2)
if ($resultCode -eq 2) {
try {
if (Test-Path -Path $AppListJsonPath) {
$appListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json
if ($appListContent.apps) {
$filteredApps = @($appListContent.apps | Where-Object { $_.id -ne $appId })
$appListContent.apps = $filteredApps
$appListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8
WriteLog "Removed '$appName' ($appId) from '$AppListJsonPath' due to missing silent install switch."
}
}
}
catch {
WriteLog "Failed to remove '$appName' from '$AppListJsonPath': $($_.Exception.Message)"
}
}
}
catch {
$status = $_.Exception.Message
WriteLog "Download error for $($appName): $($_.Exception.Message)"
$resultCode = 1
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
# Remove app from AppList.json if publisher does not support download
if ($_.Exception.Message -match "does not support downloads by the publisher") {
try {
if (Test-Path -Path $AppListJsonPath) {
$appListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json
if ($appListContent.apps) {
$filteredApps = @($appListContent.apps | Where-Object { $_.id -ne $appId })
$appListContent.apps = $filteredApps
$appListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8
WriteLog "Removed '$appName' ($appId) from '$AppListJsonPath' due to publisher download restriction."
}
}
}
catch {
WriteLog "Failed to remove '$appName' from '$AppListJsonPath': $($_.Exception.Message)"
}
}
}
}
}
catch {
$status = $_.Exception.Message
WriteLog "Unexpected error in Start-WingetAppDownloadTask for $($appName): $($_.Exception.Message)"
$resultCode = 1
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
}
finally {
# Ensure status is not empty before returning
if ([string]::IsNullOrEmpty($status)) {
$status = "Unknown failure"
WriteLog "Status was empty for $appName ($appId), setting to default error."
if ($resultCode -ne 0 -and $resultCode -ne 1 -and $resultCode -ne 2) {
$resultCode = -1
}
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
}
elseif ($resultCode -ne 0) {
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
}
else {
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
}
}
# Return the final status and result code
return @{ Id = $appId; Status = $status; ResultCode = $resultCode }
}
function Get-Apps { function Get-Apps {
[CmdletBinding()] [CmdletBinding()]
param ( param (
@@ -347,28 +687,31 @@ function Get-Apps {
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$WindowsArch, [string]$WindowsArch,
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$OrchestrationPath [string]$OrchestrationPath,
[Parameter(Mandatory = $false)]
[string]$LogFilePath,
[Parameter(Mandatory = $false)]
[int]$ThrottleLimit = 5
) )
# Load and validate app list # Load and validate app list
$apps = Get-Content -Path $AppList -Raw | ConvertFrom-Json $apps = Get-Content -Path $AppList -Raw | ConvertFrom-Json
if (-not $apps) { if (-not $apps -or -not $apps.apps -or $apps.apps.Count -eq 0) {
WriteLog "No apps were specified in AppList.json file." WriteLog "No apps were specified in AppList.json file."
return return
} }
# Process WinGet apps # Log app list summary
$wingetApps = $apps.apps | Where-Object { $_.source -eq "winget" } $wingetApps = $apps.apps | Where-Object { $_.source -eq "winget" }
if ($wingetApps) { if ($wingetApps) {
WriteLog 'Winget apps to be installed:' WriteLog 'Winget apps to be installed:'
$wingetApps | ForEach-Object { WriteLog $_.Name } $wingetApps | ForEach-Object { WriteLog $_.Name }
} }
# Process Store apps $storeApps = $apps.apps | Where-Object { $_.source -eq "msstore" }
$StoreApps = $apps.apps | Where-Object { $_.source -eq "msstore" } if ($storeApps) {
if ($StoreApps) {
WriteLog 'Store apps to be installed:' WriteLog 'Store apps to be installed:'
$StoreApps | ForEach-Object { WriteLog $_.Name } $storeApps | ForEach-Object { WriteLog $_.Name }
} }
# Ensure WinGet is available # Ensure WinGet is available
@@ -378,44 +721,51 @@ function Get-Apps {
$win32Folder = Join-Path -Path $AppsPath -ChildPath "Win32" $win32Folder = Join-Path -Path $AppsPath -ChildPath "Win32"
$storeAppsFolder = Join-Path -Path $AppsPath -ChildPath "MSStore" $storeAppsFolder = Join-Path -Path $AppsPath -ChildPath "MSStore"
# Process WinGet apps
if ($wingetApps) {
if (-not (Test-Path -Path $win32Folder -PathType Container)) { if (-not (Test-Path -Path $win32Folder -PathType Container)) {
WriteLog "Creating folder for Winget Win32 apps: $win32Folder" WriteLog "Creating folder for Winget Win32 apps: $win32Folder"
New-Item -Path $win32Folder -ItemType Directory -Force | Out-Null New-Item -Path $win32Folder -ItemType Directory -Force | Out-Null
WriteLog "Folder created successfully."
} }
foreach ($wingetApp in $wingetApps) {
try {
$appArch = if ($wingetApp.PSObject.Properties['architecture']) { $wingetApp.architecture } else { $WindowsArch }
Get-Application -AppName $wingetApp.Name -AppId $wingetApp.Id -Source 'winget' -AppsPath $AppsPath -ApplicationArch $appArch -OrchestrationPath $OrchestrationPath
}
catch {
WriteLog "Error occurred while processing $($wingetApp.Name): $_"
throw $_
}
}
}
# Process Store apps
if ($StoreApps) {
if (-not (Test-Path -Path $storeAppsFolder -PathType Container)) { if (-not (Test-Path -Path $storeAppsFolder -PathType Container)) {
WriteLog "Creating folder for MSStore apps: $storeAppsFolder"
New-Item -Path $storeAppsFolder -ItemType Directory -Force | Out-Null New-Item -Path $storeAppsFolder -ItemType Directory -Force | Out-Null
} }
foreach ($storeApp in $StoreApps) { # Transform apps into the format expected by Invoke-ParallelProcessing
try { $itemsToProcess = $apps.apps | ForEach-Object {
$appArch = if ($storeApp.PSObject.Properties['architecture']) { $storeApp.architecture } else { $WindowsArch } $appArch = if ($_.PSObject.Properties['architecture']) { $_.architecture } else { $WindowsArch }
Get-Application -AppName $storeApp.Name -AppId $storeApp.Id -Source 'msstore' -AppsPath $AppsPath -ApplicationArch $appArch -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath [PSCustomObject]@{
} Name = $_.name
catch { Id = $_.id
WriteLog "Error occurred while processing $($storeApp.Name): $_" Source = $_.source
throw $_ Architecture = $appArch
} }
} }
WriteLog "Starting parallel download of $($itemsToProcess.Count) applications with ThrottleLimit: $ThrottleLimit"
# Build task arguments for Invoke-ParallelProcessing
# CLI builds should create WinGetWin32Apps.json, so SkipWin32Json is false
$taskArguments = @{
AppsPath = $AppsPath
AppListJsonPath = $AppList
OrchestrationPath = $OrchestrationPath
WindowsArch = $WindowsArch
SkipWin32Json = $false
} }
# Invoke parallel processing in non-UI mode (no WindowObject or ListViewControl)
Invoke-ParallelProcessing -ItemsToProcess $itemsToProcess `
-IdentifierProperty 'Id' `
-StatusProperty 'DownloadStatus' `
-TaskType 'WingetDownload' `
-TaskArguments $taskArguments `
-CompletedStatusText "Completed" `
-ErrorStatusPrefix "Error: " `
-MainThreadLogPath $LogFilePath `
-ThrottleLimit $ThrottleLimit
WriteLog "Parallel download of applications completed."
# Post-processing: Override CommandLine / Arguments from AppList.json if provided # Post-processing: Override CommandLine / Arguments from AppList.json if provided
# Users may supply custom silent install commands or arguments. These optional # Users may supply custom silent install commands or arguments. These optional
# properties (CommandLine, Arguments) in AppList.json replace the auto-generated # properties (CommandLine, Arguments) in AppList.json replace the auto-generated
@@ -426,10 +776,14 @@ function Get-Apps {
if ($app.source -in @('winget', 'msstore')) { if ($app.source -in @('winget', 'msstore')) {
$hasCmd = ($app.PSObject.Properties['CommandLine'] -and -not [string]::IsNullOrWhiteSpace($app.CommandLine)) $hasCmd = ($app.PSObject.Properties['CommandLine'] -and -not [string]::IsNullOrWhiteSpace($app.CommandLine))
$hasArgs = ($app.PSObject.Properties['Arguments'] -and -not [string]::IsNullOrWhiteSpace($app.Arguments)) $hasArgs = ($app.PSObject.Properties['Arguments'] -and -not [string]::IsNullOrWhiteSpace($app.Arguments))
if ($hasCmd -or $hasArgs) { $hasAdd = ($app.PSObject.Properties['AdditionalExitCodes'] -and -not [string]::IsNullOrWhiteSpace($app.AdditionalExitCodes))
$hasIgnore = ($app.PSObject.Properties['IgnoreNonZeroExitCodes'])
if ($hasCmd -or $hasArgs -or $hasAdd -or $hasIgnore) {
$overrideMap[$app.name] = @{ $overrideMap[$app.name] = @{
CommandLine = if ($hasCmd) { $app.CommandLine } else { $null } CommandLine = if ($hasCmd) { $app.CommandLine } else { $null }
Arguments = if ($hasArgs) { $app.Arguments } else { $null } Arguments = if ($hasArgs) { $app.Arguments } else { $null }
AdditionalExitCodes = if ($hasAdd) { $app.AdditionalExitCodes } else { $null }
IgnoreNonZeroExitCodes = if ($hasIgnore) { [bool]$app.IgnoreNonZeroExitCodes } else { $null }
} }
} }
} }
@@ -453,6 +807,16 @@ function Get-Apps {
$entry.Arguments = $ov.Arguments $entry.Arguments = $ov.Arguments
$changed = $true $changed = $true
} }
if ($ov.ContainsKey('AdditionalExitCodes') -and $null -ne $ov.AdditionalExitCodes) {
WriteLog "Override (AppList.json) AdditionalExitCodes for $($entry.Name)"
$entry | Add-Member -NotePropertyName AdditionalExitCodes -NotePropertyValue $ov.AdditionalExitCodes -Force
$changed = $true
}
if ($ov.ContainsKey('IgnoreNonZeroExitCodes') -and $null -ne $ov.IgnoreNonZeroExitCodes) {
WriteLog "Override (AppList.json) IgnoreNonZeroExitCodes for $($entry.Name)"
$entry | Add-Member -NotePropertyName IgnoreNonZeroExitCodes -NotePropertyValue ([bool]$ov.IgnoreNonZeroExitCodes) -Force
$changed = $true
}
} }
} }
if ($changed) { if ($changed) {
@@ -733,4 +1097,4 @@ function Add-Win32SilentInstallCommand {
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# Export functions needed by both BuildFFUVM and the UI Core module # Export functions needed by both BuildFFUVM and the UI Core module
Export-ModuleMember -Function Get-Application, Get-Apps, Confirm-WinGetInstallation, Add-Win32SilentInstallCommand, Install-Winget Export-ModuleMember -Function Get-Application, Get-Apps, Start-WingetAppDownloadTask, Confirm-WinGetInstallation, Add-Win32SilentInstallCommand, Install-Winget
+3 -1
View File
@@ -67,8 +67,10 @@ Description = 'Common functions shared between FFU Builder UI and the BuildFFUVM
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
NestedModules = @('FFU.Common.Drivers.psm1', NestedModules = @('FFU.Common.Drivers.psm1',
'FFU.Common.Drivers.Dell.psm1',
'FFU.Common.Winget.psm1', 'FFU.Common.Winget.psm1',
'FFU.Common.Parallel.psm1') 'FFU.Common.Parallel.psm1',
'FFU.Common.Cleanup.psm1')
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = '*' FunctionsToExport = '*'
@@ -396,8 +396,8 @@ function Invoke-CopyBYOApps {
try { try {
# Ensure items are sorted by current priority before saving # Ensure items are sorted by current priority before saving
# Exclude CopyStatus when saving and ensure Priority is an integer # Exclude CopyStatus when saving and ensure Priority is an integer; include AdditionalExitCodes and IgnoreNonZeroExitCodes for parity with Save-BYOApplicationList
$applications = $listView.Items | Sort-Object Priority | Select-Object @{N = 'Priority'; E = { [int]$_.Priority } }, Name, CommandLine, Arguments, Source $applications = $listView.Items | Sort-Object Priority | Select-Object @{N = 'Priority'; E = { [int]$_.Priority } }, Name, CommandLine, Arguments, Source, AdditionalExitCodes, IgnoreNonZeroExitCodes
$applications | ConvertTo-Json -Depth 5 | Set-Content -Path $userAppListPath -Force -Encoding UTF8 $applications | ConvertTo-Json -Depth 5 | Set-Content -Path $userAppListPath -Force -Encoding UTF8
WriteLog "Successfully updated UserAppList.json with all applications from the UI." WriteLog "Successfully updated UserAppList.json with all applications from the UI."
} }
+571 -12
View File
@@ -34,8 +34,10 @@ function Get-UIConfig {
CopyDrivers = $State.Controls.chkCopyDrivers.IsChecked CopyDrivers = $State.Controls.chkCopyDrivers.IsChecked
CopyOfficeConfigXML = $State.Controls.chkCopyOfficeConfigXML.IsChecked CopyOfficeConfigXML = $State.Controls.chkCopyOfficeConfigXML.IsChecked
CopyPEDrivers = $State.Controls.chkCopyPEDrivers.IsChecked CopyPEDrivers = $State.Controls.chkCopyPEDrivers.IsChecked
UseDriversAsPEDrivers = $State.Controls.chkUseDriversAsPEDrivers.IsChecked
CopyPPKG = $State.Controls.chkCopyPPKG.IsChecked CopyPPKG = $State.Controls.chkCopyPPKG.IsChecked
CopyUnattend = $State.Controls.chkCopyUnattend.IsChecked CopyUnattend = $State.Controls.chkCopyUnattend.IsChecked
CopyAdditionalFFUFiles = $State.Controls.chkCopyAdditionalFFUFiles.IsChecked
CreateCaptureMedia = $State.Controls.chkCreateCaptureMedia.IsChecked CreateCaptureMedia = $State.Controls.chkCreateCaptureMedia.IsChecked
CreateDeploymentMedia = $State.Controls.chkCreateDeploymentMedia.IsChecked CreateDeploymentMedia = $State.Controls.chkCreateDeploymentMedia.IsChecked
InjectUnattend = $State.Controls.chkInjectUnattend.IsChecked InjectUnattend = $State.Controls.chkInjectUnattend.IsChecked
@@ -94,6 +96,7 @@ function Get-UIConfig {
USBDriveList = @{} USBDriveList = @{}
Username = $State.Controls.txtUsername.Text Username = $State.Controls.txtUsername.Text
Threads = [int]$State.Controls.txtThreads.Text Threads = [int]$State.Controls.txtThreads.Text
BitsPriority = $State.Controls.cmbBitsPriority.SelectedItem
MaxUSBDrives = [int]$State.Controls.txtMaxUSBDrives.Text MaxUSBDrives = [int]$State.Controls.txtMaxUSBDrives.Text
Verbose = $State.Controls.chkVerbose.IsChecked Verbose = $State.Controls.chkVerbose.IsChecked
VMHostIPAddress = $State.Controls.txtVMHostIPAddress.Text VMHostIPAddress = $State.Controls.txtVMHostIPAddress.Text
@@ -111,8 +114,19 @@ function Get-UIConfig {
WindowsVersion = $State.Controls.cmbWindowsVersion.SelectedItem WindowsVersion = $State.Controls.cmbWindowsVersion.SelectedItem
} }
# Save selected USB drives using UniqueId for reliable identification
$State.Controls.lstUSBDrives.Items | Where-Object { $_.IsSelected } | ForEach-Object { $State.Controls.lstUSBDrives.Items | Where-Object { $_.IsSelected } | ForEach-Object {
$config.USBDriveList[$_.Model] = $_.SerialNumber $config.USBDriveList[$_.Model] = $_.UniqueId
}
# Additional FFU file selections
$config.AdditionalFFUFiles = @()
if ($State.Controls.chkCopyAdditionalFFUFiles.IsChecked) {
$config.AdditionalFFUFiles = @(
$State.Controls.lstAdditionalFFUs.Items |
Where-Object { $_.IsSelected } |
ForEach-Object { $_.FullName }
)
} }
return $config return $config
@@ -231,6 +245,55 @@ function Set-UIValue {
} }
} }
function Get-ConfigDriverBaseName {
param(
[string]$RawName
)
if ([string]::IsNullOrWhiteSpace($RawName)) {
return $RawName
}
if ($RawName -match '^(.*?)\s*\((.+)\)\s*$') {
return $matches[1].Trim()
}
return $RawName.Trim()
}
function Get-ConfigDriverDisplayName {
param(
[string]$Make,
[string]$StoredName,
[string]$ProductName,
[string]$SystemId,
[string]$MachineType
)
$baseName = if (-not [string]::IsNullOrWhiteSpace($ProductName)) { $ProductName } else { Get-ConfigDriverBaseName -RawName $StoredName }
switch ($Make) {
'Dell' {
if ([string]::IsNullOrWhiteSpace($baseName)) { $baseName = $StoredName }
if ([string]::IsNullOrWhiteSpace($SystemId)) { return $baseName }
return "{0} ({1})" -f $baseName.Trim(), $SystemId.Trim()
}
'HP' {
if ([string]::IsNullOrWhiteSpace($baseName)) { $baseName = $StoredName }
if ([string]::IsNullOrWhiteSpace($SystemId)) { return $baseName }
return "{0} ({1})" -f $baseName.Trim(), $SystemId.Trim()
}
'Lenovo' {
if ([string]::IsNullOrWhiteSpace($baseName)) { $baseName = $StoredName }
if ([string]::IsNullOrWhiteSpace($MachineType)) { return $baseName }
return "{0} ({1})" -f $baseName.Trim(), $MachineType.Trim()
}
default {
return $StoredName
}
}
}
function Invoke-LoadConfiguration { function Invoke-LoadConfiguration {
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
@@ -242,19 +305,39 @@ function Invoke-LoadConfiguration {
WriteLog "Load configuration cancelled by user." WriteLog "Load configuration cancelled by user."
return return
} }
WriteLog "Loading configuration from: $filePath" WriteLog "Loading configuration from: $filePath"
$configContent = Get-Content -Path $filePath -Raw | ConvertFrom-Json $raw = $null
try {
$raw = Get-Content -Path $filePath -Raw -ErrorAction Stop
}
catch {
WriteLog "LoadConfig Error: Failed reading file $filePath : $($_.Exception.Message)"
[System.Windows.MessageBox]::Show("Failed to read the configuration file.`n$($_.Exception.Message)", "Load Error", "OK", "Error")
return
}
if ([string]::IsNullOrWhiteSpace($raw)) {
WriteLog "LoadConfig Error: File $filePath is empty."
[System.Windows.MessageBox]::Show("The selected configuration file is empty.", "Load Error", "OK", "Error")
return
}
$configContent = $null
try {
$configContent = $raw | ConvertFrom-Json -ErrorAction Stop
}
catch {
WriteLog "LoadConfig Error: JSON parse failure for $filePath : $($_.Exception.Message)"
[System.Windows.MessageBox]::Show("Failed to parse the configuration file (invalid JSON).`n$($_.Exception.Message)", "Load Error", "OK", "Error")
return
}
if ($null -eq $configContent) { if ($null -eq $configContent) {
WriteLog "LoadConfig Error: configContent is null after parsing $filePath. File might be empty or malformed." WriteLog "LoadConfig Error: Parsed config object is null after $filePath."
[System.Windows.MessageBox]::Show("Failed to parse the configuration file. It might be empty or not valid JSON.", "Load Error", "OK", "Error") [System.Windows.MessageBox]::Show("Parsed configuration object was null.", "Load Error", "OK", "Error")
return return
} }
WriteLog "LoadConfig: Successfully parsed config file. Top-level keys: $($configContent.PSObject.Properties.Name -join ', ')" WriteLog "LoadConfig: Successfully parsed config file. Top-level keys: $($configContent.PSObject.Properties.Name -join ', ')"
# Apply the configuration to the UI
Update-UIFromConfig -ConfigContent $configContent -State $State Update-UIFromConfig -ConfigContent $configContent -State $State
$State.Data.lastConfigFilePath = $filePath
Import-ConfigSupplementalAssets -ConfigContent $configContent -State $State -ShowWarnings:$true
} }
catch { catch {
WriteLog "LoadConfig FATAL Error: $($_.Exception.ToString())" WriteLog "LoadConfig FATAL Error: $($_.Exception.ToString())"
@@ -331,6 +414,7 @@ function Update-UIFromConfig {
Set-UIValue -ControlName 'txtShareName' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'ShareName' -State $State Set-UIValue -ControlName 'txtShareName' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'ShareName' -State $State
Set-UIValue -ControlName 'txtUsername' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Username' -State $State Set-UIValue -ControlName 'txtUsername' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Username' -State $State
Set-UIValue -ControlName 'txtThreads' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Threads' -State $State Set-UIValue -ControlName 'txtThreads' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Threads' -State $State
Set-UIValue -ControlName 'cmbBitsPriority' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'BitsPriority' -State $State
Set-UIValue -ControlName 'txtMaxUSBDrives' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'MaxUSBDrives' -State $State Set-UIValue -ControlName 'txtMaxUSBDrives' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'MaxUSBDrives' -State $State
Set-UIValue -ControlName 'chkBuildUSBDriveEnable' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'BuildUSBDrive' -State $State Set-UIValue -ControlName 'chkBuildUSBDriveEnable' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'BuildUSBDrive' -State $State
Set-UIValue -ControlName 'chkCompactOS' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CompactOS' -State $State Set-UIValue -ControlName 'chkCompactOS' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CompactOS' -State $State
@@ -339,6 +423,7 @@ function Update-UIFromConfig {
Set-UIValue -ControlName 'chkAllowVHDXCaching' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'AllowVHDXCaching' -State $State Set-UIValue -ControlName 'chkAllowVHDXCaching' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'AllowVHDXCaching' -State $State
Set-UIValue -ControlName 'chkAllowExternalHardDiskMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'AllowExternalHardDiskMedia' -State $State Set-UIValue -ControlName 'chkAllowExternalHardDiskMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'AllowExternalHardDiskMedia' -State $State
Set-UIValue -ControlName 'chkPromptExternalHardDiskMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'PromptExternalHardDiskMedia' -State $State Set-UIValue -ControlName 'chkPromptExternalHardDiskMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'PromptExternalHardDiskMedia' -State $State
Set-UIValue -ControlName 'chkCopyAdditionalFFUFiles' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyAdditionalFFUFiles' -State $State
Set-UIValue -ControlName 'chkCreateCaptureMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CreateCaptureMedia' -State $State Set-UIValue -ControlName 'chkCreateCaptureMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CreateCaptureMedia' -State $State
Set-UIValue -ControlName 'chkCreateDeploymentMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CreateDeploymentMedia' -State $State Set-UIValue -ControlName 'chkCreateDeploymentMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CreateDeploymentMedia' -State $State
Set-UIValue -ControlName 'chkInjectUnattend' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'InjectUnattend' -State $State Set-UIValue -ControlName 'chkInjectUnattend' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'InjectUnattend' -State $State
@@ -460,6 +545,7 @@ function Update-UIFromConfig {
Set-UIValue -ControlName 'txtPEDriversFolder' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'PEDriversFolder' -State $State Set-UIValue -ControlName 'txtPEDriversFolder' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'PEDriversFolder' -State $State
Set-UIValue -ControlName 'txtDriversJsonPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'DriversJsonPath' -State $State Set-UIValue -ControlName 'txtDriversJsonPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'DriversJsonPath' -State $State
Set-UIValue -ControlName 'chkCopyPEDrivers' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyPEDrivers' -State $State Set-UIValue -ControlName 'chkCopyPEDrivers' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyPEDrivers' -State $State
Set-UIValue -ControlName 'chkUseDriversAsPEDrivers' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'UseDriversAsPEDrivers' -State $State
Set-UIValue -ControlName 'chkCompressDriversToWIM' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CompressDownloadedDriversToWim' -State $State Set-UIValue -ControlName 'chkCompressDriversToWIM' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CompressDownloadedDriversToWim' -State $State
# Updates tab # Updates tab
@@ -584,8 +670,9 @@ function Update-UIFromConfig {
} }
} }
if ($propertyExists -and ($propertyValue -eq $item.SerialNumber)) { # Match USB drives by UniqueId instead of SerialNumber
WriteLog "LoadConfig: Selecting USB Drive Model '$($item.Model)' with Serial '$($item.SerialNumber)'." if ($propertyExists -and ($propertyValue -eq $item.UniqueId)) {
WriteLog "LoadConfig: Selecting USB Drive Model '$($item.Model)' with UniqueId '$($item.UniqueId)'."
$item.IsSelected = $true $item.IsSelected = $true
} }
else { else {
@@ -632,8 +719,47 @@ function Update-UIFromConfig {
else { else {
WriteLog "LoadConfig: Condition to auto-check 'Select Specific USB Drives' was NOT met." WriteLog "LoadConfig: Condition to auto-check 'Select Specific USB Drives' was NOT met."
} }
# Populate additional FFU list and apply selections
try {
if ($State.Controls.chkCopyAdditionalFFUFiles.IsChecked) {
$State.Controls.additionalFFUPanel.Visibility = 'Visible'
if ($State.Controls.btnRefreshAdditionalFFUs) {
$State.Controls.btnRefreshAdditionalFFUs.RaiseEvent([System.Windows.RoutedEventArgs]::new([System.Windows.Controls.Button]::ClickEvent))
}
$selectedFiles = @()
$addFFUKeyExists = $false
if ($ConfigContent -is [System.Management.Automation.PSCustomObject] -and $null -ne $ConfigContent.PSObject.Properties) {
if (($ConfigContent.PSObject.Properties.Match('AdditionalFFUFiles')).Count -gt 0) {
$addFFUKeyExists = $true
}
}
if ($addFFUKeyExists -and $null -ne $ConfigContent.AdditionalFFUFiles) {
$selectedFiles = @($ConfigContent.AdditionalFFUFiles)
}
if ($selectedFiles.Count -gt 0) {
foreach ($item in $State.Controls.lstAdditionalFFUs.Items) {
if ($selectedFiles -contains $item.FullName) {
$item.IsSelected = $true
}
}
$State.Controls.lstAdditionalFFUs.Items.Refresh()
$headerChk = $State.Controls.chkSelectAllAdditionalFFUs
if ($null -ne $headerChk) {
Update-SelectAllHeaderCheckBoxState -ListView $State.Controls.lstAdditionalFFUs -HeaderCheckBox $headerChk
}
}
}
else {
$State.Controls.additionalFFUPanel.Visibility = 'Collapsed'
}
}
catch {
WriteLog "LoadConfig: Error applying Additional FFU selections: $($_.Exception.Message)"
}
Update-BitsPrioritySetting -State $State
WriteLog "LoadConfig: Configuration loading process finished." WriteLog "LoadConfig: Configuration loading process finished."
} }
function Invoke-SaveConfiguration { function Invoke-SaveConfiguration {
param( param(
@@ -655,7 +781,10 @@ function Invoke-SaveConfiguration {
-DefaultExt ".json" -DefaultExt ".json"
if ($savePath) { if ($savePath) {
$config | ConvertTo-Json -Depth 10 | Set-Content -Path $savePath -Encoding UTF8 # Sort top-level keys alphabetically for consistent output
$sortedConfig = [ordered]@{}
foreach ($k in ($config.Keys | Sort-Object)) { $sortedConfig[$k] = $config[$k] }
$sortedConfig | ConvertTo-Json -Depth 10 | Set-Content -Path $savePath -Encoding UTF8
[System.Windows.MessageBox]::Show("Configuration file saved to:`n$savePath", "Success", "OK", "Information") [System.Windows.MessageBox]::Show("Configuration file saved to:`n$savePath", "Success", "OK", "Information")
} }
} }
@@ -664,4 +793,434 @@ function Invoke-SaveConfiguration {
} }
} }
function Invoke-RestoreDefaults {
param(
[Parameter(Mandatory = $true)]
[psobject]$State
)
try {
$rootPath = $State.FFUDevelopmentPath
# Normalize potential array values to single strings
function Normalize-PathScalar {
param([object]$value)
if ($null -eq $value) { return $null }
if ($value -is [System.Array]) {
foreach ($v in $value) {
if (-not [string]::IsNullOrWhiteSpace([string]$v)) {
return [string]$v
}
}
return $null
}
return [string]$value
}
$appsPath = Join-Path $rootPath 'Apps'
$driversRaw = Normalize-PathScalar -value $State.Controls.txtDriversFolder.Text
if ([string]::IsNullOrWhiteSpace($driversRaw)) {
$driversPath = Join-Path $rootPath 'Drivers'
}
else {
$driversPath = $driversRaw
}
$ffuCaptureRaw = Normalize-PathScalar -value $State.Controls.txtFFUCaptureLocation.Text
$ffuCapturePath = if ([string]::IsNullOrWhiteSpace($ffuCaptureRaw)) { Join-Path $rootPath 'FFU' } else { $ffuCaptureRaw }
$captureISOPath = Join-Path $rootPath 'WinPECaptureFFUFiles\WinPE-Capture.iso'
$deployISOPath = Join-Path $rootPath 'WinPEDeployFFUFiles\WinPE-Deploy.iso'
$appsISOPath = Join-Path $rootPath 'Apps.iso'
$msg = "Restore Defaults will:`n`n- Delete generated config and app/driver list JSON files`n- Remove ISO files (Capture, Deploy, Apps) if present`n- Remove Apps/Update/downloaded artifacts`n- Remove driver folder contents (not the folder)`n- Remove FFU files in the capture folder`n`nSample/template files and VM/VHDX cache are NOT removed.`n`nProceed?"
$result = [System.Windows.MessageBox]::Show($msg, "Confirm Restore Defaults", "YesNo", "Warning")
if ($result -ne [System.Windows.MessageBoxResult]::Yes) {
WriteLog "RestoreDefaults: User cancelled."
return
}
WriteLog "RestoreDefaults: Starting environment reset."
WriteLog "RestoreDefaults: Paths -> Apps=$appsPath Drivers=$driversPath FFUCapture=$ffuCapturePath"
# Remove JSON artifact files if present
$artifactFiles = @(
(Join-Path $rootPath 'config\FFUConfig.json'),
(Join-Path $appsPath 'AppList.json'),
(Join-Path $driversPath 'Drivers.json'),
(Join-Path $appsPath 'UserAppList.json')
) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
foreach ($file in $artifactFiles) {
if ((-not [string]::IsNullOrWhiteSpace($file)) -and (Test-Path -LiteralPath $file)) {
try {
WriteLog "RestoreDefaults: Removing $file"
Remove-Item -LiteralPath $file -Force -ErrorAction Stop
}
catch {
WriteLog "RestoreDefaults: Failed removing $file : $($_.Exception.Message)"
}
}
}
# Force all cleanup flags true
Invoke-FFUPostBuildCleanup `
-RootPath $rootPath `
-AppsPath $appsPath `
-DriversPath $driversPath `
-FFUCapturePath $ffuCapturePath `
-CaptureISOPath $captureISOPath `
-DeployISOPath $deployISOPath `
-AppsISOPath $appsISOPath `
-KBPath (Join-Path $rootPath 'KB') `
-RemoveCaptureISO:$true `
-RemoveDeployISO:$true `
-RemoveAppsISO:$true `
-RemoveDrivers:$true `
-RemoveFFU:$true `
-RemoveApps:$true `
-RemoveUpdates:$true
# Clear UI lists / state
if ($null -ne $State.Data.allDriverModels) { $State.Data.allDriverModels.Clear() }
if ($null -ne $State.Controls.lstDriverModels) { $State.Controls.lstDriverModels.Items.Refresh() }
if ($null -ne $State.Controls.lstApplications) {
try {
if ($State.Controls.lstApplications.ItemsSource) { $State.Controls.lstApplications.ItemsSource = $null }
$State.Controls.lstApplications.Items.Clear()
} catch {}
}
if ($null -ne $State.Controls.lstWingetResults) {
try {
if ($State.Controls.lstWingetResults.ItemsSource) { $State.Controls.lstWingetResults.ItemsSource = $null }
$State.Controls.lstWingetResults.Items.Clear()
} catch {}
}
if ($null -ne $State.Controls.lstAppsScriptVariables) {
try {
if ($State.Controls.lstAppsScriptVariables.ItemsSource) { $State.Controls.lstAppsScriptVariables.ItemsSource = $null }
$State.Controls.lstAppsScriptVariables.Items.Clear()
} catch {}
}
$State.Data.lastConfigFilePath = $null
Initialize-UIDefaults -State $State
WriteLog "RestoreDefaults: Completed."
[System.Windows.MessageBox]::Show("Environment restored to defaults.", "Restore Defaults", "OK", "Information")
}
catch {
WriteLog "RestoreDefaults: Failed with $($_.Exception.Message)"
[System.Windows.MessageBox]::Show("Restore Defaults failed:`n$($_.Exception.Message)", "Error", "OK", "Error")
}
}
function Invoke-AutoLoadPreviousEnvironment {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[psobject]$State
)
try {
$ffuDevRoot = $State.FFUDevelopmentPath
if ([string]::IsNullOrWhiteSpace($ffuDevRoot)) {
WriteLog "AutoLoad: FFUDevelopmentPath not set; skipping."
return
}
$configPath = Join-Path $ffuDevRoot "config\FFUConfig.json"
if (-not (Test-Path -LiteralPath $configPath)) {
WriteLog "AutoLoad: No existing FFUConfig.json found at $configPath."
return
}
WriteLog "AutoLoad: Found config file at $configPath. Parsing..."
$raw = Get-Content -Path $configPath -Raw -ErrorAction SilentlyContinue
if ([string]::IsNullOrWhiteSpace($raw)) {
WriteLog "AutoLoad: Config file empty; aborting."
return
}
$configContent = $null
try {
$configContent = $raw | ConvertFrom-Json -ErrorAction Stop
}
catch {
WriteLog "AutoLoad: JSON parse failed: $($_.Exception.Message)"
return
}
if ($null -eq $configContent) {
WriteLog "AutoLoad: Parsed object null; aborting."
return
}
WriteLog "AutoLoad: Applying core configuration."
Update-UIFromConfig -ConfigContent $configContent -State $State
$State.Data.lastConfigFilePath = $configPath
Import-ConfigSupplementalAssets -ConfigContent $configContent -State $State -ShowWarnings:$false
WriteLog "AutoLoad: Completed supplemental import with warnings disabled."
}
catch {
WriteLog "AutoLoad: Unexpected failure: $($_.Exception.ToString())"
}
}
function Import-ConfigSupplementalAssets {
param(
[Parameter(Mandatory = $true)]
[psobject]$ConfigContent,
[Parameter(Mandatory = $true)]
[psobject]$State,
[Parameter()]
[bool]$ShowWarnings = $false
)
WriteLog "SupplementalImport: Starting import of helper assets."
$loadedWinget = $false
$loadedBYO = $false
$loadedDrivers = $false
$missing = New-Object System.Collections.Generic.List[string]
# Winget AppList
$appListPath = $null
if ($ConfigContent.PSObject.Properties.Match('AppListPath').Count -gt 0) {
$appListPath = $ConfigContent.AppListPath
}
if (-not [string]::IsNullOrWhiteSpace($appListPath)) {
if (Test-Path -LiteralPath $appListPath) {
WriteLog "SupplementalImport: Loading Winget AppList from $appListPath"
try {
$importedAppsData = Get-Content -Path $appListPath -Raw | ConvertFrom-Json -ErrorAction Stop
if ($null -ne $importedAppsData -and $null -ne $importedAppsData.apps) {
$defaultArch = $State.Controls.cmbWindowsArch.SelectedItem
$appsBuffer = [System.Collections.Generic.List[object]]::new()
foreach ($appInfo in $importedAppsData.apps) {
$arch = if ($appInfo.source -eq 'msstore') { 'NA' } else {
if ($appInfo.PSObject.Properties['architecture']) { $appInfo.architecture } else { $defaultArch }
}
$appsBuffer.Add([PSCustomObject]@{
IsSelected = $true
Name = $appInfo.name
Id = $appInfo.id
Version = ""
Source = $appInfo.source
Architecture = $arch
AdditionalExitCodes = if ($appInfo.PSObject.Properties['AdditionalExitCodes']) { $appInfo.AdditionalExitCodes } else { "" }
IgnoreNonZeroExitCodes = if ($appInfo.PSObject.Properties['IgnoreNonZeroExitCodes']) { [bool]$appInfo.IgnoreNonZeroExitCodes } else { $false }
DownloadStatus = ""
})
}
$State.Controls.lstWingetResults.ItemsSource = $appsBuffer.ToArray()
$loadedWinget = $true
if ($null -ne $State.Controls.wingetSearchPanel) {
$State.Controls.wingetSearchPanel.Visibility = 'Visible'
}
if ($null -ne $State.Controls.chkSelectAllWingetResults -and (Get-Command -Name Update-SelectAllHeaderCheckBoxState -ErrorAction SilentlyContinue)) {
Update-SelectAllHeaderCheckBoxState -ListView $State.Controls.lstWingetResults -HeaderCheckBox $State.Controls.chkSelectAllWingetResults
}
WriteLog "SupplementalImport: Winget list loaded with $($appsBuffer.Count) entries."
}
else {
WriteLog "SupplementalImport: Winget AppList missing 'apps' array."
}
}
catch {
WriteLog "SupplementalImport: Failed loading Winget AppList ($appListPath): $($_.Exception.Message)"
}
}
else {
WriteLog "SupplementalImport: Winget AppList file missing: $appListPath"
$missing.Add("Winget AppList (AppListPath): $appListPath")
}
}
else {
WriteLog "SupplementalImport: AppListPath not defined in config."
}
# UserAppList (BYO)
$userAppListPath = $null
if ($ConfigContent.PSObject.Properties.Match('UserAppListPath').Count -gt 0) {
$userAppListPath = $ConfigContent.UserAppListPath
}
if (-not [string]::IsNullOrWhiteSpace($userAppListPath)) {
if (Test-Path -LiteralPath $userAppListPath) {
WriteLog "SupplementalImport: Loading UserAppList from $userAppListPath"
try {
$applications = Get-Content -Path $userAppListPath -Raw | ConvertFrom-Json -ErrorAction Stop
if ($applications) {
$listView = $State.Controls.lstApplications
$listView.Items.Clear()
$sortedApps = $applications | Sort-Object Priority
foreach ($app in $sortedApps) {
$ignoreNonZero = if ($app.PSObject.Properties['IgnoreNonZeroExitCodes']) { $app.IgnoreNonZeroExitCodes } else { $false }
$listView.Items.Add([PSCustomObject]@{
IsSelected = $false
Priority = $app.Priority
Name = $app.Name
CommandLine = $app.CommandLine
Arguments = if ($app.PSObject.Properties['Arguments']) { $app.Arguments } else { "" }
Source = $app.Source
AdditionalExitCodes = if ($app.PSObject.Properties['AdditionalExitCodes']) { $app.AdditionalExitCodes } else { "" }
IgnoreNonZeroExitCodes = $ignoreNonZero
IgnoreExitCodes = if ($ignoreNonZero) { "Yes" } else { "No" }
CopyStatus = ""
})
}
if (Get-Command -Name Update-ListViewPriorities -ErrorAction SilentlyContinue) {
Update-ListViewPriorities -ListView $listView
}
if (Get-Command -Name Update-CopyButtonState -ErrorAction SilentlyContinue) {
Update-CopyButtonState -State $State
}
if (Get-Command -Name Update-BYOAppsActionButtonsState -ErrorAction SilentlyContinue) {
Update-BYOAppsActionButtonsState -State $State
}
$loadedBYO = $true
WriteLog "SupplementalImport: UserAppList loaded with $($listView.Items.Count) entries."
}
else {
WriteLog "SupplementalImport: UserAppList JSON empty."
}
}
catch {
WriteLog "SupplementalImport: Failed loading UserAppList ($userAppListPath): $($_.Exception.Message)"
}
}
else {
WriteLog "SupplementalImport: UserAppList file missing: $userAppListPath"
$missing.Add("UserAppList (UserAppListPath): $userAppListPath")
}
}
else {
WriteLog "SupplementalImport: UserAppListPath not defined in config."
}
# Drivers JSON
$driversJsonPath = $null
if ($ConfigContent.PSObject.Properties.Match('DriversJsonPath').Count -gt 0) {
$driversJsonPath = $ConfigContent.DriversJsonPath
}
if (-not [string]::IsNullOrWhiteSpace($driversJsonPath)) {
if (Test-Path -LiteralPath $driversJsonPath) {
WriteLog "SupplementalImport: Loading Drivers JSON from $driversJsonPath"
try {
$rawDrivers = Get-Content -Path $driversJsonPath -Raw | ConvertFrom-Json -ErrorAction Stop
if ($rawDrivers -and $rawDrivers.PSObject.Properties.Count -gt 0) {
$State.Data.allDriverModels.Clear()
foreach ($makeProp in $rawDrivers.PSObject.Properties) {
$makeName = $makeProp.Name
$makeObject = $makeProp.Value
if ($null -eq $makeObject -or -not ($makeObject.PSObject.Properties['Models'])) { continue }
$models = $makeObject.Models
if ($models -and ($models -is [System.Collections.IEnumerable])) {
foreach ($modelEntry in $models) {
if ($null -eq $modelEntry -or -not ($modelEntry.PSObject.Properties['Name'])) { continue }
$modelName = $modelEntry.Name
if ([string]::IsNullOrWhiteSpace($modelName)) { continue }
$downloadStatus = if ($modelEntry.PSObject.Properties['DownloadStatus']) { $modelEntry.DownloadStatus } else { "" }
$linkValue = if ($modelEntry.PSObject.Properties['Link']) { $modelEntry.Link } else { $null }
$productName = if ($modelEntry.PSObject.Properties['ProductName']) { $modelEntry.ProductName } else { $null }
$machineType = if ($modelEntry.PSObject.Properties['MachineType']) { $modelEntry.MachineType } else { $null }
$systemId = if ($modelEntry.PSObject.Properties['SystemId']) { $modelEntry.SystemId } else { $null }
$idValue = if ($modelEntry.PSObject.Properties['Id']) { $modelEntry.Id } else { $null }
if ($null -eq $idValue -and -not [string]::IsNullOrWhiteSpace($systemId)) { $idValue = $systemId }
if ($null -eq $idValue -and -not [string]::IsNullOrWhiteSpace($machineType)) { $idValue = $machineType }
$displayModel = Get-ConfigDriverDisplayName -Make $makeName -StoredName $modelName -ProductName $productName -SystemId $systemId -MachineType $machineType
if ([string]::IsNullOrWhiteSpace($displayModel)) {
$displayModel = $modelName
}
$driverObj = [PSCustomObject]@{
IsSelected = $true
Make = $makeName
Model = $displayModel
DownloadStatus = $downloadStatus
Link = $linkValue
ProductName = $productName
MachineType = $machineType
SystemId = $systemId
Id = $idValue
}
$State.Data.allDriverModels.Add($driverObj)
}
}
}
$State.Controls.lstDriverModels.ItemsSource = $State.Data.allDriverModels
if (Get-Command -Name Update-SelectAllHeaderCheckBoxState -ErrorAction SilentlyContinue) {
$headerChk = $State.Controls.chkSelectAllDriverModels
if ($null -ne $headerChk) {
Update-SelectAllHeaderCheckBoxState -ListView $State.Controls.lstDriverModels -HeaderCheckBox $headerChk
}
}
if ($State.Data.allDriverModels.Count -gt 0) {
if ($null -ne $State.Controls.spModelFilterSection) { $State.Controls.spModelFilterSection.Visibility = 'Visible' }
if ($null -ne $State.Controls.lstDriverModels) { $State.Controls.lstDriverModels.Visibility = 'Visible' }
if ($null -ne $State.Controls.spDriverActionButtons) { $State.Controls.spDriverActionButtons.Visibility = 'Visible' }
try {
if ($State.Controls.cmbMake.SelectedIndex -lt 0 -and $State.Data.allDriverModels.Count -gt 0) {
$firstMake = ($State.Data.allDriverModels | Select-Object -First 1).Make
if (-not [string]::IsNullOrWhiteSpace($firstMake)) {
$makeItem = $State.Controls.cmbMake.Items | Where-Object { $_ -eq $firstMake } | Select-Object -First 1
if ($makeItem) { $State.Controls.cmbMake.SelectedItem = $makeItem }
}
}
}
catch {
WriteLog "SupplementalImport: Non-fatal error selecting first Make: $($_.Exception.Message)"
}
}
$loadedDrivers = $true
WriteLog "SupplementalImport: Loaded $($State.Data.allDriverModels.Count) driver models."
}
else {
WriteLog "SupplementalImport: Drivers JSON empty or structure unexpected."
}
}
catch {
WriteLog "SupplementalImport: Failed loading Drivers JSON ($driversJsonPath): $($_.Exception.Message)"
}
}
else {
WriteLog "SupplementalImport: Drivers JSON file missing: $driversJsonPath"
$missing.Add("Drivers (DriversJsonPath): $driversJsonPath")
}
}
else {
WriteLog "SupplementalImport: DriversJsonPath not defined in config."
}
if ($loadedWinget -or $loadedBYO) {
$State.Controls.chkInstallApps.IsChecked = $true
}
if ($loadedWinget) {
$State.Controls.chkInstallWingetApps.IsChecked = $true
}
if ($loadedBYO) {
$State.Controls.chkBringYourOwnApps.IsChecked = $true
}
if ($loadedDrivers) {
$State.Controls.chkDownloadDrivers.IsChecked = $true
}
if (Get-Command -Name Update-ApplicationPanelVisibility -ErrorAction SilentlyContinue) {
Update-ApplicationPanelVisibility -State $State -TriggeringControlName 'SupplementalImport'
}
if (Get-Command -Name Update-DriverDownloadPanelVisibility -ErrorAction SilentlyContinue) {
Update-DriverDownloadPanelVisibility -State $State
}
if (Get-Command -Name Update-DriverCheckboxStates -ErrorAction SilentlyContinue) {
Update-DriverCheckboxStates -State $State
}
if (Get-Command -Name Update-OfficePanelVisibility -ErrorAction SilentlyContinue) {
Update-OfficePanelVisibility -State $State
}
if (Get-Command -Name Update-CopyButtonState -ErrorAction SilentlyContinue) {
Update-CopyButtonState -State $State
}
# Updated message to clarify successful load and that missing helper files are optional if not yet created.
if ($ShowWarnings -and $missing.Count -gt 0) {
$msg = "Configuration file loaded successfully.`n`n" +
"Optional helper file(s) referenced in the configuration were not found:`n" +
($missing | ForEach-Object { "- $_" } | Out-String) +
"`nThese files are optional. They won't exist until you create Winget (AppList.json), User (UserAppList.json), or Driver (Drivers.json) manifests. You can create them later or ignore this message."
[System.Windows.MessageBox]::Show($msg.TrimEnd(), "Configuration Loaded - Optional Files Missing", "OK", "Information") | Out-Null
}
WriteLog ("SupplementalImport: Complete. Winget={0} BYO={1} Drivers={2} Missing={3}" -f $loadedWinget, $loadedBYO, $loadedDrivers, $missing.Count)
}
Export-ModuleMember -Function * Export-ModuleMember -Function *
@@ -12,147 +12,98 @@ function Get-DellDriversModelList {
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[int]$WindowsRelease, [int]$WindowsRelease,
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$DriversFolder, # Base drivers folder (e.g., C:\FFUDevelopment\Drivers) [string]$DriversFolder,
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$Make # Should be 'Dell' [string]$Make
) )
# Define Dell specific drivers folder and catalog file names # Client pathway (<=11) uses CatalogIndexPC to build full Brand Model (SystemID) strings.
if ($WindowsRelease -le 11) {
$dellModels = Get-DellClientModels -CatalogIndexXmlPath (Get-DellCatalogIndex -DriversFolder $DriversFolder)
$final = [System.Collections.Generic.List[pscustomobject]]::new()
foreach ($m in $dellModels) {
$final.Add([pscustomobject]@{
Make = $Make
Model = $m.ModelDisplay
Brand = $m.Brand
ModelNumber = $m.ModelNumber
SystemId = $m.SystemId
CabRelativePath = $m.CabRelativePath
CabUrl = $m.CabUrl
})
}
return $final
}
# Server pathway (unchanged still uses Catalog.cab)
$dellDriversFolder = Join-Path -Path $DriversFolder -ChildPath "Dell" $dellDriversFolder = Join-Path -Path $DriversFolder -ChildPath "Dell"
$catalogBaseName = if ($WindowsRelease -le 11) { "CatalogPC" } else { "Catalog" } $catalogBaseName = "Catalog"
$dellCabFile = Join-Path -Path $dellDriversFolder -ChildPath "$($catalogBaseName).cab" $dellCabFile = Join-Path -Path $dellDriversFolder -ChildPath "$($catalogBaseName).cab"
$dellCatalogXML = Join-Path -Path $dellDriversFolder -ChildPath "$($catalogBaseName).xml" $dellCatalogXML = Join-Path -Path $dellDriversFolder -ChildPath "$($catalogBaseName).xml"
$catalogUrl = if ($WindowsRelease -le 11) { "http://downloads.dell.com/catalog/CatalogPC.cab" } else { "https://downloads.dell.com/catalog/Catalog.cab" } $catalogUrl = "https://downloads.dell.com/catalog/Catalog.cab"
$uniqueModelNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) if (-not (Test-Path -Path $dellDriversFolder)) {
$reader = $null
try {
# Check if the Dell catalog XML exists and is recent
$downloadCatalog = $true
if (Test-Path -Path $dellCatalogXML -PathType Leaf) {
WriteLog "Dell Catalog XML found: $dellCatalogXML"
$dellCatalogCreationTime = (Get-Item $dellCatalogXML).CreationTime
WriteLog "Dell Catalog XML Creation time: $dellCatalogCreationTime"
# Check if the XML file is less than 7 days old
if (((Get-Date) - $dellCatalogCreationTime).TotalDays -lt 7) {
WriteLog "Using existing Dell Catalog XML (less than 7 days old): $dellCatalogXML"
$downloadCatalog = $false
}
else {
WriteLog "Existing Dell Catalog XML is older than 7 days: $dellCatalogXML"
}
}
else {
WriteLog "Dell Catalog XML not found: $dellCatalogXML"
}
if ($downloadCatalog) {
WriteLog "Attempting to download and extract Dell Catalog for Get-DellDriversModelList..."
# Ensure Dell drivers folder exists
if (-not (Test-Path -Path $dellDriversFolder -PathType Container)) {
WriteLog "Creating Dell drivers folder: $dellDriversFolder"
New-Item -Path $dellDriversFolder -ItemType Directory -Force | Out-Null New-Item -Path $dellDriversFolder -ItemType Directory -Force | Out-Null
} }
# Check URL accessibility $download = $true
try { if (Test-Path -Path $dellCatalogXML) {
$request = [System.Net.WebRequest]::Create($catalogUrl) if (((Get-Date) - (Get-Item $dellCatalogXML).CreationTime).TotalDays -lt 7) {
$request.Method = 'HEAD'; $response = $request.GetResponse(); $response.Close() $download = $false
}
} }
catch { throw "Dell Catalog URL '$catalogUrl' not accessible: $($_.Exception.Message)" }
# Remove existing files before download if they exist if ($download) {
if (Test-Path -Path $dellCabFile) { Remove-Item -Path $dellCabFile -Force -ErrorAction SilentlyContinue } if (Test-Path $dellCabFile) { Remove-Item $dellCabFile -Force -ErrorAction SilentlyContinue }
if (Test-Path -Path $dellCatalogXML) { Remove-Item -Path $dellCatalogXML -Force -ErrorAction SilentlyContinue } if (Test-Path $dellCatalogXML) { Remove-Item $dellCatalogXML -Force -ErrorAction SilentlyContinue }
WriteLog "Downloading Dell Catalog cab file: $catalogUrl to $dellCabFile"
Start-BitsTransferWithRetry -Source $catalogUrl -Destination $dellCabFile Start-BitsTransferWithRetry -Source $catalogUrl -Destination $dellCabFile
WriteLog "Dell Catalog cab file downloaded to $dellCabFile" Invoke-Process -FilePath Expand.exe -ArgumentList """$dellCabFile"" ""$dellCatalogXML""" | Out-Null
Remove-Item $dellCabFile -Force -ErrorAction SilentlyContinue
WriteLog "Extracting Dell Catalog cab file '$dellCabFile' to '$dellCatalogXML'"
Invoke-Process -FilePath "Expand.exe" -ArgumentList """$dellCabFile"" ""$dellCatalogXML""" | Out-Null
WriteLog "Dell Catalog cab file extracted to $dellCatalogXML"
# Delete the CAB file after extraction
WriteLog "Deleting Dell Catalog CAB file: $dellCabFile"
Remove-Item -Path $dellCabFile -Force -ErrorAction SilentlyContinue
} }
# Ensure the XML file exists before trying to read it if (-not (Test-Path $dellCatalogXML)) { throw "Dell server catalog XML missing: $dellCatalogXML" }
if (-not (Test-Path -Path $dellCatalogXML -PathType Leaf)) {
throw "Dell Catalog XML file '$dellCatalogXML' not found after download/check attempt."
}
# Use XmlReader for streaming from the XML file
$settings = New-Object System.Xml.XmlReaderSettings $settings = New-Object System.Xml.XmlReaderSettings
$settings.IgnoreWhitespace = $true $settings.IgnoreWhitespace = $true
$settings.IgnoreComments = $true $settings.IgnoreComments = $true
# $settings.DtdProcessing = [System.Xml.DtdProcessing]::Ignore # Optional
$reader = [System.Xml.XmlReader]::Create($dellCatalogXML, $settings) $reader = [System.Xml.XmlReader]::Create($dellCatalogXML, $settings)
WriteLog "Starting XML stream parsing for Dell models from '$dellCatalogXML'..." $inDriver = $false
$inModel = $false
$isDriverComponent = $false $depthModel = -1
$isModelElement = $false $modelsHash = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
$modelDepth = -1 # Track depth to handle nested elements if needed try {
# Read through the XML stream node by node
while ($reader.Read()) { while ($reader.Read()) {
switch ($reader.NodeType) { switch ($reader.NodeType) {
([System.Xml.XmlNodeType]::Element) { ([System.Xml.XmlNodeType]::Element) {
switch ($reader.Name) { switch ($reader.Name) {
'SoftwareComponent' { $isDriverComponent = $false } # Reset flag 'SoftwareComponent' { $inDriver = $false }
'ComponentType' { if ($reader.GetAttribute('value') -eq 'DRVR') { $isDriverComponent = $true } } 'ComponentType' { if ($reader.GetAttribute('value') -eq 'DRVR') { $inDriver = $true } }
'Model' { if ($isDriverComponent) { $isModelElement = $true; $modelDepth = $reader.Depth } } 'Model' { if ($inDriver) { $inModel = $true; $depthModel = $reader.Depth } }
} }
} }
([System.Xml.XmlNodeType]::CDATA) { ([System.Xml.XmlNodeType]::CDATA) {
if ($isModelElement -and $isDriverComponent) { if ($inDriver -and $inModel) {
$modelName = $reader.Value.Trim() $val = $reader.Value.Trim()
if (-not [string]::IsNullOrWhiteSpace($modelName)) { $uniqueModelNames.Add($modelName) | Out-Null } if ($val) { $modelsHash.Add($val) | Out-Null }
$isModelElement = $false # Reset after reading CDATA $inModel = $false
} }
} }
([System.Xml.XmlNodeType]::EndElement) { ([System.Xml.XmlNodeType]::EndElement) {
switch ($reader.Name) { if ($reader.Name -eq 'SoftwareComponent') { $inDriver = $false; $inModel = $false }
'SoftwareComponent' { $isDriverComponent = $false; $isModelElement = $false; $modelDepth = -1 } elseif ($reader.Name -eq 'Model' -and $reader.Depth -eq $depthModel) { $inModel = $false; $depthModel = -1 }
'Model' { if ($reader.Depth -eq $modelDepth) { $isModelElement = $false; $modelDepth = -1 } }
} }
} }
} }
} # End while ($reader.Read())
WriteLog "Finished XML stream parsing. Found $($uniqueModelNames.Count) unique Dell models."
}
catch {
WriteLog "Error getting Dell models: $($_.Exception.ToString())" # Log full exception
throw "Failed to retrieve Dell models. Check log for details." # Re-throw for UI handling
} }
finally { finally {
# Ensure the reader is closed and disposed
if ($null -ne $reader) {
$reader.Dispose() $reader.Dispose()
} }
# Ensure CAB file is deleted even if extraction failed but download succeeded
if (Test-Path -Path $dellCabFile) {
WriteLog "Cleaning up downloaded Dell CAB file: $dellCabFile"
Remove-Item -Path $dellCabFile -Force -ErrorAction SilentlyContinue
}
}
# Convert HashSet to sorted list of PSCustomObjects $out = [System.Collections.Generic.List[pscustomobject]]::new()
$models = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($nm in ($modelsHash | Sort-Object)) {
foreach ($modelName in ($uniqueModelNames | Sort-Object)) { $out.Add([pscustomobject]@{ Make = $Make; Model = $nm })
$models.Add([PSCustomObject]@{
Make = $Make
Model = $modelName
# Link is not applicable here like for Microsoft
})
} }
return $out
return $models
} }
# Function to download and extract drivers for a specific Dell model (Modified for ForEach-Object -Parallel) # Function to download and extract drivers for a specific Dell model (Modified for ForEach-Object -Parallel)
@@ -160,548 +111,261 @@ function Save-DellDriversTask {
[CmdletBinding()] [CmdletBinding()]
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[PSCustomObject]$DriverItemData, # Contains Model property [pscustomobject]$DriverItemData,
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$DriversFolder, # Base drivers folder (e.g., C:\FFUDevelopment\Drivers) [string]$DriversFolder,
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$WindowsArch, [string]$WindowsArch,
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[int]$WindowsRelease, [int]$WindowsRelease,
[Parameter()] # Made optional
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null, # Default to null
[Parameter()] [Parameter()]
[bool]$CompressToWim = $false # New parameter for compression [System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null,
[Parameter()]
[bool]$CompressToWim = $false,
[Parameter()]
[bool]$PreserveSourceOnCompress = $false
) )
$modelName = $DriverItemData.Model $modelDisplay = $DriverItemData.Model
$make = "Dell" # Hardcoded for this task $make = 'Dell'
$status = "Starting..." # Initial local status if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status 'Checking...' }
$success = $false
# Initial status update $sanitizedModelName = ConvertTo-SafeName -Name $modelDisplay
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status "Checking..." } $makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $make
$modelPath = Join-Path -Path $makeDriversPath -ChildPath $sanitizedModelName
$driverRelativePath = Join-Path -Path $make -ChildPath $sanitizedModelName
$makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $Make # Helper: safe folder removal
$modelPath = Join-Path -Path $makeDriversPath -ChildPath $modelName function Remove-SafeFolder {
$driverRelativePath = Join-Path -Path $make -ChildPath $modelName # Relative path for the driver folder param([string]$Path)
if ([string]::IsNullOrWhiteSpace($Path)) { return }
try { # Never allow deleting the entire Dell root folder accidentally
# Check for existing drivers $dellRoot = (Resolve-Path $makeDriversPath).ProviderPath
$existingDriver = Test-ExistingDriver -Make $make -Model $modelName -DriversFolder $DriversFolder -Identifier $modelName -ProgressQueue $ProgressQueue $target = (Resolve-Path $Path -ErrorAction SilentlyContinue)?.ProviderPath
if ($null -ne $existingDriver) { if ($null -eq $target) { return }
# Add the 'Model' property to the return object for consistency if it's not there if ($target -eq $dellRoot) { return }
if (-not $existingDriver.PSObject.Properties['Model']) { if (-not ($target.StartsWith($dellRoot, [System.StringComparison]::OrdinalIgnoreCase))) { return }
$existingDriver | Add-Member -MemberType NoteProperty -Name 'Model' -Value $modelName Remove-Item -Path $target -Recurse -Force -ErrorAction SilentlyContinue
} }
# Special handling for existing folders that need compression
if ($CompressToWim -and $existingDriver.Status -eq 'Already downloaded') {
$wimFilePath = Join-Path -Path $makeDriversPath -ChildPath "$($modelName).wim"
$sourceFolderPath = Join-Path -Path $makeDriversPath -ChildPath $modelName
WriteLog "Attempting compression of existing folder '$sourceFolderPath' to '$wimFilePath'."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status "Compressing existing..." }
try { try {
Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $modelName -WimDescription "Drivers for $modelName" -ErrorAction Stop # Existing drivers shortcircuit
$existingDriver.Status = "Already downloaded & Compressed" $existing = Test-ExistingDriver -Make $make -Model $sanitizedModelName -DriversFolder $DriversFolder -Identifier $modelDisplay -ProgressQueue $ProgressQueue
$existingDriver.DriverPath = Join-Path -Path $make -ChildPath "$($modelName).wim" if ($existing) {
$existingDriver.Success = $true if (-not $existing.PSObject.Properties['Model']) {
WriteLog "Successfully compressed existing drivers for $modelName to $wimFilePath." $existing | Add-Member -MemberType NoteProperty -Name 'Model' -Value $modelDisplay
}
if ($CompressToWim -and $existing.Status -eq 'Already downloaded') {
$wimPath = Join-Path $makeDriversPath "$sanitizedModelName.wim"
$wimRelativePath = Join-Path $make "$sanitizedModelName.wim"
$srcPath = Join-Path $makeDriversPath $sanitizedModelName
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status 'Compressing existing...' }
try {
$null = Compress-DriverFolderToWim -SourceFolderPath $srcPath -DestinationWimPath $wimPath -WimName $modelDisplay -WimDescription "Drivers for $modelDisplay" -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
$existing.Status = 'Compression successful'
$existing.DriverPath = $wimRelativePath
$existing.Success = $true
} }
catch { catch {
WriteLog "Error compressing existing drivers for $($modelName): $($_.Exception.Message)" WriteLog "Compression failed for $($modelDisplay): $($_.Exception.Message)"
$existingDriver.Status = "Already downloaded (Compression failed)" $existing.Status = 'Already downloaded (Compression failed)'
$existingDriver.Success = $false $existing.Success = $false
} }
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $existingDriver.Status } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status $existing.Status }
}
return $existing
} }
return $existingDriver if (-not (Test-Path $makeDriversPath)) { New-Item -Path $makeDriversPath -ItemType Directory -Force | Out-Null }
} if (-not (Test-Path $modelPath)) { New-Item -Path $modelPath -ItemType Directory -Force | Out-Null }
# Define paths for Dell catalog. The catalog is assumed to be prepared by the calling function. $packages = @()
$dellDriversFolder = Join-Path -Path $DriversFolder -ChildPath "Dell"
$catalogBaseName = if ($WindowsRelease -le 11) { "CatalogPC" } else { "Catalog" }
$dellCatalogXML = Join-Path -Path $dellDriversFolder -ChildPath "$($catalogBaseName).xml"
# 3. Parse the *EXISTING* XML and Find Drivers for *this specific model*
$status = "Finding drivers..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
# Check if the provided XML path exists
if (-not (Test-Path -Path $dellCatalogXML -PathType Leaf)) {
throw "Dell Catalog XML file not found at specified path: $dellCatalogXML"
}
WriteLog "Parsing existing Dell Catalog XML for model '$modelName' from: $dellCatalogXML"
# Initialize variables
$baseLocation = $null
$latestDrivers = @{} # Hashtable to store latest drivers for this model
$modelSpecificDriversFound = $false
# Create XML reader settings
$settings = New-Object System.Xml.XmlReaderSettings
$settings.IgnoreWhitespace = $true
$settings.IgnoreComments = $true
# Create XML reader
$reader = $null
try {
$reader = [System.Xml.XmlReader]::Create($dellCatalogXML, $settings)
# First pass - get baseLocation from manifest
while ($reader.Read()) {
if ($reader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $reader.Name -eq "Manifest") {
$baseLocationAttr = $reader.GetAttribute("baseLocation")
if ($null -ne $baseLocationAttr) {
$baseLocation = "https://" + $baseLocationAttr + "/"
break
}
}
}
if ($null -eq $baseLocation) {
throw "Invalid Dell Catalog XML format: Missing 'baseLocation' attribute in Manifest element."
}
# Reset reader for second pass
$reader.Dispose()
$reader = [System.Xml.XmlReader]::Create($dellCatalogXML, $settings)
# Process SoftwareComponents
while ($reader.Read()) {
if ($reader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $reader.Name -eq "SoftwareComponent") {
# Read the entire SoftwareComponent subtree
$componentXml = $reader.ReadSubtree()
$component = New-Object System.Xml.XmlDocument
$component.Load($componentXml)
$componentXml.Dispose()
# Check if it's a driver component
$componentTypeNode = $component.SelectSingleNode("//ComponentType[@value='DRVR']")
if ($null -eq $componentTypeNode) {
continue
}
# Check if component supports the model
$modelNodes = $component.SelectNodes("//SupportedSystems/Brand/Model")
$modelMatch = $false
foreach ($modelNode in $modelNodes) {
$displayNode = $modelNode.SelectSingleNode("Display")
if ($null -ne $displayNode -and $displayNode.InnerText.Trim() -eq $modelName) {
$modelMatch = $true
break
}
}
if ($modelMatch) {
# Check OS compatibility
$validOS = $null
$osNodes = $component.SelectNodes("//SupportedOperatingSystems/OperatingSystem")
if ($null -ne $osNodes) {
foreach ($osNode in $osNodes) {
$osArch = $osNode.GetAttribute("osArch")
if ($WindowsRelease -le 11) { if ($WindowsRelease -le 11) {
# Client OS check $cabUrl = $DriverItemData.CabUrl
if ($osArch -eq $WindowsArch) { if ([string]::IsNullOrWhiteSpace($cabUrl)) {
$validOS = $osNode WriteLog "CabUrl missing for '$modelDisplay' resolving via CatalogIndexPC."
break $resolved = Resolve-DellCabUrlFromModel -DriversFolder $DriversFolder -ModelDisplay $modelDisplay
if ($null -eq $resolved -or [string]::IsNullOrWhiteSpace($resolved.CabUrl)) {
throw "Unable to resolve CabUrl for $modelDisplay from CatalogIndexPC."
} }
$cabUrl = $resolved.CabUrl
# Optionally persist back into the incoming object if property exists
if ($DriverItemData.PSObject.Properties['CabUrl']) {
$DriverItemData.CabUrl = $cabUrl
}
}
# Model-based workflow (always used for client pathway now)
$modelCabName = [IO.Path]::GetFileName($cabUrl)
if ([string]::IsNullOrWhiteSpace($modelCabName)) { throw "Derived model cab name empty for $modelDisplay" }
$modelCabPath = Join-Path $makeDriversPath $modelCabName
$modelXmlPath = Join-Path $makeDriversPath ([IO.Path]::GetFileNameWithoutExtension($modelCabName) + '.xml')
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status 'Downloading catalog...' }
if (Test-Path $modelCabPath) { Remove-SafeFolder $modelCabPath }
if (Test-Path $modelXmlPath) { Remove-SafeFolder $modelXmlPath }
WriteLog "Downloading Dell model catalog from $cabUrl to $modelCabPath"
Start-BitsTransferWithRetry -Source $cabUrl -Destination $modelCabPath
Invoke-Process -FilePath Expand.exe -ArgumentList """$modelCabPath"" ""$modelXmlPath""" | Out-Null
Remove-Item $modelCabPath -Force -ErrorAction SilentlyContinue
if (-not (Test-Path $modelXmlPath)) { throw "Model XML not found after extraction: $modelXmlPath" }
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status 'Selecting latest drivers...' }
$packages = Get-DellLatestDriverPackages -ModelXmlPath $modelXmlPath -WindowsArch $WindowsArch -WindowsRelease $WindowsRelease
} }
else { else {
# Server OS check # Server legacy logic unchanged (kept as before)
$osCode = $osNode.GetAttribute("osCode") if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status 'Preparing server catalog...' }
$osCodePattern = switch ($WindowsRelease) { $catalogCab = Join-Path $makeDriversPath 'Catalog.cab'
2016 { "W14" } $catalogXml = Join-Path $makeDriversPath 'Catalog.xml'
2019 { "W19" } $catalogUrl = 'https://downloads.dell.com/catalog/Catalog.cab'
2022 { "W22" } $need = $true
2025 { "W25" } if (Test-Path $catalogXml) {
default { "W22" } if (((Get-Date) - (Get-Item $catalogXml).CreationTime).TotalDays -lt 7) { $need = $false }
}
if ($osArch -eq $WindowsArch -and $osCode -match $osCodePattern) {
$validOS = $osNode
break
}
}
} }
if ($need) {
if (Test-Path $catalogCab) { Remove-SafeFolder $catalogCab }
if (Test-Path $catalogXml) { Remove-SafeFolder $catalogXml }
WriteLog "Downloading Dell server catalog from $catalogUrl to $catalogCab"
Start-BitsTransferWithRetry -Source $catalogUrl -Destination $catalogCab
Invoke-Process -FilePath Expand.exe -ArgumentList """$catalogCab"" ""$catalogXml""" | Out-Null
Remove-Item $catalogCab -Force -ErrorAction SilentlyContinue
} }
if (-not (Test-Path $catalogXml)) { throw "Server catalog XML missing: $catalogXml" }
if ($validOS) { [xml]$xmlContent = Get-Content -Path $catalogXml -Raw
$modelSpecificDriversFound = $true $baseLocation = "https://$($xmlContent.manifest.baseLocation)/"
$softwareComponents = $xmlContent.Manifest.SoftwareComponent | Where-Object { $_.ComponentType.value -eq 'DRVR' }
# Extract driver information $latestDrivers = @{}
$driverPath = $component.SoftwareComponent.GetAttribute("path")
$downloadUrl = $baseLocation + $driverPath
$driverFileName = [System.IO.Path]::GetFileName($driverPath)
# Get name
$nameNode = $component.SelectSingleNode("//Name/Display")
$name = if ($null -ne $nameNode) { $nameNode.InnerText } else { "UnknownDriver" }
$name = $name -replace '[\\\/\:\*\?\"\<\>\| ]', '_' -replace '[\,]', '-'
# Get category
$categoryNode = $component.SelectSingleNode("//Category/Display")
$category = if ($null -ne $categoryNode) { $categoryNode.InnerText } else { "Uncategorized" }
$category = $category -replace '[\\\/\:\*\?\"\<\>\| ]', '_'
# Get version
$version = [version]"0.0"
$vendorVersion = $component.SoftwareComponent.GetAttribute("vendorVersion")
if ($null -ne $vendorVersion) {
try { $version = [version]$vendorVersion } catch { WriteLog "Warning: Could not parse version '$vendorVersion' for driver '$name'. Using 0.0." }
}
$namePrefix = ($name -split '-')[0]
# Store the latest version for each category/prefix combination
if (-not $latestDrivers.ContainsKey($category)) { $latestDrivers[$category] = @{} }
if (-not $latestDrivers[$category].ContainsKey($namePrefix) -or $latestDrivers[$category][$namePrefix].Version -lt $version) {
$latestDrivers[$category][$namePrefix] = [PSCustomObject]@{
Name = $name
DownloadUrl = $downloadUrl
DriverFileName = $driverFileName
Version = $version
Category = $category
}
}
}
}
}
}
}
finally {
if ($null -ne $reader) {
$reader.Dispose()
}
}
WriteLog "Searching $($softwareComponents.Count) DRVR components in '$dellCatalogXML' for model '$modelName'..."
foreach ($component in $softwareComponents) { foreach ($component in $softwareComponents) {
# Check if SupportedSystems and Brand exist $models = $component.SupportedSystems.Brand.Model
if ($null -eq $component.SupportedSystems -or $null -eq $component.SupportedSystems.Brand) { continue } foreach ($m in $models) {
# Ensure Model is iterable if ($m.Display.'#cdata-section' -eq $modelDisplay) {
$componentModels = @($component.SupportedSystems.Brand.Model) $validOS = $component.SupportedOperatingSystems.OperatingSystem | Where-Object { $_.osArch -eq $WindowsArch }
if ($null -eq $componentModels) { continue } if (-not $validOS) { continue }
$modelMatch = $false
foreach ($item in $componentModels) {
# Check if Display and its CDATA section exist before accessing
if ($null -ne $item.Display -and $null -ne $item.Display.'#cdata-section' -and $item.Display.'#cdata-section'.Trim() -eq $modelName) {
$modelMatch = $true
break
}
}
if ($modelMatch) {
# Model matches, now check OS compatibility
$validOS = $null
if ($null -ne $component.SupportedOperatingSystems) {
# Ensure OperatingSystem is always an array/collection
$osList = @($component.SupportedOperatingSystems.OperatingSystem)
if ($null -ne $osList) {
if ($WindowsRelease -le 11) {
# Client OS check
$validOS = $osList | Where-Object { $_.osArch -eq $WindowsArch } | Select-Object -First 1
}
else {
# Server OS check
$osCodePattern = switch ($WindowsRelease) {
2016 { "W14" } # Note: Dell uses W14 for Server 2016
2019 { "W19" }
2022 { "W22" }
2025 { "W25" }
default { "W22" } # Fallback, adjust as needed
}
$validOS = $osList | Where-Object { ($_.osArch -eq $WindowsArch) -and ($_.osCode -match $osCodePattern) } | Select-Object -First 1
}
}
}
if ($validOS) {
$modelSpecificDriversFound = $true # Mark that we found at least one relevant driver component
$driverPath = $component.path $driverPath = $component.path
$downloadUrl = $baseLocation + $driverPath $downloadUrl = $baseLocation + $driverPath
$driverFileName = [System.IO.Path]::GetFileName($driverPath) $fileName = [IO.Path]::GetFileName($driverPath)
# Check if Name, Display, and CDATA exist $name = $component.Name.Display.'#cdata-section' -replace '[\\\/\:\*\?\"\<\>\| ]', '_' -replace '[\,]', '-'
$name = "UnknownDriver" # Default name $category = $component.Category.Display.'#cdata-section' -replace '[\\\/\:\*\?\"\<\>\| ]', '_'
if ($null -ne $component.Name -and $null -ne $component.Name.Display -and $null -ne $component.Name.Display.'#cdata-section') { $version = [version]$component.vendorVersion
$name = $component.Name.Display.'#cdata-section' $namePrefix = ($name -split '-')[0]
$name = $name -replace '[\\\/\:\*\?\"\<\>\| ]', '_' -replace '[\,]', '-' if (-not $latestDrivers[$category]) { $latestDrivers[$category] = @{} }
} if (-not $latestDrivers[$category][$namePrefix] -or $latestDrivers[$category][$namePrefix].Version -lt $version) {
# Check if Category, Display, and CDATA exist $latestDrivers[$category][$namePrefix] = [pscustomobject]@{
$category = "Uncategorized" # Default category
if ($null -ne $component.Category -and $null -ne $component.Category.Display -and $null -ne $component.Category.Display.'#cdata-section') {
$category = $component.Category.Display.'#cdata-section'
$category = $category -replace '[\\\/\:\*\?\"\<\>\| ]', '_'
}
$version = [version]"0.0" # Default version
if ($null -ne $component.vendorVersion) {
try { $version = [version]$component.vendorVersion } catch { WriteLog "Warning: Could not parse version '$($component.vendorVersion)' for driver '$name'. Using 0.0." }
}
$namePrefix = ($name -split '-')[0] # Group by prefix within category
# Store the latest version for each category/prefix combination
if (-not $latestDrivers.ContainsKey($category)) { $latestDrivers[$category] = @{} }
if (-not $latestDrivers[$category].ContainsKey($namePrefix) -or $latestDrivers[$category][$namePrefix].Version -lt $version) {
$latestDrivers[$category][$namePrefix] = [PSCustomObject]@{
Name = $name Name = $name
DownloadUrl = $downloadUrl DownloadUrl = $downloadUrl
DriverFileName = $driverFileName DriverFileName = $fileName
Version = $version Version = $version
Category = $category Category = $category
} }
} }
} }
} # End if ($modelMatch)
} # End foreach ($component in $softwareComponents)
if (-not $modelSpecificDriversFound) {
$status = "No drivers found for OS"
WriteLog "No drivers found for model '$modelName' matching Windows Release '$WindowsRelease' and Arch '$WindowsArch' in '$dellCatalogXML'."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
# Consider this success as the process completed, just no drivers to download
return [PSCustomObject]@{ Model = $modelName; Status = $status; Success = $true }
}
# 4. Download and Extract Found Drivers (Logic remains largely the same)
$totalDriversToProcess = ($latestDrivers.Values | ForEach-Object { $_.Values.Count } | Measure-Object -Sum).Sum
$driversProcessed = 0
WriteLog "Found $totalDriversToProcess latest driver packages to download for $modelName."
# Ensure base directories exist before loop
if (-not (Test-Path -Path $makeDriversPath)) { New-Item -Path $makeDriversPath -ItemType Directory -Force | Out-Null }
if (-not (Test-Path -Path $modelPath)) { New-Item -Path $modelPath -ItemType Directory -Force | Out-Null }
foreach ($category in $latestDrivers.Keys) {
foreach ($driver in $latestDrivers[$category].Values) {
$driversProcessed++
$status = "Downloading $($driversProcessed)/$($totalDriversToProcess): $($driver.Name)"
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
$downloadFolder = Join-Path -Path $modelPath -ChildPath $driver.Category
$driverFilePath = Join-Path -Path $downloadFolder -ChildPath $driver.DriverFileName
$extractFolder = Join-Path -Path $downloadFolder -ChildPath $driver.DriverFileName.TrimEnd($driver.DriverFileName[-4..-1])
# Check if already extracted (more robust check)
if (Test-Path -Path $extractFolder -PathType Container) {
$extractSize = (Get-ChildItem -Path $extractFolder -Recurse -Exclude *.log | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($extractSize -gt 1KB) {
WriteLog "Driver already extracted: $($driver.Name) in $extractFolder. Skipping."
continue # Skip to next driver
} }
} }
# Check if download file exists but extraction folder doesn't or is empty foreach ($cat in $latestDrivers.Keys) { foreach ($drv in $latestDrivers[$cat].Values) { $packages += $drv } }
if (Test-Path -Path $driverFilePath -PathType Leaf) {
WriteLog "Download file $($driver.DriverFileName) exists, but extraction folder '$extractFolder' is missing or empty. Will attempt extraction."
# Proceed to extraction logic below
} }
else {
# Download the driver if (-not $packages -or $packages.Count -eq 0) {
WriteLog "Downloading driver: $($driver.Name) ($($driver.DriverFileName))" if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status 'No drivers found for OS' }
if (-not (Test-Path -Path $downloadFolder)) { return [pscustomobject]@{ Model = $modelDisplay; Status = 'No drivers found for OS'; Success = $true; DriverPath = $driverRelativePath }
WriteLog "Creating download folder: $downloadFolder"
New-Item -Path $downloadFolder -ItemType Directory -Force | Out-Null
} }
WriteLog "Downloading from: $($driver.DownloadUrl) to $driverFilePath"
try { $total = $packages.Count
Start-BitsTransferWithRetry -Source $driver.DownloadUrl -Destination $driverFilePath $idx = 0
WriteLog "Driver downloaded: $($driver.DriverFileName)" foreach ($pkg in $packages) {
$idx++
$driverName = $pkg.Name
if ([string]::IsNullOrWhiteSpace($driverName)) { $driverName = $pkg.DriverFileName }
$status = "$idx/$total Downloading $driverName"
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status $status }
$categorySafe = ($pkg.Category -replace '[\\\/\:\*\?\"\<\>\| ]', '_')
$downloadFolder = Join-Path $modelPath $categorySafe
if (-not (Test-Path $downloadFolder)) { New-Item -Path $downloadFolder -ItemType Directory -Force | Out-Null }
$driverFilePath = Join-Path $downloadFolder $pkg.DriverFileName
$plainName = [IO.Path]::GetFileNameWithoutExtension($pkg.DriverFileName)
if ([string]::IsNullOrWhiteSpace($plainName)) { $plainName = "_extract" }
$extractFolder = Join-Path $downloadFolder $plainName
if (Test-Path $extractFolder) {
$sz = (Get-ChildItem -Path $extractFolder -Recurse -Exclude *.log | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($sz -gt 1KB) { continue }
} }
if (-not (Test-Path $driverFilePath)) {
WriteLog "$status URL: $($pkg.DownloadUrl)"
try { Start-BitsTransferWithRetry -Source $pkg.DownloadUrl -Destination $driverFilePath }
catch { catch {
WriteLog "Failed to download driver: $($driver.DownloadUrl). Error: $($_.Exception.Message). Skipping." $failureMessage = "Failed to download driver '$driverName' from $($pkg.DownloadUrl): $($_.Exception.Message)"
# Update status for this specific driver failure? Maybe too granular. WriteLog $failureMessage
continue # Skip to next driver throw (New-Object System.Exception($failureMessage, $_.Exception))
} }
} }
$status = "$idx/$total Extracting $driverName"
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status $status }
# Extract the driver if (-not (Test-Path $extractFolder)) { New-Item -Path $extractFolder -ItemType Directory -Force | Out-Null }
$status = "Extracting $($driversProcessed)/$($totalDriversToProcess): $($driver.Name)"
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
# Ensure extraction folder exists before attempting extraction $arg1 = "/s /e=`"$extractFolder`" /l=`"$extractFolder\log.log`""
if (-not (Test-Path -Path $extractFolder)) { $arg2 = "/s /drivers=`"$extractFolder`" /l=`"$extractFolder\log.log`""
WriteLog "Creating extraction folder: $extractFolder" $ok = $false
try {
Invoke-Process -FilePath $driverFilePath -ArgumentList $arg1 | Out-Null
$sz = (Get-ChildItem -Path $extractFolder -Recurse -Exclude *.log | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($sz -gt 1KB) { $ok = $true }
if (-not $ok) {
Remove-SafeFolder $extractFolder
New-Item -Path $extractFolder -ItemType Directory -Force | Out-Null New-Item -Path $extractFolder -ItemType Directory -Force | Out-Null
} Invoke-Process -FilePath $driverFilePath -ArgumentList $arg2 | Out-Null
$sz = (Get-ChildItem -Path $extractFolder -Recurse -Exclude *.log | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
# Dell uses /e to extact the entire DUP while /drivers to extract only the drivers if ($sz -gt 1KB) { $ok = $true }
# In many cases /drivers will extract drivers for mutliple OS versions
# Which can cause many duplicate files and bloat your driver folder
# /e seems to be better and only extracts what is necessary and has less issues
# We will default to using /e, but will fall back to /drivers if content cannot be found
$arguments = "/s /e=`"$extractFolder`" /l=`"$extractFolder\log.log`""
$altArguments = "/s /drivers=`"$extractFolder`" /l=`"$extractFolder\log.log`""
$extractionSuccess = $false
try {
# Handle special cases (Chipset/Network) - Check if OS is Server
$osInfo = Get-CimInstance -ClassName Win32_OperatingSystem # Get OS info within the task scope
$isServer = $osInfo.Caption -match 'server'
# Chipset drivers may require killing child processes in some cases
if ($driver.Category -eq "Chipset") {
WriteLog "Extracting Chipset driver: $driverFilePath $arguments"
$process = Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments -Wait $false
Start-Sleep -Seconds 5 # Allow time for extraction
WriteLog "Extraction exited with exit code: $($process.ExitCode)"
# Attempt to gracefully close child process if needed (logic from original script)
$childProcesses = Get-CimInstance Win32_Process -Filter "ParentProcessId = $($process.Id)"
if ($childProcesses) {
$latestProcess = $childProcesses | Sort-Object CreationDate -Descending | Select-Object -First 1
WriteLog "Stopping child process for Chipset driver: $($latestProcess.Name) (PID: $($latestProcess.ProcessId))"
Stop-Process -Id $latestProcess.ProcessId -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 1
}
}
# Network drivers on client OS may require killing child processes
elseif ($driver.Category -eq "Network" -and -not $isServer) {
WriteLog "Extracting Network driver: $driverFilePath $arguments"
$process = Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments -Wait $false
Start-Sleep -Seconds 5
WriteLog "Extraction exited with exit code: $($process.ExitCode)"
if (-not $process.HasExited) {
$childProcesses = Get-CimInstance Win32_Process -Filter "ParentProcessId = $($process.Id)"
if ($childProcesses) {
$latestProcess = $childProcesses | Sort-Object CreationDate -Descending | Select-Object -First 1
WriteLog "Stopping child process for Network driver: $($latestProcess.Name) (PID: $($latestProcess.ProcessId))"
Stop-Process -Id $latestProcess.ProcessId -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 1
}
}
}
else {
WriteLog "Extracting driver: $driverFilePath $arguments"
$process = Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments
WriteLog "Extraction exited with exit code: $($process.ExitCode)"
}
# Verify extraction (check if folder has content)
if (Test-Path -Path $extractFolder -PathType Container) {
$extractSize = (Get-ChildItem -Path $extractFolder -Recurse -Exclude *.log | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($extractSize -gt 1KB) {
$extractionSuccess = $true
WriteLog "Extraction successful (Method 1) for $driverFilePath $arguments"
}
}
# If primary extraction failed or folder is empty, try alternative
if (-not $extractionSuccess) {
# $arguments = "/s /e=`"$extractFolder`""
# $altArguments = "/s /drivers=`"$extractFolder`""
WriteLog "Extraction with $arguments failed or resulted in empty folder for $driverFilePath. Retrying with $altArguments"
# Clean up potentially empty folder before retrying
Remove-Item -Path $extractFolder -Recurse -Force -ErrorAction SilentlyContinue
New-Item -Path $extractFolder -ItemType Directory -Force | Out-Null # Recreate empty folder
$process = Invoke-Process -FilePath $driverFilePath -ArgumentList $altArguments
WriteLog "Extraction exited with exit code: $($process.ExitCode)"
# Verify extraction again
if (Test-Path -Path $extractFolder -PathType Container) {
$extractSize = (Get-ChildItem -Path $extractFolder -Recurse -Exclude *.log | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($extractSize -gt 1KB) {
$extractionSuccess = $true
WriteLog "Extraction successful (Method 2) for $driverFilePath $altArguments"
}
}
} }
} }
catch { catch {
WriteLog "Error during extraction process for $($driver.DriverFileName): $($_.Exception.Message). Trying alternative method." WriteLog "Extraction error: $($_.Exception.Message)"
# Try alternative method on any error during the first attempt block
try {
if (Test-Path -Path $extractFolder) {
# Clean up before retry if needed
Remove-Item -Path $extractFolder -Recurse -Force -ErrorAction SilentlyContinue
}
New-Item -Path $extractFolder -ItemType Directory -Force | Out-Null
# $arguments = "/s /e=`"$extractFolder`""
# $altArguments = "/s /drivers=`"$extractFolder`""
WriteLog "Extracting driver (Method 2): $driverFilePath $altArguments"
$process = Invoke-Process -FilePath $driverFilePath -ArgumentList $altArguments
WriteLog "Extraction exited with exit code: $($process.ExitCode)"
# Verify extraction again
if (Test-Path -Path $extractFolder -PathType Container) {
$extractSize = (Get-ChildItem -Path $extractFolder -Recurse -Exclude *.log | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($extractSize -gt 1KB) {
$extractionSuccess = $true
WriteLog "Extraction successful (Method 2) for $driverFilePath."
}
}
}
catch {
WriteLog "Alternative extraction method also failed for $($driver.DriverFileName): $($_.Exception.Message)."
# Extraction failed completely
}
} }
# Cleanup downloaded file only if extraction was successful if ($ok) {
if ($extractionSuccess) { Remove-Item $driverFilePath -Force -ErrorAction SilentlyContinue
WriteLog "Deleting driver file: $driverFilePath"
Remove-Item -Path $driverFilePath -Force -ErrorAction SilentlyContinue
WriteLog "Driver file deleted: $driverFilePath"
} }
else { else {
WriteLog "Extraction failed for $($driver.DriverFileName). Downloaded file kept at $driverFilePath for inspection." $failureMessage = "Failed to extract driver '$driverName'."
# Update status to indicate partial failure? WriteLog $failureMessage
throw (New-Object System.Exception($failureMessage))
}
} }
} # End foreach ($driver in $latestDrivers)
} # End foreach ($category in $latestDrivers)
# --- Compress to WIM if requested (after all drivers processed) ---
if ($CompressToWim) { if ($CompressToWim) {
$status = "Compressing..." if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status 'Compressing...' }
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status } $wimPath = Join-Path $makeDriversPath "$sanitizedModelName.wim"
$wimFileName = "$($modelName).wim"
$destinationWimPath = Join-Path -Path $makeDriversPath -ChildPath $wimFileName
$driverRelativePath = Join-Path -Path $make -ChildPath $wimFileName # Update relative path to the WIM file
WriteLog "Compressing '$modelPath' to '$destinationWimPath'..."
try { try {
$compressResult = Compress-DriverFolderToWim -SourceFolderPath $modelPath -DestinationWimPath $destinationWimPath -WimName $modelName -WimDescription $modelName -ErrorAction Stop $null = Compress-DriverFolderToWim -SourceFolderPath $modelPath -DestinationWimPath $wimPath -WimName $modelDisplay -WimDescription $modelDisplay -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
if ($compressResult) { $driverRelativePath = Join-Path $make "$sanitizedModelName.wim"
WriteLog "Compression successful for '$modelName'." $statusFinal = 'Completed & Compressed'
$status = "Completed & Compressed"
}
else {
WriteLog "Compression failed for '$modelName'. Check verbose/error output from Compress-DriverFolderToWim."
$status = "Completed (Compression Failed)"
}
} }
catch { catch {
WriteLog "Error during compression for '$modelName': $($_.Exception.Message)" WriteLog "Compression failed for $($modelDisplay): $($_.Exception.Message)"
$status = "Completed (Compression Error)" $statusFinal = 'Completed (Compression Failed)'
} }
} }
else { else {
$status = "Completed" # Final status if not compressing $statusFinal = 'Completed'
} }
# --- End Compression ---
$success = $true # Mark success as download/extract was okay
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status $statusFinal }
return [pscustomobject]@{ Model = $modelDisplay; Status = $statusFinal; Success = $true; DriverPath = $driverRelativePath }
} }
catch { catch {
$status = "Error: $($_.Exception.Message.Split('.')[0])" # Shorten error message $errorStatus = "Error: $($_.Exception.Message)"
WriteLog "Error saving Dell drivers for $($modelName): $($_.Exception.ToString())" # Log full exception string WriteLog "Save-DellDriversTask error for $($modelDisplay): $($_.Exception.ToString())"
$success = $false Remove-DriverModelFolder -DriversFolder $DriversFolder -TargetFolder $modelPath -Description $modelDisplay
# Enqueue the error status before returning if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status $errorStatus }
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status } return [pscustomobject]@{ Model = $modelDisplay; Status = $errorStatus; Success = $false; DriverPath = $null }
# Ensure return object is created even on error
return [PSCustomObject]@{ Model = $modelName; Status = $status; Success = $success; DriverPath = $null }
} }
# Enqueue the final status (success or error) before returning
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
# Return the final status
return [PSCustomObject]@{ Model = $modelName; Status = $status; Success = $success; DriverPath = $driverRelativePath }
} }
Export-ModuleMember -Function * Export-ModuleMember -Function *
@@ -66,30 +66,45 @@ function Get-HPDriversModelList {
$settings.Async = $false # Ensure synchronous reading $settings.Async = $false # Ensure synchronous reading
$reader = [System.Xml.XmlReader]::Create($platformListXml, $settings) $reader = [System.Xml.XmlReader]::Create($platformListXml, $settings)
$uniqueModels = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $uniqueEntries = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
while ($reader.Read()) { while ($reader.Read()) {
if ($reader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $reader.Name -eq 'Platform') { if ($reader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $reader.Name -eq 'Platform') {
# Read the inner content of the Platform node
$platformReader = $reader.ReadSubtree() $platformReader = $reader.ReadSubtree()
$platformNames = [System.Collections.Generic.List[string]]::new()
$platformSystemId = $null
while ($platformReader.Read()) { while ($platformReader.Read()) {
if ($platformReader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $platformReader.Name -eq 'ProductName') { if ($platformReader.NodeType -eq [System.Xml.XmlNodeType]::Element) {
if ($platformReader.Name -eq 'ProductName') {
$modelName = $platformReader.ReadElementContentAsString() $modelName = $platformReader.ReadElementContentAsString()
if (-not [string]::IsNullOrWhiteSpace($modelName) -and $uniqueModels.Add($modelName)) { if (-not [string]::IsNullOrWhiteSpace($modelName)) {
# Add to list only if it's a new unique model $platformNames.Add($modelName.Trim())
$modelList.Add([PSCustomObject]@{ }
Make = $Make }
Model = $modelName elseif ($platformReader.Name -eq 'SystemID') {
}) $platformSystemId = $platformReader.ReadElementContentAsString().Trim()
} }
} }
} }
$platformReader.Close() $platformReader.Close()
foreach ($name in $platformNames) {
$systemIdKey = if (-not [string]::IsNullOrWhiteSpace($platformSystemId)) { $platformSystemId } else { '' }
$compositeKey = "$name|$systemIdKey"
if ($uniqueEntries.Add($compositeKey)) {
$modelList.Add([PSCustomObject]@{
Make = $Make
Model = $name
SystemId = $platformSystemId
})
}
}
} }
} }
$reader.Close() $reader.Close()
WriteLog "Successfully parsed $($modelList.Count) unique HP models from PlatformList.xml." WriteLog "Successfully parsed $($modelList.Count) HP model and SystemID combinations from PlatformList.xml."
} }
catch { catch {
@@ -97,7 +112,7 @@ function Get-HPDriversModelList {
} }
# Sort the list alphabetically by Model name before returning # Sort the list alphabetically by Model name before returning
return $modelList | Sort-Object -Property Model return $modelList | Sort-Object -Property Model, SystemId
} }
# Function to download and extract drivers for a specific HP model (Designed for ForEach-Object -Parallel) # Function to download and extract drivers for a specific HP model (Designed for ForEach-Object -Parallel)
function Save-HPDriversTask { function Save-HPDriversTask {
@@ -118,13 +133,22 @@ function Save-HPDriversTask {
[Parameter()] # Made optional [Parameter()] # Made optional
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null, # Default to null [System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null, # Default to null
[Parameter()] [Parameter()]
[bool]$CompressToWim = $false # New parameter for compression [bool]$CompressToWim = $false, # New parameter for compression
[Parameter()]
[bool]$PreserveSourceOnCompress = $false
) )
$modelName = $DriverItemData.Model $displayModelName = if (-not [string]::IsNullOrWhiteSpace($DriverItemData.Model)) { $DriverItemData.Model } else { $DriverItemData.Id }
$make = $DriverItemData.Make # Should be 'HP' $make = $DriverItemData.Make # Should be 'HP'
$identifier = $modelName # Unique identifier for progress updates $productName = if ($DriverItemData.PSObject.Properties['ProductName'] -and -not [string]::IsNullOrWhiteSpace($DriverItemData.ProductName)) { $DriverItemData.ProductName } else { ConvertTo-DriverBaseName -ModelString $displayModelName }
$sanitizedModelName = $modelName -replace '[\\/:"*?<>|]', '_' if ([string]::IsNullOrWhiteSpace($productName)) { $productName = $displayModelName }
$systemIdentifier = if ($DriverItemData.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($DriverItemData.SystemId)) { $DriverItemData.SystemId } else { $null }
if ([string]::IsNullOrWhiteSpace($displayModelName)) {
$displayModelName = if ([string]::IsNullOrWhiteSpace($systemIdentifier)) { $productName } else { Get-DriverDisplayName -BaseName $productName -Identifier $systemIdentifier }
}
$identifier = $displayModelName # Unique identifier for progress updates
$sanitizedModelName = ConvertTo-SafeName -Name $identifier
if ($sanitizedModelName -ne $identifier) { WriteLog "Sanitized model name: '$identifier' -> '$sanitizedModelName'" }
$hpDriversBaseFolder = Join-Path -Path $DriversFolder -ChildPath $make # Changed variable name for clarity $hpDriversBaseFolder = Join-Path -Path $DriversFolder -ChildPath $make # Changed variable name for clarity
$platformListXml = Join-Path -Path $hpDriversBaseFolder -ChildPath "PlatformList.xml" $platformListXml = Join-Path -Path $hpDriversBaseFolder -ChildPath "PlatformList.xml"
$modelSpecificFolder = Join-Path -Path $hpDriversBaseFolder -ChildPath $sanitizedModelName # Sanitize model name for folder path $modelSpecificFolder = Join-Path -Path $hpDriversBaseFolder -ChildPath $sanitizedModelName # Sanitize model name for folder path
@@ -132,7 +156,16 @@ function Save-HPDriversTask {
$finalStatus = "" # Initialize final status $finalStatus = "" # Initialize final status
$successState = $true # Assume success unless an operation fails $successState = $true # Assume success unless an operation fails
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Checking HP drivers for $modelName..." } if (-not (Test-Path -Path $DriversFolder -PathType Container)) {
WriteLog "Creating Drivers folder: $DriversFolder"
New-Item -Path $DriversFolder -ItemType Directory -Force | Out-Null
}
if (-not (Test-Path -Path $hpDriversBaseFolder -PathType Container)) {
WriteLog "Creating HP drivers folder: $hpDriversBaseFolder"
New-Item -Path $hpDriversBaseFolder -ItemType Directory -Force | Out-Null
}
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Checking HP drivers for $displayModelName..." }
try { try {
# Check for existing drivers # Check for existing drivers
@@ -146,13 +179,14 @@ function Save-HPDriversTask {
# Special handling for existing folders that need compression # Special handling for existing folders that need compression
if ($CompressToWim -and $existingDriver.Status -eq 'Already downloaded') { if ($CompressToWim -and $existingDriver.Status -eq 'Already downloaded') {
$wimFilePath = Join-Path -Path $hpDriversBaseFolder -ChildPath "$($sanitizedModelName).wim" $wimFilePath = Join-Path -Path $hpDriversBaseFolder -ChildPath "$($sanitizedModelName).wim"
$wimRelativePath = Join-Path -Path $make -ChildPath "$($sanitizedModelName).wim"
$sourceFolderPath = Join-Path -Path $hpDriversBaseFolder -ChildPath $sanitizedModelName $sourceFolderPath = Join-Path -Path $hpDriversBaseFolder -ChildPath $sanitizedModelName
WriteLog "Attempting compression of existing folder '$sourceFolderPath' to '$wimFilePath'." WriteLog "Attempting compression of existing folder '$sourceFolderPath' to '$wimFilePath'."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Compressing existing..." } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Compressing existing..." }
try { try {
Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -ErrorAction Stop $null = Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
$existingDriver.Status = "Already downloaded & Compressed" $existingDriver.Status = "Compression successful"
$existingDriver.DriverPath = Join-Path -Path $make -ChildPath "$($sanitizedModelName).wim" $existingDriver.DriverPath = $wimRelativePath
$existingDriver.Success = $true $existingDriver.Success = $true
WriteLog "Successfully compressed existing drivers for $identifier to $wimFilePath." WriteLog "Successfully compressed existing drivers for $identifier to $wimFilePath."
} }
@@ -187,13 +221,23 @@ function Save-HPDriversTask {
} }
} }
# Parse PlatformList.xml to find SystemID and OSReleaseID for the specific model # Parse the PlatformList.xml to find the SystemID based on the ProductName
WriteLog "Parsing $platformListXml for model '$modelName' details..." WriteLog "Parsing $platformListXml for model '$displayModelName' (SystemID: $systemIdentifier) details..."
[xml]$platformListContent = Get-Content -Path $platformListXml -Raw -Encoding UTF8 -ErrorAction Stop [xml]$platformListContent = Get-Content -Path $platformListXml -Raw -Encoding UTF8 -ErrorAction Stop
$platformNode = $platformListContent.ImagePal.Platform | Where-Object { $_.ProductName.'#text' -match "^$([regex]::Escape($modelName))$" } | Select-Object -First 1 $platformNode = $null
if (-not [string]::IsNullOrWhiteSpace($systemIdentifier)) {
$platformNode = $platformListContent.ImagePal.Platform | Where-Object { $_.SystemID -eq $systemIdentifier } | Select-Object -First 1
if ($null -eq $platformNode) {
WriteLog "SystemID '$systemIdentifier' not found in PlatformList.xml. Falling back to ProductName search."
}
}
if ($null -eq $platformNode) {
$searchName = if (-not [string]::IsNullOrWhiteSpace($productName)) { $productName } else { $displayModelName }
$platformNode = $platformListContent.ImagePal.Platform | Where-Object { $_.ProductName.'#text' -match "^$([regex]::Escape($searchName))$" } | Select-Object -First 1
}
if ($null -eq $platformNode) { if ($null -eq $platformNode) {
throw "Model '$modelName' not found in PlatformList.xml." throw "Model '$displayModelName' (SystemID: $systemIdentifier) not found in PlatformList.xml."
} }
$systemID = $platformNode.SystemID $systemID = $platformNode.SystemID
@@ -286,11 +330,11 @@ function Save-HPDriversTask {
} }
$availableVersionsString = ($allAvailableVersions | Select-Object -Unique) -join ', ' $availableVersionsString = ($allAvailableVersions | Select-Object -Unique) -join ', '
if ([string]::IsNullOrWhiteSpace($availableVersionsString)) { $availableVersionsString = "None" } if ([string]::IsNullOrWhiteSpace($availableVersionsString)) { $availableVersionsString = "None" }
throw "Could not find any suitable OS driver pack for model '$modelName' matching requested or fallback versions (Win$($WindowsRelease) $WindowsVersion). Available: $availableVersionsString" throw "Could not find any suitable OS driver pack for model '$displayModelName' matching requested or fallback versions (Win$($WindowsRelease) $WindowsVersion). Available: $availableVersionsString"
} }
$osReleaseIdFileName = $selectedOSNode.OSReleaseIdFileName -replace 'H', 'h' $osReleaseIdFileName = $selectedOSNode.OSReleaseIdFileName -replace 'H', 'h'
WriteLog "Using SystemID: $systemID and OS Info: Win$($selectedOSRelease) ($($selectedOSVersion)) for '$modelName'" WriteLog "Using SystemID: $systemID and OS Info: Win$($selectedOSRelease) ($($selectedOSVersion)) for '$displayModelName'"
$archSuffix = $WindowsArch -replace "^x", "" $archSuffix = $WindowsArch -replace "^x", ""
$modelRelease = "$($systemID)_$($archSuffix)_$($selectedOSRelease).0.$($selectedOSVersion.ToLower())" $modelRelease = "$($systemID)_$($archSuffix)_$($selectedOSRelease).0.$($selectedOSVersion.ToLower())"
$driverCabUrl = "https://hpia.hpcloud.hp.com/ref/$systemID/$modelRelease.cab" $driverCabUrl = "https://hpia.hpcloud.hp.com/ref/$systemID/$modelRelease.cab"
@@ -309,7 +353,7 @@ function Save-HPDriversTask {
$updates = $driverXmlContent.ImagePal.Solutions.UpdateInfo | Where-Object { $_.Category -match '^Driver' } $updates = $driverXmlContent.ImagePal.Solutions.UpdateInfo | Where-Object { $_.Category -match '^Driver' }
$totalDrivers = ($updates | Measure-Object).Count $totalDrivers = ($updates | Measure-Object).Count
$downloadedCount = 0 $downloadedCount = 0
WriteLog "Found $totalDrivers driver updates for $modelName." WriteLog "Found $totalDrivers driver updates for $displayModelName."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Found $totalDrivers drivers. Downloading..." } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Found $totalDrivers drivers. Downloading..." }
if (-not (Test-Path -Path $modelSpecificFolder)) { if (-not (Test-Path -Path $modelSpecificFolder)) {
@@ -327,7 +371,7 @@ function Save-HPDriversTask {
$extractFolder = Join-Path -Path $downloadFolder -ChildPath ($driverName + "_" + $version + "_" + ($driverFileName -replace '\.exe$', '')) $extractFolder = Join-Path -Path $downloadFolder -ChildPath ($driverName + "_" + $version + "_" + ($driverFileName -replace '\.exe$', ''))
$downloadedCount++ $downloadedCount++
$progressMsg = "($downloadedCount/$totalDrivers) Downloading $driverName..." $progressMsg = "$downloadedCount/$totalDrivers Downloading $driverName"
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $progressMsg } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $progressMsg }
WriteLog "$progressMsg URL: $driverUrl" WriteLog "$progressMsg URL: $driverUrl"
@@ -341,6 +385,8 @@ function Save-HPDriversTask {
WriteLog "Downloading driver to: $driverFilePath" WriteLog "Downloading driver to: $driverFilePath"
Start-BitsTransferWithRetry -Source $driverUrl -Destination $driverFilePath -ErrorAction Stop Start-BitsTransferWithRetry -Source $driverUrl -Destination $driverFilePath -ErrorAction Stop
WriteLog "Driver downloaded: $driverFilePath" WriteLog "Driver downloaded: $driverFilePath"
$progressMsg = "$downloadedCount/$totalDrivers Extracting $driverName"
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $progressMsg }
WriteLog "Creating extraction folder: $extractFolder" WriteLog "Creating extraction folder: $extractFolder"
New-Item -Path $extractFolder -ItemType Directory -Force -ErrorAction Stop | Out-Null New-Item -Path $extractFolder -ItemType Directory -Force -ErrorAction Stop | Out-Null
$arguments = "/s /e /f `"$extractFolder`"" $arguments = "/s /e /f `"$extractFolder`""
@@ -354,7 +400,7 @@ function Save-HPDriversTask {
} }
Remove-Item -Path $driverCabFile, $driverXmlFile -Force -ErrorAction SilentlyContinue Remove-Item -Path $driverCabFile, $driverXmlFile -Force -ErrorAction SilentlyContinue
WriteLog "Cleaned up driver cab and xml files for $modelName" WriteLog "Cleaned up driver cab and xml files for $displayModelName"
$finalStatus = "Completed" $finalStatus = "Completed"
if ($CompressToWim) { if ($CompressToWim) {
@@ -362,7 +408,7 @@ function Save-HPDriversTask {
$wimFilePath = Join-Path -Path $hpDriversBaseFolder -ChildPath "$($identifier).wim" $wimFilePath = Join-Path -Path $hpDriversBaseFolder -ChildPath "$($identifier).wim"
WriteLog "Compressing '$modelSpecificFolder' to '$wimFilePath'..." WriteLog "Compressing '$modelSpecificFolder' to '$wimFilePath'..."
try { try {
Compress-DriverFolderToWim -SourceFolderPath $modelSpecificFolder -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -ErrorAction Stop $null = Compress-DriverFolderToWim -SourceFolderPath $modelSpecificFolder -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
WriteLog "Compression successful for '$identifier'." WriteLog "Compression successful for '$identifier'."
$finalStatus = "Completed & Compressed" $finalStatus = "Completed & Compressed"
$driverRelativePath = Join-Path -Path $make -ChildPath "$($identifier).wim" # Update relative path to the WIM $driverRelativePath = Join-Path -Path $make -ChildPath "$($identifier).wim" # Update relative path to the WIM
@@ -375,15 +421,12 @@ function Save-HPDriversTask {
$successState = $true $successState = $true
} }
catch { catch {
$errorMessage = "Error saving HP drivers for $($modelName): $($_.Exception.Message)" $errorMessage = "Error saving HP drivers for $($displayModelName): $($_.Exception.Message)"
WriteLog $errorMessage WriteLog $errorMessage
$finalStatus = "Error: $($_.Exception.Message.Split([Environment]::NewLine)[0])" $finalStatus = "Error: $($_.Exception.Message.Split([Environment]::NewLine)[0])"
$successState = $false $successState = $false
$driverRelativePath = $null # Ensure path is null on error $driverRelativePath = $null # Ensure path is null on error
if (Test-Path -Path $modelSpecificFolder -PathType Container) { Remove-DriverModelFolder -DriversFolder $DriversFolder -TargetFolder $modelSpecificFolder -Description $identifier
WriteLog "Attempting to remove partially created folder $modelSpecificFolder due to error."
Remove-Item -Path $modelSpecificFolder -Recurse -Force -ErrorAction SilentlyContinue
}
} }
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $finalStatus } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $finalStatus }
@@ -94,17 +94,20 @@ function Save-LenovoDriversTask {
[hashtable]$Headers, [hashtable]$Headers,
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$UserAgent, [string]$UserAgent,
[Parameter()] # Made optional [Parameter()]
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null, [System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null,
[Parameter()] [Parameter()]
[bool]$CompressToWim = $false [bool]$CompressToWim = $false,
[Parameter()]
[bool]$PreserveSourceOnCompress = $false
) )
# The Model property from the UI already contains the combined "ProductName (MachineType)" string # The Model property from the UI already contains the combined "ProductName (MachineType)" string
$identifier = $DriverItemData.Model $identifier = $DriverItemData.Model
$machineType = $DriverItemData.MachineType $machineType = $DriverItemData.MachineType
$make = "Lenovo" $make = "Lenovo"
$sanitizedIdentifier = $identifier -replace '[\\/:"*?<>|]', '_' $sanitizedIdentifier = ConvertTo-SafeName -Name $identifier
if ($sanitizedIdentifier -ne $identifier) { WriteLog "Sanitized model identifier: '$identifier' -> '$sanitizedIdentifier'" }
$status = "Starting..." $status = "Starting..."
$success = $false $success = $false
@@ -129,13 +132,14 @@ function Save-LenovoDriversTask {
# Special handling for existing folders that need compression # Special handling for existing folders that need compression
if ($CompressToWim -and $existingDriver.Status -eq 'Already downloaded') { if ($CompressToWim -and $existingDriver.Status -eq 'Already downloaded') {
$wimFilePath = Join-Path -Path $makeDriversPath -ChildPath "$($sanitizedIdentifier).wim" $wimFilePath = Join-Path -Path $makeDriversPath -ChildPath "$($sanitizedIdentifier).wim"
$wimRelativePath = Join-Path -Path $make -ChildPath "$($sanitizedIdentifier).wim"
$sourceFolderPath = Join-Path -Path $makeDriversPath -ChildPath $sanitizedIdentifier $sourceFolderPath = Join-Path -Path $makeDriversPath -ChildPath $sanitizedIdentifier
WriteLog "Attempting compression of existing folder '$sourceFolderPath' to '$wimFilePath'." WriteLog "Attempting compression of existing folder '$sourceFolderPath' to '$wimFilePath'."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Compressing existing..." } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Compressing existing..." }
try { try {
Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -ErrorAction Stop $null = Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
$existingDriver.Status = "Already downloaded & Compressed" $existingDriver.Status = "Compression successful"
$existingDriver.DriverPath = Join-Path -Path $make -ChildPath "$($sanitizedIdentifier).wim" $existingDriver.DriverPath = $wimRelativePath
$existingDriver.Success = $true $existingDriver.Success = $true
WriteLog "Successfully compressed existing drivers for $identifier to $wimFilePath." WriteLog "Successfully compressed existing drivers for $identifier to $wimFilePath."
} }
@@ -204,17 +208,16 @@ function Save-LenovoDriversTask {
$packageXMLPath = Join-Path -Path $tempDownloadPath -ChildPath $packageName $packageXMLPath = Join-Path -Path $tempDownloadPath -ChildPath $packageName
$baseURL = $packageUrl -replace [regex]::Escape($packageName), "" # Base URL for the driver file $baseURL = $packageUrl -replace [regex]::Escape($packageName), "" # Base URL for the driver file
$status = "($processedPackages/$totalPackages) Getting package info..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
# Download the package XML # Download the package XML
WriteLog "($processedPackages/$totalPackages) Downloading package XML: $packageUrl" WriteLog "($processedPackages/$totalPackages) Downloading package XML: $packageUrl"
try { try {
Start-BitsTransferWithRetry -Source $packageUrl -Destination $packageXMLPath Start-BitsTransferWithRetry -Source $packageUrl -Destination $packageXMLPath
} }
catch { catch {
WriteLog "($processedPackages/$totalPackages) Failed to download package XML '$packageUrl'. Skipping. Error: $($_.Exception.Message)" $failureMessage = "Failed to download Lenovo package XML '$packageUrl': $($_.Exception.Message)"
continue # Skip this package WriteLog "($processedPackages/$totalPackages) $failureMessage"
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue
throw (New-Object System.Exception($failureMessage, $_.Exception))
} }
# Load and parse the package XML # Load and parse the package XML
@@ -275,7 +278,7 @@ function Save-LenovoDriversTask {
} }
# Download the driver .exe # Download the driver .exe
$status = "($processedPackages/$totalPackages) Downloading $packageTitle..." $status = "$processedPackages/$totalPackages Downloading $packageTitle"
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
WriteLog "($processedPackages/$totalPackages) Downloading driver: $driverUrl to $driverFilePath" WriteLog "($processedPackages/$totalPackages) Downloading driver: $driverUrl to $driverFilePath"
try { try {
@@ -283,13 +286,14 @@ function Save-LenovoDriversTask {
WriteLog "($processedPackages/$totalPackages) Driver downloaded: $driverFileName" WriteLog "($processedPackages/$totalPackages) Driver downloaded: $driverFileName"
} }
catch { catch {
WriteLog "($processedPackages/$totalPackages) Failed to download driver '$driverUrl'. Skipping. Error: $($_.Exception.Message)" $failureMessage = "Failed to download driver '$packageTitle' from $($driverUrl): $($_.Exception.Message)"
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue # Clean up package XML WriteLog "($processedPackages/$totalPackages) $failureMessage"
continue # Skip this driver Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue
throw (New-Object System.Exception($failureMessage, $_.Exception))
} }
# --- Extraction Logic --- # --- Extraction Logic ---
$status = "($processedPackages/$totalPackages) Extracting $packageTitle..." $status = "$processedPackages/$totalPackages Extracting $packageTitle"
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
# Always use a temporary extraction path to avoid long path issues # Always use a temporary extraction path to avoid long path issues
@@ -314,7 +318,7 @@ function Save-LenovoDriversTask {
# Modify the extract command to point to the temporary folder # Modify the extract command to point to the temporary folder
$modifiedExtractCommand = $extractCommand -replace '%PACKAGEPATH%', "`"$extractFolder`"" $modifiedExtractCommand = $extractCommand -replace '%PACKAGEPATH%', "`"$extractFolder`""
WriteLog "($processedPackages/$totalPackages) Extracting driver: $driverFilePath using command: $modifiedExtractCommand" WriteLog "$processedPackages/$totalPackages Extracting driver: $driverFilePath using command: $modifiedExtractCommand"
try { try {
Invoke-Process -FilePath $driverFilePath -ArgumentList $modifiedExtractCommand -Wait $true | Out-Null Invoke-Process -FilePath $driverFilePath -ArgumentList $modifiedExtractCommand -Wait $true | Out-Null
@@ -322,14 +326,13 @@ function Save-LenovoDriversTask {
$extractionSucceeded = $true $extractionSucceeded = $true
} }
catch { catch {
WriteLog "($processedPackages/$totalPackages) Failed to extract driver '$driverFilePath' to temporary path. Skipping. Error: $($_.Exception.Message)" $failureMessage = "Failed to extract driver package '$packageTitle': $($_.Exception.Message)"
# Don't delete the downloaded exe yet if extraction fails WriteLog "($processedPackages/$totalPackages) $failureMessage"
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue # Clean up package XML Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue
# Clean up temp folder if extraction failed
if ($tempExtractBase -and (Test-Path -Path $tempExtractBase)) { if ($tempExtractBase -and (Test-Path -Path $tempExtractBase)) {
Remove-Item -Path $tempExtractBase -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -Path $tempExtractBase -Recurse -Force -ErrorAction SilentlyContinue
} }
continue # Skip further processing for this driver throw (New-Object System.Exception($failureMessage, $_.Exception))
} }
# --- Post-Extraction Handling (Move from Temp to Final Destination) --- # --- Post-Extraction Handling (Move from Temp to Final Destination) ---
@@ -372,10 +375,9 @@ function Save-LenovoDriversTask {
Move-Item -Path $item.FullName -Destination $finalDestinationPath -Force -ErrorAction Stop Move-Item -Path $item.FullName -Destination $finalDestinationPath -Force -ErrorAction Stop
} }
catch { catch {
WriteLog "($processedPackages/$totalPackages) Failed to move item '$($item.FullName)' to '$finalDestinationPath'. Error: $($_.Exception.Message)" $failureMessage = "Failed to move extracted item '$($item.FullName)' to '$finalDestinationPath': $($_.Exception.Message)"
# Decide if this should stop the whole process or just skip this item WriteLog "($processedPackages/$totalPackages) $failureMessage"
# For now, we'll log and continue, but mark overall success as false throw (New-Object System.Exception($failureMessage, $_.Exception))
$extractionSucceeded = $false
} }
} # End foreach ($item in $extractedItems) } # End foreach ($item in $extractedItems)
@@ -412,6 +414,9 @@ function Save-LenovoDriversTask {
# Always delete the package XML # Always delete the package XML
WriteLog "($processedPackages/$totalPackages) Deleting package XML file: $packageXMLPath" WriteLog "($processedPackages/$totalPackages) Deleting package XML file: $packageXMLPath"
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue
if (-not $extractionSucceeded) {
throw (New-Object System.Exception("Failed to extract driver '$packageTitle'. See log for details."))
}
} # End foreach package } # End foreach package
@@ -424,7 +429,7 @@ function Save-LenovoDriversTask {
$driverRelativePath = Join-Path -Path $make -ChildPath $wimFileName # Update relative path to the WIM file $driverRelativePath = Join-Path -Path $make -ChildPath $wimFileName # Update relative path to the WIM file
WriteLog "Compressing '$modelPath' to '$destinationWimPath'..." WriteLog "Compressing '$modelPath' to '$destinationWimPath'..."
try { try {
$compressResult = Compress-DriverFolderToWim -SourceFolderPath $modelPath -DestinationWimPath $destinationWimPath -WimName $identifier -WimDescription $identifier -ErrorAction Stop $compressResult = Compress-DriverFolderToWim -SourceFolderPath $modelPath -DestinationWimPath $destinationWimPath -WimName $identifier -WimDescription $identifier -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
if ($compressResult) { if ($compressResult) {
WriteLog "Compression successful for '$identifier'." WriteLog "Compression successful for '$identifier'."
$status = "Completed & Compressed" $status = "Completed & Compressed"
@@ -448,12 +453,11 @@ function Save-LenovoDriversTask {
} }
catch { catch {
$status = "Error: $($_.Exception.Message.Split('.')[0])" # Shorten error message $status = "Error: $($_.Exception.Message)"
WriteLog "Error saving Lenovo drivers for '$identifier': $($_.Exception.ToString())" # Log full exception string WriteLog "Error saving Lenovo drivers for '$identifier': $($_.Exception.ToString())"
$success = $false $success = $false
# Enqueue the error status before returning Remove-DriverModelFolder -DriversFolder $DriversFolder -TargetFolder $modelPath -Description $identifier
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
# Ensure return object is created even on error
return [PSCustomObject]@{ Identifier = $identifier; Status = $status; Success = $success; DriverPath = $null } return [PSCustomObject]@{ Identifier = $identifier; Status = $status; Success = $success; DriverPath = $null }
} }
finally { finally {
@@ -94,7 +94,9 @@ function Save-MicrosoftDriversTask {
[Parameter()] # Made optional [Parameter()] # Made optional
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null, # Default to null [System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null, # Default to null
[Parameter()] [Parameter()]
[bool]$CompressToWim = $false # New parameter for compression [bool]$CompressToWim = $false, # New parameter for compression
[Parameter()]
[bool]$PreserveSourceOnCompress = $false
# REMOVED: UI-related parameters # REMOVED: UI-related parameters
) )
@@ -104,6 +106,10 @@ function Save-MicrosoftDriversTask {
$driverRelativePath = Join-Path -Path $make -ChildPath $modelName # Relative path for the driver folder $driverRelativePath = Join-Path -Path $make -ChildPath $modelName # Relative path for the driver folder
$status = "Getting download link..." # Initial local status $status = "Getting download link..." # Initial local status
$success = $false $success = $false
$sanitizedModelName = ConvertTo-SafeName -Name $modelName
if ($sanitizedModelName -ne $modelName) { WriteLog "Sanitized model name: '$modelName' -> '$sanitizedModelName'" }
$makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $make
$modelPath = Join-Path -Path $makeDriversPath -ChildPath $sanitizedModelName
# Initial status update # Initial status update
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status "Checking..." } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status "Checking..." }
@@ -121,13 +127,14 @@ function Save-MicrosoftDriversTask {
if ($CompressToWim -and $existingDriver.Status -eq 'Already downloaded') { if ($CompressToWim -and $existingDriver.Status -eq 'Already downloaded') {
$makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $make $makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $make
$wimFilePath = Join-Path -Path $makeDriversPath -ChildPath "$($modelName).wim" $wimFilePath = Join-Path -Path $makeDriversPath -ChildPath "$($modelName).wim"
$wimRelativePath = Join-Path -Path $make -ChildPath "$($modelName).wim"
$sourceFolderPath = Join-Path -Path $makeDriversPath -ChildPath $modelName $sourceFolderPath = Join-Path -Path $makeDriversPath -ChildPath $modelName
WriteLog "Attempting compression of existing folder '$sourceFolderPath' to '$wimFilePath'." WriteLog "Attempting compression of existing folder '$sourceFolderPath' to '$wimFilePath'."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status "Compressing existing..." } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status "Compressing existing..." }
try { try {
Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $modelName -WimDescription "Drivers for $modelName" -ErrorAction Stop $null = Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $modelName -WimDescription "Drivers for $modelName" -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
$existingDriver.Status = "Already downloaded & Compressed" $existingDriver.Status = "Compression successful"
$existingDriver.DriverPath = Join-Path -Path $make -ChildPath "$($modelName).wim" $existingDriver.DriverPath = $wimRelativePath
$existingDriver.Success = $true $existingDriver.Success = $true
WriteLog "Successfully compressed existing drivers for $modelName to $wimFilePath." WriteLog "Successfully compressed existing drivers for $modelName to $wimFilePath."
} }
@@ -224,7 +231,7 @@ function Save-MicrosoftDriversTask {
### DOWNLOAD AND EXTRACT ### DOWNLOAD AND EXTRACT
if ($downloadLink) { if ($downloadLink) {
WriteLog "Selected Download Link for $modelName (Actual: Windows $downloadedVersion): $downloadLink" WriteLog "Selected Download Link for $modelName (Actual: Windows $downloadedVersion): $downloadLink"
$status = "Downloading (Win$downloadedVersion)..." # Update status message $status = "Downloading Win$downloadedVersion $fileName"
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
# Create directories # Create directories
@@ -232,8 +239,6 @@ function Save-MicrosoftDriversTask {
WriteLog "Creating Drivers folder: $DriversFolder" WriteLog "Creating Drivers folder: $DriversFolder"
New-Item -Path $DriversFolder -ItemType Directory -Force | Out-Null New-Item -Path $DriversFolder -ItemType Directory -Force | Out-Null
} }
$makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $Make
$modelPath = Join-Path -Path $makeDriversPath -ChildPath $modelName
if (-Not (Test-Path -Path $modelPath)) { if (-Not (Test-Path -Path $modelPath)) {
WriteLog "Creating model folder: $modelPath" WriteLog "Creating model folder: $modelPath"
New-Item -Path $modelPath -ItemType Directory -Force | Out-Null New-Item -Path $modelPath -ItemType Directory -Force | Out-Null
@@ -253,7 +258,7 @@ function Save-MicrosoftDriversTask {
### EXTRACT ### EXTRACT
if ($fileExtension -eq ".msi") { if ($fileExtension -eq ".msi") {
$status = "Waiting for MSI lock..." # Set initial status $status = "Waiting for MSI lock..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
# Use a named mutex to ensure only one MSI extraction happens at a time across all parallel tasks # Use a named mutex to ensure only one MSI extraction happens at a time across all parallel tasks
@@ -282,14 +287,14 @@ function Save-MicrosoftDriversTask {
catch [System.Threading.WaitHandleCannotBeOpenedException] { catch [System.Threading.WaitHandleCannotBeOpenedException] {
# Mutex is clear, proceed to extraction attempt # Mutex is clear, proceed to extraction attempt
WriteLog "System MSI mutex clear. Proceeding with MSI extraction attempt for $modelName." WriteLog "System MSI mutex clear. Proceeding with MSI extraction attempt for $modelName."
$status = "Extracting MSI..." $status = "Extracting Win$downloadedVersion $fileName"
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
$mutexClear = $true $mutexClear = $true
} }
catch { catch {
# Handle other potential errors when checking the mutex # Handle other potential errors when checking the mutex
WriteLog "Warning: Error checking system MSI mutex for $($modelName): $_. Proceeding with caution." WriteLog "Warning: Error checking system MSI mutex for $($modelName): $_. Proceeding with caution."
$status = "Extracting MSI (Mutex Error)..." $status = "Extracting Win$downloadedVersion $fileName (Mutex Error)"
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
$mutexClear = $true # Proceed despite mutex error $mutexClear = $true # Proceed despite mutex error
} }
@@ -347,7 +352,7 @@ function Save-MicrosoftDriversTask {
} }
} }
elseif ($fileExtension -eq ".zip") { elseif ($fileExtension -eq ".zip") {
$status = "Extracting ZIP..." # Set status before extraction $status = "Extracting Win$downloadedVersion $fileName"
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
WriteLog "Extracting ZIP file to $modelPath" WriteLog "Extracting ZIP file to $modelPath"
$ProgressPreference = 'SilentlyContinue' $ProgressPreference = 'SilentlyContinue'
@@ -377,7 +382,7 @@ function Save-MicrosoftDriversTask {
WriteLog "Compressing '$modelPath' to '$destinationWimPath'..." WriteLog "Compressing '$modelPath' to '$destinationWimPath'..."
try { try {
# Use the function from the imported common module # Use the function from the imported common module
$compressResult = Compress-DriverFolderToWim -SourceFolderPath $modelPath -DestinationWimPath $destinationWimPath -WimName $modelName -WimDescription $modelName -ErrorAction Stop $compressResult = Compress-DriverFolderToWim -SourceFolderPath $modelPath -DestinationWimPath $destinationWimPath -WimName $modelName -WimDescription $modelName -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
if ($compressResult) { if ($compressResult) {
WriteLog "Compression successful for '$modelName'." WriteLog "Compression successful for '$modelName'."
$status = "Completed & Compressed" $status = "Completed & Compressed"
@@ -417,6 +422,7 @@ function Save-MicrosoftDriversTask {
$status = "Error: $($_.Exception.Message.Split('.')[0])" # Shorten error message $status = "Error: $($_.Exception.Message.Split('.')[0])" # Shorten error message
WriteLog "Error saving Microsoft drivers for $($modelName): $($_.Exception.Message)" WriteLog "Error saving Microsoft drivers for $($modelName): $($_.Exception.Message)"
$success = $false $success = $false
Remove-DriverModelFolder -DriversFolder $DriversFolder -TargetFolder $modelPath -Description $modelName
# Enqueue the error status before returning # Enqueue the error status before returning
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
# Ensure return object is created even on error # Ensure return object is created even on error
+380 -86
View File
@@ -5,8 +5,142 @@
This module contains all the business logic for the 'Drivers' tab in the FFU Builder UI. It handles fetching driver model lists from various manufacturers (Microsoft, Dell, HP, Lenovo), displaying and filtering them in the UI, and managing the selection state. It also includes functions to import and export driver selections to a JSON file (Drivers.json) and to orchestrate the parallel download of selected driver packages using the common parallel processing module. This module contains all the business logic for the 'Drivers' tab in the FFU Builder UI. It handles fetching driver model lists from various manufacturers (Microsoft, Dell, HP, Lenovo), displaying and filtering them in the UI, and managing the selection state. It also includes functions to import and export driver selections to a JSON file (Drivers.json) and to orchestrate the parallel download of selected driver packages using the common parallel processing module.
#> #>
# Helper function to get models for a selected Make and standardize them function ConvertTo-DriverBaseName {
function Get-ModelsForMake { param(
[string]$ModelString
)
if ([string]::IsNullOrWhiteSpace($ModelString)) {
return $ModelString
}
if ($ModelString -match '^(.*?)\s*\((.+)\)\s*$') {
return $matches[1].Trim()
}
return $ModelString.Trim()
}
function Get-DriverDisplayName {
param(
[string]$BaseName,
[string]$Identifier
)
if ([string]::IsNullOrWhiteSpace($BaseName)) {
return $Identifier
}
if ([string]::IsNullOrWhiteSpace($Identifier)) {
return $BaseName.Trim()
}
return "$($BaseName.Trim()) ($($Identifier.Trim()))"
}
function Convert-DriverItemToJsonModel {
param(
[Parameter(Mandatory = $true)]
[pscustomobject]$DriverItem
)
$makeName = $DriverItem.Make
switch ($makeName) {
'Microsoft' {
$modelObject = @{ Name = $DriverItem.Model }
if ($DriverItem.PSObject.Properties['Link'] -and -not [string]::IsNullOrWhiteSpace($DriverItem.Link)) {
$modelObject.Link = $DriverItem.Link
}
return $modelObject
}
'Dell' {
$systemId = if ($DriverItem.PSObject.Properties['SystemId']) { $DriverItem.SystemId } else { $null }
$baseName = ConvertTo-DriverBaseName -ModelString $DriverItem.Model
if ([string]::IsNullOrWhiteSpace($baseName)) {
$baseName = $DriverItem.Model
}
$modelObject = @{ Name = $baseName }
if (-not [string]::IsNullOrWhiteSpace($systemId)) {
$modelObject.SystemId = $systemId
}
if ($DriverItem.PSObject.Properties['CabUrl'] -and -not [string]::IsNullOrWhiteSpace($DriverItem.CabUrl)) {
$modelObject.CabUrl = $DriverItem.CabUrl
}
return $modelObject
}
'HP' {
$baseName = if ($DriverItem.PSObject.Properties['ProductName'] -and -not [string]::IsNullOrWhiteSpace($DriverItem.ProductName)) { $DriverItem.ProductName } else { ConvertTo-DriverBaseName -ModelString $DriverItem.Model }
if ([string]::IsNullOrWhiteSpace($baseName)) {
$baseName = $DriverItem.Model
}
$systemId = if ($DriverItem.PSObject.Properties['SystemId']) { $DriverItem.SystemId } else { $null }
$modelObject = @{ Name = $baseName.Trim() }
if (-not [string]::IsNullOrWhiteSpace($systemId)) {
$modelObject.SystemId = $systemId
}
return $modelObject
}
'Lenovo' {
$machineType = $DriverItem.MachineType
$baseName = if ($DriverItem.ProductName) { $DriverItem.ProductName } else { ConvertTo-DriverBaseName -ModelString $DriverItem.Model }
if ([string]::IsNullOrWhiteSpace($baseName) -or [string]::IsNullOrWhiteSpace($machineType)) {
WriteLog "Skipping Lenovo driver '$($DriverItem.Model)' because Name or MachineType is missing."
return $null
}
return @{
Name = $baseName
MachineType = $machineType
}
}
default {
WriteLog "Convert-DriverItemToJsonModel: Unsupported Make '$makeName'."
return $null
}
}
}
function Remove-DriverModelFolder {
param(
[Parameter(Mandatory = $true)]
[string]$DriversFolder,
[Parameter(Mandatory = $true)]
[string]$TargetFolder,
[string]$Description
)
if ([string]::IsNullOrWhiteSpace($DriversFolder) -or [string]::IsNullOrWhiteSpace($TargetFolder)) {
return
}
try {
if (-not (Test-Path -Path $TargetFolder -PathType Container)) {
return
}
$driversRoot = [System.IO.Path]::GetFullPath((Resolve-Path -Path $DriversFolder -ErrorAction Stop).ProviderPath)
$targetPath = [System.IO.Path]::GetFullPath((Resolve-Path -Path $TargetFolder -ErrorAction Stop).ProviderPath)
if ($targetPath -eq $driversRoot) {
WriteLog "Remove-DriverModelFolder skipped deleting Drivers root: $targetPath"
return
}
if (-not ($targetPath.StartsWith($driversRoot, [System.StringComparison]::OrdinalIgnoreCase))) {
WriteLog "Remove-DriverModelFolder skipped path outside Drivers root: $targetPath"
return
}
$contextMessage = if ([string]::IsNullOrWhiteSpace($Description)) { $targetPath } else { "$Description ($targetPath)" }
WriteLog "Removing driver folder $contextMessage due to failure."
Remove-Item -Path $targetPath -Recurse -Force -ErrorAction SilentlyContinue
}
catch {
WriteLog "Remove-DriverModelFolder failed for $($TargetFolder): $($_.Exception.Message)"
}
}
# Helper function to get models for a selected Make and standardize them
function Get-ModelsForMake {
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$SelectedMake, [string]$SelectedMake,
@@ -84,11 +218,12 @@ function ConvertTo-StandardizedDriverModel {
[psobject]$State [psobject]$State
) )
$modelDisplay = $RawDriverObject.Model # Default $modelDisplay = $RawDriverObject.Model
$id = $RawDriverObject.Model # Default $id = $RawDriverObject.Model
$link = $null $link = $null
$productName = $null $productName = $null
$machineType = $null $machineType = $null
$systemId = $null
if ($RawDriverObject.PSObject.Properties['Link']) { if ($RawDriverObject.PSObject.Properties['Link']) {
$link = $RawDriverObject.Link $link = $RawDriverObject.Link
@@ -102,7 +237,30 @@ function ConvertTo-StandardizedDriverModel {
$id = $RawDriverObject.MachineType $id = $RawDriverObject.MachineType
} }
return [PSCustomObject]@{ # HP specific handling
if ($Make -eq 'HP') {
$productName = if ($RawDriverObject.PSObject.Properties['ProductName'] -and -not [string]::IsNullOrWhiteSpace($RawDriverObject.ProductName)) { $RawDriverObject.ProductName } else { ConvertTo-DriverBaseName -ModelString $RawDriverObject.Model }
if ([string]::IsNullOrWhiteSpace($productName)) { $productName = $RawDriverObject.Model }
if ($RawDriverObject.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($RawDriverObject.SystemId)) {
$systemId = $RawDriverObject.SystemId
}
$modelDisplay = if ([string]::IsNullOrWhiteSpace($systemId)) { $productName } else { Get-DriverDisplayName -BaseName $productName -Identifier $systemId }
$id = if ([string]::IsNullOrWhiteSpace($systemId)) { $productName } else { $systemId }
}
# Dell-specific passthrough (needed for per-model cab workflow)
$dellBrand = $null
$dellModelNumber = $null
$dellSystemId = $null
$dellCabUrl = $null
if ($Make -eq 'Dell') {
if ($RawDriverObject.PSObject.Properties['Brand']) { $dellBrand = $RawDriverObject.Brand }
if ($RawDriverObject.PSObject.Properties['ModelNumber']) { $dellModelNumber = $RawDriverObject.ModelNumber }
if ($RawDriverObject.PSObject.Properties['SystemId']) { $dellSystemId = $RawDriverObject.SystemId }
if ($RawDriverObject.PSObject.Properties['CabUrl']) { $dellCabUrl = $RawDriverObject.CabUrl }
}
$output = [PSCustomObject]@{
IsSelected = $false IsSelected = $false
Make = $Make Make = $Make
Model = $modelDisplay Model = $modelDisplay
@@ -110,12 +268,25 @@ function ConvertTo-StandardizedDriverModel {
Id = $id Id = $id
ProductName = $productName ProductName = $productName
MachineType = $machineType MachineType = $machineType
Version = "" # Placeholder Version = ""
Type = "" # Placeholder Type = ""
Size = "" # Placeholder Size = ""
Arch = "" # Placeholder Arch = ""
DownloadStatus = "" # Initial download status DownloadStatus = ""
} }
if ($Make -eq 'Dell') {
# Add Dell-only fields so Save-DellDriversTask can use CabUrl
$output | Add-Member -NotePropertyName Brand -NotePropertyValue $dellBrand
$output | Add-Member -NotePropertyName ModelNumber -NotePropertyValue $dellModelNumber
$output | Add-Member -NotePropertyName SystemId -NotePropertyValue $dellSystemId
$output | Add-Member -NotePropertyName CabUrl -NotePropertyValue $dellCabUrl
}
elseif ($Make -eq 'HP' -and -not [string]::IsNullOrWhiteSpace($systemId)) {
$output | Add-Member -NotePropertyName SystemId -NotePropertyValue $systemId
}
return $output
} }
# Function to filter the driver model list based on text input # Function to filter the driver model list based on text input
@@ -188,35 +359,7 @@ function Save-DriversJson {
$modelsForThisMake = @() # Initialize an array to hold model objects $modelsForThisMake = @() # Initialize an array to hold model objects
foreach ($driverItem in $_.Group) { foreach ($driverItem in $_.Group) {
$modelObject = $null $modelObject = Convert-DriverItemToJsonModel -DriverItem $driverItem
switch ($makeName) {
'Microsoft' {
$modelObject = @{
Name = $driverItem.Model # Model is the display name
Link = $driverItem.Link
}
}
'Dell' {
$modelObject = @{
Name = $driverItem.Model
}
}
'HP' {
$modelObject = @{
Name = $driverItem.Model
}
}
'Lenovo' {
$modelObject = @{
Name = $driverItem.Model # This is "ProductName (MachineType)"
ProductName = $driverItem.ProductName # This is "ProductName"
MachineType = $driverItem.MachineType # This is "MachineType"
}
}
default {
WriteLog "Save-DriversJson: Unknown Make '$makeName' encountered for model '$($driverItem.Model)'. Skipping."
}
}
if ($null -ne $modelObject) { if ($null -ne $modelObject) {
$modelsForThisMake += $modelObject $modelsForThisMake += $modelObject
} }
@@ -307,11 +450,97 @@ function Import-DriversJson {
continue continue
} }
$normalizedName = $importedModelNameFromObject
$skipModel = $false
switch ($makeName) {
'Lenovo' {
$productName = if ($importedModelObject.PSObject.Properties['ProductName'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.ProductName)) { $importedModelObject.ProductName } else { ConvertTo-DriverBaseName -ModelString $normalizedName }
$machineType = if ($importedModelObject.PSObject.Properties['MachineType'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.MachineType)) { $importedModelObject.MachineType } else { $null }
if ([string]::IsNullOrWhiteSpace($machineType) -and $normalizedName -match '(.+?)\s*\((.+?)\)$') {
if ([string]::IsNullOrWhiteSpace($productName)) { $productName = $matches[1].Trim() }
$machineType = $matches[2].Trim()
}
if ([string]::IsNullOrWhiteSpace($productName) -or [string]::IsNullOrWhiteSpace($machineType)) {
WriteLog "Import-DriversJson: Skipping Lenovo model '$normalizedName' due to missing ProductName or MachineType."
$skipModel = $true
}
else {
$normalizedName = Get-DriverDisplayName -BaseName $productName -Identifier $machineType
if ($importedModelObject.PSObject.Properties['ProductName']) {
$importedModelObject.ProductName = $productName
}
else {
$importedModelObject | Add-Member -NotePropertyName ProductName -NotePropertyValue $productName
}
if ($importedModelObject.PSObject.Properties['MachineType']) {
$importedModelObject.MachineType = $machineType
}
else {
$importedModelObject | Add-Member -NotePropertyName MachineType -NotePropertyValue $machineType
}
}
}
'Dell' {
$baseName = ConvertTo-DriverBaseName -ModelString $normalizedName
if ([string]::IsNullOrWhiteSpace($baseName)) { $baseName = $normalizedName }
$systemId = if ($importedModelObject.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.SystemId)) { $importedModelObject.SystemId } else { $null }
if ([string]::IsNullOrWhiteSpace($systemId) -and $normalizedName -match '(.+?)\s*\((.+?)\)$') {
if ([string]::IsNullOrWhiteSpace($baseName)) { $baseName = $matches[1].Trim() }
$systemId = $matches[2].Trim()
}
$normalizedName = if ([string]::IsNullOrWhiteSpace($systemId)) { $baseName.Trim() } else { Get-DriverDisplayName -BaseName $baseName -Identifier $systemId }
if ($importedModelObject.PSObject.Properties['SystemId']) {
$importedModelObject.SystemId = $systemId
}
else {
$importedModelObject | Add-Member -NotePropertyName SystemId -NotePropertyValue $systemId
}
}
'HP' {
$baseName = ConvertTo-DriverBaseName -ModelString $normalizedName
if ([string]::IsNullOrWhiteSpace($baseName)) { $baseName = $normalizedName }
$systemId = if ($importedModelObject.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.SystemId)) { $importedModelObject.SystemId } else { $null }
if ([string]::IsNullOrWhiteSpace($systemId) -and $normalizedName -match '(.+?)\s*\((.+?)\)$') {
if ([string]::IsNullOrWhiteSpace($baseName)) { $baseName = $matches[1].Trim() }
$systemId = $matches[2].Trim()
}
$normalizedName = if ([string]::IsNullOrWhiteSpace($systemId)) { $baseName.Trim() } else { Get-DriverDisplayName -BaseName $baseName -Identifier $systemId }
if ($importedModelObject.PSObject.Properties['ProductName']) {
$importedModelObject.ProductName = $baseName
}
else {
$importedModelObject | Add-Member -NotePropertyName ProductName -NotePropertyValue $baseName
}
if ($importedModelObject.PSObject.Properties['SystemId']) {
$importedModelObject.SystemId = $systemId
}
else {
$importedModelObject | Add-Member -NotePropertyName SystemId -NotePropertyValue $systemId
}
}
default {
$normalizedName = $normalizedName.Trim()
}
}
if ($skipModel) {
continue
}
if ([string]::IsNullOrWhiteSpace($normalizedName)) {
WriteLog "Import-DriversJson: Skipping normalized model name for Make '$makeName'."
continue
}
$importedModelObject.Name = $normalizedName
$importedModelNameFromObject = $normalizedName
$existingModel = $State.Data.allDriverModels | Where-Object { $_.Make -eq $makeName -and $_.Model -eq $importedModelNameFromObject } | Select-Object -First 1 $existingModel = $State.Data.allDriverModels | Where-Object { $_.Make -eq $makeName -and $_.Model -eq $importedModelNameFromObject } | Select-Object -First 1
if ($null -ne $existingModel) { if ($null -ne $existingModel) {
$existingModel.IsSelected = $true $existingModel.IsSelected = $true
$existingModel.DownloadStatus = "Imported" $existingModel.DownloadStatus = "Imported"
$existingModel.Model = $importedModelNameFromObject
if ($makeName -eq 'Microsoft' -and $importedModelObject.PSObject.Properties['Link']) { if ($makeName -eq 'Microsoft' -and $importedModelObject.PSObject.Properties['Link']) {
if ($existingModel.Link -ne $importedModelObject.Link) { if ($existingModel.Link -ne $importedModelObject.Link) {
@@ -327,13 +556,36 @@ function Import-DriversJson {
} }
if ($importedModelObject.PSObject.Properties['MachineType'] -and $existingModel.PSObject.Properties['MachineType'] -and $existingModel.MachineType -ne $importedModelObject.MachineType) { if ($importedModelObject.PSObject.Properties['MachineType'] -and $existingModel.PSObject.Properties['MachineType'] -and $existingModel.MachineType -ne $importedModelObject.MachineType) {
$existingModel.MachineType = $importedModelObject.MachineType $existingModel.MachineType = $importedModelObject.MachineType
$existingModel.Id = $importedModelObject.MachineType # Update Id as well $existingModel.Id = $importedModelObject.MachineType
$updateExistingLenovo = $true $updateExistingLenovo = $true
} }
if ($updateExistingLenovo) { if ($updateExistingLenovo) {
WriteLog "Import-DriversJson: Updated ProductName/MachineType/Id for existing Lenovo model '$($existingModel.Model)'." WriteLog "Import-DriversJson: Updated ProductName/MachineType/Id for existing Lenovo model '$($existingModel.Model)'."
} }
} }
elseif ($makeName -eq 'Dell') {
# Update Dell extended fields if provided
if ($importedModelObject.PSObject.Properties['SystemId'] -and $existingModel.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.SystemId)) {
if ($existingModel.SystemId -ne $importedModelObject.SystemId) {
$existingModel.SystemId = $importedModelObject.SystemId
WriteLog "Import-DriversJson: Updated SystemId for existing Dell model '$($existingModel.Model)'."
}
}
if ($importedModelObject.PSObject.Properties['CabUrl'] -and $existingModel.PSObject.Properties['CabUrl'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.CabUrl)) {
if ($existingModel.CabUrl -ne $importedModelObject.CabUrl) {
$existingModel.CabUrl = $importedModelObject.CabUrl
WriteLog "Import-DriversJson: Updated CabUrl for existing Dell model '$($existingModel.Model)'."
}
}
}
elseif ($makeName -eq 'HP') {
$importedProductName = if ($importedModelObject.PSObject.Properties['ProductName'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.ProductName)) { $importedModelObject.ProductName } else { ConvertTo-DriverBaseName -ModelString $importedModelNameFromObject }
if ([string]::IsNullOrWhiteSpace($importedProductName)) { $importedProductName = $importedModelNameFromObject }
if ($importedModelObject.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.SystemId)) {
$importedId = $importedModelObject.SystemId
}
}
$existingModelsUpdated++ $existingModelsUpdated++
WriteLog "Import-DriversJson: Marked existing model '$($existingModel.Make) - $($existingModel.Model)' as imported." WriteLog "Import-DriversJson: Marked existing model '$($existingModel.Make) - $($existingModel.Model)' as imported."
} }
@@ -370,7 +622,7 @@ function Import-DriversJson {
$newDriverModel = [PSCustomObject]@{ $newDriverModel = [PSCustomObject]@{
IsSelected = $true IsSelected = $true
Make = $makeName Make = $makeName
Model = $importedModelNameFromObject # Full display name Model = $importedModelNameFromObject
Link = $importedLink Link = $importedLink
Id = $importedId Id = $importedId
ProductName = $importedProductName ProductName = $importedProductName
@@ -381,6 +633,20 @@ function Import-DriversJson {
Arch = "" Arch = ""
DownloadStatus = "Imported" DownloadStatus = "Imported"
} }
if ($makeName -eq 'Dell') {
# Attach optional Dell extended fields if present
if ($importedModelObject.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.SystemId)) {
$newDriverModel | Add-Member -NotePropertyName SystemId -NotePropertyValue $importedModelObject.SystemId
}
if ($importedModelObject.PSObject.Properties['CabUrl'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.CabUrl)) {
$newDriverModel | Add-Member -NotePropertyName CabUrl -NotePropertyValue $importedModelObject.CabUrl
}
}
elseif ($makeName -eq 'HP') {
if ($importedModelObject.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.SystemId)) {
$newDriverModel | Add-Member -NotePropertyName SystemId -NotePropertyValue $importedModelObject.SystemId
}
}
$State.Data.allDriverModels.Add($newDriverModel) $State.Data.allDriverModels.Add($newDriverModel)
$newModelsAdded++ $newModelsAdded++
WriteLog "Import-DriversJson: Added new model '$($newDriverModel.Make) - $($newDriverModel.Model)' from import. ID: $($newDriverModel.Id), Link: $($newDriverModel.Link)" WriteLog "Import-DriversJson: Added new model '$($newDriverModel.Make) - $($newDriverModel.Model)' from import. ID: $($newDriverModel.Id), Link: $($newDriverModel.Link)"
@@ -571,6 +837,8 @@ function Invoke-DownloadSelectedDrivers {
$localHeaders = $coreStaticVars.Headers $localHeaders = $coreStaticVars.Headers
$localUserAgent = $coreStaticVars.UserAgent $localUserAgent = $coreStaticVars.UserAgent
$compressDrivers = $State.Controls.chkCompressDriversToWIM.IsChecked $compressDrivers = $State.Controls.chkCompressDriversToWIM.IsChecked
# Determine if we must preserve source folders (used later for PE driver harvesting)
$preserveSource = ($State.Controls.chkUseDriversAsPEDrivers.IsChecked -and $State.Controls.chkCompressDriversToWIM.IsChecked)
$State.Controls.txtStatus.Text = "Processing all selected drivers..." $State.Controls.txtStatus.Text = "Processing all selected drivers..."
WriteLog "Processing all selected drivers: $($selectedDrivers.Model -join ', ')" WriteLog "Processing all selected drivers: $($selectedDrivers.Model -join ', ')"
@@ -580,10 +848,10 @@ function Invoke-DownloadSelectedDrivers {
WriteLog "Dell drivers selected. Ensuring Dell Catalog is up-to-date..." WriteLog "Dell drivers selected. Ensuring Dell Catalog is up-to-date..."
try { try {
$dellDriversFolder = Join-Path -Path $localDriversFolder -ChildPath "Dell" $dellDriversFolder = Join-Path -Path $localDriversFolder -ChildPath "Dell"
$catalogBaseName = if ($localWindowsRelease -le 11) { "CatalogPC" } else { "Catalog" } $catalogBaseName = if ($localWindowsRelease -le 11) { "CatalogIndexPC" } else { "Catalog" }
$dellCabFile = Join-Path -Path $dellDriversFolder -ChildPath "$($catalogBaseName).cab" $dellCabFile = Join-Path -Path $dellDriversFolder -ChildPath "$($catalogBaseName).cab"
$dellCatalogXML = Join-Path -Path $dellDriversFolder -ChildPath "$($catalogBaseName).xml" $dellCatalogXML = Join-Path -Path $dellDriversFolder -ChildPath "$($catalogBaseName).xml"
$catalogUrl = if ($localWindowsRelease -le 11) { "http://downloads.dell.com/catalog/CatalogPC.cab" } else { "https://downloads.dell.com/catalog/Catalog.cab" } $catalogUrl = if ($localWindowsRelease -le 11) { "https://downloads.dell.com/catalog/CatalogIndexPC.cab" } else { "https://downloads.dell.com/catalog/Catalog.cab" }
$downloadCatalog = $true $downloadCatalog = $true
if (Test-Path -Path $dellCatalogXML -PathType Leaf) { if (Test-Path -Path $dellCatalogXML -PathType Leaf) {
@@ -620,7 +888,7 @@ function Invoke-DownloadSelectedDrivers {
return return
} }
} }
$preserveSource = ($State.Controls.chkUseDriversAsPEDrivers.IsChecked -and $State.Controls.chkCompressDriversToWIM.IsChecked)
$taskArguments = @{ $taskArguments = @{
DriversFolder = $localDriversFolder DriversFolder = $localDriversFolder
WindowsRelease = $localWindowsRelease WindowsRelease = $localWindowsRelease
@@ -629,6 +897,7 @@ function Invoke-DownloadSelectedDrivers {
Headers = $localHeaders Headers = $localHeaders
UserAgent = $localUserAgent UserAgent = $localUserAgent
CompressToWim = $compressDrivers CompressToWim = $compressDrivers
PreserveSourceOnCompress = $preserveSource
} }
$parallelResults = Invoke-ParallelProcessing -ItemsToProcess $selectedDrivers ` $parallelResults = Invoke-ParallelProcessing -ItemsToProcess $selectedDrivers `
@@ -645,13 +914,17 @@ function Invoke-DownloadSelectedDrivers {
$overallSuccess = $true $overallSuccess = $true
$successfullyDownloaded = [System.Collections.Generic.List[PSCustomObject]]::new() $successfullyDownloaded = [System.Collections.Generic.List[PSCustomObject]]::new()
$failedDownloads = [System.Collections.Generic.List[PSCustomObject]]::new()
# Check the results from the parallel processing tasks # Check the results from the parallel processing tasks
if ($null -ne $parallelResults) { if ($null -ne $parallelResults) {
# Create a lookup from the original selected drivers to get the 'Make' property, # Create a lookup from the original selected drivers to retain full metadata for mapping.
# as the result object might only have 'Identifier' or 'Model'. $driverLookup = @{}
$makeLookup = @{} foreach ($driver in $selectedDrivers) {
$selectedDrivers | ForEach-Object { $makeLookup[$_.Model] = $_.Make } if (-not [string]::IsNullOrWhiteSpace($driver.Model)) {
$driverLookup[$driver.Model] = $driver
}
}
# Filter for objects that could be results, avoiding stray log strings # Filter for objects that could be results, avoiding stray log strings
foreach ($result in ($parallelResults | Where-Object { $_ -is [hashtable] })) { foreach ($result in ($parallelResults | Where-Object { $_ -is [hashtable] })) {
@@ -666,27 +939,61 @@ function Invoke-DownloadSelectedDrivers {
if ([string]::IsNullOrWhiteSpace($modelName)) { if ([string]::IsNullOrWhiteSpace($modelName)) {
WriteLog "Could not determine model name from result object: $($result | ConvertTo-Json -Compress -Depth 3)" WriteLog "Could not determine model name from result object: $($result | ConvertTo-Json -Compress -Depth 3)"
$overallSuccess = $false $overallSuccess = $false
$failedDownloads.Add([PSCustomObject]@{
Model = 'Unknown model'
Status = 'Driver task returned without a model identifier.'
})
continue continue
} }
if ($resultCode -ne 0) { if ($resultCode -ne 0) {
$overallSuccess = $false $overallSuccess = $false
$failureStatus = $result['Status']
if ([string]::IsNullOrWhiteSpace($failureStatus)) { $failureStatus = 'Driver download failed. Check the log for details.' }
$failedDownloads.Add([PSCustomObject]@{
Model = $modelName
Status = $failureStatus
})
WriteLog "Error detected for model $modelName." WriteLog "Error detected for model $modelName."
} }
elseif (-not [string]::IsNullOrWhiteSpace($driverPath)) { elseif (-not [string]::IsNullOrWhiteSpace($driverPath)) {
# The task was successful and returned a driver path. # The task was successful and returned a driver path.
$make = $makeLookup[$modelName] $driverMetadata = $null
if ($make) { if (-not [string]::IsNullOrWhiteSpace($modelName) -and $driverLookup.ContainsKey($modelName)) {
$successfullyDownloaded.Add([PSCustomObject]@{ $driverMetadata = $driverLookup[$modelName]
Make = $make }
if ($driverMetadata) {
$driverRecord = [PSCustomObject]@{
Make = $driverMetadata.Make
Model = $modelName Model = $modelName
DriverPath = $driverPath DriverPath = $driverPath
}) }
if ($driverMetadata.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($driverMetadata.SystemId)) {
$driverRecord | Add-Member -NotePropertyName SystemId -NotePropertyValue $driverMetadata.SystemId
}
if ($driverMetadata.PSObject.Properties['MachineType'] -and -not [string]::IsNullOrWhiteSpace($driverMetadata.MachineType)) {
$driverRecord | Add-Member -NotePropertyName MachineType -NotePropertyValue $driverMetadata.MachineType
}
if ($driverMetadata.PSObject.Properties['ProductName'] -and -not [string]::IsNullOrWhiteSpace($driverMetadata.ProductName)) {
$driverRecord | Add-Member -NotePropertyName ProductName -NotePropertyValue $driverMetadata.ProductName
}
$successfullyDownloaded.Add($driverRecord)
} }
else { else {
WriteLog "Warning: Could not find 'Make' for successful download of model '$modelName'. Skipping from DriverMapping.json." WriteLog "Warning: Could not find driver metadata for successful download of model '$modelName'. Skipping from DriverMapping.json."
} }
} }
else {
$overallSuccess = $false
$fallbackStatus = $result['Status']
if ([string]::IsNullOrWhiteSpace($fallbackStatus)) { $fallbackStatus = 'Driver download did not return a driver path.' }
$failedDownloads.Add([PSCustomObject]@{
Model = $modelName
Status = $fallbackStatus
})
WriteLog "Driver download did not provide a path for model $modelName."
}
} }
} }
@@ -715,42 +1022,16 @@ function Invoke-DownloadSelectedDrivers {
$modelsForThisMake = @() # Initialize an array to hold model objects $modelsForThisMake = @() # Initialize an array to hold model objects
foreach ($driverItem in $_.Group) { foreach ($driverItem in $_.Group) {
$modelObject = $null $modelObject = Convert-DriverItemToJsonModel -DriverItem $driverItem
switch ($makeName) {
'Microsoft' {
$modelObject = @{
Name = $driverItem.Model # Model is the display name
Link = $driverItem.Link
}
}
'Dell' {
$modelObject = @{
Name = $driverItem.Model # Model is the display name
}
}
'HP' {
$modelObject = @{
Name = $driverItem.Model
}
}
'Lenovo' {
$modelObject = @{
Name = $driverItem.Model
ProductName = $driverItem.ProductName
MachineType = $driverItem.MachineType
}
}
default {
WriteLog "Auto-Save Drivers.json: Unrecognized Make '$makeName' for driver '$($driverItem.Model)'. Skipping."
}
}
if ($null -ne $modelObject) { if ($null -ne $modelObject) {
$modelsForThisMake += $modelObject $modelsForThisMake += $modelObject
} }
} }
# Add the models array to the make-specific object
if ($modelsForThisMake.Count -gt 0) {
$outputJson[$makeName] = @{ Models = $modelsForThisMake } $outputJson[$makeName] = @{ Models = $modelsForThisMake }
} }
}
# Ensure directory exists # Ensure directory exists
$parentDir = Split-Path -Path $driversJsonPath -Parent $parentDir = Split-Path -Path $driversJsonPath -Parent
@@ -775,8 +1056,21 @@ function Invoke-DownloadSelectedDrivers {
[System.Windows.MessageBox]::Show("All selected driver downloads processed. Check status column for details.", "Download Process Finished", "OK", "Information") [System.Windows.MessageBox]::Show("All selected driver downloads processed. Check status column for details.", "Download Process Finished", "OK", "Information")
} }
else { else {
$State.Controls.txtStatus.Text = "Driver downloads processed with some errors. Check status column and log." $State.Controls.txtStatus.Text = "Driver download failed. Resolve the errors and try again."
[System.Windows.MessageBox]::Show("Driver downloads processed, but some errors occurred. Please check the status column for each driver and the log file for details.", "Download Process Finished with Errors", "OK", "Warning") $messageLines = [System.Collections.Generic.List[string]]::new()
if ($failedDownloads.Count -gt 0) {
$messageLines.Add("Driver download failed for:")
foreach ($item in ($failedDownloads | Select-Object -First 5)) {
$messageLines.Add("- $($item.Model): $($item.Status)")
}
if ($failedDownloads.Count -gt 5) {
$messageLines.Add("...see the log for additional failures.")
}
}
else {
$messageLines.Add("One or more driver downloads failed. Check the log for details.")
}
[System.Windows.MessageBox]::Show(($messageLines -join [System.Environment]::NewLine), "Driver Download Failed", "OK", "Error")
} }
} }
@@ -152,6 +152,61 @@ function Register-EventHandlers {
$localState.Controls.chkPromptExternalHardDiskMedia.IsChecked = $false $localState.Controls.chkPromptExternalHardDiskMedia.IsChecked = $false
}) })
if ($null -ne $State.Controls.cmbBitsPriority) {
$State.Controls.cmbBitsPriority.Add_SelectionChanged({
param($eventSource, $selectionChangedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
if ($null -eq $window -or $null -eq $window.Tag) {
return
}
Update-BitsPrioritySetting -State $window.Tag
})
}
# Additional FFU Files events
$State.Controls.chkCopyAdditionalFFUFiles.Add_Checked({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
$localState = $window.Tag
$localState.Controls.additionalFFUPanel.Visibility = 'Visible'
Update-AdditionalFFUList -State $localState
})
$State.Controls.chkCopyAdditionalFFUFiles.Add_Unchecked({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
$localState = $window.Tag
$localState.Controls.additionalFFUPanel.Visibility = 'Collapsed'
$localState.Controls.lstAdditionalFFUs.Items.Clear()
$headerChk = $localState.Controls.chkSelectAllAdditionalFFUs
if ($null -ne $headerChk) {
Update-SelectAllHeaderCheckBoxState -ListView $localState.Controls.lstAdditionalFFUs -HeaderCheckBox $headerChk
}
})
$State.Controls.btnRefreshAdditionalFFUs.Add_Click({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
$localState = $window.Tag
Update-AdditionalFFUList -State $localState
})
$State.Controls.lstAdditionalFFUs.Add_PreviewKeyDown({
param($eventSource, $keyEvent)
if ($keyEvent.Key -eq 'Space') {
$window = [System.Windows.Window]::GetWindow($eventSource)
$localState = $window.Tag
Invoke-ListViewItemToggle -ListView $eventSource -State $localState -HeaderCheckBoxKeyName 'chkSelectAllAdditionalFFUs'
$keyEvent.Handled = $true
}
})
$State.Controls.lstAdditionalFFUs.Add_SelectionChanged({
param($eventSource, $selChangeEvent)
$window = [System.Windows.Window]::GetWindow($eventSource)
$localState = $window.Tag
$headerChk = $localState.Controls.chkSelectAllAdditionalFFUs
if ($null -ne $headerChk) {
Update-SelectAllHeaderCheckBoxState -ListView $localState.Controls.lstAdditionalFFUs -HeaderCheckBox $headerChk
}
})
$State.Controls.btnCheckUSBDrives.Add_Click({ $State.Controls.btnCheckUSBDrives.Add_Click({
param($eventSource, $routedEventArgs) param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource) $window = [System.Windows.Window]::GetWindow($eventSource)
@@ -216,11 +271,11 @@ function Register-EventHandlers {
} }
else { else {
$localState.Controls.txtCustomVMSwitchName.Visibility = 'Collapsed' $localState.Controls.txtCustomVMSwitchName.Visibility = 'Collapsed'
if ($localState.Data.vmSwitchMap.ContainsKey($selectedItem)) { if ($null -ne $selectedItem -and $localState.Data.vmSwitchMap.ContainsKey($selectedItem)) {
$localState.Controls.txtVMHostIPAddress.Text = $localState.Data.vmSwitchMap[$selectedItem] $localState.Controls.txtVMHostIPAddress.Text = $localState.Data.vmSwitchMap[$selectedItem]
} }
else { else {
$localState.Controls.txtVMHostIPAddress.Text = '' # Clear IP if not found in map $localState.Controls.txtVMHostIPAddress.Text = '' # Clear IP if not found or key null
} }
} }
}) })
@@ -808,6 +863,10 @@ function Register-EventHandlers {
$State.Controls.chkCopyDrivers.Add_Unchecked($driverCheckboxHandler) $State.Controls.chkCopyDrivers.Add_Unchecked($driverCheckboxHandler)
$State.Controls.chkCompressDriversToWIM.Add_Checked($driverCheckboxHandler) $State.Controls.chkCompressDriversToWIM.Add_Checked($driverCheckboxHandler)
$State.Controls.chkCompressDriversToWIM.Add_Unchecked($driverCheckboxHandler) $State.Controls.chkCompressDriversToWIM.Add_Unchecked($driverCheckboxHandler)
$State.Controls.chkCopyPEDrivers.Add_Checked($driverCheckboxHandler)
$State.Controls.chkCopyPEDrivers.Add_Unchecked($driverCheckboxHandler)
$State.Controls.chkUseDriversAsPEDrivers.Add_Checked($driverCheckboxHandler)
$State.Controls.chkUseDriversAsPEDrivers.Add_Unchecked($driverCheckboxHandler)
$State.Controls.btnBrowseDriversFolder.Add_Click({ $State.Controls.btnBrowseDriversFolder.Add_Click({
param($eventSource, $routedEventArgs) param($eventSource, $routedEventArgs)
@@ -950,6 +1009,12 @@ function Register-EventHandlers {
$localState = $window.Tag $localState = $window.Tag
Invoke-LoadConfiguration -State $localState Invoke-LoadConfiguration -State $localState
}) })
$State.Controls.btnRestoreDefaults.Add_Click({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
$localState = $window.Tag
Invoke-RestoreDefaults -State $localState
})
$State.Controls.btnBuildConfig.Add_Click({ $State.Controls.btnBuildConfig.Add_Click({
param($eventSource, $routedEventArgs) param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource) $window = [System.Windows.Window]::GetWindow($eventSource)
@@ -59,6 +59,10 @@ function Initialize-UIControls {
$State.Controls.usbSelectionPanel = $window.FindName('usbDriveSelectionPanel') $State.Controls.usbSelectionPanel = $window.FindName('usbDriveSelectionPanel')
$State.Controls.chkAllowExternalHardDiskMedia = $window.FindName('chkAllowExternalHardDiskMedia') $State.Controls.chkAllowExternalHardDiskMedia = $window.FindName('chkAllowExternalHardDiskMedia')
$State.Controls.chkPromptExternalHardDiskMedia = $window.FindName('chkPromptExternalHardDiskMedia') $State.Controls.chkPromptExternalHardDiskMedia = $window.FindName('chkPromptExternalHardDiskMedia')
$State.Controls.chkCopyAdditionalFFUFiles = $window.FindName('chkCopyAdditionalFFUFiles')
$State.Controls.additionalFFUPanel = $window.FindName('additionalFFUPanel')
$State.Controls.lstAdditionalFFUs = $window.FindName('lstAdditionalFFUs')
$State.Controls.btnRefreshAdditionalFFUs = $window.FindName('btnRefreshAdditionalFFUs')
$State.Controls.chkInstallWingetApps = $window.FindName('chkInstallWingetApps') $State.Controls.chkInstallWingetApps = $window.FindName('chkInstallWingetApps')
$State.Controls.wingetPanel = $window.FindName('wingetPanel') $State.Controls.wingetPanel = $window.FindName('wingetPanel')
$State.Controls.btnCheckWingetModule = $window.FindName('btnCheckWingetModule') $State.Controls.btnCheckWingetModule = $window.FindName('btnCheckWingetModule')
@@ -114,6 +118,7 @@ function Initialize-UIControls {
$State.Controls.txtShareName = $window.FindName('txtShareName') $State.Controls.txtShareName = $window.FindName('txtShareName')
$State.Controls.txtUsername = $window.FindName('txtUsername') $State.Controls.txtUsername = $window.FindName('txtUsername')
$State.Controls.txtThreads = $window.FindName('txtThreads') $State.Controls.txtThreads = $window.FindName('txtThreads')
$State.Controls.cmbBitsPriority = $window.FindName('cmbBitsPriority')
$State.Controls.txtMaxUSBDrives = $window.FindName('txtMaxUSBDrives') $State.Controls.txtMaxUSBDrives = $window.FindName('txtMaxUSBDrives')
$State.Controls.chkCompactOS = $window.FindName('chkCompactOS') $State.Controls.chkCompactOS = $window.FindName('chkCompactOS')
$State.Controls.chkOptimize = $window.FindName('chkOptimize') $State.Controls.chkOptimize = $window.FindName('chkOptimize')
@@ -143,6 +148,7 @@ function Initialize-UIControls {
$State.Controls.txtDriversFolder = $window.FindName('txtDriversFolder') $State.Controls.txtDriversFolder = $window.FindName('txtDriversFolder')
$State.Controls.txtPEDriversFolder = $window.FindName('txtPEDriversFolder') $State.Controls.txtPEDriversFolder = $window.FindName('txtPEDriversFolder')
$State.Controls.chkCopyPEDrivers = $window.FindName('chkCopyPEDrivers') $State.Controls.chkCopyPEDrivers = $window.FindName('chkCopyPEDrivers')
$State.Controls.chkUseDriversAsPEDrivers = $window.FindName('chkUseDriversAsPEDrivers')
$State.Controls.chkUpdateLatestCU = $window.FindName('chkUpdateLatestCU') $State.Controls.chkUpdateLatestCU = $window.FindName('chkUpdateLatestCU')
$State.Controls.chkUpdateLatestNet = $window.FindName('chkUpdateLatestNet') $State.Controls.chkUpdateLatestNet = $window.FindName('chkUpdateLatestNet')
$State.Controls.chkUpdateLatestDefender = $window.FindName('chkUpdateLatestDefender') $State.Controls.chkUpdateLatestDefender = $window.FindName('chkUpdateLatestDefender')
@@ -170,6 +176,7 @@ function Initialize-UIControls {
$State.Controls.btnBrowseDriversJsonPath = $window.FindName('btnBrowseDriversJsonPath') $State.Controls.btnBrowseDriversJsonPath = $window.FindName('btnBrowseDriversJsonPath')
$State.Controls.chkUpdateADK = $window.FindName('chkUpdateADK') $State.Controls.chkUpdateADK = $window.FindName('chkUpdateADK')
$State.Controls.btnLoadConfig = $window.FindName('btnLoadConfig') $State.Controls.btnLoadConfig = $window.FindName('btnLoadConfig')
$State.Controls.btnRestoreDefaults = $window.FindName('btnRestoreDefaults')
$State.Controls.btnBuildConfig = $window.FindName('btnBuildConfig') $State.Controls.btnBuildConfig = $window.FindName('btnBuildConfig')
# Monitor Tab # Monitor Tab
@@ -198,11 +205,11 @@ function Initialize-VMSwitchData {
if ($State.Controls.cmbVMSwitchName.Items.Count -gt 1) { if ($State.Controls.cmbVMSwitchName.Items.Count -gt 1) {
$State.Controls.cmbVMSwitchName.SelectedIndex = 0 $State.Controls.cmbVMSwitchName.SelectedIndex = 0
$firstSwitch = $State.Controls.cmbVMSwitchName.SelectedItem $firstSwitch = $State.Controls.cmbVMSwitchName.SelectedItem
if ($State.Data.vmSwitchMap.ContainsKey($firstSwitch)) { if ($null -ne $firstSwitch -and $State.Data.vmSwitchMap.ContainsKey($firstSwitch)) {
$State.Controls.txtVMHostIPAddress.Text = $State.Data.vmSwitchMap[$firstSwitch] $State.Controls.txtVMHostIPAddress.Text = $State.Data.vmSwitchMap[$firstSwitch]
} }
else { else {
$State.Controls.txtVMHostIPAddress.Text = $State.Defaults.generalDefaults.VMHostIPAddress # Use default if IP not found $State.Controls.txtVMHostIPAddress.Text = $State.Defaults.generalDefaults.VMHostIPAddress # Use default if IP not found or key null
} }
$State.Controls.txtCustomVMSwitchName.Visibility = 'Collapsed' $State.Controls.txtCustomVMSwitchName.Visibility = 'Collapsed'
} }
@@ -228,6 +235,7 @@ function Initialize-UIDefaults {
$State.Controls.txtShareName.Text = $State.Defaults.generalDefaults.ShareName $State.Controls.txtShareName.Text = $State.Defaults.generalDefaults.ShareName
$State.Controls.txtUsername.Text = $State.Defaults.generalDefaults.Username $State.Controls.txtUsername.Text = $State.Defaults.generalDefaults.Username
$State.Controls.txtThreads.Text = $State.Defaults.generalDefaults.Threads $State.Controls.txtThreads.Text = $State.Defaults.generalDefaults.Threads
$State.Controls.cmbBitsPriority.SelectedItem = $State.Defaults.generalDefaults.BitsPriority
$State.Controls.txtMaxUSBDrives.Text = $State.Defaults.generalDefaults.MaxUSBDrives $State.Controls.txtMaxUSBDrives.Text = $State.Defaults.generalDefaults.MaxUSBDrives
$State.Controls.chkBuildUSBDriveEnable.IsChecked = $State.Defaults.generalDefaults.BuildUSBDriveEnable $State.Controls.chkBuildUSBDriveEnable.IsChecked = $State.Defaults.generalDefaults.BuildUSBDriveEnable
$State.Controls.chkCompactOS.IsChecked = $State.Defaults.generalDefaults.CompactOS $State.Controls.chkCompactOS.IsChecked = $State.Defaults.generalDefaults.CompactOS
@@ -255,6 +263,9 @@ function Initialize-UIDefaults {
$State.Controls.usbSelectionPanel.Visibility = if ($State.Controls.chkSelectSpecificUSBDrives.IsChecked) { 'Visible' } else { 'Collapsed' } $State.Controls.usbSelectionPanel.Visibility = if ($State.Controls.chkSelectSpecificUSBDrives.IsChecked) { 'Visible' } else { 'Collapsed' }
$State.Controls.chkSelectSpecificUSBDrives.IsEnabled = $State.Controls.chkBuildUSBDriveEnable.IsChecked $State.Controls.chkSelectSpecificUSBDrives.IsEnabled = $State.Controls.chkBuildUSBDriveEnable.IsChecked
$State.Controls.chkPromptExternalHardDiskMedia.IsEnabled = $State.Controls.chkAllowExternalHardDiskMedia.IsChecked $State.Controls.chkPromptExternalHardDiskMedia.IsEnabled = $State.Controls.chkAllowExternalHardDiskMedia.IsChecked
$State.Controls.chkCopyAdditionalFFUFiles.IsChecked = $State.Defaults.generalDefaults.CopyAdditionalFFUFiles
$State.Controls.additionalFFUPanel.Visibility = if ($State.Controls.chkCopyAdditionalFFUFiles.IsChecked) { 'Visible' } else { 'Collapsed' }
Update-BitsPrioritySetting -State $State
# Hyper-V Settings defaults from General Defaults # Hyper-V Settings defaults from General Defaults
Initialize-VMSwitchData -State $State Initialize-VMSwitchData -State $State
@@ -309,16 +320,21 @@ function Initialize-UIDefaults {
$State.Controls.chkInstallDrivers.IsChecked = $State.Defaults.generalDefaults.InstallDrivers $State.Controls.chkInstallDrivers.IsChecked = $State.Defaults.generalDefaults.InstallDrivers
$State.Controls.chkCopyDrivers.IsChecked = $State.Defaults.generalDefaults.CopyDrivers $State.Controls.chkCopyDrivers.IsChecked = $State.Defaults.generalDefaults.CopyDrivers
$State.Controls.chkCopyPEDrivers.IsChecked = $State.Defaults.generalDefaults.CopyPEDrivers $State.Controls.chkCopyPEDrivers.IsChecked = $State.Defaults.generalDefaults.CopyPEDrivers
$State.Controls.chkUseDriversAsPEDrivers.IsChecked = $State.Defaults.generalDefaults.UseDriversAsPEDrivers
$State.Controls.chkCompressDriversToWIM.IsChecked = $State.Defaults.generalDefaults.CompressDownloadedDriversToWim $State.Controls.chkCompressDriversToWIM.IsChecked = $State.Defaults.generalDefaults.CompressDownloadedDriversToWim
# Drivers tab UI logic # Drivers tab UI logic
$makeList = @('Microsoft', 'Dell', 'HP', 'Lenovo') $makeList = @('Microsoft', 'Dell', 'HP', 'Lenovo')
if ($null -ne $State.Controls.cmbMake) {
# Clear existing items to prevent duplication on re-initialization (e.g., after Restore Defaults)
$State.Controls.cmbMake.Items.Clear()
foreach ($m in $makeList) { foreach ($m in $makeList) {
[void]$State.Controls.cmbMake.Items.Add($m) [void]$State.Controls.cmbMake.Items.Add($m)
} }
if ($State.Controls.cmbMake.Items.Count -gt 0) { if ($State.Controls.cmbMake.Items.Count -gt 0) {
$State.Controls.cmbMake.SelectedIndex = 0 $State.Controls.cmbMake.SelectedIndex = 0
} }
}
Update-DriverDownloadPanelVisibility -State $State Update-DriverDownloadPanelVisibility -State $State
# Set initial state for driver checkbox interplay # Set initial state for driver checkbox interplay
@@ -443,6 +459,75 @@ function Initialize-DynamicUIElements {
$wingetGridView.Columns.Add($archColumn) $wingetGridView.Columns.Add($archColumn)
# --- END: Add Architecture Column --- # --- END: Add Architecture Column ---
# --- START: Add Additional Exit Codes Column ---
$exitCodesColumn = New-Object System.Windows.Controls.GridViewColumn
$exitCodesHeader = New-Object System.Windows.Controls.GridViewColumnHeader
$exitCodesHeader.Tag = "AdditionalExitCodes"
$exitCodesHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left
$exitHeaderTextFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.TextBlock])
$exitHeaderTextFactory.SetValue([System.Windows.Controls.TextBlock]::TextProperty, "Additional Exit Codes")
$exitHeaderTextFactory.SetValue([System.Windows.Controls.TextBlock]::PaddingProperty, (New-Object System.Windows.Thickness(5, 2, 5, 2)))
$exitHeaderTextFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
$exitHeaderTemplate = New-Object System.Windows.DataTemplate
$exitHeaderTemplate.VisualTree = $exitHeaderTextFactory
$exitCodesHeader.ContentTemplate = $exitHeaderTemplate
$exitCodesColumn.Header = $exitCodesHeader
$exitCodesColumn.Width = 140
$exitCodesCellTemplate = New-Object System.Windows.DataTemplate
$exitCodesTextBoxFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.TextBox])
$exitBinding = New-Object System.Windows.Data.Binding("AdditionalExitCodes")
$exitBinding.Mode = [System.Windows.Data.BindingMode]::TwoWay
$exitCodesTextBoxFactory.SetBinding([System.Windows.Controls.TextBox]::TextProperty, $exitBinding)
$exitCodesCellTemplate.VisualTree = $exitCodesTextBoxFactory
$exitCodesColumn.CellTemplate = $exitCodesCellTemplate
$wingetGridView.Columns.Add($exitCodesColumn)
# --- END: Add Additional Exit Codes Column ---
# --- START: Add Ignore Non-Zero Exit Codes Column ---
$ignoreColumn = New-Object System.Windows.Controls.GridViewColumn
$ignoreHeader = New-Object System.Windows.Controls.GridViewColumnHeader
$ignoreHeader.Tag = "IgnoreNonZeroExitCodes"
$ignoreHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left
$ignoreHeaderTextFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.TextBlock])
$ignoreHeaderTextFactory.SetValue([System.Windows.Controls.TextBlock]::TextProperty, "Ignore Exit Codes")
$ignoreHeaderTextFactory.SetValue([System.Windows.Controls.TextBlock]::PaddingProperty, (New-Object System.Windows.Thickness(5, 2, 5, 2)))
$ignoreHeaderTextFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
$ignoreHeaderTemplate = New-Object System.Windows.DataTemplate
$ignoreHeaderTemplate.VisualTree = $ignoreHeaderTextFactory
$ignoreHeader.ContentTemplate = $ignoreHeaderTemplate
$ignoreColumn.Header = $ignoreHeader
$ignoreColumn.Width = 140
$ignoreCellTemplate = New-Object System.Windows.DataTemplate
# Center the checkbox in the cell
$ignoreCellGridFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.Grid])
$ignoreCellGridFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)
$ignoreCellGridFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Stretch)
$ignoreCheckFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.CheckBox])
$ignoreCheckFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Center)
$ignoreCheckFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
$ignoreBinding = New-Object System.Windows.Data.Binding("IgnoreNonZeroExitCodes")
$ignoreBinding.Mode = [System.Windows.Data.BindingMode]::TwoWay
$ignoreCheckFactory.SetBinding([System.Windows.Controls.Primitives.ToggleButton]::IsCheckedProperty, $ignoreBinding)
# Build the visual tree: Grid -> CheckBox
$ignoreCellGridFactory.AppendChild($ignoreCheckFactory)
$ignoreCellTemplate.VisualTree = $ignoreCellGridFactory
$ignoreColumn.CellTemplate = $ignoreCellTemplate
$wingetGridView.Columns.Add($ignoreColumn)
# --- END: Add Ignore Non-Zero Exit Codes Column ---
Add-SortableColumn -gridView $wingetGridView -header "Download Status" -binding "DownloadStatus" -width 150 -headerHorizontalAlignment Left Add-SortableColumn -gridView $wingetGridView -header "Download Status" -binding "DownloadStatus" -width 150 -headerHorizontalAlignment Left
$State.Controls.lstWingetResults.AddHandler( $State.Controls.lstWingetResults.AddHandler(
[System.Windows.Controls.GridViewColumnHeader]::ClickEvent, [System.Windows.Controls.GridViewColumnHeader]::ClickEvent,
@@ -570,14 +655,14 @@ function Initialize-DynamicUIElements {
$modelColumn.Header = $modelHeader $modelColumn.Header = $modelHeader
} }
# Serial Number Column (index 1 in XAML, now 2) # Unique ID Column (index 1 in XAML, now 2)
if ($usbDrivesGridView.Columns.Count -gt 2) { if ($usbDrivesGridView.Columns.Count -gt 2) {
$serialColumn = $usbDrivesGridView.Columns[2] $uniqueIdColumn = $usbDrivesGridView.Columns[2]
$serialHeader = New-Object System.Windows.Controls.GridViewColumnHeader $uniqueIdHeader = New-Object System.Windows.Controls.GridViewColumnHeader
$serialHeader.Content = "Serial Number" $uniqueIdHeader.Content = "Unique ID"
$serialHeader.Tag = "SerialNumber" # Property to sort by $uniqueIdHeader.Tag = "UniqueId" # Property to sort by
$serialHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left $uniqueIdHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left
$serialColumn.Header = $serialHeader $uniqueIdColumn.Header = $uniqueIdHeader
} }
# Size Column (index 2 in XAML, now 3) # Size Column (index 2 in XAML, now 3)
@@ -610,6 +695,51 @@ function Initialize-DynamicUIElements {
else { else {
WriteLog "Warning: lstUSBDrives.View is not a GridView. Selectable column not added, and sorting cannot be enabled." WriteLog "Warning: lstUSBDrives.View is not a GridView. Selectable column not added, and sorting cannot be enabled."
} }
# Additional FFUs ListView setup
$itemStyleAdditionalFFUs = New-Object System.Windows.Style([System.Windows.Controls.ListViewItem])
$itemStyleAdditionalFFUs.Setters.Add((New-Object System.Windows.Setter([System.Windows.Controls.ListViewItem]::HorizontalContentAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)))
$State.Controls.lstAdditionalFFUs.ItemContainerStyle = $itemStyleAdditionalFFUs
if ($State.Controls.lstAdditionalFFUs.View -is [System.Windows.Controls.GridView]) {
Add-SelectableGridViewColumn -ListView $State.Controls.lstAdditionalFFUs -State $State -HeaderCheckBoxKeyName "chkSelectAllAdditionalFFUs" -ColumnWidth 70
$additionalFFUsGridView = $State.Controls.lstAdditionalFFUs.View
if ($additionalFFUsGridView.Columns.Count -gt 1) {
$nameColumn = $additionalFFUsGridView.Columns[1]
$nameHeader = New-Object System.Windows.Controls.GridViewColumnHeader
$nameHeader.Content = "FFU Name"
$nameHeader.Tag = "Name"
$nameHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left
$nameColumn.Header = $nameHeader
}
if ($additionalFFUsGridView.Columns.Count -gt 2) {
$lastModColumn = $additionalFFUsGridView.Columns[2]
$lastModHeader = New-Object System.Windows.Controls.GridViewColumnHeader
$lastModHeader.Content = "Last Modified"
$lastModHeader.Tag = "LastModified"
$lastModHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left
$lastModColumn.Header = $lastModHeader
}
$State.Controls.lstAdditionalFFUs.AddHandler(
[System.Windows.Controls.GridViewColumnHeader]::ClickEvent,
[System.Windows.RoutedEventHandler] {
param($eventSource, $e)
$header = $e.OriginalSource
if ($header -is [System.Windows.Controls.GridViewColumnHeader] -and $header.Tag) {
$listViewControl = $eventSource
$window = [System.Windows.Window]::GetWindow($listViewControl)
$uiStateFromWindowTag = $window.Tag
Invoke-ListViewSort -listView $eventSource -property $header.Tag -State $uiStateFromWindowTag
}
}
)
}
else {
WriteLog "Warning: lstAdditionalFFUs.View is not a GridView. Selectable column not added, and sorting cannot be enabled."
}
} }
@@ -220,6 +220,32 @@ function Invoke-ProgressUpdate {
$ProgressQueue.Enqueue(@{ Identifier = $Identifier; Status = $Status }) $ProgressQueue.Enqueue(@{ Identifier = $Identifier; Status = $Status })
} }
function Update-BitsPrioritySetting {
param(
[Parameter(Mandatory)]
[pscustomobject]$State
)
$combo = $State.Controls.cmbBitsPriority
if ($null -eq $combo) {
WriteLog "BITS priority control not available; skipping priority update."
return
}
$selectedPriority = $combo.SelectedItem
if ([string]::IsNullOrWhiteSpace($selectedPriority)) {
$selectedPriority = 'Normal'
}
try {
Set-BitsTransferPriority -Priority $selectedPriority
WriteLog "BITS transfer priority set to $selectedPriority."
}
catch {
WriteLog "Failed to set BITS transfer priority: $($_.Exception.Message)"
}
}
# Add a function to create a sortable list view # Add a function to create a sortable list view
function Add-SortableColumn { function Add-SortableColumn {
param( param(
@@ -505,21 +531,29 @@ function Invoke-ListViewSort {
[PSCustomObject]$State [PSCustomObject]$State
) )
# Preserve any active CollectionView filter so sorting does not reset a filtered driver model list
$existingFilter = $null
$existingCollectionView = $null
if ($null -ne $listView.ItemsSource) {
$existingCollectionView = [System.Windows.Data.CollectionViewSource]::GetDefaultView($listView.ItemsSource)
if ($null -ne $existingCollectionView -and $existingCollectionView.Filter) {
$existingFilter = $existingCollectionView.Filter
}
}
# Ensure $State.Flags is a hashtable and contains the required sort properties # Ensure $State.Flags is a hashtable and contains the required sort properties
if ($State.Flags -is [hashtable]) { if ($State.Flags -is [hashtable]) {
if (-not $State.Flags.ContainsKey('lastSortProperty')) { if (-not $State.Flags.ContainsKey('lastSortProperty')) {
$State.Flags['lastSortProperty'] = $null $State.Flags['lastSortProperty'] = $null
} }
if (-not $State.Flags.ContainsKey('lastSortAscending')) { if (-not $State.Flags.ContainsKey('lastSortAscending')) {
$State.Flags['lastSortAscending'] = $true # Default to ascending $State.Flags['lastSortAscending'] = $true
} }
} }
else { else {
Write-Warning "Invoke-ListViewSort: \$State.Flags is not a hashtable or is null. Sort state may not work correctly." Write-Warning "Invoke-ListViewSort: \$State.Flags is not a hashtable or is null. Sort state may not work correctly."
# Attempt to initialize if $State.Flags is null or unexpectedly not a hashtable,
# though this might indicate a deeper issue with $State.Flags initialization.
if ($null -eq $State.Flags) { $State.Flags = @{} } if ($null -eq $State.Flags) { $State.Flags = @{} }
if ($State.Flags -is [hashtable]) { # Check again after potential initialization if ($State.Flags -is [hashtable]) {
if (-not $State.Flags.ContainsKey('lastSortProperty')) { $State.Flags['lastSortProperty'] = $null } if (-not $State.Flags.ContainsKey('lastSortProperty')) { $State.Flags['lastSortProperty'] = $null }
if (-not $State.Flags.ContainsKey('lastSortAscending')) { $State.Flags['lastSortAscending'] = $true } if (-not $State.Flags.ContainsKey('lastSortAscending')) { $State.Flags['lastSortAscending'] = $true }
} }
@@ -534,10 +568,15 @@ function Invoke-ListViewSort {
} }
$State.Flags.lastSortProperty = $property $State.Flags.lastSortProperty = $property
# Get items from ItemsSource or Items collection # Build the set of items to sort, enumerating the filtered view if a filter is active
$currentItemsSource = $listView.ItemsSource $currentItemsSource = $listView.ItemsSource
$itemsToSort = @() $itemsToSort = @()
if ($null -ne $currentItemsSource) { if ($null -ne $existingCollectionView -and $null -ne $existingFilter) {
foreach ($vItem in $existingCollectionView) {
$itemsToSort += $vItem
}
}
elseif ($null -ne $currentItemsSource) {
$itemsToSort = @($currentItemsSource) $itemsToSort = @($currentItemsSource)
} }
else { else {
@@ -548,10 +587,11 @@ function Invoke-ListViewSort {
return return
} }
# Separate selected vs unselected for selected-first ordering
$selectedItems = @($itemsToSort | Where-Object { $_.IsSelected }) $selectedItems = @($itemsToSort | Where-Object { $_.IsSelected })
$unselectedItems = @($itemsToSort | Where-Object { -not $_.IsSelected }) $unselectedItems = @($itemsToSort | Where-Object { -not $_.IsSelected })
# Define the primary sort criterion # Define primary sort criterion
$primarySortDefinition = @{ $primarySortDefinition = @{
Expression = { Expression = {
$val = $_.$property $val = $_.$property
@@ -579,11 +619,11 @@ function Invoke-ListViewSort {
$secondarySortPropertyName = "Key" $secondarySortPropertyName = "Key"
} }
else { else {
# Default secondary sort for IsSelected or other properties
$secondarySortPropertyName = "Key" $secondarySortPropertyName = "Key"
} }
} }
# Add secondary sort definition if applicable
if ($null -ne $secondarySortPropertyName -and $property -ne $secondarySortPropertyName) { if ($null -ne $secondarySortPropertyName -and $property -ne $secondarySortPropertyName) {
$itemsHaveSecondaryProperty = $false $itemsHaveSecondaryProperty = $false
if ($unselectedItems.Count -gt 0) { if ($unselectedItems.Count -gt 0) {
@@ -598,35 +638,40 @@ function Invoke-ListViewSort {
} }
if ($itemsHaveSecondaryProperty) { if ($itemsHaveSecondaryProperty) {
# Create a scriptblock for the secondary sort expression dynamically
$expressionScriptBlock = [scriptblock]::Create("`$_.$secondarySortPropertyName") $expressionScriptBlock = [scriptblock]::Create("`$_.$secondarySortPropertyName")
$secondarySortDefinition = @{ $secondarySortDefinition = @{
Expression = { Expression = {
$val = Invoke-Command -ScriptBlock $expressionScriptBlock -ArgumentList $_ $val = Invoke-Command -ScriptBlock $expressionScriptBlock -ArgumentList $_
if ($null -eq $val) { '' } else { $val } if ($null -eq $val) { '' } else { $val }
} }
Ascending = $true # Secondary sort always ascending Ascending = $true
} }
$sortCriteria.Add($secondarySortDefinition) $sortCriteria.Add($secondarySortDefinition)
} }
} }
# Sort unselected items by combined sort criteria
$sortedUnselected = $unselectedItems | Sort-Object -Property $sortCriteria.ToArray() $sortedUnselected = $unselectedItems | Sort-Object -Property $sortCriteria.ToArray()
# Ensure $sortedUnselected is not null before attempting to add its range
if ($null -eq $sortedUnselected) { if ($null -eq $sortedUnselected) {
$sortedUnselected = @() $sortedUnselected = @()
} }
# Combine sorted items: selected items first, then sorted unselected items # Merge selected first, then sorted unselected
$newSortedList = [System.Collections.Generic.List[object]]::new() $newSortedList = [System.Collections.Generic.List[object]]::new()
$newSortedList.AddRange($selectedItems) $newSortedList.AddRange($selectedItems)
$newSortedList.AddRange($sortedUnselected) $newSortedList.AddRange($sortedUnselected)
# Set the new sorted list as the ItemsSource # Reset ItemsSource and assign sorted list
# Try nulling out ItemsSource first to force a more complete refresh
$listView.ItemsSource = $null $listView.ItemsSource = $null
$listView.ItemsSource = $newSortedList.ToArray() $listView.ItemsSource = $newSortedList.ToArray()
# Reapply preserved filter to maintain the user's filtered view
if ($null -ne $existingFilter) {
$newView = [System.Windows.Data.CollectionViewSource]::GetDefaultView($listView.ItemsSource)
if ($null -ne $newView) {
$newView.Filter = $existingFilter
}
}
} }
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
@@ -126,7 +126,7 @@ $script:mctWindowsReleases = @(
$script:windowsVersionMap = @{ $script:windowsVersionMap = @{
10 = @("22H2") 10 = @("22H2")
11 = @("22H2", "23H2", "24H2") 11 = @("25H2", "24H2", "23H2", "22H2")
2016 = @("1607") # Windows 10 LTSB 2016 & Server 2016 2016 = @("1607") # Windows 10 LTSB 2016 & Server 2016
2019 = @("1809") # Windows 10 LTSC 2019 & Server 2019 2019 = @("1809") # Windows 10 LTSC 2019 & Server 2019
# Note: Server 2016 and LTSB 2016 now share the key 2016, mapping to version "1607" # Note: Server 2016 and LTSB 2016 now share the key 2016, mapping to version "1607"
@@ -268,10 +268,15 @@ function Get-AvailableWindowsVersions {
# Logic for when an ISO is specified # Logic for when an ISO is specified
$result.Versions = $validVersions $result.Versions = $validVersions
# Set default selection logic (e.g., latest for Win11) # Set default selection logic (e.g., latest for Win11)
if ($SelectedRelease -eq 11 -and $validVersions -contains "24H2") { if ($SelectedRelease -eq 11) {
if ($validVersions -contains "25H2") {
$result.DefaultVersion = "25H2"
}
elseif ($validVersions -contains "24H2") {
$result.DefaultVersion = "24H2" $result.DefaultVersion = "24H2"
} }
elseif ($validVersions.Count -gt 0) { }
if (-not $result.DefaultVersion -and $validVersions.Count -gt 0) {
$result.DefaultVersion = $validVersions[0] $result.DefaultVersion = $validVersions[0]
} }
$result.IsEnabled = $true $result.IsEnabled = $true
@@ -280,7 +285,7 @@ function Get-AvailableWindowsVersions {
# Logic for when no ISO is specified (MCT scenario) # Logic for when no ISO is specified (MCT scenario)
switch ($SelectedRelease) { switch ($SelectedRelease) {
10 { $result.DefaultVersion = "22H2" } 10 { $result.DefaultVersion = "22H2" }
11 { $result.DefaultVersion = "24H2" } 11 { $result.DefaultVersion = "25H2" }
# Server versions typically require an ISO, but handle just in case # Server versions typically require an ISO, but handle just in case
2016 { $result.DefaultVersion = "1607" } 2016 { $result.DefaultVersion = "1607" }
2019 { $result.DefaultVersion = "1809" } 2019 { $result.DefaultVersion = "1809" }
@@ -515,7 +520,7 @@ function Update-WindowsArchCombo {
} }
else { else {
# Standard Windows 11 # Standard Windows 11
if ($versionValue -eq '24H2') { if ($versionValue -in @('24H2', '25H2')) {
$availableArchitectures = @('x64', 'arm64') $availableArchitectures = @('x64', 'arm64')
} }
else { else {
+47 -317
View File
@@ -98,10 +98,12 @@ function Save-WingetList {
$appList = @{ $appList = @{
apps = @($selectedApps | ForEach-Object { apps = @($selectedApps | ForEach-Object {
[ordered]@{ [ordered]@{
name = $_.Name name = (ConvertTo-SafeName -Name $_.Name)
id = $_.Id id = $_.Id
source = $_.Source.ToLower() source = $_.Source.ToLower()
architecture = $_.Architecture architecture = $_.Architecture
AdditionalExitCodes = if ($_.PSObject.Properties['AdditionalExitCodes']) { $_.AdditionalExitCodes } else { "" }
IgnoreNonZeroExitCodes = if ($_.PSObject.Properties['IgnoreNonZeroExitCodes']) { [bool]$_.IgnoreNonZeroExitCodes } else { $false }
} }
}) })
} }
@@ -154,6 +156,8 @@ function Import-WingetList {
Version = "" # Will be populated when searching or if data exists Version = "" # Will be populated when searching or if data exists
Source = $appInfo.source Source = $appInfo.source
Architecture = $arch Architecture = $arch
AdditionalExitCodes = if ($appInfo.PSObject.Properties['AdditionalExitCodes']) { $appInfo.AdditionalExitCodes } else { "" }
IgnoreNonZeroExitCodes = if ($appInfo.PSObject.Properties['IgnoreNonZeroExitCodes']) { [bool]$appInfo.IgnoreNonZeroExitCodes } else { $false }
DownloadStatus = "" DownloadStatus = ""
}) })
} }
@@ -197,6 +201,8 @@ function Search-WingetPackagesPublic {
Version = [string]$_.Version Version = [string]$_.Version
Source = [string]$_.Source Source = [string]$_.Source
Architecture = [string]$arch Architecture = [string]$arch
AdditionalExitCodes = [string]::Empty
IgnoreNonZeroExitCodes = [bool]$false
DownloadStatus = [string]::Empty DownloadStatus = [string]::Empty
} }
} -ThrottleLimit 20 } -ThrottleLimit 20
@@ -372,323 +378,9 @@ function Confirm-WingetInstallationUI {
return $result return $result
} }
# Function to handle downloading a winget application (Modified for ForEach-Object -Parallel)
function Start-WingetAppDownloadTask {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[PSCustomObject]$ApplicationItemData, # Pass data, not the UI object
[Parameter(Mandatory = $true)]
[string]$AppListJsonPath,
[Parameter(Mandatory = $true)]
[string]$AppsPath, # Pass necessary paths
[Parameter(Mandatory = $true)]
[string]$OrchestrationPath,
[Parameter(Mandatory = $true)]
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue, # Add queue parameter
[string]$WindowsArch
)
$appName = $ApplicationItemData.Name # Note: Start-WingetAppDownloadTask has been moved to FFU.Common.Winget.psm1
$appId = $ApplicationItemData.Id # to enable code reuse between UI and CLI builds. It is imported via the FFU.Common module.
$source = $ApplicationItemData.Source
$status = "Checking..." # Initial local status
$resultCode = -1 # Default to error/unknown
# Initial status update
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
WriteLog "Starting download task for $($appName) with ID $($appId) from source $($source)."
try {
# Define paths
$userAppListPath = Join-Path -Path $AppsPath -ChildPath "UserAppList.json"
$appFound = $false # Flag to track if the app is found locally
# WriteLog "UserAppList Path: $($userAppListPath)"
# WriteLog "Checking for existing app in UserAppList.json and content folder."
# 1. Check UserAppList.json and content
if (Test-Path -Path $userAppListPath) {
# WriteLog "UserAppList.json found at $($userAppListPath). Checking for app entry."
try {
$userAppListContent = Get-Content -Path $userAppListPath -Raw | ConvertFrom-Json
$userAppEntry = $userAppListContent | Where-Object { $_.Name -eq $appName }
if ($userAppEntry) {
$appFolder = Join-Path -Path "$AppsPath\Win32" -ChildPath $appName
if (Test-Path -Path $appFolder -PathType Container) {
$folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($folderSize -gt 1MB) {
$appFound = $true
$status = "Not Downloaded: App in $userAppListPath and found in $appFolder"
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
WriteLog "Found '$appName' in $userAppListPath and content exists in '$appFolder'."
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 }
}
else {
$appFound = $true
$status = "App in '$userAppListPath' but content missing/small in '$appFolder'. Copy content or remove from UserAppList.json."
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
WriteLog $status
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 1 }
}
}
else {
$appFound = $true
$status = "App in '$userAppListPath' but content folder '$appFolder' not found. Copy content or remove from UserAppList.json."
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
WriteLog $status
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 1 }
}
}
}
catch {
WriteLog "Warning: Could not read or parse '$userAppListPath'. Error: $($_.Exception.Message)"
}
}
# 2. Check existing downloaded Win32 content (folder-based; no WinGetWin32Apps.json dependency)
if (-not $appFound -and $source -eq 'winget') {
$appFolder = Join-Path -Path "$AppsPath\Win32" -ChildPath $appName
if (Test-Path -Path $appFolder -PathType Container) {
$contentFound = $false
if ($ApplicationItemData.Architecture -eq 'x86 x64') {
$x86Folder = Join-Path -Path $appFolder -ChildPath "x86"
$x64Folder = Join-Path -Path $appFolder -ChildPath "x64"
if ((Test-Path -Path $x86Folder -PathType Container) -and (Test-Path -Path $x64Folder -PathType Container)) {
$x86Size = (Get-ChildItem -Path $x86Folder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
$x64Size = (Get-ChildItem -Path $x64Folder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($x86Size -gt 1MB -and $x64Size -gt 1MB) {
$contentFound = $true
}
}
}
else {
$folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($folderSize -gt 1MB) {
$contentFound = $true
}
}
if ($contentFound) {
$appFound = $true
$status = "Not Downloaded: Existing content found in $appFolder"
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
WriteLog "Found existing content for '$appName' in '$appFolder'. Skipping download to prevent duplicate entry."
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 }
}
}
}
# Check MSStore folder
if (-not $appFound -and (Test-Path -Path "$AppsPath\MSStore" -PathType Container)) {
$appFolder = Join-Path -Path "$AppsPath\MSStore" -ChildPath $appName
if (Test-Path -Path $appFolder -PathType Container) {
$folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($folderSize -gt 1MB) {
$appFound = $true
$status = "Already downloaded (MSStore)"
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
WriteLog "Found '$appName' content in '$appFolder'."
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 }
}
}
}
# 3. If not found locally, add to AppList.json and download
if (-not $appFound) {
# Add to AppList.json
$appListContent = $null
$appListDir = Split-Path -Path $AppListJsonPath -Parent
if (-not (Test-Path -Path $appListDir -PathType Container)) {
New-Item -Path $appListDir -ItemType Directory -Force | Out-Null
}
if (Test-Path -Path $AppListJsonPath) {
try {
$appListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json
if (-not $appListContent.PSObject.Properties['apps']) {
$appListContent = @{ apps = @() }
}
}
catch {
WriteLog "Warning: Could not read or parse '$AppListJsonPath'. Creating new structure. Error: $($_.Exception.Message)"
$appListContent = @{ apps = @() }
}
}
else {
$appListContent = @{ apps = @() }
}
$appExistsInAppList = $false
if ($appListContent.apps) {
foreach ($app in $appListContent.apps) {
if ($app.id -eq $appId) {
$appExistsInAppList = $true
break
}
}
}
if (-not $appExistsInAppList) {
$newApp = @{ name = $appName; id = $appId; source = $source }
if (-not ($appListContent.apps -is [array])) { $appListContent.apps = @() }
$appListContent.apps += $newApp
try {
# Use a lock to prevent race conditions when writing to the same file
$lockName = "AppListJsonLock"
$lock = New-Object System.Threading.Mutex($false, $lockName)
try {
$lock.WaitOne() | Out-Null
# Re-read content inside lock to ensure latest version
if (Test-Path -Path $AppListJsonPath) {
$currentAppListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json
if (-not ($currentAppListContent.apps | Where-Object { $_.id -eq $appId })) {
$currentAppListContent.apps += $newApp
$currentAppListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8
WriteLog "Added '$appName' to '$AppListJsonPath'."
}
else {
WriteLog "'$appName' already exists in '$AppListJsonPath' (checked inside lock)."
}
}
else {
# File doesn't exist, write the initial content
$appListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8
WriteLog "Created '$AppListJsonPath' and added '$appName'."
}
}
finally {
$lock.ReleaseMutex()
$lock.Dispose()
}
}
catch {
WriteLog "Error saving '$AppListJsonPath'. Error: $($_.Exception.Message)"
$status = "Failed to save AppList.json: $($_.Exception.Message)"
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 1 }
}
}
else {
WriteLog "'$appName' already exists in '$AppListJsonPath'."
}
# Proceed with download
$status = "Downloading..."
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
# Ensure variables needed by Get-Application are accessible
# (Assuming they are available via $using: scope or global scope from main script)
# $global:AppsPath = $AppsPath # Potentially redundant
# $global:WindowsArch = $ApplicationItemData.Architecture # Potentially redundant
# $global:orchestrationPath = $OrchestrationPath # Potentially redundant"
WriteLog "Orchestration Path: $($OrchestrationPath)"
if (-not (Test-Path -Path $OrchestrationPath -PathType Container)) {
New-Item -Path $OrchestrationPath -ItemType Directory -Force | Out-Null
}
$win32Folder = Join-Path -Path $AppsPath -ChildPath "Win32"
if ($source -eq "winget" -and -not (Test-Path -Path $win32Folder -PathType Container)) {
New-Item -Path $win32Folder -ItemType Directory -Force | Out-Null
}
$storeAppsFolder = Join-Path -Path $AppsPath -ChildPath "MSStore"
if ($source -eq "msstore" -and -not (Test-Path -Path $storeAppsFolder -PathType Container)) {
New-Item -Path $storeAppsFolder -ItemType Directory -Force | Out-Null
}
try {
# Call Get-Application
$resultCode = Get-Application -AppName $appName -AppId $appId -Source $source -AppsPath $AppsPath -ApplicationArch $ApplicationItemData.Architecture -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath -SkipWin32Json -ErrorAction Stop
# Determine status based on result code
switch ($resultCode) {
0 { $status = "Downloaded successfully" }
1 { $status = "Error: No app installers were found" }
2 { $status = "Silent install switch could not be found. Did not download." }
3 { $status = "Error: Publisher does not support download" }
4 { $status = "Skipped: Use 'msstore' source instead." }
default { $status = "Downloaded with status: $resultCode" } # Should not happen with current Get-Application
}
# Remove app from AppList.json if silent install switch could not be found (resultCode 2)
if ($resultCode -eq 2) {
try {
if (Test-Path -Path $AppListJsonPath) {
$appListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json
if ($appListContent.apps) {
$filteredApps = @($appListContent.apps | Where-Object { $_.id -ne $appId })
$appListContent.apps = $filteredApps
$appListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8
WriteLog "Removed '$appName' ($appId) from '$AppListJsonPath' due to missing silent install switch."
}
}
}
catch {
WriteLog "Failed to remove '$appName' from '$AppListJsonPath': $($_.Exception.Message)"
}
}
}
catch {
$status = $_.Exception.Message
WriteLog "Download error for $($appName): $($_.Exception.Message)"
$resultCode = 1 # Indicate error
# Enqueue error status
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
# Remove app from AppList.json if publisher does not support download
if ($_.Exception.Message -match "does not support downloads by the publisher") {
try {
if (Test-Path -Path $AppListJsonPath) {
$appListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json
if ($appListContent.apps) {
$filteredApps = @($appListContent.apps | Where-Object { $_.id -ne $appId })
$appListContent.apps = $filteredApps
$appListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8
WriteLog "Removed '$appName' ($appId) from '$AppListJsonPath' due to publisher download restriction."
}
}
}
catch {
WriteLog "Failed to remove '$appName' from '$AppListJsonPath': $($_.Exception.Message)"
}
}
}
} # End if (-not $appFound)
}
catch {
$status = $_.Exception.Message
WriteLog "Unexpected error in Start-WingetAppDownloadTask for $($appName): $($_.Exception.Message)"
$resultCode = 1 # Indicate error
# Enqueue error status
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
}
finally {
# Ensure status is not empty before returning
if ([string]::IsNullOrEmpty($status)) {
$status = "Unknown failure" # Provide a default error status
WriteLog "Status was empty for $appName ($appId), setting to default error."
if ($resultCode -ne 0 -and $resultCode -ne 1 -and $resultCode -ne 2) {
$resultCode = -1 # Ensure resultCode reflects an error if it was empty
}
# Enqueue the final (error) status if it was previously empty
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
}
elseif ($resultCode -ne 0) {
# Enqueue the final status if it's an error (already set in try/catch)
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
}
else {
# Enqueue the final success status
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
}
}
# Prepare the return object as a Hashtable
$returnObject = @{ Id = $appId; Status = $status; ResultCode = $resultCode }
# Return the final status and result code as a Hashtable
return $returnObject
}
function Invoke-WingetDownload { function Invoke-WingetDownload {
param( param(
@@ -714,15 +406,53 @@ function Invoke-WingetDownload {
$localOrchestrationPath = Join-Path -Path $State.Controls.txtApplicationPath.Text -ChildPath "Orchestration" $localOrchestrationPath = Join-Path -Path $State.Controls.txtApplicationPath.Text -ChildPath "Orchestration"
# Create hashtable for task-specific arguments to pass to Invoke-ParallelProcessing # Create hashtable for task-specific arguments to pass to Invoke-ParallelProcessing
# UI downloads skip WinGetWin32Apps.json creation - it's generated at build time
$taskArguments = @{ $taskArguments = @{
AppsPath = $localAppsPath AppsPath = $localAppsPath
AppListJsonPath = $localAppListJsonPath AppListJsonPath = $localAppListJsonPath
OrchestrationPath = $localOrchestrationPath OrchestrationPath = $localOrchestrationPath
WindowsArch = $localWindowsArch WindowsArch = $localWindowsArch
SkipWin32Json = $true
} }
# Select only necessary properties before passing to Invoke-ParallelProcessing # Select only necessary properties before passing to Invoke-ParallelProcessing
$itemsToProcess = $selectedApps | Select-Object Name, Id, Source, Version, Architecture # Include Version and Architecture if needed $itemsToProcess = $selectedApps | Select-Object Name, Id, Source, Version, Architecture # Include Version and Architecture if needed
# Before downloading, persist the selected apps to AppList.json including exit-code fields (parity with Save-WingetList)
try {
# Determine AppList.json path; default if empty
if ([string]::IsNullOrWhiteSpace($localAppListJsonPath)) {
$localAppListJsonPath = Join-Path -Path $localAppsPath -ChildPath "AppList.json"
$taskArguments.AppListJsonPath = $localAppListJsonPath
WriteLog "AppListJsonPath was empty. Defaulting to: $localAppListJsonPath"
}
# Build apps payload from current selection, preserving AdditionalExitCodes/IgnoreNonZeroExitCodes
$appListToSave = @{
apps = @($selectedApps | ForEach-Object {
[ordered]@{
name = (ConvertTo-SafeName -Name $_.Name)
id = $_.Id
source = $_.Source.ToLower()
architecture = $_.Architecture
AdditionalExitCodes = if ($_.PSObject.Properties['AdditionalExitCodes']) { $_.AdditionalExitCodes } else { "" }
IgnoreNonZeroExitCodes = if ($_.PSObject.Properties['IgnoreNonZeroExitCodes']) { [bool]$_.IgnoreNonZeroExitCodes } else { $false }
}
})
}
# Ensure destination directory exists and write AppList.json
$destDir = Split-Path -Parent $localAppListJsonPath
if (-not (Test-Path -LiteralPath $destDir)) {
[void][System.IO.Directory]::CreateDirectory($destDir)
}
$appListToSave | ConvertTo-Json -Depth 10 | Set-Content -Path $localAppListJsonPath -Encoding UTF8
WriteLog "Persisted AppList.json with selected apps and exit-code fields to: $localAppListJsonPath"
}
catch {
WriteLog "Warning: Failed to persist AppList.json prior to download. Error: $($_.Exception.Message)"
}
# Invoke the centralized parallel processing function # Invoke the centralized parallel processing function
# Pass task type and task-specific arguments # Pass task type and task-specific arguments
Invoke-ParallelProcessing -ItemsToProcess $itemsToProcess ` Invoke-ParallelProcessing -ItemsToProcess $itemsToProcess `
+94 -3
View File
@@ -117,6 +117,7 @@ function Get-GeneralDefaults {
ShareName = "FFUCaptureShare" ShareName = "FFUCaptureShare"
Username = "ffu_user" Username = "ffu_user"
Threads = 5 Threads = 5
BitsPriority = 'Normal'
MaxUSBDrives = 5 MaxUSBDrives = 5
BuildUSBDriveEnable = $false BuildUSBDriveEnable = $false
CompactOS = $true CompactOS = $true
@@ -128,6 +129,7 @@ function Get-GeneralDefaults {
AllowExternalHardDiskMedia = $false AllowExternalHardDiskMedia = $false
PromptExternalHardDiskMedia = $true PromptExternalHardDiskMedia = $true
SelectSpecificUSBDrives = $false SelectSpecificUSBDrives = $false
CopyAdditionalFFUFiles = $false
CopyAutopilot = $false CopyAutopilot = $false
CopyUnattend = $false CopyUnattend = $false
CopyPPKG = $false CopyPPKG = $false
@@ -141,7 +143,7 @@ function Get-GeneralDefaults {
RemoveUpdates = $false RemoveUpdates = $false
# Hyper-V Settings Defaults # Hyper-V Settings Defaults
VMHostIPAddress = "" VMHostIPAddress = ""
DiskSizeGB = 30 DiskSizeGB = 50
MemoryGB = 4 MemoryGB = 4
Processors = 4 Processors = 4
VMLocation = $vmLocationPath VMLocation = $vmLocationPath
@@ -175,28 +177,104 @@ function Get-GeneralDefaults {
InstallDrivers = $false InstallDrivers = $false
CopyDrivers = $false CopyDrivers = $false
CopyPEDrivers = $false CopyPEDrivers = $false
UseDriversAsPEDrivers = $false
UpdateADK = $true UpdateADK = $true
CompressDownloadedDriversToWim = $false CompressDownloadedDriversToWim = $false
} }
} }
# Function to get USB Drives (Moved from BuildFFUVM_UI.ps1) # Function to get USB Drives (Moved from BuildFFUVM_UI.ps1)
# Uses Get-Disk to retrieve UniqueId which is more reliable than SerialNumber
# UniqueId is trimmed to remove the machine name suffix (characters after colon)
function Get-USBDrives { function Get-USBDrives {
Get-WmiObject Win32_DiskDrive | Where-Object { Get-WmiObject Win32_DiskDrive | Where-Object {
($_.MediaType -eq 'Removable Media' -or $_.MediaType -eq 'External hard disk media') ($_.MediaType -eq 'Removable Media' -or $_.MediaType -eq 'External hard disk media')
} | ForEach-Object { } | ForEach-Object {
$size = [math]::Round($_.Size / 1GB, 2) $size = [math]::Round($_.Size / 1GB, 2)
$serialNumber = if ($_.SerialNumber) { $_.SerialNumber.Trim() } else { "N/A" } # Get the disk using the index to retrieve UniqueId
$disk = Get-Disk -Number $_.Index -ErrorAction SilentlyContinue
# Trim the machine name suffix (everything after the colon) from UniqueId
$uniqueId = if ($disk -and $disk.UniqueId) {
$rawId = $disk.UniqueId
if ($rawId -match ':') {
$rawId.Split(':')[0]
}
else {
$rawId
}
}
else {
"N/A"
}
@{ @{
IsSelected = $false IsSelected = $false
Model = $_.Model.Trim() Model = $_.Model.Trim()
SerialNumber = $serialNumber UniqueId = $uniqueId
Size = $size Size = $size
DriveIndex = $_.Index DriveIndex = $_.Index
} }
} }
} }
# Returns a list of FFU files from the provided folder with selection metadata
function Get-FFUFiles {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
if (-not (Test-Path -Path $Path)) {
return @()
}
Get-ChildItem -Path $Path -Filter '*.ffu' -File -ErrorAction SilentlyContinue | ForEach-Object {
[PSCustomObject]@{
IsSelected = $false
Name = $_.Name
LastModified = $_.LastWriteTime
FullName = $_.FullName
}
}
}
# Helper: Populate Additional FFU List from the capture folder
function Update-AdditionalFFUList {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[PSCustomObject]$State
)
try {
$ffuFolder = $State.Controls.txtFFUCaptureLocation.Text
$listView = $State.Controls.lstAdditionalFFUs
if ($null -eq $listView) { return }
$listView.Items.Clear()
if ([string]::IsNullOrWhiteSpace($ffuFolder) -or -not (Test-Path -Path $ffuFolder)) {
WriteLog "Additional FFUs: Capture folder not set or not found: $ffuFolder"
}
else {
$items = Get-ChildItem -Path $ffuFolder -Filter '*.ffu' -File -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending |
ForEach-Object {
[PSCustomObject]@{
IsSelected = $false
Name = $_.Name
LastModified = $_.LastWriteTime
FullName = $_.FullName
}
}
foreach ($it in $items) { $listView.Items.Add($it) | Out-Null }
WriteLog "Additional FFUs: Found $($listView.Items.Count) FFU files in $ffuFolder."
}
$headerChk = $State.Controls.chkSelectAllAdditionalFFUs
if ($null -ne $headerChk) {
Update-SelectAllHeaderCheckBoxState -ListView $listView -HeaderCheckBox $headerChk
}
}
catch {
WriteLog "Update-AdditionalFFUList error: $($_.Exception.Message)"
}
}
# Function to manage the visibility of the application UI panels # Function to manage the visibility of the application UI panels
function Update-ApplicationPanelVisibility { function Update-ApplicationPanelVisibility {
param( param(
@@ -292,11 +370,14 @@ function Update-DriverCheckboxStates {
$installDriversChk = $State.Controls.chkInstallDrivers $installDriversChk = $State.Controls.chkInstallDrivers
$copyDriversChk = $State.Controls.chkCopyDrivers $copyDriversChk = $State.Controls.chkCopyDrivers
$compressWimChk = $State.Controls.chkCompressDriversToWIM $compressWimChk = $State.Controls.chkCompressDriversToWIM
$copyPEDriversChk = $State.Controls.chkCopyPEDrivers
$useDriversAsPeChk = $State.Controls.chkUseDriversAsPEDrivers
# Default to enabled, then apply disabling rules # Default to enabled, then apply disabling rules
$installDriversChk.IsEnabled = $true $installDriversChk.IsEnabled = $true
$copyDriversChk.IsEnabled = $true $copyDriversChk.IsEnabled = $true
$compressWimChk.IsEnabled = $true $compressWimChk.IsEnabled = $true
$copyPEDriversChk.IsEnabled = $true
if ($installDriversChk.IsChecked) { if ($installDriversChk.IsChecked) {
$copyDriversChk.IsEnabled = $false $copyDriversChk.IsEnabled = $false
@@ -310,6 +391,16 @@ function Update-DriverCheckboxStates {
if ($compressWimChk.IsChecked) { if ($compressWimChk.IsChecked) {
$installDriversChk.IsEnabled = $false $installDriversChk.IsEnabled = $false
} }
# Sub-option visibility logic: only show UseDriversAsPEDrivers when CopyPEDrivers is checked
if ($copyPEDriversChk.IsChecked) {
$useDriversAsPeChk.Visibility = 'Visible'
}
else {
# Parent unchecked: hide and clear sub-option
$useDriversAsPeChk.IsChecked = $false
$useDriversAsPeChk.Visibility = 'Collapsed'
}
} }
# Function to manage the visibility of Office UI panels # Function to manage the visibility of Office UI panels
+2 -8
View File
@@ -28,7 +28,7 @@ function Write-ProgressLog {
} }
Function Get-RemovableDrive { Function Get-RemovableDrive {
writelog "Get information for all removable drives" writelog "Get information for all removable drives"
$USBDrives = Get-WmiObject Win32_DiskDrive | Where-Object {$_.MediaType -eq "Removable media"} $USBDrives = Get-WmiObject Win32_DiskDrive | Where-Object {$_.MediaType -eq "Removable media" -or $_.MediaType -eq "External hard disk media"}
If($USBDrives -and ($null -eq $USBDrives.count)) { If($USBDrives -and ($null -eq $USBDrives.count)) {
$USBDrivesCount = 1 $USBDrivesCount = 1
} else { } else {
@@ -62,6 +62,7 @@ Function Build-DeploymentUSB{
$ScriptBlock = { $ScriptBlock = {
param($DriveNumber) param($DriveNumber)
Clear-Disk -Number $DriveNumber -RemoveData -RemoveOEM -Confirm:$false Clear-Disk -Number $DriveNumber -RemoveData -RemoveOEM -Confirm:$false
Initialize-Disk -Number $DriveNumber
$Disk = Get-Disk -Number $DriveNumber $Disk = Get-Disk -Number $DriveNumber
$PartitionStyle = $Disk.PartitionStyle $PartitionStyle = $Disk.PartitionStyle
if($PartitionStyle -ne 'MBR'){ if($PartitionStyle -ne 'MBR'){
@@ -120,13 +121,6 @@ $Destination = $Drive + ":\"
Start-Job -ScriptBlock $jobScriptBlock -ArgumentList $ImagesPath, $Destination | Out-Null Start-Job -ScriptBlock $jobScriptBlock -ArgumentList $ImagesPath, $Destination | Out-Null
} }
} }
if(!($Images)){
foreach ($Drive in $DeployDrives) {
WriteLog "Create images directory"
$drivepath = $Drive + ":\"
New-Item -Path "$drivepath" -Name Images -ItemType Directory -Force -Confirm: $false | Out-Null
}
}
if($Drivers){ if($Drivers){
writelog "Copying driver files to all drives labeled deploy concurrently" writelog "Copying driver files to all drives labeled deploy concurrently"
foreach ($Drive in $DeployDrives) { foreach ($Drive in $DeployDrives) {
File diff suppressed because it is too large Load Diff
Binary file not shown.
+1 -1
View File
@@ -20,7 +20,7 @@ The Full-Flash update (FFU) process can automatically download the latest releas
# Updates # Updates
2507.1 has been released to preview! This is a major update that brings a new user interface to preview. 2509.1 has been released to preview! This is a major update that brings a new user interface to preview.
Docs are coming, but will take a bit to write them. The youtube video is a must watch for a complete demo on how to use the UI and the changes made to apps (InstallAppsAndSysprep.cmd is gone) and drivers. I'll be recording a more formalized deep dive with slides that go a bit deeper into how things work, but the UI walkthrough should get most people going. Docs are coming, but will take a bit to write them. The youtube video is a must watch for a complete demo on how to use the UI and the changes made to apps (InstallAppsAndSysprep.cmd is gone) and drivers. I'll be recording a more formalized deep dive with slides that go a bit deeper into how things work, but the UI walkthrough should get most people going.