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 + } +}