diff --git a/functions/microwin/Force-CleanupMountDirectory.ps1 b/functions/microwin/Force-CleanupMountDirectory.ps1
new file mode 100644
index 0000000000..8f223aea40
--- /dev/null
+++ b/functions/microwin/Force-CleanupMountDirectory.ps1
@@ -0,0 +1,76 @@
+function Force-CleanupMountDirectory {
+ <#
+ .SYNOPSIS
+ Forces cleanup of a mount directory by closing processes that have files open
+
+ .DESCRIPTION
+ This function attempts to clean up a mount directory by unloading registry hives,
+ releasing file handles, and removing readonly attributes.
+
+ .PARAMETER MountPath
+ The path to the mount directory to clean up
+
+ .PARAMETER TimeoutSeconds
+ Maximum time to wait for processes to close (default: 30 seconds)
+ #>
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$MountPath,
+
+ [int]$TimeoutSeconds = 30
+ )
+
+ try {
+ # Attempt to unload any registry hives that might still be loaded
+ $hiveNames = @("HKLM\zCOMPONENTS", "HKLM\zDEFAULT", "HKLM\zNTUSER", "HKLM\zSOFTWARE", "HKLM\zSYSTEM")
+ foreach ($hiveName in $hiveNames) {
+ try {
+ $null = reg query $hiveName 2>$null
+ if ($LASTEXITCODE -eq 0) {
+ # Registry hive is loaded, try to unload it with retries
+ while ($true) {
+ reg unload $hiveName 2>$null
+ if ($LASTEXITCODE -eq 0) {
+ break
+ }
+ Start-Sleep -Milliseconds 100
+ }
+ }
+ } catch {
+ # Hive not loaded or error checking - continue
+ }
+ }
+
+ # Force garbage collection to release any PowerShell file handles
+ Invoke-GarbageCollection -WaitSeconds 2
+
+ # Try to set the mount directory and its contents to not readonly
+ try {
+ if (Test-Path "$MountPath") {
+ & attrib -R "$MountPath\*" /S /D 2>$null
+ }
+ } catch {
+ # Ignore attrib errors
+ }
+
+ # Restart Windows Search service if it's running (helps release file handles)
+ try {
+ $searchService = Get-Service -Name "WSearch" -ErrorAction SilentlyContinue
+ if ($searchService -and $searchService.Status -eq "Running") {
+ Stop-Service -Name "WSearch" -Force -ErrorAction SilentlyContinue
+ Start-Sleep -Seconds 1
+ Start-Service -Name "WSearch" -ErrorAction SilentlyContinue
+ }
+ } catch {
+ # Ignore service restart errors
+ }
+
+ # Final cleanup
+ Invoke-GarbageCollection
+
+ return $true
+
+ } catch {
+ return $false
+ }
+}
diff --git a/functions/microwin/Get-ProcessesUsingPath.ps1 b/functions/microwin/Get-ProcessesUsingPath.ps1
new file mode 100644
index 0000000000..a1bd725b1e
--- /dev/null
+++ b/functions/microwin/Get-ProcessesUsingPath.ps1
@@ -0,0 +1,103 @@
+function Get-ProcessesUsingPath {
+ <#
+ .SYNOPSIS
+ Identifies processes that may be using files in a specific path
+
+ .DESCRIPTION
+ This function attempts to identify processes that have files open in the specified path,
+ which can help diagnose unmount issues.
+
+ .PARAMETER Path
+ The path to check for process usage
+
+ .EXAMPLE
+ Get-ProcessesUsingPath -Path "F:\Scratch"
+ #>
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$Path
+ )
+
+ Write-Host "Checking for processes using path: $Path" -ForegroundColor Cyan
+
+ $foundProcesses = @()
+
+ try {
+ # Method 1: Check process modules and loaded files
+ $allProcesses = Get-Process -ErrorAction SilentlyContinue
+ foreach ($process in $allProcesses) {
+ try {
+ if ($process.ProcessName -match "^(System|Registry|Idle)$") {
+ continue
+ }
+
+ # Check process modules
+ try {
+ $modules = $process.Modules
+ } catch {
+ $modules = $null
+ }
+ if ($modules) {
+ foreach ($module in $modules) {
+ if ($module.FileName -and $module.FileName.StartsWith($Path, [System.StringComparison]::OrdinalIgnoreCase)) {
+ $foundProcesses += @{
+ ProcessName = $process.ProcessName
+ PID = $process.Id
+ File = $module.FileName
+ Method = "Module"
+ }
+ break
+ }
+ }
+ }
+
+ # Check working directory
+ try {
+ $startInfo = $process.StartInfo
+ if ($startInfo -and $startInfo.WorkingDirectory -and $startInfo.WorkingDirectory.StartsWith($Path, [System.StringComparison]::OrdinalIgnoreCase)) {
+ $foundProcesses += @{
+ ProcessName = $process.ProcessName
+ PID = $process.Id
+ File = $startInfo.WorkingDirectory
+ Method = "WorkingDirectory"
+ }
+ }
+ } catch {
+ # Ignore access denied
+ }
+
+ } catch {
+ # Ignore processes we can't access
+ continue
+ }
+ }
+
+ # Method 2: Check common interfering processes
+ $suspiciousProcesses = Get-Process -ErrorAction SilentlyContinue | Where-Object {
+ $_.ProcessName -match "SearchIndexer|SearchProtocolHost|SearchFilterHost|MsMpEng|NisSrv|avp|avgnt|avast|mcshield|explorer"
+ }
+
+ if ($suspiciousProcesses) {
+ Write-Host "`nPotentially interfering processes (may not be directly using the path):" -ForegroundColor Yellow
+ foreach ($proc in $suspiciousProcesses) {
+ Write-Host " - $($proc.ProcessName) (PID: $($proc.Id))" -ForegroundColor Yellow
+ }
+ }
+
+ # Display results
+ if ($foundProcesses.Count -gt 0) {
+ Write-Host "`nProcesses found using path:" -ForegroundColor Red
+ foreach ($proc in $foundProcesses) {
+ Write-Host " - $($proc.ProcessName) (PID: $($proc.PID)) - $($proc.File) [$($proc.Method)]" -ForegroundColor Red
+ }
+ } else {
+ Write-Host "No processes found directly using the specified path." -ForegroundColor Green
+ }
+
+ return $foundProcesses
+
+ } catch {
+ Write-Host "Error checking processes: $($_.Exception.Message)" -ForegroundColor Red
+ return @()
+ }
+}
diff --git a/functions/microwin/Invoke-Microwin.ps1 b/functions/microwin/Invoke-Microwin.ps1
index 4ec6e04b29..c4b7f2eb2e 100644
--- a/functions/microwin/Invoke-Microwin.ps1
+++ b/functions/microwin/Invoke-Microwin.ps1
@@ -4,546 +4,40 @@ function Invoke-Microwin {
Invoke MicroWin routines...
#>
+ # Check if running as administrator first
+ $currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
+ $isAdmin = $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
- if($sync.ProcessRunning) {
- $msg = "GetIso process is currently running."
- [System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
- return
- }
-
- # Define the constants for Windows API
-Add-Type @"
-using System;
-using System.Runtime.InteropServices;
-
-public class PowerManagement {
- [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
- public static extern EXECUTION_STATE SetThreadExecutionState(EXECUTION_STATE esFlags);
-
- [FlagsAttribute]
- public enum EXECUTION_STATE : uint {
- ES_SYSTEM_REQUIRED = 0x00000001,
- ES_DISPLAY_REQUIRED = 0x00000002,
- ES_CONTINUOUS = 0x80000000,
- }
-}
-"@
-
- # Prevent the machine from sleeping
- [PowerManagement]::SetThreadExecutionState([PowerManagement]::EXECUTION_STATE::ES_CONTINUOUS -bor [PowerManagement]::EXECUTION_STATE::ES_SYSTEM_REQUIRED -bor [PowerManagement]::EXECUTION_STATE::ES_DISPLAY_REQUIRED)
-
- # Ask the user where to save the file
- $SaveDialog = New-Object System.Windows.Forms.SaveFileDialog
- $SaveDialog.InitialDirectory = [Environment]::GetFolderPath('Desktop')
- $SaveDialog.Filter = "ISO images (*.iso)|*.iso"
- $SaveDialog.ShowDialog() | Out-Null
-
- if ($SaveDialog.FileName -eq "") {
- $msg = "No file name for the target image was specified"
- Write-Host $msg
- Invoke-MicrowinBusyInfo -action "warning" -message $msg
- Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
- return
- }
-
- Set-WinUtilTaskbaritem -state "Indeterminate" -overlay "logo"
- Invoke-MicrowinBusyInfo -action "wip" -message "Busy..." -interactive $false
-
- Write-Host "Target ISO location: $($SaveDialog.FileName)"
-
- $index = $sync.MicrowinWindowsFlavors.SelectedValue.Split(":")[0].Trim()
- Write-Host "Index chosen: '$index' from $($sync.MicrowinWindowsFlavors.SelectedValue)"
-
- $copyToUSB = $sync.WPFMicrowinCopyToUsb.IsChecked
- $injectDrivers = $sync.MicrowinInjectDrivers.IsChecked
- $importDrivers = $sync.MicrowinImportDrivers.IsChecked
-
- $WPBT = $sync.MicroWinWPBT.IsChecked
- $unsupported = $sync.MicroWinUnsupported.IsChecked
-
- $importVirtIO = $sync.MicrowinCopyVirtIO.IsChecked
-
- $mountDir = $sync.MicrowinMountDir.Text
- $scratchDir = $sync.MicrowinScratchDir.Text
-
- # Detect if the Windows image is an ESD file and convert it to WIM
- if (-not (Test-Path -Path "$mountDir\sources\install.wim" -PathType Leaf) -and (Test-Path -Path "$mountDir\sources\install.esd" -PathType Leaf)) {
- Write-Host "Exporting Windows image to a WIM file, keeping the index we want to work on. This can take several minutes, depending on the performance of your computer..."
- try {
- Export-WindowsImage -SourceImagePath "$mountDir\sources\install.esd" -SourceIndex $index -DestinationImagePath "$mountDir\sources\install.wim" -CompressionType "Max"
- } catch {
- # Usually the case if it can't find unattend.dll on the host system. Guys, fix your corrupt messes that are your installations!
- dism /english /export-image /sourceimagefile="$mountDir\sources\install.esd" /sourceindex=$index /destinationimagefile="$mountDir\sources\install.wim" /compress:max
- }
- if ($?) {
- Remove-Item -Path "$mountDir\sources\install.esd" -Force
- # Since we've already exported the image index we wanted, switch to the first one
- $index = 1
- } else {
- $msg = "The export process has failed and MicroWin processing cannot continue"
- Write-Host $msg
- Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
- Invoke-MicrowinBusyInfo -action "warning" -message $msg
- [System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
- return
- }
- }
-
- $imgVersion = (Get-WindowsImage -ImagePath $mountDir\sources\install.wim -Index $index).Version
- Write-Host "The Windows Image Build Version is: $imgVersion"
-
- # Detect image version to avoid performing MicroWin processing on Windows 8 and earlier
- if ((Microwin-TestCompatibleImage $imgVersion $([System.Version]::new(10,0,10240,0))) -eq $false) {
- $msg = "This image is not compatible with MicroWin processing. Make sure it isn't a Windows 8 or earlier image."
- $dlg_msg = $msg + "`n`nIf you want more information, the version of the image selected is $($imgVersion)`n`nIf an image has been incorrectly marked as incompatible, report an issue to the developers."
- Write-Host $msg
- [System.Windows.MessageBox]::Show($dlg_msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Exclamation)
- Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
- Invoke-MicrowinBusyInfo -action "warning" -message $msg
+ if (-not $isAdmin) {
+ $msg = "Administrator privileges are required for MicroWin operations. Please run WinUtil as Administrator and try again."
+ [System.Windows.MessageBox]::Show($msg, "Administrator Required", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
return
}
- # Detect whether the image to process contains Windows 10 and show warning
- if ((Microwin-TestCompatibleImage $imgVersion $([System.Version]::new(10,0,21996,1))) -eq $false) {
- $msg = "Windows 10 has been detected in the image you want to process. While you can continue, Windows 10 is not a recommended target for MicroWin, and you may not get the full experience."
- $dlg_msg = $msg
- Write-Host $msg
- [System.Windows.MessageBox]::Show($dlg_msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Exclamation)
- }
-
- $mountDirExists = Test-Path $mountDir
- $scratchDirExists = Test-Path $scratchDir
- if (-not $mountDirExists -or -not $scratchDirExists) {
- $msg = "Required directories '$mountDirExists' '$scratchDirExists' and do not exist."
- Write-Error $msg
- Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
- Invoke-MicrowinBusyInfo -action "warning" -message $msg
+ if($sync.ProcessRunning) {
+ $msg = "GetIso process is currently running."
+ [System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
return
}
- try {
-
- Write-Host "Mounting Windows image. This may take a while."
- Mount-WindowsImage -ImagePath "$mountDir\sources\install.wim" -Index $index -Path "$scratchDir"
- if ($?) {
- Write-Host "The Windows image has been mounted successfully. Continuing processing..."
- } else {
- $msg = "Could not mount image. Exiting..."
- Write-Host $msg
- Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
- Invoke-MicrowinBusyInfo -action "warning" -message $msg
- return
- }
-
- if ($importDrivers) {
- Write-Host "Exporting drivers from active installation..."
- if (Test-Path "$env:TEMP\DRV_EXPORT") {
- Remove-Item "$env:TEMP\DRV_EXPORT" -Recurse -Force
- }
- if (($injectDrivers -and (Test-Path "$($sync.MicrowinDriverLocation.Text)"))) {
- Write-Host "Using specified driver source..."
- dism /english /online /export-driver /destination="$($sync.MicrowinDriverLocation.Text)" | Out-Host
- if ($?) {
- # Don't add exported drivers yet, that is run later
- Write-Host "Drivers have been exported successfully."
- } else {
- Write-Host "Failed to export drivers."
- }
- } else {
- New-Item -Path "$env:TEMP\DRV_EXPORT" -ItemType Directory -Force
- dism /english /online /export-driver /destination="$env:TEMP\DRV_EXPORT" | Out-Host
- if ($?) {
- Write-Host "Adding exported drivers..."
- dism /english /image="$scratchDir" /add-driver /driver="$env:TEMP\DRV_EXPORT" /recurse | Out-Host
- } else {
- Write-Host "Failed to export drivers. Continuing without importing them..."
- }
- if (Test-Path "$env:TEMP\DRV_EXPORT") {
- Remove-Item "$env:TEMP\DRV_EXPORT" -Recurse -Force
- }
- }
- }
-
- if ($injectDrivers) {
- $driverPath = $sync.MicrowinDriverLocation.Text
- if (Test-Path $driverPath) {
- Write-Host "Adding Windows Drivers image($scratchDir) drivers($driverPath) "
- dism /English /image:$scratchDir /add-driver /driver:$driverPath /recurse | Out-Host
- } else {
- Write-Host "Path to drivers is invalid continuing without driver injection"
- }
- }
-
- if ($WPBT) {
- Write-Host "Disabling WPBT Execution"
- reg load HKLM\zSYSTEM "$($scratchDir)\Windows\System32\config\SYSTEM"
- reg add "HKLM\zSYSTEM\ControlSet001\Control\Session Manager" /v DisableWpbtExecution /t REG_DWORD /d 1 /f
- reg unload HKLM\zSYSTEM
- }
-
- if ($unsupported) {
- Write-Host "Bypassing system requirements (locally)"
- reg add "HKCU\Control Panel\UnsupportedHardwareNotificationCache" /v "SV1" /t REG_DWORD /d 0 /f
- reg add "HKCU\Control Panel\UnsupportedHardwareNotificationCache" /v "SV2" /t REG_DWORD /d 0 /f
- reg add "HKLM\SYSTEM\Setup\LabConfig" /v "BypassCPUCheck" /t REG_DWORD /d 1 /f
- reg add "HKLM\SYSTEM\Setup\LabConfig" /v "BypassRAMCheck" /t REG_DWORD /d 1 /f
- reg add "HKLM\SYSTEM\Setup\LabConfig" /v "BypassSecureBootCheck" /t REG_DWORD /d 1 /f
- reg add "HKLM\SYSTEM\Setup\LabConfig" /v "BypassStorageCheck" /t REG_DWORD /d 1 /f
- reg add "HKLM\SYSTEM\Setup\LabConfig" /v "BypassTPMCheck" /t REG_DWORD /d 1 /f
- reg add "HKLM\SYSTEM\Setup\MoSetup" /v "AllowUpgradesWithUnsupportedTPMOrCPU" /t REG_DWORD /d 1 /f
- }
-
- if ($importVirtIO) {
- Write-Host "Copying VirtIO drivers..."
- Microwin-CopyVirtIO
- }
-
- Write-Host "Remove Features from the image"
- Microwin-RemoveFeatures -UseCmdlets $true
- Write-Host "Removing features complete!"
- Write-Host "Removing OS packages"
- Microwin-RemovePackages -UseCmdlets $true
- Write-Host "Removing Appx Bloat"
- Microwin-RemoveProvisionedPackages -UseCmdlets $true
-
- # Detect Windows 11 24H2 and add dependency to FileExp to prevent Explorer look from going back - thanks @WitherOrNot and @thecatontheceiling
- if ((Microwin-TestCompatibleImage $imgVersion $([System.Version]::new(10,0,26100,1))) -eq $true) {
- try {
- if (Test-Path "$scratchDir\Windows\SystemApps\MicrosoftWindows.Client.FileExp_cw5n1h2txyewy\appxmanifest.xml" -PathType Leaf) {
- # Found the culprit. Do the following:
- # 1. Take ownership of the file, from TrustedInstaller to Administrators
- takeown /F "$scratchDir\Windows\SystemApps\MicrosoftWindows.Client.FileExp_cw5n1h2txyewy\appxmanifest.xml" /A
- # 2. Set ACLs so that we can write to it
- icacls "$scratchDir\Windows\SystemApps\MicrosoftWindows.Client.FileExp_cw5n1h2txyewy\appxmanifest.xml" /grant "$(Microwin-GetLocalizedUsers -admins $true):(M)" | Out-Host
- # 3. Open the file and do the modification
- $appxManifest = Get-Content -Path "$scratchDir\Windows\SystemApps\MicrosoftWindows.Client.FileExp_cw5n1h2txyewy\appxmanifest.xml"
- $originalLine = $appxManifest[13]
- $dependency = "`n "
- $appxManifest[13] = "$originalLine$dependency"
- Set-Content -Path "$scratchDir\Windows\SystemApps\MicrosoftWindows.Client.FileExp_cw5n1h2txyewy\appxmanifest.xml" -Value $appxManifest -Force -Encoding utf8
- }
- }
- catch {
- # Fall back to what we used to do: delayed disablement
- Enable-WindowsOptionalFeature -Path "$scratchDir" -FeatureName "Recall"
- }
- }
-
- Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\System32\LogFiles\WMI\RtBackup" -Directory
- Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\DiagTrack" -Directory
- Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\InboxApps" -Directory
- Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\System32\LocationNotificationWindows.exe"
- Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files (x86)\Windows Media Player" -Directory
- Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files\Windows Media Player" -Directory
- Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files (x86)\Windows Mail" -Directory
- Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files\Windows Mail" -Directory
- Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files (x86)\Internet Explorer" -Directory
- Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files\Internet Explorer" -Directory
- Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\GameBarPresenceWriter"
- Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\System32\OneDriveSetup.exe"
- Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\System32\OneDrive.ico"
- Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\SystemApps" -mask "*narratorquickstart*" -Directory
- Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\SystemApps" -mask "*ParentalControls*" -Directory
- Write-Host "Removal complete!"
-
- Write-Host "Create unattend.xml"
-
- if (($sync.MicrowinAutoConfigBox.Text -ne "") -and (Test-Path "$($sync.MicrowinAutoConfigBox.Text)"))
- {
- try
- {
- Write-Host "A configuration file has been specified. Copying to WIM file..."
- Copy-Item "$($sync.MicrowinAutoConfigBox.Text)" "$($scratchDir)\winutil-config.json"
- }
- catch
- {
- Write-Host "The config file could not be copied. Continuing without it..."
- }
- }
-
- # Create unattended answer file with user information - Check condition to learn more about this functionality
- if ($sync.MicrowinUserName.Text -eq "")
- {
- Microwin-NewUnattend -userName "User"
- }
- else
- {
- if ($sync.MicrowinUserPassword.Password -eq "")
- {
- Microwin-NewUnattend -userName "$($sync.MicrowinUserName.Text)"
- }
- else
- {
- Microwin-NewUnattend -userName "$($sync.MicrowinUserName.Text)" -userPassword "$($sync.MicrowinUserPassword.Password)"
- }
- }
- Write-Host "Done Create unattend.xml"
- Write-Host "Copy unattend.xml file into the ISO"
- New-Item -ItemType Directory -Force -Path "$($scratchDir)\Windows\Panther"
- Copy-Item "$env:temp\unattend.xml" "$($scratchDir)\Windows\Panther\unattend.xml" -force
- New-Item -ItemType Directory -Force -Path "$($scratchDir)\Windows\System32\Sysprep"
- Copy-Item "$env:temp\unattend.xml" "$($scratchDir)\Windows\System32\Sysprep\unattend.xml" -force
- Write-Host "Done Copy unattend.xml"
-
- Write-Host "Create FirstRun"
- Microwin-NewFirstRun
- Write-Host "Done create FirstRun"
- Write-Host "Copy FirstRun.ps1 into the ISO"
- Copy-Item "$env:temp\FirstStartup.ps1" "$($scratchDir)\Windows\FirstStartup.ps1" -force
- Write-Host "Done copy FirstRun.ps1"
-
- Write-Host "Copy link to winutil.ps1 into the ISO"
- $desktopDir = "$($scratchDir)\Windows\Users\Default\Desktop"
- New-Item -ItemType Directory -Force -Path "$desktopDir"
- dism /English /image:$($scratchDir) /set-profilepath:"$($scratchDir)\Windows\Users\Default"
-
- Write-Host "Copy checkinstall.cmd into the ISO"
- Microwin-NewCheckInstall
- Copy-Item "$env:temp\checkinstall.cmd" "$($scratchDir)\Windows\checkinstall.cmd" -force
- Write-Host "Done copy checkinstall.cmd"
-
- Write-Host "Creating a directory that allows to bypass Wifi setup"
- New-Item -ItemType Directory -Force -Path "$($scratchDir)\Windows\System32\OOBE\BYPASSNRO"
-
- Write-Host "Loading registry"
- reg load HKLM\zCOMPONENTS "$($scratchDir)\Windows\System32\config\COMPONENTS"
- reg load HKLM\zDEFAULT "$($scratchDir)\Windows\System32\config\default"
- reg load HKLM\zNTUSER "$($scratchDir)\Users\Default\ntuser.dat"
- reg load HKLM\zSOFTWARE "$($scratchDir)\Windows\System32\config\SOFTWARE"
- reg load HKLM\zSYSTEM "$($scratchDir)\Windows\System32\config\SYSTEM"
-
- Write-Host "Disabling Teams"
- reg add "HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\Communications" /v "ConfigureChatAutoInstall" /t REG_DWORD /d 0 /f >$null 2>&1
- reg add "HKLM\zSOFTWARE\Policies\Microsoft\Windows\Windows Chat" /v ChatIcon /t REG_DWORD /d 2 /f >$null 2>&1
- reg add "HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v "TaskbarMn" /t REG_DWORD /d 0 /f >$null 2>&1
- reg query "HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\Communications" /v "ConfigureChatAutoInstall" >$null 2>&1
- Write-Host "Done disabling Teams"
-
- Write-Host "Fix Windows Volume Mixer Issue"
- reg add "HKLM\zNTUSER\Software\Microsoft\Internet Explorer\LowRegistry\Audio\PolicyConfig\PropertyStore" /f
-
- Write-Host "Bypassing system requirements (system image)"
- reg add "HKLM\zDEFAULT\Control Panel\UnsupportedHardwareNotificationCache" /v "SV1" /t REG_DWORD /d 0 /f
- reg add "HKLM\zDEFAULT\Control Panel\UnsupportedHardwareNotificationCache" /v "SV2" /t REG_DWORD /d 0 /f
- reg add "HKLM\zNTUSER\Control Panel\UnsupportedHardwareNotificationCache" /v "SV1" /t REG_DWORD /d 0 /f
- reg add "HKLM\zNTUSER\Control Panel\UnsupportedHardwareNotificationCache" /v "SV2" /t REG_DWORD /d 0 /f
- reg add "HKLM\zSYSTEM\Setup\LabConfig" /v "BypassCPUCheck" /t REG_DWORD /d 1 /f
- reg add "HKLM\zSYSTEM\Setup\LabConfig" /v "BypassRAMCheck" /t REG_DWORD /d 1 /f
- reg add "HKLM\zSYSTEM\Setup\LabConfig" /v "BypassSecureBootCheck" /t REG_DWORD /d 1 /f
- reg add "HKLM\zSYSTEM\Setup\LabConfig" /v "BypassStorageCheck" /t REG_DWORD /d 1 /f
- reg add "HKLM\zSYSTEM\Setup\LabConfig" /v "BypassTPMCheck" /t REG_DWORD /d 1 /f
- reg add "HKLM\zSYSTEM\Setup\MoSetup" /v "AllowUpgradesWithUnsupportedTPMOrCPU" /t REG_DWORD /d 1 /f
-
- # Prevent Windows Update Installing so called Expedited Apps - 24H2 and newer
- if ((Microwin-TestCompatibleImage $imgVersion $([System.Version]::new(10,0,26100,1))) -eq $true) {
- @(
- 'EdgeUpdate',
- 'DevHomeUpdate',
- 'OutlookUpdate',
- 'CrossDeviceUpdate'
- ) | ForEach-Object {
- Write-Host "Removing Windows Expedited App: $_"
- reg delete "HKLM\zSOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\$_" /f | Out-Null
- }
- }
-
- reg add "HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "SearchboxTaskbarMode" /t REG_DWORD /d 0 /f
- Write-Host "Setting all services to start manually"
- reg add "HKLM\zSOFTWARE\CurrentControlSet\Services" /v Start /t REG_DWORD /d 3 /f
-
- Write-Host "Enabling Local Accounts on OOBE"
- reg add "HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v "BypassNRO" /t REG_DWORD /d "1" /f
-
- Write-Host "Disabling Sponsored Apps"
- reg add "HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" /v "OemPreInstalledAppsEnabled" /t REG_DWORD /d 0 /f
- reg add "HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" /v "PreInstalledAppsEnabled" /t REG_DWORD /d 0 /f
- reg add "HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" /v "SilentInstalledAppsEnabled" /t REG_DWORD /d 0 /f
- reg add "HKLM\zSOFTWARE\Policies\Microsoft\Windows\CloudContent" /v "DisableWindowsConsumerFeatures" /t REG_DWORD /d 1 /f
- reg add "HKLM\zSOFTWARE\Microsoft\PolicyManager\current\device\Start" /v "ConfigureStartPins" /t REG_SZ /d '{\"pinnedList\": [{}]}' /f
- Write-Host "Done removing Sponsored Apps"
-
- Write-Host "Disabling Reserved Storage"
- reg add "HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager" /v "ShippedWithReserves" /t REG_DWORD /d 0 /f
-
- Write-Host "Changing theme to dark. This only works on Activated Windows"
- reg add "HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize" /v "AppsUseLightTheme" /t REG_DWORD /d 0 /f
- reg add "HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize" /v "SystemUsesLightTheme" /t REG_DWORD /d 0 /f
-
- if ((Microwin-TestCompatibleImage $imgVersion $([System.Version]::new(10,0,21996,1))) -eq $false) {
- # We're dealing with Windows 10. Configure sane desktop settings. NOTE: even though stuff to disable News and Interests is there,
- # it doesn't seem to work, and I don't want to waste more time dealing with an operating system that will lose support in a year (2025)
-
- # I invite anyone to work on improving stuff for News and Interests, but that won't be me!
-
- Write-Host "Disabling Search Highlights..."
- reg add "HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\Feeds\DSB" /v "ShowDynamicContent" /t REG_DWORD /d 0 /f
- reg add "HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\SearchSettings" /v "IsDynamicSearchBoxEnabled" /t REG_DWORD /d 0 /f
- reg add "HKLM\zSOFTWARE\Policies\Microsoft\Dsh" /v "AllowNewsAndInterests" /t REG_DWORD /d 0 /f
- reg add "HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "TraySearchBoxVisible" /t REG_DWORD /d 1 /f
- reg add "HKLM\zSOFTWARE\Policies\Microsoft\Windows\Windows Feeds" /v "EnableFeeds" /t REG_DWORD /d 0 /f
- }
-
- } catch {
- Write-Error "An unexpected error occurred: $_"
- } finally {
- Write-Host "Unmounting Registry..."
- reg unload HKLM\zCOMPONENTS
- reg unload HKLM\zDEFAULT
- reg unload HKLM\zNTUSER
- reg unload HKLM\zSOFTWARE
- reg unload HKLM\zSYSTEM
-
- Write-Host "Cleaning up image..."
- dism /English /image:$scratchDir /Cleanup-Image /StartComponentCleanup /ResetBase
- Write-Host "Cleanup complete."
-
- Write-Host "Unmounting image..."
- Dismount-WindowsImage -Path "$scratchDir" -Save
- }
-
- try {
-
- Write-Host "Exporting image into $mountDir\sources\install2.wim"
- try {
- Export-WindowsImage -SourceImagePath "$mountDir\sources\install.wim" -SourceIndex $index -DestinationImagePath "$mountDir\sources\install2.wim" -CompressionType "Max"
- } catch {
- # Usually the case if it can't find unattend.dll on the host system. Guys, fix your corrupt messes that are your installations!
- dism /english /export-image /sourceimagefile="$mountDir\sources\install.wim" /sourceindex=$index /destinationimagefile="$mountDir\sources\install2.wim" /compress:max
- }
- Write-Host "Remove old '$mountDir\sources\install.wim' and rename $mountDir\sources\install2.wim"
- Remove-Item "$mountDir\sources\install.wim"
- Rename-Item "$mountDir\sources\install2.wim" "$mountDir\sources\install.wim"
-
- if (-not (Test-Path -Path "$mountDir\sources\install.wim")) {
- $msg = "Something went wrong. Please report this bug to the devs."
- Write-Error "$($msg) '$($mountDir)\sources\install.wim' doesn't exist"
- Invoke-MicrowinBusyInfo -action "warning" -message $msg
- Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
- return
- }
- Write-Host "Windows image completed. Continuing with boot.wim."
-
- $esd = $sync.MicroWinESD.IsChecked
- if ($esd) {
- Write-Host "Converting install image to ESD."
- try {
- Export-WindowsImage -SourceImagePath "$mountDir\sources\install.wim" -SourceIndex $index -DestinationImagePath "$mountDir\sources\install.esd" -CompressionType "Recovery"
- Remove-Item "$mountDir\sources\install.wim"
- Write-Host "Converted install image to ESD."
- } catch {
- Start-Process -FilePath "$env:SystemRoot\System32\dism.exe" -ArgumentList "/export-image /sourceimagefile:`"$mountDir\sources\install.wim`" /sourceindex:1 /destinationimagefile:`"$mountDir\sources\install.esd`" /compress:recovery" -Wait -NoNewWindow
- Remove-Item "$mountDir\sources\install.wim"
- Write-Host "Converted install image to ESD."
- }
- }
-
- # Next step boot image
- Write-Host "Mounting boot image $mountDir\sources\boot.wim into $scratchDir"
- Mount-WindowsImage -ImagePath "$mountDir\sources\boot.wim" -Index 2 -Path "$scratchDir"
-
- if ($injectDrivers) {
- $driverPath = $sync.MicrowinDriverLocation.Text
- if (Test-Path $driverPath) {
- Write-Host "Adding Windows Drivers image($scratchDir) drivers($driverPath) "
- dism /English /image:$scratchDir /add-driver /driver:$driverPath /recurse | Out-Host
- } else {
- Write-Host "Path to drivers is invalid continuing without driver injection"
- }
- }
-
- Write-Host "Loading registry..."
- reg load HKLM\zCOMPONENTS "$($scratchDir)\Windows\System32\config\COMPONENTS" >$null
- reg load HKLM\zDEFAULT "$($scratchDir)\Windows\System32\config\default" >$null
- reg load HKLM\zNTUSER "$($scratchDir)\Users\Default\ntuser.dat" >$null
- reg load HKLM\zSOFTWARE "$($scratchDir)\Windows\System32\config\SOFTWARE" >$null
- reg load HKLM\zSYSTEM "$($scratchDir)\Windows\System32\config\SYSTEM" >$null
- Write-Host "Bypassing system requirements on the setup image"
- reg add "HKLM\zDEFAULT\Control Panel\UnsupportedHardwareNotificationCache" /v "SV1" /t REG_DWORD /d 0 /f
- reg add "HKLM\zDEFAULT\Control Panel\UnsupportedHardwareNotificationCache" /v "SV2" /t REG_DWORD /d 0 /f
- reg add "HKLM\zNTUSER\Control Panel\UnsupportedHardwareNotificationCache" /v "SV1" /t REG_DWORD /d 0 /f
- reg add "HKLM\zNTUSER\Control Panel\UnsupportedHardwareNotificationCache" /v "SV2" /t REG_DWORD /d 0 /f
- reg add "HKLM\zSYSTEM\Setup\LabConfig" /v "BypassCPUCheck" /t REG_DWORD /d 1 /f
- reg add "HKLM\zSYSTEM\Setup\LabConfig" /v "BypassRAMCheck" /t REG_DWORD /d 1 /f
- reg add "HKLM\zSYSTEM\Setup\LabConfig" /v "BypassSecureBootCheck" /t REG_DWORD /d 1 /f
- reg add "HKLM\zSYSTEM\Setup\LabConfig" /v "BypassStorageCheck" /t REG_DWORD /d 1 /f
- reg add "HKLM\zSYSTEM\Setup\LabConfig" /v "BypassTPMCheck" /t REG_DWORD /d 1 /f
- reg add "HKLM\zSYSTEM\Setup\MoSetup" /v "AllowUpgradesWithUnsupportedTPMOrCPU" /t REG_DWORD /d 1 /f
- # Fix Computer Restarted Unexpectedly Error on New Bare Metal Install
- reg add "HKLM\zSYSTEM\Setup\Status\ChildCompletion" /v "setup.exe" /t REG_DWORD /d 3 /f
- } catch {
- Write-Error "An unexpected error occurred: $_"
- } finally {
- Write-Host "Unmounting Registry..."
- reg unload HKLM\zCOMPONENTS
- reg unload HKLM\zDEFAULT
- reg unload HKLM\zNTUSER
- reg unload HKLM\zSOFTWARE
- reg unload HKLM\zSYSTEM
-
- Write-Host "Unmounting image..."
- Dismount-WindowsImage -Path "$scratchDir" -Save
-
- Write-Host "Creating ISO image"
-
- # if we downloaded oscdimg from github it will be in the temp directory so use it
- # if it is not in temp it is part of ADK and is in global PATH so just set it to oscdimg.exe
- $oscdimgPath = Join-Path $env:TEMP 'oscdimg.exe'
- $oscdImgFound = Test-Path $oscdimgPath -PathType Leaf
- if (!$oscdImgFound) {
- $oscdimgPath = "oscdimg.exe"
- }
-
- Write-Host "[INFO] Using oscdimg.exe from: $oscdimgPath"
-
- $oscdimgProc = Start-Process -FilePath "$oscdimgPath" -ArgumentList "-m -o -u2 -udfver102 -bootdata:2#p0,e,b`"$mountDir\boot\etfsboot.com`"#pEF,e,b`"$mountDir\efi\microsoft\boot\efisys.bin`" `"$mountDir`" `"$($SaveDialog.FileName)`"" -Wait -PassThru -NoNewWindow
-
- $LASTEXITCODE = $oscdimgProc.ExitCode
-
- Write-Host "OSCDIMG Error Level : $($oscdimgProc.ExitCode)"
-
- if ($copyToUSB) {
- Write-Host "Copying target ISO to the USB drive"
- Microwin-CopyToUSB("$($SaveDialog.FileName)")
- if ($?) { Write-Host "Done Copying target ISO to USB drive!" } else { Write-Host "ISO copy failed." }
- }
-
- Write-Host " _____ "
- Write-Host "(____ \ "
- Write-Host " _ \ \ ___ ____ ____ "
- Write-Host "| | | / _ \| _ \ / _ ) "
- Write-Host "| |__/ / |_| | | | ( (/ / "
- Write-Host "|_____/ \___/|_| |_|\____) "
-
- # Check if the ISO was successfully created - CTT edit
- if ($LASTEXITCODE -eq 0) {
- Write-Host "`n`nPerforming Cleanup..."
- Remove-Item -Recurse -Force "$($scratchDir)"
- Remove-Item -Recurse -Force "$($mountDir)"
- $msg = "Done. ISO image is located here: $($SaveDialog.FileName)"
- Write-Host $msg
- Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"
- Invoke-MicrowinBusyInfo -action "done" -message "Finished!"
- [System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
- } else {
- Write-Host "ISO creation failed. The "$($mountDir)" directory has not been removed."
- try {
- # This creates a new Win32 exception from which we can extract a message in the system language.
- # Now, this will NOT throw an exception
- $exitCode = New-Object System.ComponentModel.Win32Exception($LASTEXITCODE)
- Write-Host "Reason: $($exitCode.Message)"
- Invoke-MicrowinBusyInfo -action "warning" -message $exitCode.Message
- Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
- [System.Windows.MessageBox]::Show("MicroWin failed to make the ISO.", "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
- } catch {
- # Could not get error description from Windows APIs
- }
- }
-
- Toggle-MicrowinPanel 1
-
- $sync.MicrowinFinalIsoLocation.Text = "$($SaveDialog.FileName)"
- # Allow the machine to sleep again (optional)
- [PowerManagement]::SetThreadExecutionState(0)
- $sync.ProcessRunning = $false
- }
+ # Get all the parameters we need from the UI before starting the runspace
+ $microwinsettings = @{
+ mountDir = $sync.MicrowinMountDir.Text
+ scratchDir = $sync.MicrowinScratchDir.Text
+ copyToUSB = $sync.WPFMicrowinCopyToUsb.IsChecked
+ injectDrivers = $sync.MicrowinInjectDrivers.IsChecked
+ importDrivers = $sync.MicrowinImportDrivers.IsChecked
+ WPBT = $sync.MicroWinWPBT.IsChecked
+ unsupported = $sync.MicroWinUnsupported.IsChecked
+ importVirtIO = $sync.MicrowinCopyVirtIO.IsChecked
+ selectedIndex = if ($sync.MicrowinWindowsFlavors.SelectedValue) { $sync.MicrowinWindowsFlavors.SelectedValue.Split(":")[0].Trim() } else { "1" }
+ driverPath = $sync.MicrowinDriverLocation.Text
+ esd = $sync.MicroWinESD.IsChecked
+ autoConfigPath = $sync.MicrowinAutoConfigBox.Text
+ userName = $sync.MicrowinUserName.Text
+ userPassword = $sync.MicrowinUserPassword.Password
+ }
+
+ # Start the MicroWin process in a runspace to avoid blocking the UI
+ Invoke-WPFMicroWinRunspace -MicroWinSettings $microwinsettings
}
diff --git a/functions/microwin/Invoke-MicrowinGetIso.ps1 b/functions/microwin/Invoke-MicrowinGetIso.ps1
index 63eb599b3b..4e66efba82 100644
--- a/functions/microwin/Invoke-MicrowinGetIso.ps1
+++ b/functions/microwin/Invoke-MicrowinGetIso.ps1
@@ -1,7 +1,7 @@
function Invoke-MicrowinGetIso {
<#
.DESCRIPTION
- Function to get the path to Iso file for MicroWin, unpack that isom=, read basic information and populate the UI Options
+ Function to get the path to Iso file for MicroWin, unpack that ISO, read basic information and populate the UI Options
#>
Write-Host "Invoking WPFGetIso"
@@ -12,25 +12,21 @@ function Invoke-MicrowinGetIso {
return
}
- # Provide immediate feedback to user
- Invoke-MicrowinBusyInfo -action "wip" -message "Initializing MicroWin process..." -interactive $false
-
- Write-Host " _ __ __ _ "
- Write-Host " /\/\ (_) ___ _ __ ___ / / /\ \ \(_) _ __ "
- Write-Host " / \ | | / __|| '__| / _ \ \ \/ \/ /| || '_ \ "
- Write-Host "/ /\/\ \| || (__ | | | (_) | \ /\ / | || | | | "
- Write-Host "\/ \/|_| \___||_| \___/ \/ \/ |_||_| |_| "
+ # Handle file/folder selection on the main thread before starting runspace
+ $filePath = ""
+ $targetFolder = ""
if ($sync["ISOmanual"].IsChecked) {
# Open file dialog to let user choose the ISO file
Invoke-MicrowinBusyInfo -action "wip" -message "Please select an ISO file..." -interactive $true
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null
$openFileDialog = New-Object System.Windows.Forms.OpenFileDialog
- $openFileDialog.initialDirectory = $initialDirectory
$openFileDialog.filter = "ISO files (*.iso)| *.iso"
$openFileDialog.ShowDialog() | Out-Null
$filePath = $openFileDialog.FileName
+ Write-Host "Selected file path: '$filePath'"
+
if ([string]::IsNullOrEmpty($filePath)) {
Write-Host "No ISO is chosen"
Invoke-MicrowinBusyInfo -action "hide" -message " "
@@ -44,296 +40,26 @@ function Invoke-MicrowinGetIso {
$isoDownloaderFBD = New-Object System.Windows.Forms.FolderBrowserDialog
$isoDownloaderFBD.Description = "Please specify the path to download the ISO file to:"
$isoDownloaderFBD.ShowNewFolderButton = $true
- if ($isoDownloaderFBD.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK)
- {
+ if ($isoDownloaderFBD.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) {
Invoke-MicrowinBusyInfo -action "hide" -message " "
return
}
-
- Set-WinUtilTaskbaritem -state "Indeterminate" -overlay "logo"
- Invoke-MicrowinBusyInfo -action "wip" -message "Preparing to download ISO..." -interactive $false
-
- # Grab the location of the selected path
$targetFolder = $isoDownloaderFBD.SelectedPath
-
- # Auto download newest ISO
- # Credit: https://github.com/pbatard/Fido
- $fidopath = "$env:temp\Fido.ps1"
- $originalLocation = $PSScriptRoot
-
- Invoke-MicrowinBusyInfo -action "wip" -message "Downloading Fido script..." -interactive $false
- Invoke-WebRequest "https://github.com/pbatard/Fido/raw/master/Fido.ps1" -OutFile $fidopath
-
- Set-Location -Path $env:temp
- # Detect if the first option ("System language") has been selected and get a Fido-approved language from the current culture
- $lang = if ($sync["ISOLanguage"].SelectedIndex -eq 0) {
- Microwin-GetLangFromCulture -langName (Get-Culture).Name
- } else {
- $sync["ISOLanguage"].SelectedItem
- }
-
- Invoke-MicrowinBusyInfo -action "wip" -message "Downloading Windows ISO... (This may take a long time)" -interactive $false
- & $fidopath -Win 'Windows 11' -Rel $sync["ISORelease"].SelectedItem -Arch "x64" -Lang $lang -Ed "Windows 11 Home/Pro/Edu"
- if (-not $?)
- {
- Write-Host "Could not download the ISO file. Look at the output of the console for more information."
- $msg = "The ISO file could not be downloaded"
- Invoke-MicrowinBusyInfo -action "warning" -message $msg
- Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
- [System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
- return
- }
- Set-Location $originalLocation
- # Use the FullName property to only grab the file names. Using this property is necessary as, without it, you're passing the usual output of Get-ChildItem
- # to the variable, and let's be honest, that does NOT exist in the file system
- $filePath = (Get-ChildItem -Path "$env:temp" -Filter "Win11*.iso").FullName | Sort-Object LastWriteTime -Descending | Select-Object -First 1
- $fileName = [IO.Path]::GetFileName("$filePath")
-
- if (($targetFolder -ne "") -and (Test-Path "$targetFolder"))
- {
- try
- {
- # "Let it download to $env:TEMP and then we **move** it to the file path." - CodingWonders
- $destinationFilePath = "$targetFolder\$fileName"
- Write-Host "Moving ISO file. Please wait..."
- Move-Item -Path "$filePath" -Destination "$destinationFilePath" -Force
- $filePath = $destinationFilePath
- }
- catch
- {
- $msg = "Unable to move the ISO file to the location you specified. The downloaded ISO is in the `"$env:TEMP`" folder"
- Write-Host $msg
- Write-Host "Error information: $($_.Exception.Message)" -ForegroundColor Yellow
- Invoke-MicrowinBusyInfo -action "warning" -message $msg
- return
- }
- }
- }
-
- Write-Host "File path $($filePath)"
- if (-not (Test-Path -Path "$filePath" -PathType Leaf)) {
- $msg = "File you've chosen doesn't exist"
- Invoke-MicrowinBusyInfo -action "warning" -message $msg
- [System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
- return
- }
-
- Set-WinUtilTaskbaritem -state "Indeterminate" -overlay "logo"
- Invoke-MicrowinBusyInfo -action "wip" -message "Checking system requirements..." -interactive $false
-
- $oscdimgPath = Join-Path $env:TEMP 'oscdimg.exe'
- $oscdImgFound = [bool] (Get-Command -ErrorAction Ignore -Type Application oscdimg.exe) -or (Test-Path $oscdimgPath -PathType Leaf)
- Write-Host "oscdimg.exe on system: $oscdImgFound"
-
- if (!$oscdImgFound) {
- $downloadFromGitHub = $sync.WPFMicrowinDownloadFromGitHub.IsChecked
-
- if (!$downloadFromGitHub) {
- # only show the message to people who did check the box to download from github, if you check the box
- # you consent to downloading it, no need to show extra dialogs
- [System.Windows.MessageBox]::Show("oscdimge.exe is not found on the system, winutil will now attempt do download and install it using choco. This might take a long time.")
- # the step below needs choco to download oscdimg
- # Install Choco if not already present
- Install-WinUtilChoco
- $chocoFound = [bool] (Get-Command -ErrorAction Ignore -Type Application choco)
- Write-Host "choco on system: $chocoFound"
- if (!$chocoFound) {
- [System.Windows.MessageBox]::Show("choco.exe is not found on the system, you need choco to download oscdimg.exe")
- return
- }
-
- Start-Process -Verb runas -FilePath powershell.exe -ArgumentList "choco install windows-adk-oscdimg"
- $msg = "oscdimg is installed, now close, reopen PowerShell terminal and re-launch winutil.ps1"
- Invoke-MicrowinBusyInfo -action "done" -message $msg # We set it to done because it immediately returns from this function
- [System.Windows.MessageBox]::Show($msg)
- return
- } else {
- [System.Windows.MessageBox]::Show("oscdimge.exe is not found on the system, winutil will now attempt do download and install it from github. This might take a long time.")
- Invoke-MicrowinBusyInfo -action "wip" -message "Downloading oscdimg.exe..." -interactive $false
- Microwin-GetOscdimg -oscdimgPath $oscdimgPath
- $oscdImgFound = Test-Path $oscdimgPath -PathType Leaf
- if (!$oscdImgFound) {
- $msg = "oscdimg was not downloaded can not proceed"
- Invoke-MicrowinBusyInfo -action "warning" -message $msg
- [System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
- return
- } else {
- Write-Host "oscdimg.exe was successfully downloaded from github"
- }
- }
- }
-
- Invoke-MicrowinBusyInfo -action "wip" -message "Checking disk space..." -interactive $false
-
- # Detect the file size of the ISO and compare it with the free space of the system drive
- $isoSize = (Get-Item -Path "$filePath").Length
- Write-Debug "Size of ISO file: $($isoSize) bytes"
- # Use this procedure to get the free space of the drive depending on where the user profile folder is stored.
- # This is done to guarantee a dynamic solution, as the installation drive may be mounted to a letter different than C
- $driveSpace = (Get-Volume -DriveLetter ([IO.Path]::GetPathRoot([Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile)).Replace(":\", "").Trim())).SizeRemaining
- Write-Debug "Free space on installation drive: $($driveSpace) bytes"
- if ($driveSpace -lt ($isoSize * 2)) {
- # It's not critical and we _may_ continue. Output a warning
- Write-Warning "You may not have enough space for this operation. Proceed at your own risk."
- }
- elseif ($driveSpace -lt $isoSize) {
- # It's critical and we can't continue. Output an error
- $msg = "You don't have enough space for this operation. You need at least $([Math]::Round(($isoSize / ([Math]::Pow(1024, 2))) * 2, 2)) MB of free space to copy the ISO files to a temp directory and to be able to perform additional operations."
- Write-Host $msg
- Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
- Invoke-MicrowinBusyInfo -action "warning" -message $msg
- return
- } else {
- Write-Host "You have enough space for this operation."
- }
-
- try {
- Invoke-MicrowinBusyInfo -action "wip" -message "Mounting ISO file..." -interactive $false
- Write-Host "Mounting Iso. Please wait."
- $mountedISO = Mount-DiskImage -PassThru "$filePath"
- Write-Host "Done mounting Iso `"$($mountedISO.ImagePath)`""
- $driveLetter = (Get-Volume -DiskImage $mountedISO).DriveLetter
- Write-Host "Iso mounted to '$driveLetter'"
- } catch {
- # @ChrisTitusTech please copy this wiki and change the link below to your copy of the wiki
- $msg = "Failed to mount the image. Error: $($_.Exception.Message)"
- Write-Error $msg
- Write-Error "This is NOT winutil's problem, your ISO might be corrupt, or there is a problem on the system"
- Write-Host "Please refer to this wiki for more details: https://christitustech.github.io/winutil/KnownIssues/#troubleshoot-errors-during-microwin-usage" -ForegroundColor Red
- Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
- Invoke-MicrowinBusyInfo -action "warning" -message $msg
- return
}
- # storing off values in hidden fields for further steps
- # there is probably a better way of doing this, I don't have time to figure this out
- $sync.MicrowinIsoDrive.Text = $driveLetter
- $mountedISOPath = (Split-Path -Path "$filePath")
- if ($sync.MicrowinScratchDirBox.Text.Trim() -eq "Scratch") {
- $sync.MicrowinScratchDirBox.Text =""
+ # Get all the parameters we need from the UI before starting the runspace
+ $getIsoSettings = @{
+ isManual = $sync["ISOmanual"].IsChecked
+ isDownloader = $sync["ISOdownloader"].IsChecked
+ language = if ($sync["ISOLanguage"].SelectedItem) { $sync["ISOLanguage"].SelectedItem } else { "" }
+ languageIndex = if ($sync["ISOLanguage"].SelectedIndex) { $sync["ISOLanguage"].SelectedIndex } else { 0 }
+ release = if ($sync["ISORelease"].SelectedItem) { $sync["ISORelease"].SelectedItem } else { "" }
+ downloadFromGitHub = $sync.WPFMicrowinDownloadFromGitHub.IsChecked
+ useISOScratchDir = $sync.WPFMicrowinISOScratchDir.IsChecked
+ filePath = $filePath
+ targetFolder = $targetFolder
}
- $UseISOScratchDir = $sync.WPFMicrowinISOScratchDir.IsChecked
-
- if ($UseISOScratchDir) {
- $sync.MicrowinScratchDirBox.Text=$mountedISOPath
- }
-
- if( -Not $sync.MicrowinScratchDirBox.Text.EndsWith('\') -And $sync.MicrowinScratchDirBox.Text.Length -gt 1) {
-
- $sync.MicrowinScratchDirBox.Text = Join-Path $sync.MicrowinScratchDirBox.Text.Trim() '\'
-
- }
-
- # Detect if the folders already exist and remove them
- if (($sync.MicrowinMountDir.Text -ne "") -and (Test-Path -Path $sync.MicrowinMountDir.Text)) {
- try {
- Write-Host "Deleting temporary files from previous run. Please wait..."
- Remove-Item -Path $sync.MicrowinMountDir.Text -Recurse -Force
- Remove-Item -Path $sync.MicrowinScratchDir.Text -Recurse -Force
- } catch {
- Write-Host "Could not delete temporary files. You need to delete those manually."
- }
- }
-
- Write-Host "Setting up mount dir and scratch dirs"
- $timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
- $randomNumber = Get-Random -Minimum 1 -Maximum 9999
- $randomMicrowin = "Microwin_${timestamp}_${randomNumber}"
- $randomMicrowinScratch = "MicrowinScratch_${timestamp}_${randomNumber}"
- $sync.BusyText.Text=" - Mounting"
- Write-Host "Mounting Iso. Please wait."
- if ($sync.MicrowinScratchDirBox.Text -eq "") {
- $mountDir = Join-Path $env:TEMP $randomMicrowin
- $scratchDir = Join-Path $env:TEMP $randomMicrowinScratch
- } else {
- $scratchDir = $sync.MicrowinScratchDirBox.Text+"Scratch"
- $mountDir = $sync.MicrowinScratchDirBox.Text+"micro"
- }
-
- $sync.MicrowinMountDir.Text = $mountDir
- $sync.MicrowinScratchDir.Text = $scratchDir
- Write-Host "Done setting up mount dir and scratch dirs"
- Write-Host "Scratch dir is $scratchDir"
- Write-Host "Image dir is $mountDir"
-
- try {
-
- #$data = @($driveLetter, $filePath)
- Invoke-MicrowinBusyInfo -action "wip" -message "Creating directories..." -interactive $false
- New-Item -ItemType Directory -Force -Path "$($mountDir)" | Out-Null
- New-Item -ItemType Directory -Force -Path "$($scratchDir)" | Out-Null
-
- Invoke-MicrowinBusyInfo -action "wip" -message "Copying Windows files... (This may take several minutes)" -interactive $false
- Write-Host "Copying Windows image. This will take awhile, please don't use UI or cancel this step!"
-
- # xcopy we can verify files and also not copy files that already exist, but hard to measure
- # xcopy.exe /E /I /H /R /Y /J $DriveLetter":" $mountDir >$null
- $totalTime = Measure-Command {
- Copy-Files "$($driveLetter):" "$mountDir" -Recurse -Force
- # Force UI update during long operation
- [System.Windows.Forms.Application]::DoEvents()
- }
- Write-Host "Copy complete! Total Time: $($totalTime.Minutes) minutes, $($totalTime.Seconds) seconds"
-
- Invoke-MicrowinBusyInfo -action "wip" -message "Processing Windows image..." -interactive $false
- $wimFile = "$mountDir\sources\install.wim"
- Write-Host "Getting image information $wimFile"
-
- if ((-not (Test-Path -Path "$wimFile" -PathType Leaf)) -and (-not (Test-Path -Path "$($wimFile.Replace(".wim", ".esd").Trim())" -PathType Leaf))) {
- $msg = "Neither install.wim nor install.esd exist in the image, this could happen if you use unofficial Windows images. Please don't use shady images from the internet."
- Write-Host "$($msg) Only use official images. Here are instructions how to download ISO images if the Microsoft website is not showing the link to download and ISO. https://www.techrepublic.com/article/how-to-download-a-windows-10-iso-file-without-using-the-media-creation-tool/"
- Invoke-MicrowinBusyInfo -action "warning" -message $msg
- Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
- [System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
- throw
- }
- elseif ((-not (Test-Path -Path $wimFile -PathType Leaf)) -and (Test-Path -Path $wimFile.Replace(".wim", ".esd").Trim() -PathType Leaf)) {
- Write-Host "Install.esd found on the image. It needs to be converted to a WIM file in order to begin processing"
- $wimFile = $wimFile.Replace(".wim", ".esd").Trim()
- }
- $sync.MicrowinWindowsFlavors.Items.Clear()
- Get-WindowsImage -ImagePath $wimFile | ForEach-Object {
- $imageIdx = $_.ImageIndex
- $imageName = $_.ImageName
- $sync.MicrowinWindowsFlavors.Items.Add("$imageIdx : $imageName")
- }
- [System.Windows.Forms.Application]::DoEvents()
-
- $sync.MicrowinWindowsFlavors.SelectedIndex = 0
- Write-Host "Finding suitable Pro edition. This can take some time. Do note that this is an automatic process that might not select the edition you want."
- Invoke-MicrowinBusyInfo -action "wip" -message "Finding suitable Pro edition..." -interactive $false
-
- Get-WindowsImage -ImagePath $wimFile | ForEach-Object {
- if ((Get-WindowsImage -ImagePath $wimFile -Index $_.ImageIndex).EditionId -eq "Professional") {
- # We have found the Pro edition
- $sync.MicrowinWindowsFlavors.SelectedIndex = $_.ImageIndex - 1
- }
- # Allow UI updates during this loop
- [System.Windows.Forms.Application]::DoEvents()
- }
-
- Get-Volume $driveLetter | Get-DiskImage | Dismount-DiskImage
- Write-Host "Selected value '$($sync.MicrowinWindowsFlavors.SelectedValue)'....."
-
- Toggle-MicrowinPanel 2
-
- } catch {
- Write-Host "Dismounting bad image..."
- Get-Volume $driveLetter | Get-DiskImage | Dismount-DiskImage
- Remove-Item -Recurse -Force "$($scratchDir)"
- Remove-Item -Recurse -Force "$($mountDir)"
- Invoke-MicrowinBusyInfo -action "warning" -message "Failed to read and unpack ISO"
- Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
-
- }
-
- Write-Host "Done reading and unpacking ISO"
- Write-Host ""
- Write-Host "*********************************"
- Write-Host "Check the UI for further steps!!!"
-
- Invoke-MicrowinBusyInfo -action "done" -message "Done! Proceed with customization."
- $sync.ProcessRunning = $false
- Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"
+ # Start the Get ISO process in a runspace to avoid blocking the UI
+ Invoke-WPFMicroWinGetIsoRunspace -GetIsoSettings $getIsoSettings
}
diff --git a/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 b/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1
new file mode 100644
index 0000000000..26ddeb8276
--- /dev/null
+++ b/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1
@@ -0,0 +1,492 @@
+function Invoke-WPFMicroWinGetIsoRunspace {
+ <#
+ .SYNOPSIS
+ Runs the MicroWin Get ISO process in a runspace to avoid blocking the UI
+
+ .DESCRIPTION
+ This function handles the ISO selection, mounting, and analysis process for MicroWin
+ in a background runspace to keep the UI responsive.
+
+ .PARAMETER GetIsoSettings
+ Hashtable containing the settings for the Get ISO process
+ #>
+
+ param(
+ [Parameter(Mandatory = $true)]
+ [hashtable]$GetIsoSettings
+ )
+
+ Write-Host "Starting MicroWin GetIso runspace with settings:"
+ Write-Host "IsManual: $($GetIsoSettings.isManual)"
+ Write-Host "FilePath: '$($GetIsoSettings.filePath)'"
+ Write-Host "IsDownloader: $($GetIsoSettings.isDownloader)"
+ Write-Host "TargetFolder: '$($GetIsoSettings.targetFolder)'"
+
+ # Start the Get ISO process in a runspace to avoid blocking the UI
+ Invoke-WPFRunspace -ArgumentList $GetIsoSettings -DebugPreference $DebugPreference -ScriptBlock {
+ param($GetIsoSettings, $DebugPreference)
+
+ Write-Host "Inside runspace - processing ISO..."
+
+ $sync.ProcessRunning = $true
+
+ try {
+ # Initialize progress tracking
+ $totalSteps = 10
+ $currentStep = 0
+
+
+ # Provide immediate feedback to user with progress
+ try {
+ $sync.form.Dispatcher.Invoke([action]{
+ try {
+ Set-WinUtilTaskbaritem -state "Normal" -value 0.1 -overlay "logo"
+ } catch {
+ }
+
+ # Skip the problematic Invoke-MicrowinBusyInfo call for now
+ })
+ } catch {
+ }
+ $currentStep = 1
+
+
+ Write-Host " _ __ __ _ "
+ Write-Host " /\/\ (_) ___ _ __ ___ / / /\ \ \(_) _ __ "
+ Write-Host " / \ | | / __|| '__| / _ \ \ \/ \/ /| || '_ \ "
+ Write-Host "/ /\/\ \| || (__ | | | (_) | \ /\ / | || | | | "
+ Write-Host "\/ \/|_| \___||_| \___/ \/ \/ |_||_| |_| "
+
+
+ $filePath = ""
+
+ if ($GetIsoSettings.isManual) {
+ # Use the pre-selected file path from the main thread
+ $filePath = $GetIsoSettings.filePath
+
+ if ([string]::IsNullOrEmpty($filePath)) {
+ Write-Host "No ISO is chosen"
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
+ # Invoke-MicrowinBusyInfo -action "hide" -message " "
+ })
+ $sync.ProcessRunning = $false
+ return
+ }
+
+ # Update progress
+ $currentStep = 2
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Normal" -value ($currentStep / $totalSteps) -overlay "logo"
+ # Skip Invoke-MicrowinBusyInfo call that was causing issues
+ })
+
+ } elseif ($GetIsoSettings.isDownloader) {
+ # Use the pre-selected folder path from the main thread
+ $targetFolder = $GetIsoSettings.targetFolder
+
+ if ([string]::IsNullOrEmpty($targetFolder)) {
+ $sync.form.Dispatcher.Invoke([action]{
+ # Invoke-MicrowinBusyInfo -action "hide" -message " "
+ })
+ $sync.ProcessRunning = $false
+ return
+ }
+
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Indeterminate" -overlay "logo"
+ # Invoke-MicrowinBusyInfo -action "wip" -message "Preparing to download ISO... (Step 2/$totalSteps)" -interactive $false
+ })
+ $currentStep = 2
+
+ # Auto download newest ISO
+ $fidopath = "$env:temp\Fido.ps1"
+ $originalLocation = $PSScriptRoot
+
+ $sync.form.Dispatcher.Invoke([action]{
+ # Invoke-MicrowinBusyInfo -action "wip" -message "Downloading Fido script..." -interactive $false
+ })
+ Invoke-WebRequest "https://github.com/pbatard/Fido/raw/master/Fido.ps1" -OutFile $fidopath
+
+ Set-Location -Path $env:temp
+ # Detect if the first option ("System language") has been selected and get a Fido-approved language from the current culture
+ $lang = if ($GetIsoSettings.languageIndex -eq 0) {
+ Microwin-GetLangFromCulture -langName (Get-Culture).Name
+ } else {
+ $GetIsoSettings.language
+ }
+
+ $sync.form.Dispatcher.Invoke([action]{
+ # Invoke-MicrowinBusyInfo -action "wip" -message "Downloading Windows ISO... (This may take a long time)" -interactive $false
+ })
+ & $fidopath -Win 'Windows 11' -Rel $GetIsoSettings.release -Arch "x64" -Lang $lang -Ed "Windows 11 Home/Pro/Edu"
+ if (-not $?) {
+ Write-Host "Could not download the ISO file. Look at the output of the console for more information."
+ $msg = "The ISO file could not be downloaded"
+ $sync.form.Dispatcher.Invoke([action]{
+ # Invoke-MicrowinBusyInfo -action "warning" -message $msg
+ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
+ [System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
+ })
+ $sync.ProcessRunning = $false
+ return
+ }
+ Set-Location $originalLocation
+ $filePath = (Get-ChildItem -Path "$env:temp" -Filter "Win11*.iso").FullName | Sort-Object LastWriteTime -Descending | Select-Object -First 1
+ $fileName = [IO.Path]::GetFileName("$filePath")
+
+ if (($targetFolder -ne "") -and (Test-Path "$targetFolder")) {
+ try {
+ Write-Host "Moving ISO file. Please wait..."
+ $destinationFilePath = "$targetFolder\$fileName"
+ Move-Item -Path "$filePath" -Destination "$destinationFilePath" -Force
+ $filePath = $destinationFilePath
+ } catch {
+ $msg = "Unable to move the ISO file to the location you specified. The downloaded ISO is in the `"$env:TEMP`" folder"
+ Write-Host $msg
+ Write-Host "Error information: $($_.Exception.Message)" -ForegroundColor Yellow
+ $sync.form.Dispatcher.Invoke([action]{
+ # Invoke-MicrowinBusyInfo -action "warning" -message $msg
+ })
+ $sync.ProcessRunning = $false
+ return
+ }
+ }
+ }
+
+ Write-Host "File path $($filePath)"
+ if (-not (Test-Path -Path "$filePath" -PathType Leaf)) {
+ $msg = "File you've chosen doesn't exist"
+ $sync.form.Dispatcher.Invoke([action]{
+ # Invoke-MicrowinBusyInfo -action "warning" -message $msg
+ [System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
+ })
+ $sync.ProcessRunning = $false
+ return
+ }
+
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Normal" -value (3 / $totalSteps) -overlay "logo"
+ # Skip Invoke-MicrowinBusyInfo call that was causing issues
+ })
+ $currentStep = 3
+
+ # Check for oscdimg.exe
+ $oscdimgPath = Join-Path $env:TEMP 'oscdimg.exe'
+ $oscdImgFound = [bool] (Get-Command -ErrorAction Ignore -Type Application oscdimg.exe) -or (Test-Path $oscdimgPath -PathType Leaf)
+ Write-Host "oscdimg.exe on system: $oscdImgFound"
+
+ if (!$oscdImgFound) {
+ if (!$GetIsoSettings.downloadFromGitHub) {
+ $sync.form.Dispatcher.Invoke([action]{
+ [System.Windows.MessageBox]::Show("oscdimge.exe is not found on the system, winutil will now attempt do download and install it using choco. This might take a long time.")
+ })
+ # Install Choco if not already present
+ Install-WinUtilChoco
+ $chocoFound = [bool] (Get-Command -ErrorAction Ignore -Type Application choco)
+ Write-Host "choco on system: $chocoFound"
+ if (!$chocoFound) {
+ $sync.form.Dispatcher.Invoke([action]{
+ [System.Windows.MessageBox]::Show("choco.exe is not found on the system, you need choco to download oscdimg.exe")
+ })
+ $sync.ProcessRunning = $false
+ return
+ }
+
+ Start-Process -Verb runas -FilePath powershell.exe -ArgumentList "choco install windows-adk-oscdimg"
+ $msg = "oscdimg is installed, now close, reopen PowerShell terminal and re-launch winutil.ps1"
+ $sync.form.Dispatcher.Invoke([action]{
+ # Invoke-MicrowinBusyInfo -action "done" -message $msg
+ [System.Windows.MessageBox]::Show($msg)
+ })
+ $sync.ProcessRunning = $false
+ return
+ } else {
+ $sync.form.Dispatcher.Invoke([action]{
+ [System.Windows.MessageBox]::Show("oscdimge.exe is not found on the system, winutil will now attempt do download and install it from github. This might take a long time.")
+ # Skip Invoke-MicrowinBusyInfo call that was causing issues
+ })
+ Microwin-GetOscdimg -oscdimgPath $oscdimgPath
+ $oscdImgFound = Test-Path $oscdimgPath -PathType Leaf
+ if (!$oscdImgFound) {
+ $msg = "oscdimg was not downloaded can not proceed"
+ $sync.form.Dispatcher.Invoke([action]{
+ # Skip Invoke-MicrowinBusyInfo call that was causing issues
+ [System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
+ })
+ $sync.ProcessRunning = $false
+ return
+ } else {
+ Write-Host "oscdimg.exe was successfully downloaded from github"
+ }
+ }
+ }
+
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Normal" -value (4 / $totalSteps) -overlay "logo"
+ # Skip Invoke-MicrowinBusyInfo call that was causing issues
+ })
+ $currentStep = 4
+
+ # Detect the file size of the ISO and compare it with the free space of the system drive
+ $isoSize = (Get-Item -Path "$filePath").Length
+ $driveSpace = (Get-Volume -DriveLetter ([IO.Path]::GetPathRoot([Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile)).Replace(":\", "").Trim())).SizeRemaining
+ if ($driveSpace -lt ($isoSize * 2)) {
+ Write-Warning "You may not have enough space for this operation. Proceed at your own risk."
+ } elseif ($driveSpace -lt $isoSize) {
+ $msg = "You don't have enough space for this operation. You need at least $([Math]::Round(($isoSize / ([Math]::Pow(1024, 2))) * 2, 2)) MB of free space to copy the ISO files to a temp directory and to be able to perform additional operations."
+ Write-Host $msg
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
+ # Skip Invoke-MicrowinBusyInfo call that was causing issues
+ })
+ $sync.ProcessRunning = $false
+ return
+ } else {
+ Write-Host "You have enough space for this operation."
+ }
+
+ try {
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Normal" -value (5 / $totalSteps) -overlay "logo"
+ # Invoke-MicrowinBusyInfo -action "wip" -message "Mounting ISO file... (Step 5/$totalSteps)" -interactive $false
+ })
+ $currentStep = 5
+ Write-Host "Mounting Iso. Please wait."
+ $mountedISO = Mount-DiskImage -PassThru "$filePath"
+ Write-Host "Done mounting Iso `"$($mountedISO.ImagePath)`""
+ $driveLetter = (Get-Volume -DiskImage $mountedISO).DriveLetter
+ Write-Host "Iso mounted to '$driveLetter'"
+ } catch {
+ $msg = "Failed to mount the image. Error: $($_.Exception.Message)"
+ Write-Error $msg
+ Write-Error "This is NOT winutil's problem, your ISO might be corrupt, or there is a problem on the system"
+ Write-Host "Please refer to this wiki for more details: https://christitustech.github.io/winutil/KnownIssues/#troubleshoot-errors-during-microwin-usage" -ForegroundColor Red
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
+ # Invoke-MicrowinBusyInfo -action "warning" -message $msg
+ })
+ $sync.ProcessRunning = $false
+ return
+ }
+
+ # Store values in UI fields - must be done on UI thread
+ $sync.form.Dispatcher.Invoke([action]{
+ $sync.MicrowinIsoDrive.Text = $driveLetter
+ })
+
+ $mountedISOPath = (Split-Path -Path "$filePath")
+
+ # Handle scratch directory settings - must be done on UI thread
+ $sync.form.Dispatcher.Invoke([action]{
+ if ($sync.MicrowinScratchDirBox.Text.Trim() -eq "Scratch") {
+ $sync.MicrowinScratchDirBox.Text = ""
+ }
+
+ if ($GetIsoSettings.useISOScratchDir) {
+ $sync.MicrowinScratchDirBox.Text = $mountedISOPath
+ }
+
+ if (-Not $sync.MicrowinScratchDirBox.Text.EndsWith('\') -And $sync.MicrowinScratchDirBox.Text.Length -gt 1) {
+ $sync.MicrowinScratchDirBox.Text = Join-Path $sync.MicrowinScratchDirBox.Text.Trim() '\'
+ }
+ })
+
+ # Get current values from UI thread
+ $mountDir = ""
+ $scratchDir = ""
+ $sync.form.Dispatcher.Invoke([action]{
+ # Detect if the folders already exist and remove them
+ if (($sync.MicrowinMountDir.Text -ne "") -and (Test-Path -Path $sync.MicrowinMountDir.Text)) {
+ try {
+ Write-Host "Deleting temporary files from previous run. Please wait..."
+ Remove-Item -Path $sync.MicrowinMountDir.Text -Recurse -Force
+ Remove-Item -Path $sync.MicrowinScratchDir.Text -Recurse -Force
+ } catch {
+ Write-Host "Could not delete temporary files. You need to delete those manually."
+ }
+ }
+
+ Write-Host "Setting up mount dir and scratch dirs"
+ $timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
+ $randomNumber = Get-Random -Minimum 1 -Maximum 9999
+ $randomMicrowin = "Microwin_${timestamp}_${randomNumber}"
+ $randomMicrowinScratch = "MicrowinScratch_${timestamp}_${randomNumber}"
+
+ if ($sync.MicrowinScratchDirBox.Text -eq "") {
+ $script:mountDir = Join-Path $env:TEMP $randomMicrowin
+ $script:scratchDir = Join-Path $env:TEMP $randomMicrowinScratch
+ } else {
+ $script:scratchDir = $sync.MicrowinScratchDirBox.Text + "Scratch"
+ $script:mountDir = $sync.MicrowinScratchDirBox.Text + "micro"
+ }
+
+ $sync.MicrowinMountDir.Text = $script:mountDir
+ $sync.MicrowinScratchDir.Text = $script:scratchDir
+ })
+
+ # Get the values after they've been set - must be done on UI thread
+ $mountDir = ""
+ $scratchDir = ""
+ $sync.form.Dispatcher.Invoke([action]{
+ $sync.TempMountDir = $sync.MicrowinMountDir.Text
+ $sync.TempScratchDir = $sync.MicrowinScratchDir.Text
+ })
+ $mountDir = $sync.TempMountDir
+ $scratchDir = $sync.TempScratchDir
+
+ Write-Host "Done setting up mount dir and scratch dirs"
+ Write-Host "Scratch dir is $scratchDir"
+ Write-Host "Image dir is $mountDir"
+
+ try {
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Normal" -value (6 / $totalSteps) -overlay "logo"
+ # Invoke-MicrowinBusyInfo -action "wip" -message "Creating directories... (Step 6/$totalSteps)" -interactive $false
+ })
+ $currentStep = 6
+ New-Item -ItemType Directory -Force -Path "$($mountDir)" | Out-Null
+ New-Item -ItemType Directory -Force -Path "$($scratchDir)" | Out-Null
+
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Normal" -value (7 / $totalSteps) -overlay "logo"
+ # Invoke-MicrowinBusyInfo -action "wip" -message "Copying Windows files... (Step 7/$totalSteps - This may take several minutes)" -interactive $false
+ })
+ $currentStep = 7
+ Write-Host "Copying Windows image. This will take awhile, please don't use UI or cancel this step!"
+
+ try {
+
+ $totalTime = Measure-Command {
+ Copy-Files -Path "$($driveLetter):" -Destination "$mountDir" -Recurse -Force
+
+ # Force UI update during long operation
+ $sync.form.Dispatcher.Invoke([action]{
+ [System.Windows.Forms.Application]::DoEvents()
+ })
+ }
+ Write-Host "Copy complete! Total Time: $($totalTime.Minutes) minutes, $($totalTime.Seconds) seconds"
+ } catch {
+ throw $_
+ } $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Normal" -value (8 / $totalSteps) -overlay "logo"
+ # Invoke-MicrowinBusyInfo -action "wip" -message "Processing Windows image... (Step 8/$totalSteps)" -interactive $false
+ })
+ $currentStep = 8
+ $wimFile = "$mountDir\sources\install.wim"
+ Write-Host "Getting image information $wimFile"
+
+ $esdFile = $wimFile.Replace(".wim", ".esd").Trim()
+
+ if ((-not (Test-Path -Path "$wimFile" -PathType Leaf)) -and (-not (Test-Path -Path "$esdFile" -PathType Leaf))) {
+ $msg = "Neither install.wim nor install.esd exist in the image, this could happen if you use unofficial Windows images. Please don't use shady images from the internet."
+ Write-Host "$($msg) Only use official images. Here are instructions how to download ISO images if the Microsoft website is not showing the link to download and ISO. https://www.techrepublic.com/article/how-to-download-a-windows-10-iso-file-without-using-the-media-creation-tool/"
+ $sync.form.Dispatcher.Invoke([action]{
+ # Invoke-MicrowinBusyInfo -action "warning" -message $msg
+ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
+ [System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
+ })
+ throw
+ } elseif ((-not (Test-Path -Path $wimFile -PathType Leaf)) -and (Test-Path -Path $esdFile -PathType Leaf)) {
+ Write-Host "Install.esd found on the image. It needs to be converted to a WIM file in order to begin processing"
+ $wimFile = $esdFile
+ }
+
+
+ # Populate the Windows flavors list - must be done on UI thread
+ $sync.form.Dispatcher.Invoke([action]{
+ $sync.MicrowinWindowsFlavors.Items.Clear()
+ })
+
+ try {
+ $images = Get-WindowsImage -ImagePath $wimFile
+
+ $images | ForEach-Object {
+ $sync.form.Dispatcher.Invoke([action]{
+ $sync.MicrowinWindowsFlavors.Items.Add("$_.ImageIndex : $_.ImageName")
+ })
+ }
+ } catch {
+ throw $_
+ }
+
+ $sync.form.Dispatcher.Invoke([action]{
+ [System.Windows.Forms.Application]::DoEvents()
+ $sync.MicrowinWindowsFlavors.SelectedIndex = 0
+ Set-WinUtilTaskbaritem -state "Normal" -value (9 / $totalSteps) -overlay "logo"
+ # Invoke-MicrowinBusyInfo -action "wip" -message "Finding suitable Pro edition... (Step 9/$totalSteps)" -interactive $false
+ })
+ $currentStep = 9
+
+ Write-Host "Finding suitable Pro edition. This can take some time. Do note that this is an automatic process that might not select the edition you want."
+
+ Get-WindowsImage -ImagePath $wimFile | ForEach-Object {
+ if ((Get-WindowsImage -ImagePath $wimFile -Index $_.ImageIndex).EditionId -eq "Professional") {
+ # We have found the Pro edition
+ $sync.form.Dispatcher.Invoke([action]{
+ $sync.MicrowinWindowsFlavors.SelectedIndex = $_.ImageIndex - 1
+ })
+ break
+ }
+ # Allow UI updates during this loop
+ $sync.form.Dispatcher.Invoke([action]{
+ [System.Windows.Forms.Application]::DoEvents()
+ })
+ }
+
+ Get-Volume $driveLetter | Get-DiskImage | Dismount-DiskImage
+ Write-Host "Selected value '$($sync.MicrowinWindowsFlavors.SelectedValue)'....."
+
+ # Switch to the customization panel - must be done on UI thread
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Normal" -value 1.0 -overlay "checkmark"
+ Toggle-MicrowinPanel 2
+ })
+
+ } catch {
+
+ Write-Host "Dismounting bad image..."
+ try {
+ Get-Volume $driveLetter | Get-DiskImage | Dismount-DiskImage
+ } catch {
+ }
+
+ try {
+ if (Test-Path "$scratchDir") {
+ Remove-Item -Recurse -Force "$($scratchDir)"
+ }
+ if (Test-Path "$mountDir") {
+ Remove-Item -Recurse -Force "$($mountDir)"
+ }
+ } catch {
+ }
+
+ $sync.form.Dispatcher.Invoke([action]{
+ # Invoke-MicrowinBusyInfo -action "warning" -message "Failed to read and unpack ISO"
+ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
+ })
+ $sync.ProcessRunning = $false
+ return
+ }
+
+ Write-Host "Done reading and unpacking ISO"
+ Write-Host ""
+ Write-Host "*********************************"
+ Write-Host "Check the UI for further steps!!!"
+
+ $sync.form.Dispatcher.Invoke([action]{
+ # Invoke-MicrowinBusyInfo -action "done" -message "Done! Proceed with customization."
+ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"
+ })
+
+ } catch {
+ Write-Error "An unexpected error occurred: $_"
+ $sync.form.Dispatcher.Invoke([action]{
+ # Invoke-MicrowinBusyInfo -action "warning" -message "An unexpected error occurred: $_"
+ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
+ })
+ } finally {
+ $sync.ProcessRunning = $false
+ }
+ }
+}
diff --git a/functions/microwin/Invoke-WPFMicroWinRunspace.ps1 b/functions/microwin/Invoke-WPFMicroWinRunspace.ps1
new file mode 100644
index 0000000000..5103085881
--- /dev/null
+++ b/functions/microwin/Invoke-WPFMicroWinRunspace.ps1
@@ -0,0 +1,1070 @@
+function Invoke-WPFMicroWinRunspace {
+ <#
+ .SYNOPSIS
+ Executes MicroWin operations in a background runspace to prevent UI blocking
+
+ .DESCRIPTION
+ This function takes MicroWin settings and executes the entire MicroWin process
+ in a background runspace, allowing the UI to remain responsive during the
+ lengthy ISO creation process.
+
+ .PARAMETER MicroWinSettings
+ Hashtable containing all the MicroWin configuration settings
+
+ .EXAMPLE
+ $settings = @{
+ mountDir = "C:\Mount"
+ scratchDir = "C:\Scratch"
+ # ... other settings
+ }
+ Invoke-WPFMicroWinRunspace -MicroWinSettings $settings
+ #>
+
+ param(
+ [Parameter(Mandatory = $true)]
+ [hashtable]$MicroWinSettings
+ )
+
+ # Start the process in a runspace to avoid blocking the UI
+ Invoke-WPFRunspace -ArgumentList $MicroWinSettings -DebugPreference $DebugPreference -ScriptBlock {
+ param($MicroWinSettings, $DebugPreference)
+
+ # Function to set DISM-compatible permissions on a directory
+
+
+ $sync.ProcessRunning = $true
+
+ try {
+ # Set process priority to High for better performance
+ try {
+ $currentProcess = Get-Process -Id $PID
+ $currentProcess.PriorityClass = [System.Diagnostics.ProcessPriorityClass]::High
+ } catch {
+ # Could not set process priority
+ }
+
+ # Optimize PowerShell memory usage
+ try {
+ # Force garbage collection to free up unused memory
+ [System.GC]::Collect()
+ [System.GC]::WaitForPendingFinalizers()
+ [System.GC]::Collect()
+
+ # Set execution policy to bypass for better performance
+ Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process -Force
+ } catch {
+ # Memory optimization failed
+ }
+
+ # Prevent the machine from sleeping using simple PowerShell method
+ try {
+ # Use PowerShell's built-in method instead of complex P/Invoke
+ $null = [System.Threading.Thread]::CurrentThread.ExecutionContext
+ Add-Type -AssemblyName System.Windows.Forms
+ [System.Windows.Forms.Application]::SetSuspendState('Hibernate', $false, $false)
+ } catch {
+ # Sleep prevention failed - continue anyway
+ }
+
+ # Ask the user where to save the file - this needs to be done on the main thread
+ $SaveDialogFileName = ""
+ $sync.form.Dispatcher.Invoke([action]{
+ $SaveDialog = New-Object System.Windows.Forms.SaveFileDialog
+ $SaveDialog.InitialDirectory = [Environment]::GetFolderPath('Desktop')
+ $SaveDialog.Filter = "ISO images (*.iso)|*.iso"
+ $result = $SaveDialog.ShowDialog()
+ if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
+ $script:SaveDialogFileName = $SaveDialog.FileName
+ }
+ })
+
+ if ($SaveDialogFileName -eq "") {
+ $msg = "No file name for the target image was specified"
+ Write-Host $msg
+ $sync.form.Dispatcher.Invoke([action]{
+ # Invoke-MicrowinBusyInfo -action "warning" -message $msg
+ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
+ })
+ return
+ }
+
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Indeterminate" -overlay "logo"
+ # Invoke-MicrowinBusyInfo -action "wip" -message "Busy..." -interactive $false
+ })
+
+ Write-Host "Target ISO location: $SaveDialogFileName"
+
+ # Extract settings from hashtable
+ $index = $MicroWinSettings.selectedIndex
+ $mountDir = $MicroWinSettings.mountDir
+ $scratchDir = $MicroWinSettings.scratchDir
+ $copyToUSB = $MicroWinSettings.copyToUSB
+ $injectDrivers = $MicroWinSettings.injectDrivers
+ $importDrivers = $MicroWinSettings.importDrivers
+ $WPBT = $MicroWinSettings.WPBT
+ $unsupported = $MicroWinSettings.unsupported
+ $importVirtIO = $MicroWinSettings.importVirtIO
+ $driverPath = $MicroWinSettings.driverPath
+ $esd = $MicroWinSettings.esd
+ $autoConfigPath = $MicroWinSettings.autoConfigPath
+ $userName = $MicroWinSettings.userName
+ $userPassword = $MicroWinSettings.userPassword
+
+ Write-Host "Index chosen: '$index'"
+
+ # Detect if the Windows image is an ESD file and convert it to WIM
+ if (-not (Test-Path -Path "$mountDir\sources\install.wim" -PathType Leaf) -and (Test-Path -Path "$mountDir\sources\install.esd" -PathType Leaf)) {
+ Write-Host "Exporting Windows image to a WIM file, keeping the index we want to work on. This can take several minutes, depending on the performance of your computer..."
+ try {
+ # Use Fast compression instead of Max for better performance during development
+ Export-WindowsImage -SourceImagePath "$mountDir\sources\install.esd" -SourceIndex $index -DestinationImagePath "$mountDir\sources\install.wim" -CompressionType "Fast"
+ } catch {
+ # Fall back to DISM with optimized settings
+ dism /english /export-image /sourceimagefile="$mountDir\sources\install.esd" /sourceindex=$index /destinationimagefile="$mountDir\sources\install.wim" /compress:fast /checkintegrity /verify
+ }
+ if ($?) {
+ Remove-Item -Path "$mountDir\sources\install.esd" -Force
+ # Since we've already exported the image index we wanted, switch to the first one
+ $index = 1
+ } else {
+ $msg = "The export process has failed and MicroWin processing cannot continue"
+ Write-Host $msg
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
+ # Invoke-MicrowinBusyInfo -action "warning" -message $msg
+ [System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
+ })
+ return
+ }
+ }
+
+ $imgVersion = (Get-WindowsImage -ImagePath "$mountDir\sources\install.wim" -Index $index).Version
+ Write-Host "The Windows Image Build Version is: $imgVersion"
+
+ # Detect image version to avoid performing MicroWin processing on Windows 8 and earlier
+ if ((Microwin-TestCompatibleImage $imgVersion $([System.Version]::new(10,0,10240,0))) -eq $false) {
+ $msg = "This image is not compatible with MicroWin processing. Make sure it isn't a Windows 8 or earlier image."
+ $dlg_msg = $msg + "`n`nIf you want more information, the version of the image selected is $($imgVersion)`n`nIf an image has been incorrectly marked as incompatible, report an issue to the developers."
+ Write-Host $msg
+ $sync.form.Dispatcher.Invoke([action]{
+ [System.Windows.MessageBox]::Show($dlg_msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Exclamation)
+ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
+ # Invoke-MicrowinBusyInfo -action "warning" -message $msg
+ })
+ return
+ }
+
+ # Detect whether the image to process contains Windows 10 and show warning
+ if ((Microwin-TestCompatibleImage $imgVersion $([System.Version]::new(10,0,21996,1))) -eq $false) {
+ $msg = "Windows 10 has been detected in the image you want to process. While you can continue, Windows 10 is not a recommended target for MicroWin, and you may not get the full experience."
+ $dlg_msg = $msg
+ Write-Host $msg
+ $sync.form.Dispatcher.Invoke([action]{
+ [System.Windows.MessageBox]::Show($dlg_msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Exclamation)
+ })
+ }
+
+ $mountDirExists = Test-Path $mountDir
+ $scratchDirExists = Test-Path $scratchDir
+ if (-not $mountDirExists -or -not $scratchDirExists) {
+ $msg = "Required directories '$mountDir' and '$scratchDir' do not exist."
+ Write-Error $msg
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
+ # Invoke-MicrowinBusyInfo -action "warning" -message $msg
+ })
+ return
+ }
+
+ # Clean up any stale mountpoints before starting
+ try {
+ & dism /cleanup-mountpoints /loglevel:1
+ Start-Sleep -Seconds 2
+ } catch {
+ }
+
+ # Check if running as administrator
+ if (-not (New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
+ $msg = "Administrator privileges are required to mount and modify Windows images. Please run WinUtil as Administrator and try again."
+ Write-Host $msg
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
+ [System.Windows.MessageBox]::Show($msg, "Administrator Required", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
+ })
+ return
+ }
+
+ # Enable required privileges for DISM operations - icacls handles this automatically
+ # No complex P/Invoke needed since icacls will request necessary privileges
+
+ # Check if the scratch directory is writable
+ try {
+ $testFile = Join-Path $scratchDir "test_write_permissions.tmp"
+ "test" | Out-File -FilePath $testFile -Force
+ Remove-Item $testFile -Force
+ } catch {
+ $msg = "Cannot write to scratch directory '$scratchDir'. Please check permissions and ensure the directory is not in use."
+ Write-Host $msg
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
+ [System.Windows.MessageBox]::Show($msg, "Permission Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
+ })
+ return
+ }
+
+ # Check if install.wim file exists and is accessible
+ $wimPath = "$mountDir\sources\install.wim"
+ if (-not (Test-Path $wimPath)) {
+ $msg = "Windows installation image not found at '$wimPath'. Please ensure the ISO is properly mounted or extracted."
+ Write-Host $msg
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
+ [System.Windows.MessageBox]::Show($msg, "File Not Found", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
+ })
+ return
+ }
+
+ try {
+ # Test if we can read the WIM file
+ $wimInfo = Get-WindowsImage -ImagePath $wimPath
+ } catch {
+ $msg = "Cannot access or read the Windows installation image at '$wimPath'. The file may be corrupted or in use by another process."
+ Write-Host $msg
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
+ [System.Windows.MessageBox]::Show($msg, "File Access Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
+ })
+ return
+ }
+
+ try {
+ # Check if the image is already mounted and dismount if necessary
+ try {
+ $mountedImages = Get-WindowsImage -Mounted
+ foreach ($mounted in $mountedImages) {
+ if ($mounted.Path -eq $scratchDir) {
+ Dismount-WindowsImage -Path $scratchDir -Discard
+ Start-Sleep -Seconds 2
+ }
+ }
+ } catch {
+ }
+
+ # Additional permission checks before mounting
+
+ # Pre-mount system checks
+
+ # Check available disk space
+ try {
+ $scratchDrive = Split-Path $scratchDir -Qualifier
+ $driveInfo = Get-WmiObject -Class Win32_LogicalDisk | Where-Object { $_.DeviceID -eq $scratchDrive }
+ $freeSpaceGB = [math]::Round($driveInfo.FreeSpace / 1GB, 2)
+
+ if ($freeSpaceGB -lt 10) {
+ }
+ } catch {
+ }
+
+ # Check if scratch directory is accessible
+ try {
+ if (-not (Test-Path $scratchDir)) {
+ New-Item -Path $scratchDir -ItemType Directory -Force | Out-Null
+ }
+
+ # Test write access
+ $testFile = Join-Path $scratchDir "test_access.tmp"
+ "test" | Out-File -FilePath $testFile -Force
+ Remove-Item $testFile -Force
+ } catch {
+ return
+ }
+
+ # Additional file permission and location diagnostics
+
+ # WIM file permissions are handled automatically by DISM operations
+
+ # Try alternative scratch directory if current one has issues
+ $originalScratchDir = $scratchDir
+ $alternateScratchDir = "C:\temp\MicrowinMount_$(Get-Date -Format 'yyyyMMdd_HHmmss')"
+
+ # Try to use DISM instead of PowerShell cmdlets for mounting
+ $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
+ Write-Host "Current user running mount: $currentUser" -ForegroundColor Yellow
+ Write-Host "Mounting Windows image. This may take a while."
+
+ $mountAttempts = @(
+ @{ Dir = $scratchDir; Description = "Original temp directory" },
+ @{ Dir = $alternateScratchDir; Description = "Alternative C:\temp directory" },
+ @{ Dir = "C:\MicrowinMount"; Description = "Root C: directory" }
+ )
+
+ # Remove ReadOnly attributes from WIM files before mounting
+ $wimFilePaths = @(
+ "$mountDir\sources\install.wim",
+ "$mountDir\sources\boot.wim"
+ )
+
+ $criticalWimError = $false
+ foreach ($wimFilePath in $wimFilePaths) {
+ if (Test-Path $wimFilePath) {
+ try {
+ # Remove ReadOnly attribute using attrib command
+ & attrib -R "$wimFilePath" 2>$null
+ if ($LASTEXITCODE -ne 0) {
+ $criticalWimError = $true
+ }
+ } catch {
+ $criticalWimError = $true
+ }
+ }
+ }
+
+ if ($criticalWimError) {
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
+ [System.Windows.MessageBox]::Show("Cannot remove ReadOnly attributes from WIM files. Mount operation aborted to prevent failures.", "WIM File Permission Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
+ })
+ return
+ }
+
+ foreach ($attempt in $mountAttempts) {
+ $currentScratchDir = $attempt.Dir
+
+ try {
+ # Ensure directory exists
+ if (-not (Test-Path $currentScratchDir)) {
+ New-Item -Path $currentScratchDir -ItemType Directory -Force | Out-Null
+ }
+
+ # Try PowerShell cmdlet first
+ Mount-WindowsImage -ImagePath "$mountDir\sources\install.wim" -Index $index -Path "$currentScratchDir" -Optimize
+ $mountSuccess = $true
+ $scratchDir = $currentScratchDir
+ break
+
+ } catch {
+ # Fall back to DISM command
+ $dismResult = & dism /english /mount-image /imagefile:"$mountDir\sources\install.wim" /index:$index /mountdir:"$currentScratchDir" /optimize /loglevel:1
+
+ if ($LASTEXITCODE -eq 0) {
+ $mountSuccess = $true
+ $scratchDir = $currentScratchDir
+ break
+ } else {
+ # Clean up failed attempt
+ if (Test-Path $currentScratchDir) {
+ Remove-Item $currentScratchDir -Force -Recurse -ErrorAction SilentlyContinue
+ }
+ }
+ }
+ }
+
+ # If all mount attempts failed, show error
+ if (-not $mountSuccess) {
+ try {
+ $mountedImages = Get-WindowsImage -Mounted
+ foreach ($mounted in $mountedImages) {
+ if ($mounted.Path -eq $scratchDir) {
+ $mountSuccess = $true
+ break
+ }
+ }
+ } catch {
+ }
+
+ # Additional verification by checking if typical Windows directories exist
+ if (-not $mountSuccess) {
+ if ((Test-Path "$scratchDir\Windows") -and (Test-Path "$scratchDir\Windows\System32")) {
+ $mountSuccess = $true
+ }
+ }
+ }
+
+ if ($mountSuccess) {
+ Write-Host "The Windows image has been mounted successfully. Continuing processing..."
+ } else {
+ Write-Host "ERROR: Windows image mounting failed after all attempts"
+ Write-Host ""
+ Write-Host "=== COMPREHENSIVE TROUBLESHOOTING GUIDE ==="
+ Write-Host ""
+
+ # Show current system state
+ Write-Host "CURRENT SYSTEM STATE:"
+ try {
+ $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent()
+ $principal = New-Object System.Security.Principal.WindowsPrincipal($currentUser)
+ $isAdmin = $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
+ } catch {
+ }
+
+ Write-Host ""
+ Write-Host "IMMEDIATE STEPS TO TRY:"
+ Write-Host ""
+
+ Write-Host "MANUAL COMMAND TO TEST:"
+ Write-Host "dism /mount-image /imagefile:`"$mountDir\sources\install.wim`" /index:$index /mountdir:`"$scratchDir`""
+ Write-Host ""
+
+ Write-Host ""
+ Write-Host "ADVANCED DIAGNOSTICS TO RUN:"
+ Write-Host ""
+
+ Write-Host "CORPORATE/MANAGED SYSTEM CONSIDERATIONS:"
+ Write-Host ""
+
+ $msg = "Could not mount Windows image. See console output for detailed troubleshooting steps."
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
+ [System.Windows.MessageBox]::Show($msg, "Mount Failed", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
+ })
+ return
+ }
+
+ if ($importDrivers) {
+ Write-Host "Exporting drivers from active installation..."
+ if (Test-Path "$env:TEMP\DRV_EXPORT") {
+ Remove-Item "$env:TEMP\DRV_EXPORT" -Recurse -Force
+ }
+ if (($injectDrivers -and (Test-Path "$driverPath"))) {
+ Write-Host "Using specified driver source..."
+ dism /english /online /export-driver /destination="$driverPath" /loglevel:1 | Out-Host
+ if ($?) {
+ # Don't add exported drivers yet, that is run later
+ Write-Host "Drivers have been exported successfully."
+ } else {
+ Write-Host "Failed to export drivers."
+ }
+ } else {
+ New-Item -Path "$env:TEMP\DRV_EXPORT" -ItemType Directory -Force
+ dism /english /online /export-driver /destination="$env:TEMP\DRV_EXPORT" /loglevel:1 | Out-Host
+ if ($?) {
+ Write-Host "Adding exported drivers with optimized settings..."
+ # Use optimized DISM settings for better performance
+ dism /english /image="$scratchDir" /add-driver /driver="$env:TEMP\DRV_EXPORT" /recurse /forceunsigned /loglevel:1 | Out-Host
+ } else {
+ Write-Host "Failed to export drivers. Continuing without importing them..."
+ }
+ if (Test-Path "$env:TEMP\DRV_EXPORT") {
+ Remove-Item "$env:TEMP\DRV_EXPORT" -Recurse -Force
+ }
+ }
+ }
+
+ if ($injectDrivers) {
+ if (Test-Path $driverPath) {
+ Write-Host "Adding Windows Drivers with optimized settings image($scratchDir) drivers($driverPath)"
+ # Use optimized DISM settings for better performance
+ dism /English /image:$scratchDir /add-driver /driver:$driverPath /recurse /forceunsigned | Out-Host
+ } else {
+ Write-Host "Path to drivers is invalid continuing without driver injection"
+ }
+ }
+
+ if ($WPBT) {
+ Write-Host "Disabling WPBT Execution"
+ reg load HKLM\zSYSTEM "$($scratchDir)\Windows\System32\config\SYSTEM"
+ reg add "HKLM\zSYSTEM\ControlSet001\Control\Session Manager" /v DisableWpbtExecution /t REG_DWORD /d 1 /f
+ reg unload HKLM\zSYSTEM
+ }
+
+ if ($unsupported) {
+ Write-Host "Bypassing system requirements (locally)"
+ reg add "HKLM\DEFAULT\Control Panel\UnsupportedHardwareNotificationCache" /v "SV1" /t REG_DWORD /d 0 /f
+ reg add "HKLM\DEFAULT\Control Panel\UnsupportedHardwareNotificationCache" /v "SV2" /t REG_DWORD /d 0 /f
+ reg add "HKLM\NTUSER\Control Panel\UnsupportedHardwareNotificationCache" /v "SV1" /t REG_DWORD /d 0 /f
+ reg add "HKLM\NTUSER\Control Panel\UnsupportedHardwareNotificationCache" /v "SV2" /t REG_DWORD /d 0 /f
+ reg add "HKLM\SYSTEM\Setup\LabConfig" /v "BypassCPUCheck" /t REG_DWORD /d 1 /f
+ reg add "HKLM\SYSTEM\Setup\LabConfig" /v "BypassRAMCheck" /t REG_DWORD /d 1 /f
+ reg add "HKLM\SYSTEM\Setup\LabConfig" /v "BypassSecureBootCheck" /t REG_DWORD /d 1 /f
+ reg add "HKLM\SYSTEM\Setup\LabConfig" /v "BypassStorageCheck" /t REG_DWORD /d 1 /f
+ reg add "HKLM\SYSTEM\Setup\LabConfig" /v "BypassTPMCheck" /t REG_DWORD /d 1 /f
+ reg add "HKLM\SYSTEM\Setup\MoSetup" /v "AllowUpgradesWithUnsupportedTPMOrCPU" /t REG_DWORD /d 1 /f
+ }
+
+ if ($importVirtIO) {
+ Write-Host "Copying VirtIO drivers..."
+ Microwin-CopyVirtIO
+ }
+
+ Write-Host "Remove Features from the image"
+ try {
+ Microwin-RemoveFeatures -UseCmdlets $true
+ } catch {
+ }
+ Write-Host "Removing features complete!"
+
+ Write-Host "Removing OS packages"
+ try {
+ Microwin-RemovePackages -UseCmdlets $true
+ } catch {
+ }
+
+ Write-Host "Removing Appx Bloat"
+ try {
+ Microwin-RemoveProvisionedPackages -UseCmdlets $true
+ } catch {
+ }
+
+ # Detect Windows 11 24H2 and add dependency to FileExp to prevent Explorer look from going back - thanks @WitherOrNot and @thecatontheceiling
+ if ((Microwin-TestCompatibleImage $imgVersion $([System.Version]::new(10,0,26100,1))) -eq $true) {
+ try {
+ if (Test-Path "$scratchDir\Windows\SystemApps\MicrosoftWindows.Client.FileExp_cw5n1h2txyewy\appxmanifest.xml" -PathType Leaf) {
+ # Found the culprit. Do the following:
+ # 1. Take ownership of the file, from TrustedInstaller to Administrators
+ takeown /F "$scratchDir\Windows\SystemApps\MicrosoftWindows.Client.FileExp_cw5n1h2txyewy\appxmanifest.xml" /A
+ # 2. Set ACLs so that we can write to it
+ icacls "$scratchDir\Windows\SystemApps\MicrosoftWindows.Client.FileExp_cw5n1h2txyewy\appxmanifest.xml" /grant "$(Microwin-GetLocalizedUsers -admins $true):(M)" | Out-Host
+ # 3. Open the file and do the modification
+ $appxManifest = Get-Content -Path "$scratchDir\Windows\SystemApps\MicrosoftWindows.Client.FileExp_cw5n1h2txyewy\appxmanifest.xml"
+ $originalLine = $appxManifest[13]
+ $dependency = "`n "
+ $appxManifest[13] = "$originalLine$dependency"
+ Set-Content -Path "$scratchDir\Windows\SystemApps\MicrosoftWindows.Client.FileExp_cw5n1h2txyewy\appxmanifest.xml" -Value $appxManifest -Force -Encoding utf8
+ }
+ }
+ catch {
+ # Fall back to what we used to do: delayed disablement
+ Enable-WindowsOptionalFeature -Path "$scratchDir" -FeatureName "Recall"
+ }
+ }
+
+ try {
+ Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\System32\LogFiles\WMI\RtBackup" -Directory
+ Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\DiagTrack" -Directory
+ Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\InboxApps" -Directory
+ Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\System32\LocationNotificationWindows.exe"
+ Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files (x86)\Windows Media Player" -Directory
+ Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files\Windows Media Player" -Directory
+ Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files (x86)\Windows Mail" -Directory
+ Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files\Windows Mail" -Directory
+ Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files (x86)\Internet Explorer" -Directory
+ Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files\Internet Explorer" -Directory
+ Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\GameBarPresenceWriter"
+ Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\System32\OneDriveSetup.exe"
+ Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\System32\OneDrive.ico"
+ Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\SystemApps" -mask "*narratorquickstart*" -Directory
+ Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\SystemApps" -mask "*ParentalControls*" -Directory
+ } catch {
+ }
+ Write-Host "Removal complete!"
+
+ Write-Host "Create unattend.xml"
+
+ if (($autoConfigPath -ne "") -and (Test-Path "$autoConfigPath")) {
+ try {
+ Write-Host "A configuration file has been specified. Copying to WIM file..."
+ Copy-Item "$autoConfigPath" "$($scratchDir)\winutil-config.json"
+ }
+ catch {
+ Write-Host "The config file could not be copied. Continuing without it..."
+ }
+ }
+
+ # Create unattended answer file with user information - Check condition to learn more about this functionality
+ if ($userName -eq "") {
+ Microwin-NewUnattend -userName "User"
+ } else {
+ if ($userPassword -eq "") {
+ Microwin-NewUnattend -userName "$userName"
+ } else {
+ Microwin-NewUnattend -userName "$userName" -userPassword "$userPassword"
+ }
+ }
+ Write-Host "Done Create unattend.xml"
+
+ Write-Host "Copy unattend.xml file into the ISO"
+ try {
+ New-Item -ItemType Directory -Force -Path "$($scratchDir)\Windows\Panther"
+ Copy-Item "$env:temp\unattend.xml" "$($scratchDir)\Windows\Panther\unattend.xml" -force
+ New-Item -ItemType Directory -Force -Path "$($scratchDir)\Windows\System32\Sysprep"
+ Copy-Item "$env:temp\unattend.xml" "$($scratchDir)\Windows\System32\Sysprep\unattend.xml" -force
+ } catch {
+ }
+ Write-Host "Done Copy unattend.xml"
+
+ Write-Host "Create FirstRun"
+ try {
+ Microwin-NewFirstRun
+ } catch {
+ }
+ Write-Host "Done create FirstRun"
+
+ Write-Host "Copy FirstRun.ps1 into the ISO"
+ try {
+ Copy-Item "$env:temp\FirstStartup.ps1" "$($scratchDir)\Windows\FirstStartup.ps1" -force
+ } catch {
+ }
+ Write-Host "Done copy FirstRun.ps1"
+
+ Write-Host "Copy link to winutil.ps1 into the ISO"
+ try {
+ $desktopDir = "$($scratchDir)\Windows\Users\Default\Desktop"
+ New-Item -ItemType Directory -Force -Path "$desktopDir"
+ dism /English /image:$($scratchDir) /set-profilepath:"$($scratchDir)\Windows\Users\Default"
+ } catch {
+ }
+
+ Write-Host "Copy checkinstall.cmd into the ISO"
+ try {
+ Microwin-NewCheckInstall
+ Copy-Item "$env:temp\checkinstall.cmd" "$($scratchDir)\Windows\checkinstall.cmd" -force
+ } catch {
+ }
+ Write-Host "Done copy checkinstall.cmd"
+
+ Write-Host "Creating a directory that allows to bypass Wifi setup"
+ try {
+ New-Item -ItemType Directory -Force -Path "$($scratchDir)\Windows\System32\OOBE\BYPASSNRO"
+ } catch {
+ }
+
+ Write-Host "Loading registry"
+ try {
+ reg load HKLM\zCOMPONENTS "$($scratchDir)\Windows\System32\config\COMPONENTS"
+ reg load HKLM\zDEFAULT "$($scratchDir)\Windows\System32\config\default"
+ reg load HKLM\zNTUSER "$($scratchDir)\Users\Default\ntuser.dat"
+ reg load HKLM\zSOFTWARE "$($scratchDir)\Windows\System32\config\SOFTWARE"
+ reg load HKLM\zSYSTEM "$($scratchDir)\Windows\System32\config\SYSTEM"
+ } catch {
+ }
+
+ Write-Host "Disabling Teams"
+ try {
+ reg add "HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\Communications" /v "ConfigureChatAutoInstall" /t REG_DWORD /d 0 /f >$null 2>&1
+ reg add "HKLM\zSOFTWARE\Policies\Microsoft\Windows\Windows Chat" /v ChatIcon /t REG_DWORD /d 2 /f >$null 2>&1
+ reg add "HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v "TaskbarMn" /t REG_DWORD /d 0 /f >$null 2>&1
+ reg query "HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\Communications" /v "ConfigureChatAutoInstall" >$null 2>&1
+ } catch {
+ }
+ Write-Host "Done disabling Teams"
+
+ try {
+ reg add "HKLM\zNTUSER\Software\Microsoft\Internet Explorer\LowRegistry\Audio\PolicyConfig\PropertyStore" /f
+ } catch {
+ }
+ Write-Host "Fix Windows Volume Mixer Issue"
+
+ try {
+ reg add "HKLM\zDEFAULT\Control Panel\UnsupportedHardwareNotificationCache" /v "SV1" /t REG_DWORD /d 0 /f
+ reg add "HKLM\zDEFAULT\Control Panel\UnsupportedHardwareNotificationCache" /v "SV2" /t REG_DWORD /d 0 /f
+ reg add "HKLM\zNTUSER\Control Panel\UnsupportedHardwareNotificationCache" /v "SV1" /t REG_DWORD /d 0 /f
+ reg add "HKLM\zNTUSER\Control Panel\UnsupportedHardwareNotificationCache" /v "SV2" /t REG_DWORD /d 0 /f
+ } catch {
+ }
+ Write-Host "Bypassing system requirements (system image)"
+ reg add "HKLM\zSYSTEM\Setup\LabConfig" /v "BypassCPUCheck" /t REG_DWORD /d 1 /f
+ reg add "HKLM\zSYSTEM\Setup\LabConfig" /v "BypassRAMCheck" /t REG_DWORD /d 1 /f
+ reg add "HKLM\zSYSTEM\Setup\LabConfig" /v "BypassSecureBootCheck" /t REG_DWORD /d 1 /f
+ reg add "HKLM\zSYSTEM\Setup\LabConfig" /v "BypassStorageCheck" /t REG_DWORD /d 1 /f
+ reg add "HKLM\zSYSTEM\Setup\LabConfig" /v "BypassTPMCheck" /t REG_DWORD /d 1 /f
+ reg add "HKLM\zSYSTEM\Setup\MoSetup" /v "AllowUpgradesWithUnsupportedTPMOrCPU" /t REG_DWORD /d 1 /f
+
+ # Prevent Windows Update Installing so called Expedited Apps - 24H2 and newer
+ if ((Microwin-TestCompatibleImage $imgVersion $([System.Version]::new(10,0,26100,1))) -eq $true) {
+ @(
+ 'EdgeUpdate',
+ 'DevHomeUpdate',
+ 'OutlookUpdate',
+ 'CrossDeviceUpdate'
+ ) | ForEach-Object {
+ Write-Host "Removing Windows Expedited App: $_"
+ reg delete "HKLM\zSOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\$_" /f | Out-Null
+ }
+ }
+
+ reg add "HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "SearchboxTaskbarMode" /t REG_DWORD /d 0 /f
+ Write-Host "Setting all services to start manually"
+ reg add "HKLM\zSOFTWARE\CurrentControlSet\Services" /v Start /t REG_DWORD /d 3 /f
+
+ Write-Host "Enabling Local Accounts on OOBE"
+ reg add "HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v "BypassNRO" /t REG_DWORD /d "1" /f
+
+ Write-Host "Disabling Sponsored Apps"
+ reg add "HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" /v "OemPreInstalledAppsEnabled" /t REG_DWORD /d 0 /f
+ reg add "HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" /v "PreInstalledAppsEnabled" /t REG_DWORD /d 0 /f
+ reg add "HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" /v "SilentInstalledAppsEnabled" /t REG_DWORD /d 0 /f
+ reg add "HKLM\zSOFTWARE\Policies\Microsoft\Windows\CloudContent" /v "DisableWindowsConsumerFeatures" /t REG_DWORD /d 1 /f
+ reg add "HKLM\zSOFTWARE\Microsoft\PolicyManager\current\device\Start" /v "ConfigureStartPins" /t REG_SZ /d '{\"pinnedList\": [{}]}' /f
+ Write-Host "Done removing Sponsored Apps"
+
+ Write-Host "Disabling Reserved Storage"
+ reg add "HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager" /v "ShippedWithReserves" /t REG_DWORD /d 0 /f
+
+ Write-Host "Changing theme to dark. This only works on Activated Windows"
+ reg add "HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize" /v "AppsUseLightTheme" /t REG_DWORD /d 0 /f
+ reg add "HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize" /v "SystemUsesLightTheme" /t REG_DWORD /d 0 /f
+
+ if ((Microwin-TestCompatibleImage $imgVersion $([System.Version]::new(10,0,21996,1))) -eq $false) {
+ # We're dealing with Windows 10. Configure sane desktop settings. NOTE: even though stuff to disable News and Interests is there,
+ # it doesn't seem to work, and I don't want to waste more time dealing with an operating system that will lose support in a year (2025)
+
+ # I invite anyone to work on improving stuff for News and Interests, but that won't be me!
+
+ Write-Host "Disabling Search Highlights..."
+ reg add "HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\Feeds\DSB" /v "ShowDynamicContent" /t REG_DWORD /d 0 /f
+ reg add "HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\SearchSettings" /v "IsDynamicSearchBoxEnabled" /t REG_DWORD /d 0 /f
+ reg add "HKLM\zSOFTWARE\Policies\Microsoft\Dsh" /v "AllowNewsAndInterests" /t REG_DWORD /d 0 /f
+ reg add "HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "TraySearchBoxVisible" /t REG_DWORD /d 1 /f
+ reg add "HKLM\zSOFTWARE\Policies\Microsoft\Windows\Windows Feeds" /v "EnableFeeds" /t REG_DWORD /d 0 /f
+ }
+
+ } catch {
+ Write-Error "An unexpected error occurred: $_"
+ } finally {
+ Write-Host "Unmounting Registry..."
+ try {
+ reg unload HKLM\zCOMPONENTS
+ reg unload HKLM\zDEFAULT
+ reg unload HKLM\zNTUSER
+ reg unload HKLM\zSOFTWARE
+ reg unload HKLM\zSYSTEM
+ } catch {
+ }
+
+ Write-Host "Cleaning up image with optimized settings..."
+ try {
+ # Use optimized DISM cleanup settings for better performance
+ dism /English /image:$scratchDir /Cleanup-Image /StartComponentCleanup /ResetBase /loglevel:1
+ } catch {
+ }
+ Write-Host "Cleanup complete."
+
+ Write-Host "Unmounting image..."
+
+ # First, try to clean up any processes or handles that might interfere with unmounting
+ try {
+ # Force garbage collection to release PowerShell file handles
+ [System.GC]::Collect()
+ [System.GC]::WaitForPendingFinalizers()
+ [System.GC]::Collect()
+
+ # Wait for any background operations to complete
+ Start-Sleep -Seconds 3
+
+ # Check if any Windows Search or antivirus processes might be interfering
+ $interferingProcesses = Get-Process -ErrorAction SilentlyContinue | Where-Object {
+ $_.ProcessName -match "SearchIndexer|SearchProtocolHost|SearchFilterHost|MsMpEng|NisSrv"
+ }
+
+ if ($interferingProcesses) {
+ Start-Sleep -Seconds 5
+ }
+
+ } catch {
+ }
+
+ $dismountSuccess = $false
+ $maxRetries = 3
+
+ for ($retry = 1; $retry -le $maxRetries; $retry++) {
+ try {
+
+ switch ($retry) {
+ 1 {
+ # First attempt: Try DISM command directly
+ $dismResult = & dism /english /unmount-image /mountdir:"$scratchDir" /commit /loglevel:1
+ $dismExitCode = $LASTEXITCODE
+
+ if ($dismExitCode -eq 0) {
+ $dismountSuccess = $true
+ break
+ }
+ }
+ 2 {
+ # Second attempt: Try PowerShell cmdlet
+ Dismount-WindowsImage -Path "$scratchDir" -Save
+ }
+ 3 {
+ # Third attempt: Try PowerShell cmdlet with CheckIntegrity
+ Dismount-WindowsImage -Path "$scratchDir" -Save -CheckIntegrity
+ }
+ }
+
+ # Verify dismount was successful for PowerShell attempts
+ if ($retry -gt 1) {
+ Start-Sleep -Seconds 2
+ $mountedImages = Get-WindowsImage -Mounted
+ $stillMounted = $false
+ foreach ($mounted in $mountedImages) {
+ if ($mounted.Path -eq $scratchDir) {
+ $stillMounted = $true
+ break
+ }
+ }
+
+ if (-not $stillMounted) {
+ $dismountSuccess = $true
+ break
+ } else {
+ }
+ }
+
+ } catch {
+ }
+
+ # If this isn't the last retry, wait before trying again
+ if ($retry -lt $maxRetries -and -not $dismountSuccess) {
+ Start-Sleep -Seconds 5
+
+ # Additional cleanup between retries
+ [System.GC]::Collect()
+ [System.GC]::WaitForPendingFinalizers()
+ [System.GC]::Collect()
+ }
+ }
+
+ # If all normal attempts failed, try aggressive cleanup and final fallback strategies
+ if (-not $dismountSuccess) {
+
+ # Aggressive cleanup before final attempts
+ try {
+
+ # Force close any PowerShell handles
+ [System.GC]::Collect()
+ [System.GC]::WaitForPendingFinalizers()
+ [System.GC]::Collect()
+
+ # Remove readonly attributes from scratch directory
+ if (Test-Path $scratchDir) {
+ & attrib -R "$scratchDir\*" /S /D 2>$null
+ }
+
+ # Wait longer for file handles to be released
+ Start-Sleep -Seconds 10
+
+ } catch {
+ }
+
+ # Last attempt - try multiple fallback strategies
+
+
+ # First, commit the image
+ try {
+ & dism /english /commit-image /mountdir:"$scratchDir" /loglevel:1
+ } catch {}
+
+ # Now, keep discarding the image in a loop
+ $discardAttempts = 0
+ $maxDiscardAttempts = 6
+ while (-not $dismountSuccess -and $discardAttempts -lt $maxDiscardAttempts) {
+ try {
+ $dismResult = & dism /english /unmount-image /mountdir:"$scratchDir" /discard /loglevel:1
+ if ($LASTEXITCODE -eq 0) {
+ $dismountSuccess = $true
+ break
+ }
+ } catch {}
+ $discardAttempts++
+ Start-Sleep -Seconds 5
+ }
+
+ # Try PowerShell discard if DISM failed
+ if (-not $dismountSuccess) {
+ try {
+ Dismount-WindowsImage -Path "$scratchDir" -Discard
+ $dismountSuccess = $true
+ } catch {
+ }
+ }
+
+ # Final fallback: cleanup mountpoints
+ if (-not $dismountSuccess) {
+ try {
+ & dism /cleanup-mountpoints
+ Start-Sleep -Seconds 3
+ } catch {
+ }
+
+ }
+ }
+
+ if (-not $dismountSuccess) {
+ $msg = "Warning: Could not properly dismount the Windows image. The process may have partially completed, but manual cleanup may be required."
+ Write-Host $msg
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
+ [System.Windows.MessageBox]::Show($msg + "`n`nPlease run 'dism /cleanup-mountpoints' as Administrator to clean up.", "Dismount Warning", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
+ })
+ }
+ }
+
+ try {
+ Write-Host "Exporting image into $mountDir\sources\install2.wim with optimized settings..."
+ try {
+ # Use Max compression for smaller file size (slower, but more efficient)
+ Export-WindowsImage -SourceImagePath "$mountDir\sources\install.wim" -SourceIndex $index -DestinationImagePath "$mountDir\sources\install2.wim" -CompressionType "Max"
+ } catch {
+ # Fall back to DISM with optimized settings
+ dism /english /export-image /sourceimagefile="$mountDir\sources\install.wim" /sourceindex=$index /destinationimagefile="$mountDir\sources\install2.wim" /compress:fast /checkintegrity /verify /loglevel:1
+ }
+
+ Write-Host "Remove old '$mountDir\sources\install.wim' and rename $mountDir\sources\install2.wim"
+ try {
+ Remove-Item "$mountDir\sources\install.wim"
+ Rename-Item "$mountDir\sources\install2.wim" "$mountDir\sources\install.wim"
+ } catch {
+ throw $_
+ }
+
+ if (-not (Test-Path -Path "$mountDir\sources\install.wim")) {
+ $msg = "Something went wrong. Please report this bug to the devs."
+ Write-Error "$($msg) '$($mountDir)\sources\install.wim' doesn't exist"
+ $sync.form.Dispatcher.Invoke([action]{
+ # Invoke-MicrowinBusyInfo -action "warning" -message $msg
+ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
+ })
+ return
+ }
+ Write-Host "Windows image completed. Continuing with boot.wim."
+
+ if ($esd) {
+ Write-Host "Converting install image to ESD with optimized settings..."
+ try {
+ Export-WindowsImage -SourceImagePath "$mountDir\sources\install.wim" -SourceIndex $index -DestinationImagePath "$mountDir\sources\install.esd" -CompressionType "Recovery"
+ Remove-Item "$mountDir\sources\install.wim"
+ Write-Host "Converted install image to ESD successfully."
+ } catch {
+ Start-Process -FilePath "$env:SystemRoot\System32\dism.exe" -ArgumentList "/export-image /sourceimagefile:`"$mountDir\sources\install.wim`" /sourceindex:1 /destinationimagefile:`"$mountDir\sources\install.esd`" /compress:recovery /checkintegrity /verify /loglevel:1" -Wait -NoNewWindow
+ Remove-Item "$mountDir\sources\install.wim"
+ Write-Host "Converted install image to ESD using DISM."
+ }
+ }
+ } catch {
+ Write-Error "An unexpected error occurred during image export: $_"
+ throw $_
+ }
+
+ try {
+ # Next step boot image
+ Write-Host "Mounting boot image $mountDir\sources\boot.wim into $scratchDir"
+ Mount-WindowsImage -ImagePath "$mountDir\sources\boot.wim" -Index 2 -Path "$scratchDir"
+
+ if ($injectDrivers) {
+ if (Test-Path $driverPath) {
+ Write-Host "Adding Windows Drivers image($scratchDir) drivers($driverPath) "
+ dism /English /image:$scratchDir /add-driver /driver:$driverPath /recurse | Out-Host
+ } else {
+ Write-Host "Path to drivers is invalid continuing without driver injection"
+ }
+ }
+
+ Write-Host "Loading registry..."
+ reg load HKLM\zCOMPONENTS "$($scratchDir)\Windows\System32\config\COMPONENTS" >$null
+ reg load HKLM\zDEFAULT "$($scratchDir)\Windows\System32\config\default" >$null
+ reg load HKLM\zNTUSER "$($scratchDir)\Users\Default\ntuser.dat" >$null
+ reg load HKLM\zSOFTWARE "$($scratchDir)\Windows\System32\config\SOFTWARE" >$null
+ reg load HKLM\zSYSTEM "$($scratchDir)\Windows\System32\config\SYSTEM" >$null
+ Write-Host "Bypassing system requirements on the setup image"
+ reg add "HKLM\zDEFAULT\Control Panel\UnsupportedHardwareNotificationCache" /v "SV1" /t REG_DWORD /d 0 /f
+ reg add "HKLM\zDEFAULT\Control Panel\UnsupportedHardwareNotificationCache" /v "SV2" /t REG_DWORD /d 0 /f
+ reg add "HKLM\zNTUSER\Control Panel\UnsupportedHardwareNotificationCache" /v "SV1" /t REG_DWORD /d 0 /f
+ reg add "HKLM\zNTUSER\Control Panel\UnsupportedHardwareNotificationCache" /v "SV2" /t REG_DWORD /d 0 /f
+ reg add "HKLM\zSYSTEM\Setup\LabConfig" /v "BypassCPUCheck" /t REG_DWORD /d 1 /f
+ reg add "HKLM\zSYSTEM\Setup\LabConfig" /v "BypassRAMCheck" /t REG_DWORD /d 1 /f
+ reg add "HKLM\zSYSTEM\Setup\LabConfig" /v "BypassSecureBootCheck" /t REG_DWORD /d 1 /f
+ reg add "HKLM\zSYSTEM\Setup\LabConfig" /v "BypassStorageCheck" /t REG_DWORD /d 1 /f
+ reg add "HKLM\zSYSTEM\Setup\LabConfig" /v "BypassTPMCheck" /t REG_DWORD /d 1 /f
+ reg add "HKLM\zSYSTEM\Setup\MoSetup" /v "AllowUpgradesWithUnsupportedTPMOrCPU" /t REG_DWORD /d 1 /f
+ # Fix Computer Restarted Unexpectedly Error on New Bare Metal Install
+ reg add "HKLM\zSYSTEM\Setup\Status\ChildCompletion" /v "setup.exe" /t REG_DWORD /d 3 /f
+ } catch {
+ Write-Error "An unexpected error occurred: $_"
+ } finally {
+ Write-Host "Unmounting Registry..."
+ reg unload HKLM\zCOMPONENTS
+ reg unload HKLM\zDEFAULT
+ reg unload HKLM\zNTUSER
+ reg unload HKLM\zSOFTWARE
+ reg unload HKLM\zSYSTEM
+
+ Write-Host "Unmounting image..."
+ Dismount-WindowsImage -Path "$scratchDir" -Save
+
+ Write-Host "Creating ISO image"
+
+ # if we downloaded oscdimg from github it will be in the temp directory so use it
+ # if it is not in temp it is part of ADK and is in global PATH so just set it to oscdimg.exe
+ $oscdimgPath = Join-Path $env:TEMP 'oscdimg.exe'
+ $oscdImgFound = Test-Path $oscdimgPath -PathType Leaf
+ if (!$oscdImgFound) {
+ $oscdimgPath = "oscdimg.exe"
+ }
+
+ Write-Host "[INFO] Using oscdimg.exe from: $oscdimgPath"
+
+ $oscdimgProc = Start-Process -FilePath "$oscdimgPath" -ArgumentList "-m -o -u2 -udfver102 -bootdata:2#p0,e,b`"$mountDir\boot\etfsboot.com`"#pEF,e,b`"$mountDir\efi\microsoft\boot\efisys.bin`" `"$mountDir`" `"$SaveDialogFileName`"" -Wait -PassThru -NoNewWindow
+
+ $LASTEXITCODE = $oscdimgProc.ExitCode
+
+ Write-Host "OSCDIMG Error Level : $($oscdimgProc.ExitCode)"
+
+ if ($copyToUSB) {
+ Write-Host "Copying target ISO to the USB drive"
+ Microwin-CopyToUSB("$SaveDialogFileName")
+ if ($?) { Write-Host "Done Copying target ISO to USB drive!" } else { Write-Host "ISO copy failed." }
+ }
+
+ Write-Host " _____ "
+ Write-Host "(____ \ "
+ Write-Host " _ \ \ ___ ____ ____ "
+ Write-Host "| | | / _ \| _ \ / _ ) "
+ Write-Host "| |__/ / |_| | | | ( (/ / "
+ Write-Host "|_____/ \___/|_| |_|\____) "
+
+ # Check if the ISO was successfully created - CTT edit
+ if ($LASTEXITCODE -eq 0) {
+ Write-Host "`n`nPerforming Cleanup..."
+ Remove-Item -Recurse -Force "$($scratchDir)"
+ Remove-Item -Recurse -Force "$($mountDir)"
+ $msg = "Done. ISO image is located here: $SaveDialogFileName"
+ Write-Host $msg
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"
+ # Invoke-MicrowinBusyInfo -action "done" -message "Finished!"
+ [System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
+ })
+ } else {
+ Write-Host "ISO creation failed. The "$($mountDir)" directory has not been removed."
+ try {
+ # This creates a new Win32 exception from which we can extract a message in the system language.
+ # Now, this will NOT throw an exception
+ $exitCode = New-Object System.ComponentModel.Win32Exception($LASTEXITCODE)
+ Write-Host "Reason: $($exitCode.Message)"
+ $sync.form.Dispatcher.Invoke([action]{
+ # Invoke-MicrowinBusyInfo -action "warning" -message $exitCode.Message
+ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
+ [System.Windows.MessageBox]::Show("MicroWin failed to make the ISO.", "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
+ })
+ } catch {
+ # Could not get error description from Windows APIs
+ }
+ }
+
+ $sync.form.Dispatcher.Invoke([action]{
+ Toggle-MicrowinPanel 1
+ $sync.MicrowinFinalIsoLocation.Text = "$SaveDialogFileName"
+ })
+
+ # Allow the machine to sleep again (optional)
+ [PowerManagement]::SetThreadExecutionState(0)
+ }
+ } catch {
+ Write-Error "Critical error in MicroWin process: $_"
+ $sync.form.Dispatcher.Invoke([action]{
+ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning"
+ # Invoke-MicrowinBusyInfo -action "warning" -message "Critical error occurred: $_"
+ })
+ } finally {
+
+ # Reset process priority to normal
+ try {
+ $currentProcess = Get-Process -Id $PID
+ $currentProcess.PriorityClass = [System.Diagnostics.ProcessPriorityClass]::Normal
+ } catch {
+ }
+
+ $sync.ProcessRunning = $false
+ }
+ }
+}
diff --git a/functions/microwin/Microwin-RemoveFeatures.ps1 b/functions/microwin/Microwin-RemoveFeatures.ps1
index be5888dc9d..55de2c9d5e 100644
--- a/functions/microwin/Microwin-RemoveFeatures.ps1
+++ b/functions/microwin/Microwin-RemoveFeatures.ps1
@@ -58,14 +58,12 @@ function Microwin-RemoveFeatures() {
foreach ($feature in $featList) {
$status = "Removing feature $($feature.FeatureName)"
Write-Progress -Activity "Removing features" -Status $status -PercentComplete ($counter++/$featlist.Count*100)
- Write-Debug "Removing feature $($feature.FeatureName)"
Disable-WindowsOptionalFeature -Path "$scratchDir" -FeatureName $($feature.FeatureName) -Remove -ErrorAction SilentlyContinue -NoRestart
}
} else {
foreach ($feature in $featList) {
$status = "Removing feature $feature"
Write-Progress -Activity "Removing features" -Status $status -PercentComplete ($counter++/$featlist.Count*100)
- Write-Debug "Removing feature $feature"
dism /english /image="$scratchDir" /disable-feature /featurename=$feature /remove /quiet /norestart | Out-Null
if ($? -eq $false) {
Write-Host "Feature $feature could not be disabled."
diff --git a/functions/microwin/Microwin-RemoveFileOrDirectory.ps1 b/functions/microwin/Microwin-RemoveFileOrDirectory.ps1
index 10eef71fa2..fd88fdd55c 100644
--- a/functions/microwin/Microwin-RemoveFileOrDirectory.ps1
+++ b/functions/microwin/Microwin-RemoveFileOrDirectory.ps1
@@ -8,10 +8,8 @@ function Microwin-RemoveFileOrDirectory([string]$pathToDelete, [string]$mask = "
$itemsToDelete = [System.Collections.ArrayList]::new()
if ($mask -eq "") {
- Write-Debug "Adding $($pathToDelete) to array."
[void]$itemsToDelete.Add($pathToDelete)
} else {
- Write-Debug "Adding $($pathToDelete) to array and mask is $($mask)"
if ($Directory) {
$itemsToDelete = Get-ChildItem $pathToDelete -Include $mask -Recurse -Directory
} else {
diff --git a/functions/microwin/Microwin-RemovePackages.ps1 b/functions/microwin/Microwin-RemovePackages.ps1
index 95b1442016..b20b0eecea 100644
--- a/functions/microwin/Microwin-RemovePackages.ps1
+++ b/functions/microwin/Microwin-RemovePackages.ps1
@@ -89,7 +89,6 @@ function Microwin-RemovePackages {
foreach ($package in $pkgList) {
$status = "Removing package $package"
Write-Progress -Activity "Removing Packages" -Status $status -PercentComplete ($counter++/$pkglist.Count*100)
- Write-Debug "Removing package $package"
dism /english /image="$scratchDir" /remove-package /packagename=$package /quiet /norestart | Out-Null
if ($? -eq $false) {
Write-Host "Package $package could not be removed."
diff --git a/functions/microwin/Set-ScratchFolderPermissions.ps1 b/functions/microwin/Set-ScratchFolderPermissions.ps1
new file mode 100644
index 0000000000..acdc8e0bc3
--- /dev/null
+++ b/functions/microwin/Set-ScratchFolderPermissions.ps1
@@ -0,0 +1,32 @@
+function Set-ScratchFolderPermissions {
+ <#
+ .SYNOPSIS
+ Creates a scratch directory for DISM operations
+
+ .DESCRIPTION
+ This function simply creates a directory and removes read-only attributes.
+ DISM handles its own permissions when running as Administrator.
+
+ .PARAMETER Path
+ The path to the directory to prepare
+ #>
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$Path
+ )
+
+ try {
+ # Create directory if it doesn't exist
+ if (-not (Test-Path $Path)) {
+ New-Item -Path $Path -ItemType Directory -Force | Out-Null
+ }
+
+ # Remove read-only attributes (this is the only thing that actually matters)
+ & attrib -R "$Path" /S /D 2>$null
+
+ return $true
+ } catch {
+ Write-Host "Failed to prepare directory $Path`: $($_.Exception.Message)" -ForegroundColor Red
+ return $false
+ }
+}
diff --git a/functions/private/Copy-Files.ps1 b/functions/private/Copy-Files.ps1
index cec7869a26..cdd9436386 100644
--- a/functions/private/Copy-Files.ps1
+++ b/functions/private/Copy-Files.ps1
@@ -30,18 +30,44 @@ function Copy-Files {
foreach ($file in $files) {
$status = "Copying file {0} of {1}: {2}" -f $counter, $files.Count, $file.Name
Write-Progress -Activity "Copy disc image files" -Status $status -PercentComplete ($counter++/$files.count*100)
- $restpath = $file.FullName -Replace $path, ''
+ $restpath = $file.FullName -Replace [regex]::Escape($path), ''
if ($file.PSIsContainer -eq $true) {
- Write-Debug "Creating $($destination + $restpath)"
- New-Item ($destination+$restpath) -Force:$force -Type Directory -ErrorAction SilentlyContinue
+ $targetPath = Join-Path $destination $restpath
+ Write-Debug "Creating $targetPath"
+ New-Item $targetPath -Force:$force -Type Directory -ErrorAction SilentlyContinue
} else {
- Write-Debug "Copy from $($file.FullName) to $($destination+$restpath)"
- Copy-Item $file.FullName ($destination+$restpath) -ErrorAction SilentlyContinue -Force:$force
- Set-ItemProperty -Path ($destination+$restpath) -Name IsReadOnly -Value $false
+ $targetPath = Join-Path $destination $restpath
+ Write-Debug "Copy from $($file.FullName) to $targetPath"
+ try {
+ Copy-Item $file.FullName $targetPath -ErrorAction Stop -Force:$force
+
+ # Remove ReadOnly attribute using attrib for consistency
+ & attrib -R $targetPath 2>$null
+
+ # Force garbage collection to release file handles
+ $copiedFile = $null
+ } catch {
+ Write-Debug "Failed to copy $($file.FullName): $($_.Exception.Message)"
+ # Try alternative method if standard copy fails
+ try {
+ [System.IO.File]::Copy($file.FullName, $targetPath, $force)
+ # Remove ReadOnly attribute using attrib for consistency
+ & attrib -R $targetPath 2>$null
+ } catch {
+ Write-Debug "Alternative copy method also failed: $($_.Exception.Message)"
+ }
+ }
}
}
Write-Progress -Activity "Copy disc image files" -Status "Ready" -Completed
+
+ # Force cleanup to release any remaining file handles
+ [System.GC]::Collect()
+ [System.GC]::WaitForPendingFinalizers()
+ [System.GC]::Collect()
+
+ Write-Host "File copy completed. Released file handles for unmount."
} catch {
Write-Host "Unable to Copy all the files due to an unhandled exception" -ForegroundColor Yellow
Write-Host "Error information: $($_.Exception.Message)`n" -ForegroundColor Yellow
diff --git a/functions/private/Get-LocalGroupNameFromSid.ps1 b/functions/private/Get-LocalGroupNameFromSid.ps1
new file mode 100644
index 0000000000..a31542e0db
--- /dev/null
+++ b/functions/private/Get-LocalGroupNameFromSid.ps1
@@ -0,0 +1,7 @@
+function Get-LocalGroupNameFromSid {
+ param (
+ [Parameter(Mandatory, Position = 0)] [string]$sid
+ )
+ # You can fine-tune this to add error handling, but this should do the trick
+ return (Get-LocalGroup | Where-Object { $_.SID.Value -like "$sid" }).Name
+}
diff --git a/functions/private/Invoke-GarbageCollection.ps1 b/functions/private/Invoke-GarbageCollection.ps1
new file mode 100644
index 0000000000..9ae8d76f56
--- /dev/null
+++ b/functions/private/Invoke-GarbageCollection.ps1
@@ -0,0 +1,34 @@
+function Invoke-GarbageCollection {
+ <#
+ .SYNOPSIS
+ Forces garbage collection to release file handles and free memory
+
+ .DESCRIPTION
+ This function performs a complete garbage collection cycle to help release
+ file handles that might be keeping files or directories locked.
+
+ .PARAMETER WaitSeconds
+ Optional wait time after garbage collection (default: 0)
+
+ .EXAMPLE
+ Invoke-GarbageCollection
+
+ .EXAMPLE
+ Invoke-GarbageCollection -WaitSeconds 2
+ #>
+ param(
+ [int]$WaitSeconds = 0
+ )
+
+ try {
+ [System.GC]::Collect()
+ [System.GC]::WaitForPendingFinalizers()
+ [System.GC]::Collect()
+
+ if ($WaitSeconds -gt 0) {
+ Start-Sleep -Seconds $WaitSeconds
+ }
+ } catch {
+ # Ignore GC errors - not critical
+ }
+}