From 6e739dfbdfd8677bf235faed0290a04129f42239 Mon Sep 17 00:00:00 2001 From: Real-MullaC Date: Tue, 5 Aug 2025 19:20:19 +0100 Subject: [PATCH 01/10] Runspace --- MICROWIN_IMPROVEMENTS.md | 74 + .../microwin/Force-CleanupMountDirectory.ps1 | 133 ++ functions/microwin/Get-ProcessesUsingPath.ps1 | 103 ++ functions/microwin/Invoke-Microwin.ps1 | 512 +----- .../microwin/Invoke-MicrowinGetIso-new.ps1 | 28 + functions/microwin/Invoke-MicrowinGetIso.ps1 | 314 +--- .../Invoke-WPFMicroWinGetIsoRunspace.ps1 | 578 ++++++ .../microwin/Invoke-WPFMicroWinRunspace.ps1 | 1603 +++++++++++++++++ .../microwin/Set-ScratchFolderPermissions.ps1 | 244 +++ functions/microwin/Set-WimFilesWritable.ps1 | 0 functions/private/Copy-Files.ps1 | 31 +- 11 files changed, 2841 insertions(+), 779 deletions(-) create mode 100644 MICROWIN_IMPROVEMENTS.md create mode 100644 functions/microwin/Force-CleanupMountDirectory.ps1 create mode 100644 functions/microwin/Get-ProcessesUsingPath.ps1 create mode 100644 functions/microwin/Invoke-MicrowinGetIso-new.ps1 create mode 100644 functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 create mode 100644 functions/microwin/Invoke-WPFMicroWinRunspace.ps1 create mode 100644 functions/microwin/Set-ScratchFolderPermissions.ps1 create mode 100644 functions/microwin/Set-WimFilesWritable.ps1 diff --git a/MICROWIN_IMPROVEMENTS.md b/MICROWIN_IMPROVEMENTS.md new file mode 100644 index 0000000000..9b1f64a7f1 --- /dev/null +++ b/MICROWIN_IMPROVEMENTS.md @@ -0,0 +1,74 @@ +# MicroWin Improvements Summary + +## Latest Enhancements (Focus on Mount Permission Issues) + +### 1. Privilege Elevation System +- **Windows API Integration**: Added P/Invoke declarations for Windows API functions to enable system privileges +- **Required Privileges**: Automatically enables SeBackupPrivilege, SeRestorePrivilege, SeSecurityPrivilege, SeTakeOwnershipPrivilege, SeManageVolumePrivilege +- **Privilege Status Monitoring**: Real-time checking and reporting of current privilege status +- **Alternative Methods**: Fallback to PowerShell's Enable-Privilege if available + +### 2. Pre-Mount System Checks +- **DISM Availability**: Verifies DISM is installed and responsive +- **Disk Space Monitoring**: Checks available space on target drive (warns if < 10GB) +- **Directory Access Testing**: Validates write permissions to scratch directory +- **System Architecture Detection**: Reports process and OS architecture for compatibility +- **DISM-Compatible Permissions**: Automatically applies optimal file permissions to scratch directories + +### 3. Enhanced Mount Process +- **Retry Logic**: Up to 3 attempts with cleanup between retries +- **Multiple Methods**: Primary DISM command-line, fallback to PowerShell cmdlets +- **Better Error Capture**: Redirected output to log files for detailed error analysis +- **Stale Mount Cleanup**: Automatic cleanup of previous mount points before starting +- **Multiple Scratch Directories**: Automatically tries different locations (temp, C:\temp, C:\MicrowinMount) +- **Permission Application**: Applies DISM-compatible permissions to all scratch directories + +### 4. Comprehensive Error Reporting +- **System State Display**: Shows administrator status, current user, architecture +- **Current Privileges**: Lists all security privileges and their status +- **Step-by-Step Troubleshooting**: Detailed manual steps for users to follow +- **Manual Command Examples**: Exact commands users can run to test independently +- **Alternative Approaches**: Multiple workaround suggestions + +### 5. Performance Optimizations (Previous) +- **Process Priority**: Elevated to High priority for faster processing +- **Memory Optimization**: Increased working set for better performance +- **Multi-Core Detection**: Utilizes all available CPU cores +- **Fast Compression**: Uses fastest compression settings for speed + +### 6. Debug and Monitoring Improvements +- **Extensive Logging**: Debug output at every major step +- **Progress Tracking**: Real-time progress updates via taskbar +- **Error Context**: Detailed error messages with context +- **Verification Steps**: Multiple methods to verify successful operations + +## Key Files Modified +- `functions/microwin/Invoke-WPFMicroWinRunspace.ps1` - Main runspace with all improvements +- `functions/microwin/Invoke-Microwin.ps1` - Entry point with runspace integration +- `functions/microwin/Invoke-MicrowinGetIso.ps1` - ISO selection with UI thread safety +- `functions/microwin/Set-ScratchFolderPermissions.ps1` - Standalone script to apply DISM-compatible permissions + +## Testing Recommendations +1. **Administrator Rights**: Always run as Administrator +2. **Antivirus**: Temporarily disable real-time protection during testing +3. **Clean Environment**: Run `dism /cleanup-mountpoints` before starting +4. **Monitor Output**: Watch console for detailed debug information +5. **Manual Verification**: Use provided manual commands if automated process fails + +## Troubleshooting Steps (If Mount Still Fails) +1. Check Event Viewer for DISM-related errors +2. Verify Windows ADK/DISM tools are properly installed +3. Test with different scratch directory locations +4. Run from elevated Command Prompt instead of PowerShell +5. Check Group Policy restrictions on DISM operations +6. Ensure no other processes are using the WIM file +7. **NEW: Use the standalone permission script**: `.\functions\microwin\Set-ScratchFolderPermissions.ps1 -Path "C:\temp\mount" -ShowPermissions` +8. **NEW: Disable Windows Defender real-time protection** (most common cause of permission issues) + +## Expected Behavior +- Clear privilege status reporting +- Detailed pre-mount system checks +- Retry logic with cleanup between attempts +- Comprehensive error messages with actionable steps +- Progress updates throughout the process +- Graceful failure handling with detailed troubleshooting guidance diff --git a/functions/microwin/Force-CleanupMountDirectory.ps1 b/functions/microwin/Force-CleanupMountDirectory.ps1 new file mode 100644 index 0000000000..0b65224d26 --- /dev/null +++ b/functions/microwin/Force-CleanupMountDirectory.ps1 @@ -0,0 +1,133 @@ +function Force-CleanupMountDirectory { + <# + .SYNOPSIS + Forces cleanup of a mount directory by closing processes that have files open + + .DESCRIPTION + This function attempts to identify and close processes that have files open in the specified + mount directory, which is often the cause of unmount failures. + + .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 + ) + + Write-Host "DEBUG: Starting forced cleanup of mount directory: $MountPath" -ForegroundColor Yellow + + try { + # First, try to identify processes using files in the mount directory + Write-Host "DEBUG: Checking for processes using files in mount directory..." -ForegroundColor Yellow + + # Get all processes and check if they have files open in the mount directory + $processesToKill = @() + + try { + # Use Get-Process with file handle information + $allProcesses = Get-Process -ErrorAction SilentlyContinue + foreach ($process in $allProcesses) { + try { + if ($process.ProcessName -eq "System" -or $process.ProcessName -eq "Idle") { + continue + } + + # Check if process has any modules or files loaded from the mount path + try { + $processModules = $process.Modules + } catch { + $processModules = $null + } + if ($processModules) { + foreach ($module in $processModules) { + if ($module.FileName -and $module.FileName.StartsWith($MountPath, [System.StringComparison]::OrdinalIgnoreCase)) { + Write-Host "DEBUG: Found process using mount directory: $($process.ProcessName) (PID: $($process.Id))" -ForegroundColor Red + $processesToKill += $process + break + } + } + } + } catch { + # Ignore processes we can't access + continue + } + } + } catch { + Write-Host "DEBUG: Could not enumerate all processes: $($_.Exception.Message)" -ForegroundColor Yellow + } + + # Also check for common processes that might interfere + $commonInterferingProcesses = @("explorer", "dwm", "winlogon", "csrss", "svchost") + Write-Host "DEBUG: Checking for Windows Search, antivirus, and other interfering processes..." -ForegroundColor Yellow + + $suspiciousProcesses = Get-Process -ErrorAction SilentlyContinue | Where-Object { + $_.ProcessName -match "SearchIndexer|SearchProtocolHost|SearchFilterHost|MsMpEng|NisSrv|avp|avgnt|avast|mcshield|norton|kaspersky|bitdefender|eset|fsecure|gdata|panda|sophos|trendmicro|webroot|malwarebytes" + } + + if ($suspiciousProcesses) { + Write-Host "DEBUG: Found potentially interfering processes:" -ForegroundColor Yellow + foreach ($proc in $suspiciousProcesses) { + Write-Host "DEBUG: - $($proc.ProcessName) (PID: $($proc.Id))" -ForegroundColor Yellow + } + } + + # Force garbage collection to release any PowerShell file handles + Write-Host "DEBUG: Forcing garbage collection to release file handles..." -ForegroundColor Yellow + [System.GC]::Collect() + [System.GC]::WaitForPendingFinalizers() + [System.GC]::Collect() + + # Wait a moment for handles to be released + Start-Sleep -Seconds 3 + + # Try to set the mount directory and its contents to not readonly + Write-Host "DEBUG: Removing readonly attributes from mount directory contents..." -ForegroundColor Yellow + try { + if (Test-Path $MountPath) { + & attrib -R "$MountPath\*" /S /D 2>$null + Write-Host "DEBUG: Readonly attributes removed from mount directory" -ForegroundColor Green + } + } catch { + Write-Host "DEBUG: Could not remove readonly attributes: $($_.Exception.Message)" -ForegroundColor Yellow + } + + # Try to close any remaining file handles using system tools + Write-Host "DEBUG: Attempting to close file handles using system methods..." -ForegroundColor Yellow + + # Use PowerShell to try and close any open file handles + try { + # This is a more aggressive approach - restart the Windows Search service if it's running + $searchService = Get-Service -Name "WSearch" -ErrorAction SilentlyContinue + if ($searchService -and $searchService.Status -eq "Running") { + Write-Host "DEBUG: Temporarily stopping Windows Search service..." -ForegroundColor Yellow + Stop-Service -Name "WSearch" -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 + Write-Host "DEBUG: Restarting Windows Search service..." -ForegroundColor Yellow + Start-Service -Name "WSearch" -ErrorAction SilentlyContinue + } + } catch { + Write-Host "DEBUG: Could not restart Windows Search service: $($_.Exception.Message)" -ForegroundColor Yellow + } + + # Final cleanup attempt + Write-Host "DEBUG: Performing final cleanup..." -ForegroundColor Yellow + [System.GC]::Collect() + [System.GC]::WaitForPendingFinalizers() + [System.GC]::Collect() + + Start-Sleep -Seconds 2 + + Write-Host "DEBUG: Mount directory cleanup completed" -ForegroundColor Green + return $true + + } catch { + Write-Host "DEBUG: Error during mount directory cleanup: $($_.Exception.Message)" -ForegroundColor Red + return $false + } +} diff --git a/functions/microwin/Get-ProcessesUsingPath.ps1 b/functions/microwin/Get-ProcessesUsingPath.ps1 new file mode 100644 index 0000000000..c69a322885 --- /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|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 f55492835c..c4b7f2eb2e 100644 --- a/functions/microwin/Invoke-Microwin.ps1 +++ b/functions/microwin/Invoke-Microwin.ps1 @@ -4,494 +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) + 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 } - # 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 - - $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..." - Export-WindowsImage -SourceImagePath $mountDir\sources\install.esd -SourceIndex $index -DestinationImagePath $mountDir\sources\install.wim -CompressionType "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 - 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 ($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.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 - Copy-Item "$env:temp\unattend.xml" "$($scratchDir)\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 Error code $LASTEXITCODE - 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: $_" - - # Copied here After Installation (Online) - # reg delete "HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler\$_" /f | Out-Null - - # When in Offline Image - 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 $LASTEXITCODE - - 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" - Export-WindowsImage -SourceImagePath "$mountDir\sources\install.wim" -SourceIndex $index -DestinationImagePath "$mountDir\sources\install2.wim" -CompressionType "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." - - # 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" - } catch { - # Could not get error description from Windows APIs - } - } - - Toggle-MicrowinPanel 1 - - #$sync.MicrowinFinalIsoLocation.Text = "$env:temp\microwin.iso" - $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-new.ps1 b/functions/microwin/Invoke-MicrowinGetIso-new.ps1 new file mode 100644 index 0000000000..43cd3fb9a8 --- /dev/null +++ b/functions/microwin/Invoke-MicrowinGetIso-new.ps1 @@ -0,0 +1,28 @@ +function Invoke-MicrowinGetIso { + <# + .DESCRIPTION + 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" + + if($sync.ProcessRunning) { + $msg = "GetIso process is currently running." + [System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning) + return + } + + # 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 + } + + # Start the Get ISO process in a runspace to avoid blocking the UI + Invoke-WPFMicroWinGetIsoRunspace -GetIsoSettings $getIsoSettings +} 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..57f59d62e3 --- /dev/null +++ b/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 @@ -0,0 +1,578 @@ +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 + + Write-Host "DEBUG: About to set initial progress..." + + # Provide immediate feedback to user with progress + try { + Write-Host "DEBUG: Attempting dispatcher invoke..." + $sync.form.Dispatcher.Invoke([action]{ + Write-Host "DEBUG: Inside dispatcher invoke - calling Set-WinUtilTaskbaritem..." + try { + Set-WinUtilTaskbaritem -state "Normal" -value 0.1 -overlay "logo" + Write-Host "DEBUG: Set-WinUtilTaskbaritem completed" + } catch { + Write-Host "DEBUG: Error in Set-WinUtilTaskbaritem: $($_.Exception.Message)" + } + + Write-Host "DEBUG: Skipping Invoke-MicrowinBusyInfo - function appears to be blocking" + # Skip the problematic Invoke-MicrowinBusyInfo call for now + }) + Write-Host "DEBUG: Dispatcher invoke completed successfully" + } catch { + Write-Host "DEBUG: Error in dispatcher invoke: $($_.Exception.Message)" + Write-Host "DEBUG: Continuing without UI updates..." + } + $currentStep = 1 + + Write-Host "DEBUG: Initial progress set, displaying ASCII art..." + + Write-Host " _ __ __ _ " + Write-Host " /\/\ (_) ___ _ __ ___ / / /\ \ \(_) _ __ " + Write-Host " / \ | | / __|| '__| / _ \ \ \/ \/ /| || '_ \ " + Write-Host "/ /\/\ \| || (__ | | | (_) | \ /\ / | || | | | " + Write-Host "\/ \/|_| \___||_| \___/ \/ \/ |_||_| |_| " + + Write-Host "DEBUG: ASCII art displayed, checking file path..." + + $filePath = "" + + if ($GetIsoSettings.isManual) { + Write-Host "DEBUG: Processing manual ISO selection..." + # Use the pre-selected file path from the main thread + $filePath = $GetIsoSettings.filePath + Write-Host "DEBUG: File path from settings: '$filePath'" + + if ([string]::IsNullOrEmpty($filePath)) { + Write-Host "DEBUG: No ISO is chosen - exiting" + 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 + } + + Write-Host "DEBUG: ISO file is valid, updating progress..." + # 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 + }) + Write-Host "DEBUG: Progress updated to step 2" + + } 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 "DEBUG: Checking if file exists..." + Write-Host "File path $($filePath)" + if (-not (Test-Path -Path "$filePath" -PathType Leaf)) { + Write-Host "DEBUG: File doesn't exist - showing error" + $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 + } + Write-Host "DEBUG: File exists, proceeding to system requirements check..." + + $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 + Write-Debug "Size of ISO file: $($isoSize) bytes" + $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)) { + 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 { + Write-Host "DEBUG: Starting file copy operation..." + Write-Host "DEBUG: Source: '$($driveLetter):'" + Write-Host "DEBUG: Destination: '$mountDir'" + Write-Host "DEBUG: Checking if source exists: $(Test-Path "$($driveLetter):")" + Write-Host "DEBUG: Checking if destination exists: $(Test-Path "$mountDir")" + + $totalTime = Measure-Command { + # Use native Copy-Item instead of Copy-Files function + Write-Host "Starting copy operation with robocopy for better performance..." + $robocopyArgs = @( + "$($driveLetter):", + "$mountDir", + "/E", # Copy subdirectories, including empty ones + "/R:3", # Retry 3 times on failed copies + "/W:1", # Wait 1 second between retries + "/MT:8", # Multi-threaded copying with 8 threads + "/XJ" # Exclude junction points + ) + + $robocopyResult = Start-Process -FilePath "robocopy" -ArgumentList $robocopyArgs -Wait -PassThru -NoNewWindow + + # Robocopy exit codes: 0-7 are success, 8+ are errors + if ($robocopyResult.ExitCode -gt 7) { + throw "Robocopy failed with exit code: $($robocopyResult.ExitCode)" + } + + Write-Host "Robocopy completed with exit code: $($robocopyResult.ExitCode)" + + # 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" + Write-Host "DEBUG: File copy operation completed successfully" + } catch { + Write-Host "DEBUG: ERROR during file copy: $($_.Exception.Message)" + Write-Host "DEBUG: Full error details: $_" + Write-Host "DEBUG: Error type: $($_.Exception.GetType().FullName)" + + # Fallback to PowerShell Copy-Item if robocopy fails + Write-Host "DEBUG: Falling back to PowerShell Copy-Item..." + try { + $totalTime = Measure-Command { + Copy-Item -Path "$($driveLetter):*" -Destination "$mountDir" -Recurse -Force + # Force UI update during long operation + $sync.form.Dispatcher.Invoke([action]{ + [System.Windows.Forms.Application]::DoEvents() + }) + } + Write-Host "Fallback copy complete! Total Time: $($totalTime.Minutes) minutes, $($totalTime.Seconds) seconds" + Write-Host "DEBUG: Fallback copy operation completed successfully" + } catch { + Write-Host "DEBUG: ERROR during fallback copy: $($_.Exception.Message)" + 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" + Write-Host "DEBUG: Checking for WIM file at: '$wimFile'" + Write-Host "DEBUG: WIM file exists: $(Test-Path "$wimFile" -PathType Leaf)" + + $esdFile = $wimFile.Replace(".wim", ".esd").Trim() + Write-Host "DEBUG: Checking for ESD file at: '$esdFile'" + Write-Host "DEBUG: ESD file exists: $(Test-Path "$esdFile" -PathType Leaf)" + + 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 + } + + Write-Host "DEBUG: Final WIM file path: '$wimFile'" + Write-Host "DEBUG: Final WIM file exists: $(Test-Path "$wimFile" -PathType Leaf)" + + # Populate the Windows flavors list - must be done on UI thread + $sync.form.Dispatcher.Invoke([action]{ + $sync.MicrowinWindowsFlavors.Items.Clear() + }) + + Write-Host "DEBUG: About to enumerate Windows images..." + try { + $images = Get-WindowsImage -ImagePath $wimFile + Write-Host "DEBUG: Found $($images.Count) Windows images" + + $images | ForEach-Object { + $imageIdx = $_.ImageIndex + $imageName = $_.ImageName + Write-Host "DEBUG: Processing image $imageIdx : $imageName" + $sync.form.Dispatcher.Invoke([action]{ + $sync.MicrowinWindowsFlavors.Items.Add("$imageIdx : $imageName") + }) + } + } catch { + Write-Host "DEBUG: ERROR enumerating Windows images: $($_.Exception.Message)" + 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 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 + }) + } + # 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 "DEBUG: CATCH BLOCK TRIGGERED - Processing error..." + Write-Host "DEBUG: Error message: $($_.Exception.Message)" + Write-Host "DEBUG: Error type: $($_.Exception.GetType().FullName)" + Write-Host "DEBUG: Stack trace: $($_.ScriptStackTrace)" + Write-Host "DEBUG: Full error object: $_" + + Write-Host "Dismounting bad image..." + try { + Get-Volume $driveLetter | Get-DiskImage | Dismount-DiskImage + Write-Host "DEBUG: Image dismounted successfully" + } catch { + Write-Host "DEBUG: Error dismounting image: $($_.Exception.Message)" + } + + try { + if (Test-Path "$scratchDir") { + Remove-Item -Recurse -Force "$($scratchDir)" + Write-Host "DEBUG: Scratch directory '$scratchDir' removed" + } + if (Test-Path "$mountDir") { + Remove-Item -Recurse -Force "$($mountDir)" + Write-Host "DEBUG: Mount directory '$mountDir' removed" + } + } catch { + Write-Host "DEBUG: Error cleaning up directories: $($_.Exception.Message)" + } + + $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..ef64582c8e --- /dev/null +++ b/functions/microwin/Invoke-WPFMicroWinRunspace.ps1 @@ -0,0 +1,1603 @@ +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 + function Set-DismCompatiblePermissions { + param([string]$Path) + + try { + Write-Host "DEBUG: Setting DISM-compatible permissions on: $Path" + $acl = Get-Acl -Path $Path + + # Remove inherited permissions and set explicit ones + # $acl.SetAccessRuleProtection($true, $false) + + # Administrators - Full Control + $adminRule = New-Object System.Security.AccessControl.FileSystemAccessRule( + "Administrators", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" + ) + $acl.SetAccessRule($adminRule) + + # SYSTEM - Full Control + $systemRule = New-Object System.Security.AccessControl.FileSystemAccessRule( + "SYSTEM", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" + ) + $acl.SetAccessRule($systemRule) + + # Current User - Full Control + $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name + $userRule = New-Object System.Security.AccessControl.FileSystemAccessRule( + $currentUser, "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" + ) + $acl.SetAccessRule($userRule) + + # Authenticated Users - Modify (subfolders and files only) + $authUsersRule = New-Object System.Security.AccessControl.FileSystemAccessRule( + "Authenticated Users", "Modify", "ContainerInherit,ObjectInherit", "InheritOnly", "Allow" + ) + $acl.SetAccessRule($authUsersRule) + + # Authenticated Users - Create folders/append data (this folder only) + $authUsersThisFolderRule = New-Object System.Security.AccessControl.FileSystemAccessRule( + "Authenticated Users", "CreateDirectories,AppendData", "None", "None", "Allow" + ) + $acl.SetAccessRule($authUsersThisFolderRule) + + Set-Acl -Path $Path -AclObject $acl + Write-Host "DEBUG: Successfully applied DISM-compatible permissions to: $Path" + return $true + } catch { + Write-Host "DEBUG: Failed to set permissions on $Path`: $($_.Exception.Message)" + return $false + } + } + + $sync.ProcessRunning = $true + + try { + # Set process priority to High for better performance + Write-Host "DEBUG: Setting process priority to High for better performance..." + try { + $currentProcess = Get-Process -Id $PID + $currentProcess.PriorityClass = [System.Diagnostics.ProcessPriorityClass]::High + Write-Host "DEBUG: Process priority set to High successfully" + } catch { + Write-Host "DEBUG: WARNING - Could not set process priority: $($_.Exception.Message)" + } + + # Optimize PowerShell memory usage + Write-Host "DEBUG: Optimizing PowerShell memory settings..." + try { + # Increase the memory limit for PowerShell operations + [System.GC]::Collect() + [System.GC]::WaitForPendingFinalizers() + [System.GC]::Collect() + + # Set execution policy to bypass for better performance + Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process -Force + Write-Host "DEBUG: Memory optimization completed" + } catch { + Write-Host "DEBUG: WARNING - Memory optimization failed: $($_.Exception.Message)" + } + + # 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, + } +} + +public class PrivilegeManager { + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, out LUID lpLuid); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool AdjustTokenPrivileges(IntPtr TokenHandle, bool DisableAllPrivileges, ref TOKEN_PRIVILEGES NewState, uint BufferLength, IntPtr PreviousState, IntPtr ReturnLength); + + [DllImport("kernel32.dll")] + public static extern IntPtr GetCurrentProcess(); + + [DllImport("kernel32.dll")] + public static extern bool CloseHandle(IntPtr hObject); + + public const uint TOKEN_ADJUST_PRIVILEGES = 0x0020; + public const uint SE_PRIVILEGE_ENABLED = 0x00000002; + + [StructLayout(LayoutKind.Sequential)] + public struct LUID { + public uint LowPart; + public int HighPart; + } + + [StructLayout(LayoutKind.Sequential)] + public struct TOKEN_PRIVILEGES { + public uint PrivilegeCount; + public LUID_AND_ATTRIBUTES Privileges; + } + + [StructLayout(LayoutKind.Sequential)] + public struct LUID_AND_ATTRIBUTES { + public LUID Luid; + public uint Attributes; + } + + public static bool EnablePrivilege(string privilegeName) { + IntPtr token = IntPtr.Zero; + try { + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, out token)) { + return false; + } + + LUID luid; + if (!LookupPrivilegeValue(null, privilegeName, out luid)) { + return false; + } + + TOKEN_PRIVILEGES tp = new TOKEN_PRIVILEGES(); + tp.PrivilegeCount = 1; + tp.Privileges.Luid = luid; + tp.Privileges.Attributes = SE_PRIVILEGE_ENABLED; + + return AdjustTokenPrivileges(token, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero); + } finally { + if (token != IntPtr.Zero) { + CloseHandle(token); + } + } + } +} +"@ + + # 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 - this needs to be done on the main thread + $SaveDialog = $null + $sync.form.Dispatcher.Invoke([action]{ + $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 + $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: $($SaveDialog.FileName)" + + # Performance optimization: Determine optimal thread count + $coreCount = (Get-WmiObject -Class Win32_Processor | Measure-Object -Property NumberOfCores -Sum).Sum + $logicalProcessors = (Get-WmiObject -Class Win32_ComputerSystem).NumberOfLogicalProcessors + $optimalThreads = [Math]::Min($logicalProcessors, [Math]::Max(2, $coreCount)) + Write-Host "DEBUG: System has $coreCount cores, $logicalProcessors logical processors. Using $optimalThreads threads for optimal performance." + + # 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..." + Write-Host "DEBUG: Using optimized compression settings for better performance..." + 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" + Write-Host "DEBUG: PowerShell export with Fast compression completed" + } catch { + # Fall back to DISM with optimized settings + Write-Host "DEBUG: PowerShell export failed, using DISM with performance optimizations..." + dism /english /export-image /sourceimagefile="$mountDir\sources\install.esd" /sourceindex=$index /destinationimagefile="$mountDir\sources\install.wim" /compress:fast /checkintegrity /verify + Write-Host "DEBUG: DISM export with Fast compression completed" + } + 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 + Write-Host "DEBUG: Cleaning up any stale DISM mountpoints..." + try { + & dism /cleanup-mountpoints /loglevel:1 + Write-Host "DEBUG: Mountpoints cleanup completed" + Start-Sleep -Seconds 2 + } catch { + Write-Host "DEBUG: Mountpoints cleanup warning: $($_.Exception.Message)" + } + + # Check if running as administrator + $currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) + $isAdmin = $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + + if (-not $isAdmin) { + $msg = "Administrator privileges are required to mount and modify Windows images. Please run WinUtil as Administrator and try again." + Write-Host "DEBUG: ERROR - Not running as administrator" + 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 + Write-Host "DEBUG: Enabling required privileges for DISM operations..." + try { + $requiredPrivileges = @( + "SeBackupPrivilege", + "SeRestorePrivilege", + "SeSecurityPrivilege", + "SeTakeOwnershipPrivilege", + "SeManageVolumePrivilege" + ) + + $privilegesEnabled = 0 + foreach ($privilege in $requiredPrivileges) { + try { + if ([PrivilegeManager]::EnablePrivilege($privilege)) { + Write-Host "DEBUG: Successfully enabled $privilege" + $privilegesEnabled++ + } else { + Write-Host "DEBUG: WARNING - Could not enable $privilege" + } + } catch { + Write-Host "DEBUG: WARNING - Error enabling $privilege : $($_.Exception.Message)" + } + } + + Write-Host "DEBUG: Enabled $privilegesEnabled out of $($requiredPrivileges.Count) privileges" + + if ($privilegesEnabled -ge 2) { + Write-Host "DEBUG: Sufficient privileges enabled for DISM operations" + } else { + Write-Host "DEBUG: WARNING - May not have sufficient privileges for DISM operations" + } + } catch { + Write-Host "DEBUG: WARNING - Could not enable privileges: $($_.Exception.Message)" + } + + # 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 + Write-Host "DEBUG: Write permissions verified for scratch directory" + } catch { + $msg = "Cannot write to scratch directory '$scratchDir'. Please check permissions and ensure the directory is not in use." + Write-Host "DEBUG: ERROR - Cannot write to scratch directory: $($_.Exception.Message)" + 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 "DEBUG: ERROR - install.wim not found at $wimPath" + 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 + Write-Host "DEBUG: WIM file verification successful - found $($wimInfo.Count) image(s)" + } 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 "DEBUG: ERROR - Cannot read WIM file: $($_.Exception.Message)" + 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 { + Write-Host "DEBUG: Checking if image is already mounted..." + # 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) { + Write-Host "DEBUG: Found existing mount at $scratchDir, dismounting..." + Dismount-WindowsImage -Path $scratchDir -Discard + Start-Sleep -Seconds 2 + } + } + } catch { + Write-Host "DEBUG: Error checking mounted images: $($_.Exception.Message)" + } + + # Additional permission checks before mounting + Write-Host "DEBUG: Performing additional permission and system checks..." + + # Check if DISM service is running + try { + $dismService = Get-Service -Name "DISM" -ErrorAction SilentlyContinue + if ($dismService -and $dismService.Status -ne "Running") { + Write-Host "DEBUG: Starting DISM service..." + Start-Service -Name "DISM" + Start-Sleep -Seconds 3 + } + } catch { + Write-Host "DEBUG: Could not manage DISM service: $($_.Exception.Message)" + } + + # Check UAC and token privileges + try { + $tokenPrivs = whoami /priv | Out-String + Write-Host "DEBUG: Re-checking privileges after elevation attempt..." + if ($tokenPrivs -match "SeBackupPrivilege.*Enabled" -and $tokenPrivs -match "SeRestorePrivilege.*Enabled") { + Write-Host "DEBUG: Required privileges (SeBackupPrivilege, SeRestorePrivilege) are now enabled" + } else { + Write-Host "DEBUG: WARNING - Some required privileges may still not be enabled" + + # Try alternative privilege elevation method + Write-Host "DEBUG: Attempting alternative privilege elevation..." + try { + # Try using PowerShell's built-in privilege functions if available + if (Get-Command "Enable-Privilege" -ErrorAction SilentlyContinue) { + Enable-Privilege SeBackupPrivilege, SeRestorePrivilege -Force + Write-Host "DEBUG: Alternative privilege elevation attempted" + } + } catch { + Write-Host "DEBUG: Alternative privilege elevation failed: $($_.Exception.Message)" + } + + Write-Host "DEBUG: Current privilege status:" + $tokenPrivs -split "`n" | Where-Object { $_ -match "Se(Backup|Restore|Security|TakeOwnership|ManageVolume)Privilege" } | ForEach-Object { + Write-Host "DEBUG: $($_.Trim())" + } + } + } catch { + Write-Host "DEBUG: Could not check token privileges: $($_.Exception.Message)" + } + + # Pre-mount system checks + Write-Host "DEBUG: Performing pre-mount system checks..." + + # Check if DISM is available and working + try { + $dismCheck = & dism /? 2>&1 + Write-Host "DEBUG: DISM is available and responding" + } catch { + Write-Host "DEBUG: WARNING - DISM may not be available: $($_.Exception.Message)" + } + + # 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) + Write-Host "DEBUG: Available disk space on $scratchDrive`: $freeSpaceGB GB" + + if ($freeSpaceGB -lt 10) { + Write-Host "DEBUG: WARNING - Low disk space may cause mount issues" + } + } catch { + Write-Host "DEBUG: Could not check disk space: $($_.Exception.Message)" + } + + # Check if scratch directory is accessible and set proper permissions + try { + if (-not (Test-Path $scratchDir)) { + New-Item -Path $scratchDir -ItemType Directory -Force | Out-Null + Write-Host "DEBUG: Created scratch directory: $scratchDir" + } + + # Set proper permissions for DISM operations using the helper function + Write-Host "DEBUG: Setting optimal permissions for scratch directory..." + $permissionsSet = Set-DismCompatiblePermissions -Path $scratchDir + + if ($permissionsSet) { + # Verify permissions were set correctly + $newAcl = Get-Acl -Path $scratchDir + Write-Host "DEBUG: Scratch directory permissions summary:" + foreach ($access in $newAcl.Access) { + Write-Host "DEBUG: $($access.IdentityReference): $($access.FileSystemRights) ($($access.AccessControlType))" + } + } else { + Write-Host "DEBUG: Using default permissions (may cause DISM issues)" + } + + # Test write access + $testFile = Join-Path $scratchDir "test_access.tmp" + "test" | Out-File -FilePath $testFile -Force + Remove-Item $testFile -Force + Write-Host "DEBUG: Scratch directory write access confirmed" + } catch { + Write-Host "DEBUG: ERROR - Cannot access scratch directory: $($_.Exception.Message)" + return + } + + # Additional file permission and location diagnostics + Write-Host "DEBUG: Performing additional permission diagnostics..." + + # Check WIM file permissions and ownership + try { + $wimPath = "$mountDir\sources\install.wim" + $wimAcl = Get-Acl -Path $wimPath + Write-Host "DEBUG: WIM file owner: $($wimAcl.Owner)" + + $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $hasFullControl = $false + foreach ($access in $wimAcl.Access) { + if ($access.IdentityReference.Value -eq $currentUser.Name -or $access.IdentityReference.Value -eq "BUILTIN\Administrators") { + if ($access.FileSystemRights -match "FullControl|Write|Modify") { + $hasFullControl = $true + break + } + } + } + Write-Host "DEBUG: Current user has sufficient WIM file access: $hasFullControl" + } catch { + Write-Host "DEBUG: Could not check WIM file permissions: $($_.Exception.Message)" + } + + # 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." + Write-Host "DEBUG: Attempting mount using DISM command directly for better compatibility..." + + $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 + Write-Host "DEBUG: Checking and removing ReadOnly attributes from WIM files..." -ForegroundColor Yellow + $wimFilePaths = @( + "$mountDir\sources\install.wim", + "$mountDir\sources\boot.wim" + ) + + $criticalWimError = $false + foreach ($wimFilePath in $wimFilePaths) { + if (Test-Path $wimFilePath) { + try { + $wimItem = Get-Item -Path $wimFilePath -Force + if ($wimItem.Attributes -band [System.IO.FileAttributes]::ReadOnly) { + Write-Host "DEBUG: Removing ReadOnly attribute from $wimFilePath" -ForegroundColor Yellow + $wimItem.Attributes = $wimItem.Attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly) + + # Verify the change was successful + $wimItem.Refresh() + if ($wimItem.Attributes -band [System.IO.FileAttributes]::ReadOnly) { + Write-Host "DEBUG: CRITICAL - Failed to remove ReadOnly attribute from $wimFilePath" -ForegroundColor Red + $criticalWimError = $true + } else { + Write-Host "DEBUG: Successfully removed ReadOnly attribute from $wimFilePath" -ForegroundColor Green + } + } else { + Write-Host "DEBUG: $wimFilePath is already writable" -ForegroundColor Green + } + } catch { + Write-Host "DEBUG: CRITICAL - Cannot modify ReadOnly attribute on $wimFilePath - $($_.Exception.Message)" -ForegroundColor Red + $criticalWimError = $true + } + } else { + Write-Host "DEBUG: WIM file not found: $wimFilePath (may be optional)" -ForegroundColor Yellow + } + } + + if ($criticalWimError) { + Write-Host "DEBUG: ABORTING - Cannot proceed with mount due to WIM file ReadOnly issues" -ForegroundColor Red + $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 + Write-Host "DEBUG: Trying mount with $($attempt.Description): $currentScratchDir" + + try { + # Ensure directory exists and is accessible + if (-not (Test-Path $currentScratchDir)) { + New-Item -Path $currentScratchDir -ItemType Directory -Force | Out-Null + Write-Host "DEBUG: Created scratch directory: $currentScratchDir" + + # Apply DISM-compatible permissions to the new directory + $permissionsSet = Set-DismCompatiblePermissions -Path $currentScratchDir + if ($permissionsSet) { + Write-Host "DEBUG: Applied DISM-compatible permissions to: $currentScratchDir" + } + } + + # Try DISM mount + Write-Host "DEBUG: Attempting DISM mount to $currentScratchDir..." + $dismountResult = & dism /english /mount-image /imagefile:"$mountDir\sources\install.wim" /index:$index /mountdir:"$currentScratchDir" /optimize /loglevel:1 + $dismountExitCode = $LASTEXITCODE + + if ($dismountExitCode -eq 0) { + Write-Host "DEBUG: DISM mount successful with $($attempt.Description)" + $mountSuccess = $true + $scratchDir = $currentScratchDir # Update scratch directory for rest of process + break + } else { + Write-Host "DEBUG: DISM mount failed with exit code $dismountExitCode" + Write-Host "DEBUG: DISM output: $dismountResult" + + # Clean up failed attempt + try { + if (Test-Path $currentScratchDir) { + Remove-Item $currentScratchDir -Force -Recurse -ErrorAction SilentlyContinue + } + } catch { + Write-Host "DEBUG: Could not clean up failed mount directory: $($_.Exception.Message)" + } + } + } catch { + Write-Host "DEBUG: Mount attempt failed for $($attempt.Description): $($_.Exception.Message)" + continue + } + } + + # If DISM attempts failed, try PowerShell cmdlet as final fallback + if (-not $mountSuccess) { + Write-Host "DEBUG: All DISM attempts failed, trying PowerShell cmdlet as final fallback..." + + foreach ($attempt in $mountAttempts) { + $currentScratchDir = $attempt.Dir + Write-Host "DEBUG: Trying PowerShell mount with $($attempt.Description): $currentScratchDir" + + try { + if (-not (Test-Path $currentScratchDir)) { + New-Item -Path $currentScratchDir -ItemType Directory -Force | Out-Null + + # Apply DISM-compatible permissions to the new directory + $permissionsSet = Set-DismCompatiblePermissions -Path $currentScratchDir + if ($permissionsSet) { + Write-Host "DEBUG: Applied DISM-compatible permissions to: $currentScratchDir" + } + } + + Mount-WindowsImage -ImagePath "$mountDir\sources\install.wim" -Index $index -Path "$currentScratchDir" -Optimize + $mountSuccess = $true + $scratchDir = $currentScratchDir # Update scratch directory for rest of process + Write-Host "DEBUG: PowerShell cmdlet mount successful with $($attempt.Description)" + break + } catch { + Write-Host "DEBUG: PowerShell cmdlet mount failed for $($attempt.Description): $($_.Exception.Message)" + continue + } + } + } + + # If all mount attempts failed, try readonly mount as last resort + if (-not $mountSuccess) { + Write-Host "DEBUG: All read/write mount attempts failed. Trying readonly mount as diagnostic step..." + Write-Host "DEBUG: NOTE: Readonly mount will not allow modifications, but helps identify permission issues" + + try { + $readonlyScratchDir = "C:\temp\MicrowinReadOnly_$(Get-Date -Format 'yyyyMMdd_HHmmss')" + if (-not (Test-Path $readonlyScratchDir)) { + New-Item -Path $readonlyScratchDir -ItemType Directory -Force | Out-Null + } + + $dismountResult = & dism /english /mount-image /imagefile:"$mountDir\sources\install.wim" /index:$index /mountdir:"$readonlyScratchDir" /readonly /loglevel:1 + $dismountExitCode = $LASTEXITCODE + + if ($dismountExitCode -eq 0) { + Write-Host "DEBUG: READONLY mount successful - this confirms the WIM file is accessible" + Write-Host "DEBUG: The issue is specifically with read/write permissions, not basic access" + + # Clean up readonly mount + & dism /english /unmount-image /mountdir:"$readonlyScratchDir" /discard /loglevel:1 + Remove-Item $readonlyScratchDir -Force -Recurse -ErrorAction SilentlyContinue + + Write-Host "DEBUG: This suggests one of the following issues:" + Write-Host "DEBUG: 1. Antivirus software blocking write access to WIM files" + Write-Host "DEBUG: 2. Windows Defender real-time protection interfering" + Write-Host "DEBUG: 3. Corporate/Group Policy restrictions on DISM operations" + Write-Host "DEBUG: 4. File system permissions on temp directories" + Write-Host "DEBUG: 5. UAC virtualization affecting file access" + } else { + Write-Host "DEBUG: Even READONLY mount failed - this indicates a deeper system issue" + Write-Host "DEBUG: Readonly mount output: $dismountResult" + } + } catch { + Write-Host "DEBUG: Readonly mount test failed: $($_.Exception.Message)" + } + } + if (-not $mountSuccess) { + try { + $mountedImages = Get-WindowsImage -Mounted + foreach ($mounted in $mountedImages) { + if ($mounted.Path -eq $scratchDir) { + $mountSuccess = $true + Write-Host "DEBUG: Mount verification successful via Get-WindowsImage" + break + } + } + } catch { + Write-Host "DEBUG: Error verifying mount status: $($_.Exception.Message)" + } + + # Additional verification by checking if typical Windows directories exist + if (-not $mountSuccess) { + if ((Test-Path "$scratchDir\Windows") -and (Test-Path "$scratchDir\Windows\System32")) { + Write-Host "DEBUG: Mount verification successful via directory structure check" + $mountSuccess = $true + } + } + } + + if ($mountSuccess) { + Write-Host "The Windows image has been mounted successfully. Continuing processing..." + Write-Host "DEBUG: Mount verification successful" + } 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) + Write-Host "DEBUG: - Running as Administrator: $isAdmin" + Write-Host "DEBUG: - Current User: $($currentUser.Name)" + Write-Host "DEBUG: - Process Architecture: $([System.Environment]::Is64BitProcess)" + Write-Host "DEBUG: - OS Architecture: $([System.Environment]::Is64BitOperatingSystem)" + + # Show privilege status again + $tokenPrivs = whoami /priv | Out-String + Write-Host "DEBUG: - Current Privileges:" + $tokenPrivs -split "`n" | Where-Object { $_ -match "Se(Backup|Restore|Security|TakeOwnership|ManageVolume)Privilege" } | ForEach-Object { + Write-Host "DEBUG: $($_.Trim())" + } + } catch { + Write-Host "DEBUG: Could not determine system state: $($_.Exception.Message)" + } + + Write-Host "" + Write-Host "IMMEDIATE STEPS TO TRY:" + Write-Host "DEBUG: 1. **DISABLE WINDOWS DEFENDER REAL-TIME PROTECTION** (most common cause)" + Write-Host "DEBUG: - Open Windows Security > Virus & threat protection" + Write-Host "DEBUG: - Turn off Real-time protection temporarily" + Write-Host "DEBUG: 2. **DISABLE OTHER ANTIVIRUS SOFTWARE** (if present)" + Write-Host "DEBUG: 3. **TRY DIFFERENT TEMP DIRECTORY** (this script will attempt automatically)" + Write-Host "DEBUG: 4. **RUN FROM COMMAND PROMPT AS ADMIN** instead of PowerShell:" + Write-Host "DEBUG: - Open Command Prompt as Administrator" + Write-Host "DEBUG: - Navigate to: $($PWD.Path)" + Write-Host "DEBUG: - Run: powershell -ExecutionPolicy Bypass -File winutil.ps1" + Write-Host "DEBUG: 5. **CHECK DISM LOG** at: C:\Windows\Logs\DISM\dism.log" + Write-Host "DEBUG: 6. **RUN DISM CLEANUP**: dism /cleanup-mountpoints" + 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 "DEBUG: 1. **CHECK GROUP POLICY RESTRICTIONS**:" + Write-Host "DEBUG: - Run: gpedit.msc" + Write-Host "DEBUG: - Navigate to: Computer Configuration > Administrative Templates > System > Device Installation" + Write-Host "DEBUG: - Look for any DISM or imaging restrictions" + Write-Host "DEBUG: 2. **CHECK REGISTRY PERMISSIONS**:" + Write-Host "DEBUG: - Ensure HKLM\SOFTWARE\Microsoft\WIMMount is accessible" + Write-Host "DEBUG: 3. **VERIFY DISM SERVICE STATUS**:" + Write-Host "DEBUG: - Run: sc query TrustedInstaller" + Write-Host "DEBUG: - Run: sc query CryptSvc" + Write-Host "DEBUG: 4. **CHECK WINDOWS FEATURES**:" + Write-Host "DEBUG: - Run: dism /online /get-features | findstr DISM" + Write-Host "DEBUG: 5. **TEST WITH DIFFERENT WIM FILE**:" + Write-Host "DEBUG: - Try mounting a different WIM file to isolate the issue" + Write-Host "" + + Write-Host "CORPORATE/MANAGED SYSTEM CONSIDERATIONS:" + Write-Host "DEBUG: - If this is a corporate/managed system, IT policies may restrict DISM" + Write-Host "DEBUG: - Contact your system administrator about DISM operation restrictions" + Write-Host "DEBUG: - Some enterprise antivirus solutions block WIM modifications" + Write-Host "DEBUG: - Windows 10/11 in S Mode has restrictions on DISM operations" + 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..." + Write-Host "DEBUG: Using optimized DISM settings for driver operations..." + 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 /loglevel:1 | 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 "DEBUG: ========== STARTING FEATURES REMOVAL ==========" + Write-Host "Remove Features from the image" + try { + Microwin-RemoveFeatures -UseCmdlets $true + Write-Host "DEBUG: Features removal completed successfully" + } catch { + Write-Host "DEBUG: ERROR during features removal: $($_.Exception.Message)" + Write-Host "DEBUG: Continuing with next step..." + } + Write-Host "Removing features complete!" + + Write-Host "DEBUG: ========== STARTING PACKAGES REMOVAL ==========" + Write-Host "Removing OS packages" + try { + Microwin-RemovePackages -UseCmdlets $true + Write-Host "DEBUG: Packages removal completed successfully" + } catch { + Write-Host "DEBUG: ERROR during packages removal: $($_.Exception.Message)" + Write-Host "DEBUG: Continuing with next step..." + } + + Write-Host "DEBUG: ========== STARTING APPX BLOAT REMOVAL ==========" + Write-Host "Removing Appx Bloat" + try { + Microwin-RemoveProvisionedPackages -UseCmdlets $true + Write-Host "DEBUG: Appx bloat removal completed successfully" + } catch { + Write-Host "DEBUG: ERROR during Appx bloat removal: $($_.Exception.Message)" + Write-Host "DEBUG: Continuing with next step..." + } + + # 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" + } + } + + Write-Host "DEBUG: ========== STARTING FILE/DIRECTORY CLEANUP ==========" + try { + Write-Host "DEBUG: Removing RtBackup directory..." + Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\System32\LogFiles\WMI\RtBackup" -Directory + Write-Host "DEBUG: Removing DiagTrack directory..." + Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\DiagTrack" -Directory + Write-Host "DEBUG: Removing InboxApps directory..." + Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\InboxApps" -Directory + Write-Host "DEBUG: Removing LocationNotificationWindows.exe..." + Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\System32\LocationNotificationWindows.exe" + Write-Host "DEBUG: Removing Windows Media Player directories..." + Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files (x86)\Windows Media Player" -Directory + Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files\Windows Media Player" -Directory + Write-Host "DEBUG: Removing Windows Mail directories..." + Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files (x86)\Windows Mail" -Directory + Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files\Windows Mail" -Directory + Write-Host "DEBUG: Removing Internet Explorer directories..." + Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files (x86)\Internet Explorer" -Directory + Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files\Internet Explorer" -Directory + Write-Host "DEBUG: Removing gaming and OneDrive components..." + Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\GameBarPresenceWriter" + Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\System32\OneDriveSetup.exe" + Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\System32\OneDrive.ico" + Write-Host "DEBUG: Removing system apps..." + Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\SystemApps" -mask "*narratorquickstart*" -Directory + Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\SystemApps" -mask "*ParentalControls*" -Directory + Write-Host "DEBUG: File/directory cleanup completed successfully" + } catch { + Write-Host "DEBUG: ERROR during file/directory cleanup: $($_.Exception.Message)" + Write-Host "DEBUG: Continuing with next step..." + } + Write-Host "Removal complete!" + + Write-Host "DEBUG: ========== STARTING UNATTEND.XML CREATION ==========" + 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 "DEBUG: ========== COPYING UNATTEND.XML INTO ISO ==========" + 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 + Write-Host "DEBUG: unattend.xml copied successfully" + } catch { + Write-Host "DEBUG: ERROR copying unattend.xml: $($_.Exception.Message)" + } + Write-Host "Done Copy unattend.xml" + + Write-Host "DEBUG: ========== CREATING FIRSTRUN SCRIPT ==========" + Write-Host "Create FirstRun" + try { + Microwin-NewFirstRun + Write-Host "DEBUG: FirstRun script created successfully" + } catch { + Write-Host "DEBUG: ERROR creating FirstRun: $($_.Exception.Message)" + } + Write-Host "Done create FirstRun" + + Write-Host "DEBUG: ========== COPYING FIRSTRUN INTO ISO ==========" + Write-Host "Copy FirstRun.ps1 into the ISO" + try { + Copy-Item "$env:temp\FirstStartup.ps1" "$($scratchDir)\Windows\FirstStartup.ps1" -force + Write-Host "DEBUG: FirstRun.ps1 copied successfully" + } catch { + Write-Host "DEBUG: ERROR copying FirstRun.ps1: $($_.Exception.Message)" + } + Write-Host "Done copy FirstRun.ps1" + + Write-Host "DEBUG: ========== SETTING UP DESKTOP AND LINKS ==========" + 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" + Write-Host "DEBUG: Desktop setup completed successfully" + } catch { + Write-Host "DEBUG: ERROR setting up desktop: $($_.Exception.Message)" + } + + Write-Host "DEBUG: ========== CREATING CHECKINSTALL SCRIPT ==========" + Write-Host "Copy checkinstall.cmd into the ISO" + try { + Microwin-NewCheckInstall + Copy-Item "$env:temp\checkinstall.cmd" "$($scratchDir)\Windows\checkinstall.cmd" -force + Write-Host "DEBUG: checkinstall.cmd created and copied successfully" + } catch { + Write-Host "DEBUG: ERROR with checkinstall.cmd: $($_.Exception.Message)" + } + Write-Host "Done copy checkinstall.cmd" + + Write-Host "DEBUG: ========== CREATING BYPASSNRO DIRECTORY ==========" + Write-Host "Creating a directory that allows to bypass Wifi setup" + try { + New-Item -ItemType Directory -Force -Path "$($scratchDir)\Windows\System32\OOBE\BYPASSNRO" + Write-Host "DEBUG: BYPASSNRO directory created successfully" + } catch { + Write-Host "DEBUG: ERROR creating BYPASSNRO directory: $($_.Exception.Message)" + } + + Write-Host "DEBUG: ========== LOADING REGISTRY ==========" + Write-Host "Loading registry" + try { + Write-Host "DEBUG: Loading COMPONENTS registry..." + reg load HKLM\zCOMPONENTS "$($scratchDir)\Windows\System32\config\COMPONENTS" + Write-Host "DEBUG: Loading DEFAULT registry..." + reg load HKLM\zDEFAULT "$($scratchDir)\Windows\System32\config\default" + Write-Host "DEBUG: Loading NTUSER registry..." + reg load HKLM\zNTUSER "$($scratchDir)\Users\Default\ntuser.dat" + Write-Host "DEBUG: Loading SOFTWARE registry..." + reg load HKLM\zSOFTWARE "$($scratchDir)\Windows\System32\config\SOFTWARE" + Write-Host "DEBUG: Loading SYSTEM registry..." + reg load HKLM\zSYSTEM "$($scratchDir)\Windows\System32\config\SYSTEM" + Write-Host "DEBUG: All registry hives loaded successfully" + } catch { + Write-Host "DEBUG: ERROR loading registry: $($_.Exception.Message)" + } + + Write-Host "DEBUG: ========== APPLYING REGISTRY TWEAKS ==========" + 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 + Write-Host "DEBUG: Teams disabled successfully" + } catch { + Write-Host "DEBUG: ERROR disabling Teams: $($_.Exception.Message)" + } + Write-Host "Done disabling Teams" + + Write-Host "DEBUG: Fixing Windows Volume Mixer Issue..." + try { + reg add "HKLM\zNTUSER\Software\Microsoft\Internet Explorer\LowRegistry\Audio\PolicyConfig\PropertyStore" /f + Write-Host "DEBUG: Volume Mixer fix applied successfully" + } catch { + Write-Host "DEBUG: ERROR applying Volume Mixer fix: $($_.Exception.Message)" + } + Write-Host "Fix Windows Volume Mixer Issue" + + Write-Host "DEBUG: Bypassing system requirements..." + 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 + Write-Host "DEBUG: System requirements bypass applied successfully" + } catch { + Write-Host "DEBUG: ERROR bypassing system requirements: $($_.Exception.Message)" + } + 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: $_" + Write-Host "DEBUG: ERROR in main processing: $($_.Exception.Message)" + Write-Host "DEBUG: Stack trace: $($_.ScriptStackTrace)" + } finally { + Write-Host "DEBUG: ========== STARTING CLEANUP PROCESS ==========" + Write-Host "Unmounting Registry..." + try { + Write-Host "DEBUG: Unloading registry hives..." + reg unload HKLM\zCOMPONENTS + reg unload HKLM\zDEFAULT + reg unload HKLM\zNTUSER + reg unload HKLM\zSOFTWARE + reg unload HKLM\zSYSTEM + Write-Host "DEBUG: Registry unloaded successfully" + } catch { + Write-Host "DEBUG: ERROR unloading registry: $($_.Exception.Message)" + } + + Write-Host "DEBUG: ========== CLEANING UP IMAGE ==========" + Write-Host "Cleaning up image with optimized settings..." + try { + # Use optimized DISM cleanup settings for better performance + Write-Host "DEBUG: Running component cleanup with optimized settings..." + dism /English /image:$scratchDir /Cleanup-Image /StartComponentCleanup /ResetBase /loglevel:1 + Write-Host "DEBUG: Image cleanup completed successfully" + } catch { + Write-Host "DEBUG: ERROR during image cleanup: $($_.Exception.Message)" + } + Write-Host "Cleanup complete." + + Write-Host "DEBUG: ========== UNMOUNTING IMAGE ==========" + Write-Host "Unmounting image..." + + # First, try to clean up any processes or handles that might interfere with unmounting + Write-Host "DEBUG: Performing pre-unmount cleanup..." -ForegroundColor Yellow + 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) { + Write-Host "DEBUG: Found potentially interfering processes, waiting for them to finish..." -ForegroundColor Yellow + Start-Sleep -Seconds 5 + } + + Write-Host "DEBUG: Pre-unmount cleanup completed" -ForegroundColor Green + } catch { + Write-Host "DEBUG: Pre-unmount cleanup failed: $($_.Exception.Message)" -ForegroundColor Yellow + } + + $dismountSuccess = $false + $maxRetries = 3 + + for ($retry = 1; $retry -le $maxRetries; $retry++) { + try { + Write-Host "DEBUG: Dismount attempt $retry of $maxRetries..." + + if ($retry -eq 1) { + # First attempt: Try DISM command directly + Write-Host "DEBUG: Using DISM command for dismount..." + $dismResult = & dism /english /unmount-image /mountdir:"$scratchDir" /commit /loglevel:1 + $dismExitCode = $LASTEXITCODE + + if ($dismExitCode -eq 0) { + Write-Host "DEBUG: DISM dismount successful" + $dismountSuccess = $true + break + } else { + Write-Host "DEBUG: DISM dismount failed with exit code $dismExitCode" + Write-Host "DEBUG: DISM output: $dismResult" + } + } elseif ($retry -eq 2) { + # Second attempt: Try PowerShell cmdlet + Write-Host "DEBUG: Using PowerShell cmdlet for dismount..." + Dismount-WindowsImage -Path "$scratchDir" -Save + } else { + # Third attempt: Try PowerShell cmdlet with CheckIntegrity + Write-Host "DEBUG: Using 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) { + Write-Host "DEBUG: PowerShell cmdlet dismount successful on attempt $retry" + $dismountSuccess = $true + break + } else { + Write-Host "DEBUG: Image still appears to be mounted after attempt $retry" + } + } + + } catch { + Write-Host "DEBUG: ERROR on dismount attempt $retry`: $($_.Exception.Message)" + } + + # If this isn't the last retry, wait before trying again + if ($retry -lt $maxRetries -and -not $dismountSuccess) { + Write-Host "DEBUG: Waiting 5 seconds before next retry..." + 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) { + Write-Host "DEBUG: All normal dismount attempts failed - trying aggressive cleanup..." -ForegroundColor Red + + # Aggressive cleanup before final attempts + try { + Write-Host "DEBUG: Performing aggressive file handle cleanup..." -ForegroundColor Yellow + + # 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 + + Write-Host "DEBUG: Aggressive cleanup completed" -ForegroundColor Green + } catch { + Write-Host "DEBUG: Aggressive cleanup failed: $($_.Exception.Message)" -ForegroundColor Yellow + } + + # Last attempt - try multiple fallback strategies + Write-Host "DEBUG: Final attempt - trying multiple fallback strategies..." + + # Try DISM discard + try { + Write-Host "DEBUG: Trying DISM discard..." + $dismResult = & dism /english /unmount-image /mountdir:"$scratchDir" /discard /loglevel:1 + if ($LASTEXITCODE -eq 0) { + Write-Host "DEBUG: DISM discard successful" + $dismountSuccess = $true + } else { + Write-Host "DEBUG: DISM discard failed with exit code $LASTEXITCODE" + } + } catch { + Write-Host "DEBUG: DISM discard failed: $($_.Exception.Message)" + } + + # Try PowerShell discard if DISM failed + if (-not $dismountSuccess) { + try { + Write-Host "DEBUG: Trying PowerShell discard..." + Dismount-WindowsImage -Path "$scratchDir" -Discard + Write-Host "DEBUG: PowerShell discard completed (changes discarded)" + $dismountSuccess = $true + } catch { + Write-Host "DEBUG: PowerShell discard failed: $($_.Exception.Message)" + } + } + + # Final fallback: cleanup mountpoints + if (-not $dismountSuccess) { + try { + Write-Host "DEBUG: Trying DISM cleanup-mountpoints..." + & dism /cleanup-mountpoints + Start-Sleep -Seconds 3 + Write-Host "DEBUG: Mountpoints cleanup completed" + } catch { + Write-Host "DEBUG: Cleanup mountpoints failed: $($_.Exception.Message)" + } + + Write-Host "DEBUG: CRITICAL ERROR - Could not dismount image with any method" + Write-Host "DEBUG: Manual cleanup commands:" + Write-Host "DEBUG: 1. dism /unmount-image /mountdir:`"$scratchDir`" /discard" + Write-Host "DEBUG: 2. dism /cleanup-mountpoints" + Write-Host "DEBUG: 3. Remove-Item -Recurse -Force `"$scratchDir`"" + } + } + + 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 "DEBUG: ========== EXPORTING INSTALL IMAGE ==========" + Write-Host "Exporting image into $mountDir\sources\install2.wim with optimized settings..." + try { + Write-Host "DEBUG: Trying PowerShell Export-WindowsImage with optimized compression for speed..." + # Use Fast compression for better performance, especially during development/testing + # Users can change this to "Max" if they prefer smaller file size over speed + Export-WindowsImage -SourceImagePath "$mountDir\sources\install.wim" -SourceIndex $index -DestinationImagePath "$mountDir\sources\install2.wim" -CompressionType "Fast" + Write-Host "DEBUG: PowerShell export with Fast compression completed successfully" + } catch { + # Fall back to DISM with optimized settings + Write-Host "DEBUG: PowerShell export failed, falling back to DISM with optimized settings..." + Write-Host "DEBUG: Error was: $($_.Exception.Message)" + dism /english /export-image /sourceimagefile="$mountDir\sources\install.wim" /sourceindex=$index /destinationimagefile="$mountDir\sources\install2.wim" /compress:fast /checkintegrity /verify /loglevel:1 + Write-Host "DEBUG: DISM export with optimized settings completed" + } + + Write-Host "DEBUG: ========== REPLACING INSTALL.WIM ==========" + 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" + Write-Host "DEBUG: install.wim replaced successfully" + } catch { + Write-Host "DEBUG: ERROR replacing install.wim: $($_.Exception.Message)" + 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 { + Write-Host "DEBUG: Using PowerShell Export with Recovery compression..." + 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 { + Write-Host "DEBUG: PowerShell ESD export failed, falling back to DISM with optimized settings..." + 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: $_" + Write-Host "DEBUG: ERROR during image export/processing: $($_.Exception.Message)" + 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`" `"$($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 + $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 = "$($SaveDialog.FileName)" + }) + + # 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 { + Write-Host "DEBUG: ========== PERFORMANCE OPTIMIZATIONS APPLIED ==========" + Write-Host "DEBUG: - Process priority set to High" + Write-Host "DEBUG: - Memory optimization with garbage collection" + Write-Host "DEBUG: - Multi-core processing support detected ($optimalThreads threads available)" + Write-Host "DEBUG: - Fast compression used instead of Max for better performance" + Write-Host "DEBUG: - Optimized DISM settings with reduced logging" + Write-Host "DEBUG: - Enhanced error handling and retry mechanisms" + Write-Host "DEBUG: - Streamlined registry and cleanup operations" + Write-Host "DEBUG: Performance optimizations complete. Process should be significantly faster." + + # Reset process priority to normal + try { + $currentProcess = Get-Process -Id $PID + $currentProcess.PriorityClass = [System.Diagnostics.ProcessPriorityClass]::Normal + Write-Host "DEBUG: Process priority reset to Normal" + } catch { + Write-Host "DEBUG: Could not reset process priority: $($_.Exception.Message)" + } + + $sync.ProcessRunning = $false + } + } +} diff --git a/functions/microwin/Set-ScratchFolderPermissions.ps1 b/functions/microwin/Set-ScratchFolderPermissions.ps1 new file mode 100644 index 0000000000..8c41a41aeb --- /dev/null +++ b/functions/microwin/Set-ScratchFolderPermissions.ps1 @@ -0,0 +1,244 @@ +function Set-ScratchFolderPermissions { + <# + .SYNOPSIS + Sets DISM-compatible permissions on any directory + + .DESCRIPTION + This function sets proper permissions on a directory to make it compatible with DISM operations + + .PARAMETER Path + The path to the directory to set permissions on + + .PARAMETER ShowPermissions + Switch to display the current permissions after setting them + #> + param( + [Parameter(Mandatory = $true)] + [string]$Path, + + [switch]$ShowPermissions + ) + +function Set-DismCompatiblePermissions { + param([string]$Path) + + # Log which user is setting permissions and which folder + $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name + Write-Host "Current user setting permissions: $currentUser for folder: $Path" -ForegroundColor Magenta + + # Remove ReadOnly attribute from install.wim and boot.wim in the scratch folder + $wimFiles = @('install.wim', 'boot.wim') + Write-Host "Removing ReadOnly attributes from WIM files before mount..." -ForegroundColor Yellow + $wimFilesProcessed = 0 + $wimFilesFound = 0 + + foreach ($wim in $wimFiles) { + $wimPath = Join-Path $Path $wim + if (Test-Path $wimPath) { + $wimFilesFound++ + $item = Get-Item -Path $wimPath -Force + if ($item.Attributes -band [System.IO.FileAttributes]::ReadOnly) { + try { + $item.Attributes = $item.Attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly) + # Verify the change was successful + $item.Refresh() + if ($item.Attributes -band [System.IO.FileAttributes]::ReadOnly) { + Write-Host "CRITICAL: Failed to remove ReadOnly attribute from $wimPath - mount will fail" -ForegroundColor Red + Write-Host "Cannot proceed with mount operation. Exiting." -ForegroundColor Red + return $false + } + Write-Host "Removed ReadOnly attribute from $wimPath" -ForegroundColor Green + $wimFilesProcessed++ + } catch { + Write-Host "CRITICAL: Unable to modify ReadOnly attribute on $wimPath - $($_.Exception.Message)" -ForegroundColor Red + Write-Host "Cannot proceed with mount operation. Exiting." -ForegroundColor Red + return $false + } + } else { + Write-Host "$wimPath is already writable" -ForegroundColor Yellow + $wimFilesProcessed++ + } + Write-Host ("Current attributes for {0}: {1}" -f $wimPath, $item.Attributes) -ForegroundColor Cyan + } else { + Write-Host "$wimPath not found in scratch folder" -ForegroundColor Red + } + } + + # If we found WIM files but couldn't process them all, exit + if ($wimFilesFound -gt 0 -and $wimFilesProcessed -ne $wimFilesFound) { + Write-Host "CRITICAL: Could not make all WIM files writable. Mount operation aborted." -ForegroundColor Red + return $false + } + + if ($wimFilesFound -eq 0) { + Write-Host "No WIM files found in scratch folder - proceeding with folder permissions only" -ForegroundColor Yellow + } else { + Write-Host "Successfully processed $wimFilesProcessed WIM file(s) - ready for mount" -ForegroundColor Green + } + try { + Write-Host "Setting DISM-compatible permissions on: $Path" -ForegroundColor Green + + if (-not (Test-Path $Path)) { + New-Item -Path $Path -ItemType Directory -Force | Out-Null + Write-Host "Created directory: $Path" -ForegroundColor Yellow + } + + # Remove read-only attribute from the directory and all subdirectories + try { + Write-Host "Removing read-only attributes from directory and contents..." -ForegroundColor Yellow + + # Remove read-only from the main directory + $item = Get-Item -Path $Path -Force + if ($item.Attributes -band [System.IO.FileAttributes]::ReadOnly) { + $item.Attributes = $item.Attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly) + Write-Host "Removed read-only attribute from directory: $Path" -ForegroundColor Yellow + } + + # Use attrib command to remove read-only from folder and all contents recursively + # This is more reliable than PowerShell for folder attributes + try { + & attrib -R "$Path" /S /D 2>$null + Write-Host "Successfully removed read-only attributes using attrib command" -ForegroundColor Green + } catch { + Write-Host "Warning: attrib command failed, using PowerShell fallback" -ForegroundColor Yellow + + # PowerShell fallback - remove read-only from any existing subdirectories and files + Get-ChildItem -Path $Path -Recurse -Force -ErrorAction SilentlyContinue | ForEach-Object { + if ($_.Attributes -band [System.IO.FileAttributes]::ReadOnly) { + $_.Attributes = $_.Attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly) + } + } + } + } catch { + Write-Host "Warning: Could not modify read-only attributes: $($_.Exception.Message)" -ForegroundColor Yellow + } $acl = Get-Acl -Path $Path + + # Remove inherited permissions and set explicit ones + $acl.SetAccessRuleProtection($true, $false) + + # Clear all existing access rules by creating a new ACL with only essential rules + # This is more reliable than trying to remove individual rules + try { + # Get the current owner + $owner = $acl.Owner + + # Create a fresh ACL object + $newAcl = New-Object System.Security.AccessControl.DirectorySecurity + $newAcl.SetOwner($acl.Owner) + $newAcl.SetAccessRuleProtection($true, $false) + + # Use the fresh ACL instead of trying to modify the existing one + $acl = $newAcl + Write-Host "Created fresh ACL for directory" -ForegroundColor Green + } catch { + Write-Host "Warning: Could not create fresh ACL, attempting manual rule removal" -ForegroundColor Yellow + + # Fallback: Try to remove rules one by one with better error handling + $accessRules = @($acl.Access) # Create array copy to avoid collection modification issues + foreach ($rule in $accessRules) { + if ($rule -ne $null -and $rule.GetType().Name -eq "FileSystemAccessRule") { + try { + $acl.RemoveAccessRule($rule) | Out-Null + } catch { + # Ignore individual rule removal failures + Write-Host "Skipped removing rule for: $($rule.IdentityReference)" -ForegroundColor Gray + } + } + } + } + + # Administrators - Full Control + $adminRule = New-Object System.Security.AccessControl.FileSystemAccessRule( + "Administrators", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" + ) + $acl.SetAccessRule($adminRule) + + # SYSTEM - Full Control + $systemRule = New-Object System.Security.AccessControl.FileSystemAccessRule( + "SYSTEM", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" + ) + $acl.SetAccessRule($systemRule) + + # Current User - Full Control + $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name + Write-Host "Setting Full Control permissions for current user: $currentUser" -ForegroundColor Cyan + $userRule = New-Object System.Security.AccessControl.FileSystemAccessRule( + $currentUser, "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" + ) + $acl.SetAccessRule($userRule) + + # Current User - Full Control + $everyoneRule = New-Object System.Security.AccessControl.FileSystemAccessRule( + "Everyone", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" + ) + $acl.SetAccessRule($everyoneRule) + + # Authenticated Users - Modify (subfolders and files only) + $authUsersRule = New-Object System.Security.AccessControl.FileSystemAccessRule( + "Authenticated Users", "Modify", "ContainerInherit,ObjectInherit", "InheritOnly", "Allow" + ) + $acl.SetAccessRule($authUsersRule) + + # Authenticated Users - Create folders/append data (this folder only) + $authUsersThisFolderRule = New-Object System.Security.AccessControl.FileSystemAccessRule( + "Authenticated Users", "CreateDirectories,AppendData", "None", "None", "Allow" + ) + $acl.SetAccessRule($authUsersThisFolderRule) + + Set-Acl -Path $Path -AclObject $acl + Write-Host "Successfully applied DISM-compatible permissions to: $Path" -ForegroundColor Green + return $true + } catch { + Write-Host "Failed to set permissions on $Path`: $($_.Exception.Message)" -ForegroundColor Red + return $false + } +} + +# Main execution +Write-Host "=== DISM-Compatible Permission Setter ===" -ForegroundColor Cyan +Write-Host "" + +# Show who is running the commands +$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name +$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator") +Write-Host "Running as user: $currentUser" -ForegroundColor Yellow +if ($isAdmin) { + Write-Host "Administrator privileges: Yes" -ForegroundColor Green +} else { + Write-Host "Administrator privileges: No" -ForegroundColor Red +} +Write-Host "" + +# Apply permissions +$success = Set-DismCompatiblePermissions -Path $Path + +if ($success -and $ShowPermissions) { + Write-Host "" + Write-Host "Current permissions on $Path`:" -ForegroundColor Cyan + $acl = Get-Acl -Path $Path + foreach ($access in $acl.Access) { + $color = switch ($access.AccessControlType) { + "Allow" { "Green" } + "Deny" { "Red" } + default { "White" } + } + Write-Host " $($access.IdentityReference): $($access.FileSystemRights) ($($access.AccessControlType))" -ForegroundColor $color + } +} + +Write-Host "" +if ($success) { + Write-Host "✅ Permissions applied successfully!" -ForegroundColor Green + Write-Host "" + Write-Host "This directory now has the same permissions as a working DISM scratch folder:" -ForegroundColor Cyan + Write-Host "• Administrators: Full control" -ForegroundColor White + Write-Host "• SYSTEM: Full control" -ForegroundColor White + Write-Host "• Current User ($([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)): Full control" -ForegroundColor White + Write-Host "• Authenticated Users: Modify (subfolders and files only)" -ForegroundColor White + Write-Host "• Authenticated Users: Create folders/append data (this folder only)" -ForegroundColor White +} else { + Write-Host "❌ Failed to apply permissions. Check the error messages above." -ForegroundColor Red +} + + return $success +} diff --git a/functions/microwin/Set-WimFilesWritable.ps1 b/functions/microwin/Set-WimFilesWritable.ps1 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/functions/private/Copy-Files.ps1 b/functions/private/Copy-Files.ps1 index cec7869a26..f9a36ed259 100644 --- a/functions/private/Copy-Files.ps1 +++ b/functions/private/Copy-Files.ps1 @@ -37,11 +37,38 @@ function Copy-Files { New-Item ($destination+$restpath) -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 + try { + Copy-Item $file.FullName ($destination+$restpath) -ErrorAction Stop -Force:$force + + # Use more robust method to remove ReadOnly attribute + $copiedFile = Get-Item -Path ($destination+$restpath) -Force -ErrorAction SilentlyContinue + if ($copiedFile -and ($copiedFile.Attributes -band [System.IO.FileAttributes]::ReadOnly)) { + $copiedFile.Attributes = $copiedFile.Attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly) + } + + # 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, ($destination+$restpath), $force) + # Remove ReadOnly using .NET method + [System.IO.File]::SetAttributes(($destination+$restpath), [System.IO.File]::GetAttributes(($destination+$restpath)) -band (-bnot [System.IO.FileAttributes]::ReadOnly)) + } 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 From 761723390c41c95ac9a9e90a43cf41e15047b596 Mon Sep 17 00:00:00 2001 From: Real-MullaC Date: Tue, 5 Aug 2025 20:57:33 +0100 Subject: [PATCH 02/10] Remove rubbish --- MICROWIN_IMPROVEMENTS.md | 74 --- .../microwin/Force-CleanupMountDirectory.ps1 | 92 +-- .../microwin/Invoke-MicrowinGetIso-new.ps1 | 28 - .../Invoke-WPFMicroWinGetIsoRunspace.ps1 | 55 +- .../microwin/Invoke-WPFMicroWinRunspace.ps1 | 624 ++---------------- .../microwin/Microwin-RemoveFeatures.ps1 | 2 - .../Microwin-RemoveFileOrDirectory.ps1 | 2 - .../microwin/Microwin-RemovePackages.ps1 | 1 - .../microwin/Set-ScratchFolderPermissions.ps1 | 230 +------ 9 files changed, 98 insertions(+), 1010 deletions(-) delete mode 100644 MICROWIN_IMPROVEMENTS.md delete mode 100644 functions/microwin/Invoke-MicrowinGetIso-new.ps1 diff --git a/MICROWIN_IMPROVEMENTS.md b/MICROWIN_IMPROVEMENTS.md deleted file mode 100644 index 9b1f64a7f1..0000000000 --- a/MICROWIN_IMPROVEMENTS.md +++ /dev/null @@ -1,74 +0,0 @@ -# MicroWin Improvements Summary - -## Latest Enhancements (Focus on Mount Permission Issues) - -### 1. Privilege Elevation System -- **Windows API Integration**: Added P/Invoke declarations for Windows API functions to enable system privileges -- **Required Privileges**: Automatically enables SeBackupPrivilege, SeRestorePrivilege, SeSecurityPrivilege, SeTakeOwnershipPrivilege, SeManageVolumePrivilege -- **Privilege Status Monitoring**: Real-time checking and reporting of current privilege status -- **Alternative Methods**: Fallback to PowerShell's Enable-Privilege if available - -### 2. Pre-Mount System Checks -- **DISM Availability**: Verifies DISM is installed and responsive -- **Disk Space Monitoring**: Checks available space on target drive (warns if < 10GB) -- **Directory Access Testing**: Validates write permissions to scratch directory -- **System Architecture Detection**: Reports process and OS architecture for compatibility -- **DISM-Compatible Permissions**: Automatically applies optimal file permissions to scratch directories - -### 3. Enhanced Mount Process -- **Retry Logic**: Up to 3 attempts with cleanup between retries -- **Multiple Methods**: Primary DISM command-line, fallback to PowerShell cmdlets -- **Better Error Capture**: Redirected output to log files for detailed error analysis -- **Stale Mount Cleanup**: Automatic cleanup of previous mount points before starting -- **Multiple Scratch Directories**: Automatically tries different locations (temp, C:\temp, C:\MicrowinMount) -- **Permission Application**: Applies DISM-compatible permissions to all scratch directories - -### 4. Comprehensive Error Reporting -- **System State Display**: Shows administrator status, current user, architecture -- **Current Privileges**: Lists all security privileges and their status -- **Step-by-Step Troubleshooting**: Detailed manual steps for users to follow -- **Manual Command Examples**: Exact commands users can run to test independently -- **Alternative Approaches**: Multiple workaround suggestions - -### 5. Performance Optimizations (Previous) -- **Process Priority**: Elevated to High priority for faster processing -- **Memory Optimization**: Increased working set for better performance -- **Multi-Core Detection**: Utilizes all available CPU cores -- **Fast Compression**: Uses fastest compression settings for speed - -### 6. Debug and Monitoring Improvements -- **Extensive Logging**: Debug output at every major step -- **Progress Tracking**: Real-time progress updates via taskbar -- **Error Context**: Detailed error messages with context -- **Verification Steps**: Multiple methods to verify successful operations - -## Key Files Modified -- `functions/microwin/Invoke-WPFMicroWinRunspace.ps1` - Main runspace with all improvements -- `functions/microwin/Invoke-Microwin.ps1` - Entry point with runspace integration -- `functions/microwin/Invoke-MicrowinGetIso.ps1` - ISO selection with UI thread safety -- `functions/microwin/Set-ScratchFolderPermissions.ps1` - Standalone script to apply DISM-compatible permissions - -## Testing Recommendations -1. **Administrator Rights**: Always run as Administrator -2. **Antivirus**: Temporarily disable real-time protection during testing -3. **Clean Environment**: Run `dism /cleanup-mountpoints` before starting -4. **Monitor Output**: Watch console for detailed debug information -5. **Manual Verification**: Use provided manual commands if automated process fails - -## Troubleshooting Steps (If Mount Still Fails) -1. Check Event Viewer for DISM-related errors -2. Verify Windows ADK/DISM tools are properly installed -3. Test with different scratch directory locations -4. Run from elevated Command Prompt instead of PowerShell -5. Check Group Policy restrictions on DISM operations -6. Ensure no other processes are using the WIM file -7. **NEW: Use the standalone permission script**: `.\functions\microwin\Set-ScratchFolderPermissions.ps1 -Path "C:\temp\mount" -ShowPermissions` -8. **NEW: Disable Windows Defender real-time protection** (most common cause of permission issues) - -## Expected Behavior -- Clear privilege status reporting -- Detailed pre-mount system checks -- Retry logic with cleanup between attempts -- Comprehensive error messages with actionable steps -- Progress updates throughout the process -- Graceful failure handling with detailed troubleshooting guidance diff --git a/functions/microwin/Force-CleanupMountDirectory.ps1 b/functions/microwin/Force-CleanupMountDirectory.ps1 index 0b65224d26..3d52f5debb 100644 --- a/functions/microwin/Force-CleanupMountDirectory.ps1 +++ b/functions/microwin/Force-CleanupMountDirectory.ps1 @@ -4,8 +4,8 @@ function Force-CleanupMountDirectory { Forces cleanup of a mount directory by closing processes that have files open .DESCRIPTION - This function attempts to identify and close processes that have files open in the specified - mount directory, which is often the cause of unmount failures. + 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 @@ -20,114 +20,56 @@ function Force-CleanupMountDirectory { [int]$TimeoutSeconds = 30 ) - Write-Host "DEBUG: Starting forced cleanup of mount directory: $MountPath" -ForegroundColor Yellow - try { - # First, try to identify processes using files in the mount directory - Write-Host "DEBUG: Checking for processes using files in mount directory..." -ForegroundColor Yellow - - # Get all processes and check if they have files open in the mount directory - $processesToKill = @() - - try { - # Use Get-Process with file handle information - $allProcesses = Get-Process -ErrorAction SilentlyContinue - foreach ($process in $allProcesses) { - try { - if ($process.ProcessName -eq "System" -or $process.ProcessName -eq "Idle") { - continue - } - - # Check if process has any modules or files loaded from the mount path - try { - $processModules = $process.Modules - } catch { - $processModules = $null - } - if ($processModules) { - foreach ($module in $processModules) { - if ($module.FileName -and $module.FileName.StartsWith($MountPath, [System.StringComparison]::OrdinalIgnoreCase)) { - Write-Host "DEBUG: Found process using mount directory: $($process.ProcessName) (PID: $($process.Id))" -ForegroundColor Red - $processesToKill += $process - break - } - } - } - } catch { - # Ignore processes we can't access - continue + # 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) { + reg unload $hiveName 2>$null } - } - } catch { - Write-Host "DEBUG: Could not enumerate all processes: $($_.Exception.Message)" -ForegroundColor Yellow - } - - # Also check for common processes that might interfere - $commonInterferingProcesses = @("explorer", "dwm", "winlogon", "csrss", "svchost") - Write-Host "DEBUG: Checking for Windows Search, antivirus, and other interfering processes..." -ForegroundColor Yellow - - $suspiciousProcesses = Get-Process -ErrorAction SilentlyContinue | Where-Object { - $_.ProcessName -match "SearchIndexer|SearchProtocolHost|SearchFilterHost|MsMpEng|NisSrv|avp|avgnt|avast|mcshield|norton|kaspersky|bitdefender|eset|fsecure|gdata|panda|sophos|trendmicro|webroot|malwarebytes" - } - - if ($suspiciousProcesses) { - Write-Host "DEBUG: Found potentially interfering processes:" -ForegroundColor Yellow - foreach ($proc in $suspiciousProcesses) { - Write-Host "DEBUG: - $($proc.ProcessName) (PID: $($proc.Id))" -ForegroundColor Yellow + } catch { + # Hive not loaded or error checking - continue } } # Force garbage collection to release any PowerShell file handles - Write-Host "DEBUG: Forcing garbage collection to release file handles..." -ForegroundColor Yellow [System.GC]::Collect() [System.GC]::WaitForPendingFinalizers() [System.GC]::Collect() # Wait a moment for handles to be released - Start-Sleep -Seconds 3 + Start-Sleep -Seconds 2 # Try to set the mount directory and its contents to not readonly - Write-Host "DEBUG: Removing readonly attributes from mount directory contents..." -ForegroundColor Yellow try { if (Test-Path $MountPath) { & attrib -R "$MountPath\*" /S /D 2>$null - Write-Host "DEBUG: Readonly attributes removed from mount directory" -ForegroundColor Green } } catch { - Write-Host "DEBUG: Could not remove readonly attributes: $($_.Exception.Message)" -ForegroundColor Yellow + # Ignore attrib errors } - # Try to close any remaining file handles using system tools - Write-Host "DEBUG: Attempting to close file handles using system methods..." -ForegroundColor Yellow - - # Use PowerShell to try and close any open file handles + # Restart Windows Search service if it's running (helps release file handles) try { - # This is a more aggressive approach - restart the Windows Search service if it's running $searchService = Get-Service -Name "WSearch" -ErrorAction SilentlyContinue if ($searchService -and $searchService.Status -eq "Running") { - Write-Host "DEBUG: Temporarily stopping Windows Search service..." -ForegroundColor Yellow Stop-Service -Name "WSearch" -Force -ErrorAction SilentlyContinue - Start-Sleep -Seconds 2 - Write-Host "DEBUG: Restarting Windows Search service..." -ForegroundColor Yellow + Start-Sleep -Seconds 1 Start-Service -Name "WSearch" -ErrorAction SilentlyContinue } } catch { - Write-Host "DEBUG: Could not restart Windows Search service: $($_.Exception.Message)" -ForegroundColor Yellow + # Ignore service restart errors } - # Final cleanup attempt - Write-Host "DEBUG: Performing final cleanup..." -ForegroundColor Yellow + # Final cleanup [System.GC]::Collect() [System.GC]::WaitForPendingFinalizers() - [System.GC]::Collect() - - Start-Sleep -Seconds 2 - Write-Host "DEBUG: Mount directory cleanup completed" -ForegroundColor Green return $true } catch { - Write-Host "DEBUG: Error during mount directory cleanup: $($_.Exception.Message)" -ForegroundColor Red return $false } } diff --git a/functions/microwin/Invoke-MicrowinGetIso-new.ps1 b/functions/microwin/Invoke-MicrowinGetIso-new.ps1 deleted file mode 100644 index 43cd3fb9a8..0000000000 --- a/functions/microwin/Invoke-MicrowinGetIso-new.ps1 +++ /dev/null @@ -1,28 +0,0 @@ -function Invoke-MicrowinGetIso { - <# - .DESCRIPTION - 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" - - if($sync.ProcessRunning) { - $msg = "GetIso process is currently running." - [System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning) - return - } - - # 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 - } - - # 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 index 57f59d62e3..4290ea2e53 100644 --- a/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 +++ b/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 @@ -35,31 +35,21 @@ function Invoke-WPFMicroWinGetIsoRunspace { $totalSteps = 10 $currentStep = 0 - Write-Host "DEBUG: About to set initial progress..." # Provide immediate feedback to user with progress try { - Write-Host "DEBUG: Attempting dispatcher invoke..." $sync.form.Dispatcher.Invoke([action]{ - Write-Host "DEBUG: Inside dispatcher invoke - calling Set-WinUtilTaskbaritem..." try { Set-WinUtilTaskbaritem -state "Normal" -value 0.1 -overlay "logo" - Write-Host "DEBUG: Set-WinUtilTaskbaritem completed" } catch { - Write-Host "DEBUG: Error in Set-WinUtilTaskbaritem: $($_.Exception.Message)" } - Write-Host "DEBUG: Skipping Invoke-MicrowinBusyInfo - function appears to be blocking" # Skip the problematic Invoke-MicrowinBusyInfo call for now }) - Write-Host "DEBUG: Dispatcher invoke completed successfully" } catch { - Write-Host "DEBUG: Error in dispatcher invoke: $($_.Exception.Message)" - Write-Host "DEBUG: Continuing without UI updates..." } $currentStep = 1 - Write-Host "DEBUG: Initial progress set, displaying ASCII art..." Write-Host " _ __ __ _ " Write-Host " /\/\ (_) ___ _ __ ___ / / /\ \ \(_) _ __ " @@ -67,18 +57,14 @@ function Invoke-WPFMicroWinGetIsoRunspace { Write-Host "/ /\/\ \| || (__ | | | (_) | \ /\ / | || | | | " Write-Host "\/ \/|_| \___||_| \___/ \/ \/ |_||_| |_| " - Write-Host "DEBUG: ASCII art displayed, checking file path..." $filePath = "" if ($GetIsoSettings.isManual) { - Write-Host "DEBUG: Processing manual ISO selection..." # Use the pre-selected file path from the main thread $filePath = $GetIsoSettings.filePath - Write-Host "DEBUG: File path from settings: '$filePath'" if ([string]::IsNullOrEmpty($filePath)) { - Write-Host "DEBUG: No ISO is chosen - exiting" Write-Host "No ISO is chosen" $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning" @@ -88,14 +74,12 @@ function Invoke-WPFMicroWinGetIsoRunspace { return } - Write-Host "DEBUG: ISO file is valid, updating progress..." # 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 }) - Write-Host "DEBUG: Progress updated to step 2" } elseif ($GetIsoSettings.isDownloader) { # Use the pre-selected folder path from the main thread @@ -170,10 +154,8 @@ function Invoke-WPFMicroWinGetIsoRunspace { } } - Write-Host "DEBUG: Checking if file exists..." Write-Host "File path $($filePath)" if (-not (Test-Path -Path "$filePath" -PathType Leaf)) { - Write-Host "DEBUG: File doesn't exist - showing error" $msg = "File you've chosen doesn't exist" $sync.form.Dispatcher.Invoke([action]{ # Invoke-MicrowinBusyInfo -action "warning" -message $msg @@ -182,7 +164,6 @@ function Invoke-WPFMicroWinGetIsoRunspace { $sync.ProcessRunning = $false return } - Write-Host "DEBUG: File exists, proceeding to system requirements check..." $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value (3 / $totalSteps) -overlay "logo" @@ -249,9 +230,7 @@ function Invoke-WPFMicroWinGetIsoRunspace { # 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" $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)) { Write-Warning "You may not have enough space for this operation. Proceed at your own risk." } elseif ($driveSpace -lt $isoSize) { @@ -377,11 +356,6 @@ function Invoke-WPFMicroWinGetIsoRunspace { Write-Host "Copying Windows image. This will take awhile, please don't use UI or cancel this step!" try { - Write-Host "DEBUG: Starting file copy operation..." - Write-Host "DEBUG: Source: '$($driveLetter):'" - Write-Host "DEBUG: Destination: '$mountDir'" - Write-Host "DEBUG: Checking if source exists: $(Test-Path "$($driveLetter):")" - Write-Host "DEBUG: Checking if destination exists: $(Test-Path "$mountDir")" $totalTime = Measure-Command { # Use native Copy-Item instead of Copy-Files function @@ -411,14 +385,9 @@ function Invoke-WPFMicroWinGetIsoRunspace { }) } Write-Host "Copy complete! Total Time: $($totalTime.Minutes) minutes, $($totalTime.Seconds) seconds" - Write-Host "DEBUG: File copy operation completed successfully" } catch { - Write-Host "DEBUG: ERROR during file copy: $($_.Exception.Message)" - Write-Host "DEBUG: Full error details: $_" - Write-Host "DEBUG: Error type: $($_.Exception.GetType().FullName)" # Fallback to PowerShell Copy-Item if robocopy fails - Write-Host "DEBUG: Falling back to PowerShell Copy-Item..." try { $totalTime = Measure-Command { Copy-Item -Path "$($driveLetter):*" -Destination "$mountDir" -Recurse -Force @@ -428,9 +397,7 @@ function Invoke-WPFMicroWinGetIsoRunspace { }) } Write-Host "Fallback copy complete! Total Time: $($totalTime.Minutes) minutes, $($totalTime.Seconds) seconds" - Write-Host "DEBUG: Fallback copy operation completed successfully" } catch { - Write-Host "DEBUG: ERROR during fallback copy: $($_.Exception.Message)" throw $_ } } $sync.form.Dispatcher.Invoke([action]{ @@ -440,12 +407,8 @@ function Invoke-WPFMicroWinGetIsoRunspace { $currentStep = 8 $wimFile = "$mountDir\sources\install.wim" Write-Host "Getting image information $wimFile" - Write-Host "DEBUG: Checking for WIM file at: '$wimFile'" - Write-Host "DEBUG: WIM file exists: $(Test-Path "$wimFile" -PathType Leaf)" $esdFile = $wimFile.Replace(".wim", ".esd").Trim() - Write-Host "DEBUG: Checking for ESD file at: '$esdFile'" - Write-Host "DEBUG: ESD file exists: $(Test-Path "$esdFile" -PathType Leaf)" 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." @@ -461,29 +424,23 @@ function Invoke-WPFMicroWinGetIsoRunspace { $wimFile = $esdFile } - Write-Host "DEBUG: Final WIM file path: '$wimFile'" - Write-Host "DEBUG: Final WIM file exists: $(Test-Path "$wimFile" -PathType Leaf)" # Populate the Windows flavors list - must be done on UI thread $sync.form.Dispatcher.Invoke([action]{ $sync.MicrowinWindowsFlavors.Items.Clear() }) - Write-Host "DEBUG: About to enumerate Windows images..." try { $images = Get-WindowsImage -ImagePath $wimFile - Write-Host "DEBUG: Found $($images.Count) Windows images" $images | ForEach-Object { $imageIdx = $_.ImageIndex $imageName = $_.ImageName - Write-Host "DEBUG: Processing image $imageIdx : $imageName" $sync.form.Dispatcher.Invoke([action]{ $sync.MicrowinWindowsFlavors.Items.Add("$imageIdx : $imageName") }) } } catch { - Write-Host "DEBUG: ERROR enumerating Windows images: $($_.Exception.Message)" throw $_ } @@ -495,7 +452,7 @@ function Invoke-WPFMicroWinGetIsoRunspace { }) $currentStep = 9 - Write-Host "Finding suitable edition. This can take some time. Do note that this is an automatic process that might not select the edition you want." + 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") { @@ -520,31 +477,21 @@ function Invoke-WPFMicroWinGetIsoRunspace { }) } catch { - Write-Host "DEBUG: CATCH BLOCK TRIGGERED - Processing error..." - Write-Host "DEBUG: Error message: $($_.Exception.Message)" - Write-Host "DEBUG: Error type: $($_.Exception.GetType().FullName)" - Write-Host "DEBUG: Stack trace: $($_.ScriptStackTrace)" - Write-Host "DEBUG: Full error object: $_" Write-Host "Dismounting bad image..." try { Get-Volume $driveLetter | Get-DiskImage | Dismount-DiskImage - Write-Host "DEBUG: Image dismounted successfully" } catch { - Write-Host "DEBUG: Error dismounting image: $($_.Exception.Message)" } try { if (Test-Path "$scratchDir") { Remove-Item -Recurse -Force "$($scratchDir)" - Write-Host "DEBUG: Scratch directory '$scratchDir' removed" } if (Test-Path "$mountDir") { Remove-Item -Recurse -Force "$($mountDir)" - Write-Host "DEBUG: Mount directory '$mountDir' removed" } } catch { - Write-Host "DEBUG: Error cleaning up directories: $($_.Exception.Message)" } $sync.form.Dispatcher.Invoke([action]{ diff --git a/functions/microwin/Invoke-WPFMicroWinRunspace.ps1 b/functions/microwin/Invoke-WPFMicroWinRunspace.ps1 index ef64582c8e..4cbf3c445f 100644 --- a/functions/microwin/Invoke-WPFMicroWinRunspace.ps1 +++ b/functions/microwin/Invoke-WPFMicroWinRunspace.ps1 @@ -34,48 +34,22 @@ function Invoke-WPFMicroWinRunspace { param([string]$Path) try { - Write-Host "DEBUG: Setting DISM-compatible permissions on: $Path" - $acl = Get-Acl -Path $Path - - # Remove inherited permissions and set explicit ones - # $acl.SetAccessRuleProtection($true, $false) - - # Administrators - Full Control - $adminRule = New-Object System.Security.AccessControl.FileSystemAccessRule( - "Administrators", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" - ) - $acl.SetAccessRule($adminRule) - - # SYSTEM - Full Control - $systemRule = New-Object System.Security.AccessControl.FileSystemAccessRule( - "SYSTEM", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" - ) - $acl.SetAccessRule($systemRule) - - # Current User - Full Control + # Use icacls for reliable permission setting with language-independent SIDs + # Grant full control to Administrators (S-1-5-32-544) + & icacls "$Path" /grant "*S-1-5-32-544:(OI)(CI)F" /T /C | Out-Null + + # Grant full control to SYSTEM (S-1-5-18) + & icacls "$Path" /grant "*S-1-5-18:(OI)(CI)F" /T /C | Out-Null + + # Grant full control to current user $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name - $userRule = New-Object System.Security.AccessControl.FileSystemAccessRule( - $currentUser, "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" - ) - $acl.SetAccessRule($userRule) - - # Authenticated Users - Modify (subfolders and files only) - $authUsersRule = New-Object System.Security.AccessControl.FileSystemAccessRule( - "Authenticated Users", "Modify", "ContainerInherit,ObjectInherit", "InheritOnly", "Allow" - ) - $acl.SetAccessRule($authUsersRule) - - # Authenticated Users - Create folders/append data (this folder only) - $authUsersThisFolderRule = New-Object System.Security.AccessControl.FileSystemAccessRule( - "Authenticated Users", "CreateDirectories,AppendData", "None", "None", "Allow" - ) - $acl.SetAccessRule($authUsersThisFolderRule) - - Set-Acl -Path $Path -AclObject $acl - Write-Host "DEBUG: Successfully applied DISM-compatible permissions to: $Path" + & icacls "$Path" /grant "${currentUser}:(OI)(CI)F" /T /C | Out-Null + + # Grant modify to Authenticated Users (S-1-5-11) for subfolders and files + & icacls "$Path" /grant "*S-1-5-11:(OI)(CI)M" /T /C | Out-Null + return $true } catch { - Write-Host "DEBUG: Failed to set permissions on $Path`: $($_.Exception.Message)" return $false } } @@ -84,124 +58,49 @@ function Invoke-WPFMicroWinRunspace { try { # Set process priority to High for better performance - Write-Host "DEBUG: Setting process priority to High for better performance..." try { $currentProcess = Get-Process -Id $PID $currentProcess.PriorityClass = [System.Diagnostics.ProcessPriorityClass]::High - Write-Host "DEBUG: Process priority set to High successfully" } catch { - Write-Host "DEBUG: WARNING - Could not set process priority: $($_.Exception.Message)" + # Could not set process priority } # Optimize PowerShell memory usage - Write-Host "DEBUG: Optimizing PowerShell memory settings..." try { - # Increase the memory limit for PowerShell operations + # 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 - Write-Host "DEBUG: Memory optimization completed" } catch { - Write-Host "DEBUG: WARNING - Memory optimization failed: $($_.Exception.Message)" - } - - # 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, - } -} - -public class PrivilegeManager { - [DllImport("advapi32.dll", SetLastError = true)] - public static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle); - - [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, out LUID lpLuid); - - [DllImport("advapi32.dll", SetLastError = true)] - public static extern bool AdjustTokenPrivileges(IntPtr TokenHandle, bool DisableAllPrivileges, ref TOKEN_PRIVILEGES NewState, uint BufferLength, IntPtr PreviousState, IntPtr ReturnLength); - - [DllImport("kernel32.dll")] - public static extern IntPtr GetCurrentProcess(); - - [DllImport("kernel32.dll")] - public static extern bool CloseHandle(IntPtr hObject); - - public const uint TOKEN_ADJUST_PRIVILEGES = 0x0020; - public const uint SE_PRIVILEGE_ENABLED = 0x00000002; - - [StructLayout(LayoutKind.Sequential)] - public struct LUID { - public uint LowPart; - public int HighPart; - } - - [StructLayout(LayoutKind.Sequential)] - public struct TOKEN_PRIVILEGES { - public uint PrivilegeCount; - public LUID_AND_ATTRIBUTES Privileges; - } - - [StructLayout(LayoutKind.Sequential)] - public struct LUID_AND_ATTRIBUTES { - public LUID Luid; - public uint Attributes; - } - - public static bool EnablePrivilege(string privilegeName) { - IntPtr token = IntPtr.Zero; - try { - if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, out token)) { - return false; + # Memory optimization failed } - LUID luid; - if (!LookupPrivilegeValue(null, privilegeName, out luid)) { - return false; - } - - TOKEN_PRIVILEGES tp = new TOKEN_PRIVILEGES(); - tp.PrivilegeCount = 1; - tp.Privileges.Luid = luid; - tp.Privileges.Attributes = SE_PRIVILEGE_ENABLED; - - return AdjustTokenPrivileges(token, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero); - } finally { - if (token != IntPtr.Zero) { - CloseHandle(token); + # 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 } - } - } -} -"@ - - # 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 - this needs to be done on the main thread - $SaveDialog = $null + $SaveDialogFileName = "" $sync.form.Dispatcher.Invoke([action]{ $SaveDialog = New-Object System.Windows.Forms.SaveFileDialog $SaveDialog.InitialDirectory = [Environment]::GetFolderPath('Desktop') $SaveDialog.Filter = "ISO images (*.iso)|*.iso" - $SaveDialog.ShowDialog() | Out-Null + $result = $SaveDialog.ShowDialog() + if ($result -eq [System.Windows.Forms.DialogResult]::OK) { + $script:SaveDialogFileName = $SaveDialog.FileName + } }) - if ($SaveDialog.FileName -eq "") { + if ($SaveDialogFileName -eq "") { $msg = "No file name for the target image was specified" Write-Host $msg $sync.form.Dispatcher.Invoke([action]{ @@ -216,13 +115,13 @@ public class PrivilegeManager { # Invoke-MicrowinBusyInfo -action "wip" -message "Busy..." -interactive $false }) - Write-Host "Target ISO location: $($SaveDialog.FileName)" + Write-Host "Target ISO location: $SaveDialogFileName" # Performance optimization: Determine optimal thread count $coreCount = (Get-WmiObject -Class Win32_Processor | Measure-Object -Property NumberOfCores -Sum).Sum $logicalProcessors = (Get-WmiObject -Class Win32_ComputerSystem).NumberOfLogicalProcessors $optimalThreads = [Math]::Min($logicalProcessors, [Math]::Max(2, $coreCount)) - Write-Host "DEBUG: System has $coreCount cores, $logicalProcessors logical processors. Using $optimalThreads threads for optimal performance." + Write-Host "System has $coreCount cores, $logicalProcessors logical processors. Using $optimalThreads threads for optimal performance." # Extract settings from hashtable $index = $MicroWinSettings.selectedIndex @@ -245,16 +144,12 @@ public class PrivilegeManager { # 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..." - Write-Host "DEBUG: Using optimized compression settings for better performance..." 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" - Write-Host "DEBUG: PowerShell export with Fast compression completed" } catch { # Fall back to DISM with optimized settings - Write-Host "DEBUG: PowerShell export failed, using DISM with performance optimizations..." dism /english /export-image /sourceimagefile="$mountDir\sources\install.esd" /sourceindex=$index /destinationimagefile="$mountDir\sources\install.wim" /compress:fast /checkintegrity /verify - Write-Host "DEBUG: DISM export with Fast compression completed" } if ($?) { Remove-Item -Path "$mountDir\sources\install.esd" -Force @@ -311,13 +206,10 @@ public class PrivilegeManager { } # Clean up any stale mountpoints before starting - Write-Host "DEBUG: Cleaning up any stale DISM mountpoints..." try { & dism /cleanup-mountpoints /loglevel:1 - Write-Host "DEBUG: Mountpoints cleanup completed" Start-Sleep -Seconds 2 } catch { - Write-Host "DEBUG: Mountpoints cleanup warning: $($_.Exception.Message)" } # Check if running as administrator @@ -326,7 +218,6 @@ public class PrivilegeManager { if (-not $isAdmin) { $msg = "Administrator privileges are required to mount and modify Windows images. Please run WinUtil as Administrator and try again." - Write-Host "DEBUG: ERROR - Not running as administrator" Write-Host $msg $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning" @@ -335,51 +226,16 @@ public class PrivilegeManager { return } - # Enable required privileges for DISM operations - Write-Host "DEBUG: Enabling required privileges for DISM operations..." - try { - $requiredPrivileges = @( - "SeBackupPrivilege", - "SeRestorePrivilege", - "SeSecurityPrivilege", - "SeTakeOwnershipPrivilege", - "SeManageVolumePrivilege" - ) - - $privilegesEnabled = 0 - foreach ($privilege in $requiredPrivileges) { - try { - if ([PrivilegeManager]::EnablePrivilege($privilege)) { - Write-Host "DEBUG: Successfully enabled $privilege" - $privilegesEnabled++ - } else { - Write-Host "DEBUG: WARNING - Could not enable $privilege" - } - } catch { - Write-Host "DEBUG: WARNING - Error enabling $privilege : $($_.Exception.Message)" - } - } - - Write-Host "DEBUG: Enabled $privilegesEnabled out of $($requiredPrivileges.Count) privileges" - - if ($privilegesEnabled -ge 2) { - Write-Host "DEBUG: Sufficient privileges enabled for DISM operations" - } else { - Write-Host "DEBUG: WARNING - May not have sufficient privileges for DISM operations" - } - } catch { - Write-Host "DEBUG: WARNING - Could not enable privileges: $($_.Exception.Message)" - } + # 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 - Write-Host "DEBUG: Write permissions verified for scratch directory" } catch { $msg = "Cannot write to scratch directory '$scratchDir'. Please check permissions and ensure the directory is not in use." - Write-Host "DEBUG: ERROR - Cannot write to scratch directory: $($_.Exception.Message)" Write-Host $msg $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning" @@ -392,7 +248,6 @@ public class PrivilegeManager { $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 "DEBUG: ERROR - install.wim not found at $wimPath" Write-Host $msg $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning" @@ -404,10 +259,8 @@ public class PrivilegeManager { try { # Test if we can read the WIM file $wimInfo = Get-WindowsImage -ImagePath $wimPath - Write-Host "DEBUG: WIM file verification successful - found $($wimInfo.Count) image(s)" } 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 "DEBUG: ERROR - Cannot read WIM file: $($_.Exception.Message)" Write-Host $msg $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Error" -value 1 -overlay "warning" @@ -417,75 +270,26 @@ public class PrivilegeManager { } try { - Write-Host "DEBUG: Checking if image is already mounted..." # 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) { - Write-Host "DEBUG: Found existing mount at $scratchDir, dismounting..." Dismount-WindowsImage -Path $scratchDir -Discard Start-Sleep -Seconds 2 } } } catch { - Write-Host "DEBUG: Error checking mounted images: $($_.Exception.Message)" } # Additional permission checks before mounting - Write-Host "DEBUG: Performing additional permission and system checks..." - - # Check if DISM service is running - try { - $dismService = Get-Service -Name "DISM" -ErrorAction SilentlyContinue - if ($dismService -and $dismService.Status -ne "Running") { - Write-Host "DEBUG: Starting DISM service..." - Start-Service -Name "DISM" - Start-Sleep -Seconds 3 - } - } catch { - Write-Host "DEBUG: Could not manage DISM service: $($_.Exception.Message)" - } - - # Check UAC and token privileges - try { - $tokenPrivs = whoami /priv | Out-String - Write-Host "DEBUG: Re-checking privileges after elevation attempt..." - if ($tokenPrivs -match "SeBackupPrivilege.*Enabled" -and $tokenPrivs -match "SeRestorePrivilege.*Enabled") { - Write-Host "DEBUG: Required privileges (SeBackupPrivilege, SeRestorePrivilege) are now enabled" - } else { - Write-Host "DEBUG: WARNING - Some required privileges may still not be enabled" - - # Try alternative privilege elevation method - Write-Host "DEBUG: Attempting alternative privilege elevation..." - try { - # Try using PowerShell's built-in privilege functions if available - if (Get-Command "Enable-Privilege" -ErrorAction SilentlyContinue) { - Enable-Privilege SeBackupPrivilege, SeRestorePrivilege -Force - Write-Host "DEBUG: Alternative privilege elevation attempted" - } - } catch { - Write-Host "DEBUG: Alternative privilege elevation failed: $($_.Exception.Message)" - } - - Write-Host "DEBUG: Current privilege status:" - $tokenPrivs -split "`n" | Where-Object { $_ -match "Se(Backup|Restore|Security|TakeOwnership|ManageVolume)Privilege" } | ForEach-Object { - Write-Host "DEBUG: $($_.Trim())" - } - } - } catch { - Write-Host "DEBUG: Could not check token privileges: $($_.Exception.Message)" - } # Pre-mount system checks - Write-Host "DEBUG: Performing pre-mount system checks..." # Check if DISM is available and working try { $dismCheck = & dism /? 2>&1 - Write-Host "DEBUG: DISM is available and responding" } catch { - Write-Host "DEBUG: WARNING - DISM may not be available: $($_.Exception.Message)" } # Check available disk space @@ -493,70 +297,40 @@ public class PrivilegeManager { $scratchDrive = Split-Path $scratchDir -Qualifier $driveInfo = Get-WmiObject -Class Win32_LogicalDisk | Where-Object { $_.DeviceID -eq $scratchDrive } $freeSpaceGB = [math]::Round($driveInfo.FreeSpace / 1GB, 2) - Write-Host "DEBUG: Available disk space on $scratchDrive`: $freeSpaceGB GB" if ($freeSpaceGB -lt 10) { - Write-Host "DEBUG: WARNING - Low disk space may cause mount issues" } } catch { - Write-Host "DEBUG: Could not check disk space: $($_.Exception.Message)" } # Check if scratch directory is accessible and set proper permissions try { if (-not (Test-Path $scratchDir)) { New-Item -Path $scratchDir -ItemType Directory -Force | Out-Null - Write-Host "DEBUG: Created scratch directory: $scratchDir" } # Set proper permissions for DISM operations using the helper function - Write-Host "DEBUG: Setting optimal permissions for scratch directory..." $permissionsSet = Set-DismCompatiblePermissions -Path $scratchDir if ($permissionsSet) { # Verify permissions were set correctly $newAcl = Get-Acl -Path $scratchDir - Write-Host "DEBUG: Scratch directory permissions summary:" foreach ($access in $newAcl.Access) { - Write-Host "DEBUG: $($access.IdentityReference): $($access.FileSystemRights) ($($access.AccessControlType))" } } else { - Write-Host "DEBUG: Using default permissions (may cause DISM issues)" } # Test write access $testFile = Join-Path $scratchDir "test_access.tmp" "test" | Out-File -FilePath $testFile -Force Remove-Item $testFile -Force - Write-Host "DEBUG: Scratch directory write access confirmed" } catch { - Write-Host "DEBUG: ERROR - Cannot access scratch directory: $($_.Exception.Message)" return } # Additional file permission and location diagnostics - Write-Host "DEBUG: Performing additional permission diagnostics..." - # Check WIM file permissions and ownership - try { - $wimPath = "$mountDir\sources\install.wim" - $wimAcl = Get-Acl -Path $wimPath - Write-Host "DEBUG: WIM file owner: $($wimAcl.Owner)" - - $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $hasFullControl = $false - foreach ($access in $wimAcl.Access) { - if ($access.IdentityReference.Value -eq $currentUser.Name -or $access.IdentityReference.Value -eq "BUILTIN\Administrators") { - if ($access.FileSystemRights -match "FullControl|Write|Modify") { - $hasFullControl = $true - break - } - } - } - Write-Host "DEBUG: Current user has sufficient WIM file access: $hasFullControl" - } catch { - Write-Host "DEBUG: Could not check WIM file permissions: $($_.Exception.Message)" - } + # WIM file permissions are handled automatically by DISM operations # Try alternative scratch directory if current one has issues $originalScratchDir = $scratchDir @@ -566,7 +340,6 @@ public class PrivilegeManager { $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." - Write-Host "DEBUG: Attempting mount using DISM command directly for better compatibility..." $mountAttempts = @( @{ Dir = $scratchDir; Description = "Original temp directory" }, @@ -575,7 +348,6 @@ public class PrivilegeManager { ) # Remove ReadOnly attributes from WIM files before mounting - Write-Host "DEBUG: Checking and removing ReadOnly attributes from WIM files..." -ForegroundColor Yellow $wimFilePaths = @( "$mountDir\sources\install.wim", "$mountDir\sources\boot.wim" @@ -587,31 +359,24 @@ public class PrivilegeManager { try { $wimItem = Get-Item -Path $wimFilePath -Force if ($wimItem.Attributes -band [System.IO.FileAttributes]::ReadOnly) { - Write-Host "DEBUG: Removing ReadOnly attribute from $wimFilePath" -ForegroundColor Yellow $wimItem.Attributes = $wimItem.Attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly) # Verify the change was successful $wimItem.Refresh() if ($wimItem.Attributes -band [System.IO.FileAttributes]::ReadOnly) { - Write-Host "DEBUG: CRITICAL - Failed to remove ReadOnly attribute from $wimFilePath" -ForegroundColor Red $criticalWimError = $true } else { - Write-Host "DEBUG: Successfully removed ReadOnly attribute from $wimFilePath" -ForegroundColor Green } } else { - Write-Host "DEBUG: $wimFilePath is already writable" -ForegroundColor Green } } catch { - Write-Host "DEBUG: CRITICAL - Cannot modify ReadOnly attribute on $wimFilePath - $($_.Exception.Message)" -ForegroundColor Red $criticalWimError = $true } } else { - Write-Host "DEBUG: WIM file not found: $wimFilePath (may be optional)" -ForegroundColor Yellow } } if ($criticalWimError) { - Write-Host "DEBUG: ABORTING - Cannot proceed with mount due to WIM file ReadOnly issues" -ForegroundColor Red $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) @@ -621,135 +386,52 @@ public class PrivilegeManager { foreach ($attempt in $mountAttempts) { $currentScratchDir = $attempt.Dir - Write-Host "DEBUG: Trying mount with $($attempt.Description): $currentScratchDir" try { - # Ensure directory exists and is accessible + # Ensure directory exists if (-not (Test-Path $currentScratchDir)) { New-Item -Path $currentScratchDir -ItemType Directory -Force | Out-Null - Write-Host "DEBUG: Created scratch directory: $currentScratchDir" - - # Apply DISM-compatible permissions to the new directory - $permissionsSet = Set-DismCompatiblePermissions -Path $currentScratchDir - if ($permissionsSet) { - Write-Host "DEBUG: Applied DISM-compatible permissions to: $currentScratchDir" - } } - # Try DISM mount - Write-Host "DEBUG: Attempting DISM mount to $currentScratchDir..." - $dismountResult = & dism /english /mount-image /imagefile:"$mountDir\sources\install.wim" /index:$index /mountdir:"$currentScratchDir" /optimize /loglevel:1 - $dismountExitCode = $LASTEXITCODE + # Try PowerShell cmdlet first + Mount-WindowsImage -ImagePath "$mountDir\sources\install.wim" -Index $index -Path "$currentScratchDir" -Optimize + $mountSuccess = $true + $scratchDir = $currentScratchDir + break - if ($dismountExitCode -eq 0) { - Write-Host "DEBUG: DISM mount successful with $($attempt.Description)" + } 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 # Update scratch directory for rest of process + $scratchDir = $currentScratchDir break } else { - Write-Host "DEBUG: DISM mount failed with exit code $dismountExitCode" - Write-Host "DEBUG: DISM output: $dismountResult" - # Clean up failed attempt - try { - if (Test-Path $currentScratchDir) { - Remove-Item $currentScratchDir -Force -Recurse -ErrorAction SilentlyContinue - } - } catch { - Write-Host "DEBUG: Could not clean up failed mount directory: $($_.Exception.Message)" + if (Test-Path $currentScratchDir) { + Remove-Item $currentScratchDir -Force -Recurse -ErrorAction SilentlyContinue } } - } catch { - Write-Host "DEBUG: Mount attempt failed for $($attempt.Description): $($_.Exception.Message)" - continue } } - # If DISM attempts failed, try PowerShell cmdlet as final fallback - if (-not $mountSuccess) { - Write-Host "DEBUG: All DISM attempts failed, trying PowerShell cmdlet as final fallback..." - - foreach ($attempt in $mountAttempts) { - $currentScratchDir = $attempt.Dir - Write-Host "DEBUG: Trying PowerShell mount with $($attempt.Description): $currentScratchDir" - - try { - if (-not (Test-Path $currentScratchDir)) { - New-Item -Path $currentScratchDir -ItemType Directory -Force | Out-Null - - # Apply DISM-compatible permissions to the new directory - $permissionsSet = Set-DismCompatiblePermissions -Path $currentScratchDir - if ($permissionsSet) { - Write-Host "DEBUG: Applied DISM-compatible permissions to: $currentScratchDir" - } - } - - Mount-WindowsImage -ImagePath "$mountDir\sources\install.wim" -Index $index -Path "$currentScratchDir" -Optimize - $mountSuccess = $true - $scratchDir = $currentScratchDir # Update scratch directory for rest of process - Write-Host "DEBUG: PowerShell cmdlet mount successful with $($attempt.Description)" - break - } catch { - Write-Host "DEBUG: PowerShell cmdlet mount failed for $($attempt.Description): $($_.Exception.Message)" - continue - } - } - } - - # If all mount attempts failed, try readonly mount as last resort - if (-not $mountSuccess) { - Write-Host "DEBUG: All read/write mount attempts failed. Trying readonly mount as diagnostic step..." - Write-Host "DEBUG: NOTE: Readonly mount will not allow modifications, but helps identify permission issues" - - try { - $readonlyScratchDir = "C:\temp\MicrowinReadOnly_$(Get-Date -Format 'yyyyMMdd_HHmmss')" - if (-not (Test-Path $readonlyScratchDir)) { - New-Item -Path $readonlyScratchDir -ItemType Directory -Force | Out-Null - } - - $dismountResult = & dism /english /mount-image /imagefile:"$mountDir\sources\install.wim" /index:$index /mountdir:"$readonlyScratchDir" /readonly /loglevel:1 - $dismountExitCode = $LASTEXITCODE - - if ($dismountExitCode -eq 0) { - Write-Host "DEBUG: READONLY mount successful - this confirms the WIM file is accessible" - Write-Host "DEBUG: The issue is specifically with read/write permissions, not basic access" - - # Clean up readonly mount - & dism /english /unmount-image /mountdir:"$readonlyScratchDir" /discard /loglevel:1 - Remove-Item $readonlyScratchDir -Force -Recurse -ErrorAction SilentlyContinue - - Write-Host "DEBUG: This suggests one of the following issues:" - Write-Host "DEBUG: 1. Antivirus software blocking write access to WIM files" - Write-Host "DEBUG: 2. Windows Defender real-time protection interfering" - Write-Host "DEBUG: 3. Corporate/Group Policy restrictions on DISM operations" - Write-Host "DEBUG: 4. File system permissions on temp directories" - Write-Host "DEBUG: 5. UAC virtualization affecting file access" - } else { - Write-Host "DEBUG: Even READONLY mount failed - this indicates a deeper system issue" - Write-Host "DEBUG: Readonly mount output: $dismountResult" - } - } catch { - Write-Host "DEBUG: Readonly mount test failed: $($_.Exception.Message)" - } - } + # 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 - Write-Host "DEBUG: Mount verification successful via Get-WindowsImage" break } } } catch { - Write-Host "DEBUG: Error verifying mount status: $($_.Exception.Message)" } # Additional verification by checking if typical Windows directories exist if (-not $mountSuccess) { if ((Test-Path "$scratchDir\Windows") -and (Test-Path "$scratchDir\Windows\System32")) { - Write-Host "DEBUG: Mount verification successful via directory structure check" $mountSuccess = $true } } @@ -757,7 +439,6 @@ public class PrivilegeManager { if ($mountSuccess) { Write-Host "The Windows image has been mounted successfully. Continuing processing..." - Write-Host "DEBUG: Mount verification successful" } else { Write-Host "ERROR: Windows image mounting failed after all attempts" Write-Host "" @@ -770,34 +451,16 @@ public class PrivilegeManager { $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object System.Security.Principal.WindowsPrincipal($currentUser) $isAdmin = $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - Write-Host "DEBUG: - Running as Administrator: $isAdmin" - Write-Host "DEBUG: - Current User: $($currentUser.Name)" - Write-Host "DEBUG: - Process Architecture: $([System.Environment]::Is64BitProcess)" - Write-Host "DEBUG: - OS Architecture: $([System.Environment]::Is64BitOperatingSystem)" # Show privilege status again $tokenPrivs = whoami /priv | Out-String - Write-Host "DEBUG: - Current Privileges:" $tokenPrivs -split "`n" | Where-Object { $_ -match "Se(Backup|Restore|Security|TakeOwnership|ManageVolume)Privilege" } | ForEach-Object { - Write-Host "DEBUG: $($_.Trim())" } } catch { - Write-Host "DEBUG: Could not determine system state: $($_.Exception.Message)" } Write-Host "" Write-Host "IMMEDIATE STEPS TO TRY:" - Write-Host "DEBUG: 1. **DISABLE WINDOWS DEFENDER REAL-TIME PROTECTION** (most common cause)" - Write-Host "DEBUG: - Open Windows Security > Virus & threat protection" - Write-Host "DEBUG: - Turn off Real-time protection temporarily" - Write-Host "DEBUG: 2. **DISABLE OTHER ANTIVIRUS SOFTWARE** (if present)" - Write-Host "DEBUG: 3. **TRY DIFFERENT TEMP DIRECTORY** (this script will attempt automatically)" - Write-Host "DEBUG: 4. **RUN FROM COMMAND PROMPT AS ADMIN** instead of PowerShell:" - Write-Host "DEBUG: - Open Command Prompt as Administrator" - Write-Host "DEBUG: - Navigate to: $($PWD.Path)" - Write-Host "DEBUG: - Run: powershell -ExecutionPolicy Bypass -File winutil.ps1" - Write-Host "DEBUG: 5. **CHECK DISM LOG** at: C:\Windows\Logs\DISM\dism.log" - Write-Host "DEBUG: 6. **RUN DISM CLEANUP**: dism /cleanup-mountpoints" Write-Host "" Write-Host "MANUAL COMMAND TO TEST:" @@ -806,26 +469,9 @@ public class PrivilegeManager { Write-Host "" Write-Host "ADVANCED DIAGNOSTICS TO RUN:" - Write-Host "DEBUG: 1. **CHECK GROUP POLICY RESTRICTIONS**:" - Write-Host "DEBUG: - Run: gpedit.msc" - Write-Host "DEBUG: - Navigate to: Computer Configuration > Administrative Templates > System > Device Installation" - Write-Host "DEBUG: - Look for any DISM or imaging restrictions" - Write-Host "DEBUG: 2. **CHECK REGISTRY PERMISSIONS**:" - Write-Host "DEBUG: - Ensure HKLM\SOFTWARE\Microsoft\WIMMount is accessible" - Write-Host "DEBUG: 3. **VERIFY DISM SERVICE STATUS**:" - Write-Host "DEBUG: - Run: sc query TrustedInstaller" - Write-Host "DEBUG: - Run: sc query CryptSvc" - Write-Host "DEBUG: 4. **CHECK WINDOWS FEATURES**:" - Write-Host "DEBUG: - Run: dism /online /get-features | findstr DISM" - Write-Host "DEBUG: 5. **TEST WITH DIFFERENT WIM FILE**:" - Write-Host "DEBUG: - Try mounting a different WIM file to isolate the issue" Write-Host "" Write-Host "CORPORATE/MANAGED SYSTEM CONSIDERATIONS:" - Write-Host "DEBUG: - If this is a corporate/managed system, IT policies may restrict DISM" - Write-Host "DEBUG: - Contact your system administrator about DISM operation restrictions" - Write-Host "DEBUG: - Some enterprise antivirus solutions block WIM modifications" - Write-Host "DEBUG: - Windows 10/11 in S Mode has restrictions on DISM operations" Write-Host "" $msg = "Could not mount Windows image. See console output for detailed troubleshooting steps." @@ -838,7 +484,6 @@ public class PrivilegeManager { if ($importDrivers) { Write-Host "Exporting drivers from active installation..." - Write-Host "DEBUG: Using optimized DISM settings for driver operations..." if (Test-Path "$env:TEMP\DRV_EXPORT") { Remove-Item "$env:TEMP\DRV_EXPORT" -Recurse -Force } @@ -903,35 +548,23 @@ public class PrivilegeManager { Microwin-CopyVirtIO } - Write-Host "DEBUG: ========== STARTING FEATURES REMOVAL ==========" Write-Host "Remove Features from the image" try { Microwin-RemoveFeatures -UseCmdlets $true - Write-Host "DEBUG: Features removal completed successfully" } catch { - Write-Host "DEBUG: ERROR during features removal: $($_.Exception.Message)" - Write-Host "DEBUG: Continuing with next step..." } Write-Host "Removing features complete!" - Write-Host "DEBUG: ========== STARTING PACKAGES REMOVAL ==========" Write-Host "Removing OS packages" try { Microwin-RemovePackages -UseCmdlets $true - Write-Host "DEBUG: Packages removal completed successfully" } catch { - Write-Host "DEBUG: ERROR during packages removal: $($_.Exception.Message)" - Write-Host "DEBUG: Continuing with next step..." } - Write-Host "DEBUG: ========== STARTING APPX BLOAT REMOVAL ==========" Write-Host "Removing Appx Bloat" try { Microwin-RemoveProvisionedPackages -UseCmdlets $true - Write-Host "DEBUG: Appx bloat removal completed successfully" } catch { - Write-Host "DEBUG: ERROR during Appx bloat removal: $($_.Exception.Message)" - Write-Host "DEBUG: Continuing with next step..." } # Detect Windows 11 24H2 and add dependency to FileExp to prevent Explorer look from going back - thanks @WitherOrNot and @thecatontheceiling @@ -957,40 +590,26 @@ public class PrivilegeManager { } } - Write-Host "DEBUG: ========== STARTING FILE/DIRECTORY CLEANUP ==========" try { - Write-Host "DEBUG: Removing RtBackup directory..." Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\System32\LogFiles\WMI\RtBackup" -Directory - Write-Host "DEBUG: Removing DiagTrack directory..." Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\DiagTrack" -Directory - Write-Host "DEBUG: Removing InboxApps directory..." Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\InboxApps" -Directory - Write-Host "DEBUG: Removing LocationNotificationWindows.exe..." Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\System32\LocationNotificationWindows.exe" - Write-Host "DEBUG: Removing Windows Media Player directories..." Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files (x86)\Windows Media Player" -Directory Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files\Windows Media Player" -Directory - Write-Host "DEBUG: Removing Windows Mail directories..." Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files (x86)\Windows Mail" -Directory Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files\Windows Mail" -Directory - Write-Host "DEBUG: Removing Internet Explorer directories..." Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files (x86)\Internet Explorer" -Directory Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Program Files\Internet Explorer" -Directory - Write-Host "DEBUG: Removing gaming and OneDrive components..." Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\GameBarPresenceWriter" Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\System32\OneDriveSetup.exe" Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\System32\OneDrive.ico" - Write-Host "DEBUG: Removing system apps..." Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\SystemApps" -mask "*narratorquickstart*" -Directory Microwin-RemoveFileOrDirectory -pathToDelete "$($scratchDir)\Windows\SystemApps" -mask "*ParentalControls*" -Directory - Write-Host "DEBUG: File/directory cleanup completed successfully" } catch { - Write-Host "DEBUG: ERROR during file/directory cleanup: $($_.Exception.Message)" - Write-Host "DEBUG: Continuing with next step..." } Write-Host "Removal complete!" - Write-Host "DEBUG: ========== STARTING UNATTEND.XML CREATION ==========" Write-Host "Create unattend.xml" if (($autoConfigPath -ne "") -and (Test-Path "$autoConfigPath")) { @@ -1015,119 +634,84 @@ public class PrivilegeManager { } Write-Host "Done Create unattend.xml" - Write-Host "DEBUG: ========== COPYING UNATTEND.XML INTO ISO ==========" 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 - Write-Host "DEBUG: unattend.xml copied successfully" } catch { - Write-Host "DEBUG: ERROR copying unattend.xml: $($_.Exception.Message)" } Write-Host "Done Copy unattend.xml" - Write-Host "DEBUG: ========== CREATING FIRSTRUN SCRIPT ==========" Write-Host "Create FirstRun" try { Microwin-NewFirstRun - Write-Host "DEBUG: FirstRun script created successfully" } catch { - Write-Host "DEBUG: ERROR creating FirstRun: $($_.Exception.Message)" } Write-Host "Done create FirstRun" - Write-Host "DEBUG: ========== COPYING FIRSTRUN INTO ISO ==========" Write-Host "Copy FirstRun.ps1 into the ISO" try { Copy-Item "$env:temp\FirstStartup.ps1" "$($scratchDir)\Windows\FirstStartup.ps1" -force - Write-Host "DEBUG: FirstRun.ps1 copied successfully" } catch { - Write-Host "DEBUG: ERROR copying FirstRun.ps1: $($_.Exception.Message)" } Write-Host "Done copy FirstRun.ps1" - Write-Host "DEBUG: ========== SETTING UP DESKTOP AND LINKS ==========" 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" - Write-Host "DEBUG: Desktop setup completed successfully" } catch { - Write-Host "DEBUG: ERROR setting up desktop: $($_.Exception.Message)" } - Write-Host "DEBUG: ========== CREATING CHECKINSTALL SCRIPT ==========" Write-Host "Copy checkinstall.cmd into the ISO" try { Microwin-NewCheckInstall Copy-Item "$env:temp\checkinstall.cmd" "$($scratchDir)\Windows\checkinstall.cmd" -force - Write-Host "DEBUG: checkinstall.cmd created and copied successfully" } catch { - Write-Host "DEBUG: ERROR with checkinstall.cmd: $($_.Exception.Message)" } Write-Host "Done copy checkinstall.cmd" - Write-Host "DEBUG: ========== CREATING BYPASSNRO DIRECTORY ==========" Write-Host "Creating a directory that allows to bypass Wifi setup" try { New-Item -ItemType Directory -Force -Path "$($scratchDir)\Windows\System32\OOBE\BYPASSNRO" - Write-Host "DEBUG: BYPASSNRO directory created successfully" } catch { - Write-Host "DEBUG: ERROR creating BYPASSNRO directory: $($_.Exception.Message)" } - Write-Host "DEBUG: ========== LOADING REGISTRY ==========" Write-Host "Loading registry" try { - Write-Host "DEBUG: Loading COMPONENTS registry..." reg load HKLM\zCOMPONENTS "$($scratchDir)\Windows\System32\config\COMPONENTS" - Write-Host "DEBUG: Loading DEFAULT registry..." reg load HKLM\zDEFAULT "$($scratchDir)\Windows\System32\config\default" - Write-Host "DEBUG: Loading NTUSER registry..." reg load HKLM\zNTUSER "$($scratchDir)\Users\Default\ntuser.dat" - Write-Host "DEBUG: Loading SOFTWARE registry..." reg load HKLM\zSOFTWARE "$($scratchDir)\Windows\System32\config\SOFTWARE" - Write-Host "DEBUG: Loading SYSTEM registry..." reg load HKLM\zSYSTEM "$($scratchDir)\Windows\System32\config\SYSTEM" - Write-Host "DEBUG: All registry hives loaded successfully" } catch { - Write-Host "DEBUG: ERROR loading registry: $($_.Exception.Message)" } - Write-Host "DEBUG: ========== APPLYING REGISTRY TWEAKS ==========" 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 - Write-Host "DEBUG: Teams disabled successfully" } catch { - Write-Host "DEBUG: ERROR disabling Teams: $($_.Exception.Message)" } Write-Host "Done disabling Teams" - Write-Host "DEBUG: Fixing Windows Volume Mixer Issue..." try { reg add "HKLM\zNTUSER\Software\Microsoft\Internet Explorer\LowRegistry\Audio\PolicyConfig\PropertyStore" /f - Write-Host "DEBUG: Volume Mixer fix applied successfully" } catch { - Write-Host "DEBUG: ERROR applying Volume Mixer fix: $($_.Exception.Message)" } Write-Host "Fix Windows Volume Mixer Issue" - Write-Host "DEBUG: Bypassing system requirements..." 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 - Write-Host "DEBUG: System requirements bypass applied successfully" } catch { - Write-Host "DEBUG: ERROR bypassing system requirements: $($_.Exception.Message)" } Write-Host "Bypassing system requirements (system image)" reg add "HKLM\zSYSTEM\Setup\LabConfig" /v "BypassCPUCheck" /t REG_DWORD /d 1 /f @@ -1188,40 +772,28 @@ public class PrivilegeManager { } catch { Write-Error "An unexpected error occurred: $_" - Write-Host "DEBUG: ERROR in main processing: $($_.Exception.Message)" - Write-Host "DEBUG: Stack trace: $($_.ScriptStackTrace)" } finally { - Write-Host "DEBUG: ========== STARTING CLEANUP PROCESS ==========" Write-Host "Unmounting Registry..." try { - Write-Host "DEBUG: Unloading registry hives..." reg unload HKLM\zCOMPONENTS reg unload HKLM\zDEFAULT reg unload HKLM\zNTUSER reg unload HKLM\zSOFTWARE reg unload HKLM\zSYSTEM - Write-Host "DEBUG: Registry unloaded successfully" } catch { - Write-Host "DEBUG: ERROR unloading registry: $($_.Exception.Message)" } - Write-Host "DEBUG: ========== CLEANING UP IMAGE ==========" Write-Host "Cleaning up image with optimized settings..." try { # Use optimized DISM cleanup settings for better performance - Write-Host "DEBUG: Running component cleanup with optimized settings..." dism /English /image:$scratchDir /Cleanup-Image /StartComponentCleanup /ResetBase /loglevel:1 - Write-Host "DEBUG: Image cleanup completed successfully" } catch { - Write-Host "DEBUG: ERROR during image cleanup: $($_.Exception.Message)" } Write-Host "Cleanup complete." - Write-Host "DEBUG: ========== UNMOUNTING IMAGE ==========" Write-Host "Unmounting image..." # First, try to clean up any processes or handles that might interfere with unmounting - Write-Host "DEBUG: Performing pre-unmount cleanup..." -ForegroundColor Yellow try { # Force garbage collection to release PowerShell file handles [System.GC]::Collect() @@ -1237,13 +809,10 @@ public class PrivilegeManager { } if ($interferingProcesses) { - Write-Host "DEBUG: Found potentially interfering processes, waiting for them to finish..." -ForegroundColor Yellow Start-Sleep -Seconds 5 } - Write-Host "DEBUG: Pre-unmount cleanup completed" -ForegroundColor Green } catch { - Write-Host "DEBUG: Pre-unmount cleanup failed: $($_.Exception.Message)" -ForegroundColor Yellow } $dismountSuccess = $false @@ -1251,30 +820,26 @@ public class PrivilegeManager { for ($retry = 1; $retry -le $maxRetries; $retry++) { try { - Write-Host "DEBUG: Dismount attempt $retry of $maxRetries..." - if ($retry -eq 1) { - # First attempt: Try DISM command directly - Write-Host "DEBUG: Using DISM command for dismount..." - $dismResult = & dism /english /unmount-image /mountdir:"$scratchDir" /commit /loglevel:1 - $dismExitCode = $LASTEXITCODE + 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) { - Write-Host "DEBUG: DISM dismount successful" - $dismountSuccess = $true - break - } else { - Write-Host "DEBUG: DISM dismount failed with exit code $dismExitCode" - Write-Host "DEBUG: DISM output: $dismResult" + 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 } - } elseif ($retry -eq 2) { - # Second attempt: Try PowerShell cmdlet - Write-Host "DEBUG: Using PowerShell cmdlet for dismount..." - Dismount-WindowsImage -Path "$scratchDir" -Save - } else { - # Third attempt: Try PowerShell cmdlet with CheckIntegrity - Write-Host "DEBUG: Using PowerShell cmdlet with CheckIntegrity..." - Dismount-WindowsImage -Path "$scratchDir" -Save -CheckIntegrity } # Verify dismount was successful for PowerShell attempts @@ -1290,21 +855,17 @@ public class PrivilegeManager { } if (-not $stillMounted) { - Write-Host "DEBUG: PowerShell cmdlet dismount successful on attempt $retry" $dismountSuccess = $true break } else { - Write-Host "DEBUG: Image still appears to be mounted after attempt $retry" } } } catch { - Write-Host "DEBUG: ERROR on dismount attempt $retry`: $($_.Exception.Message)" } # If this isn't the last retry, wait before trying again if ($retry -lt $maxRetries -and -not $dismountSuccess) { - Write-Host "DEBUG: Waiting 5 seconds before next retry..." Start-Sleep -Seconds 5 # Additional cleanup between retries @@ -1316,11 +877,9 @@ public class PrivilegeManager { # If all normal attempts failed, try aggressive cleanup and final fallback strategies if (-not $dismountSuccess) { - Write-Host "DEBUG: All normal dismount attempts failed - trying aggressive cleanup..." -ForegroundColor Red # Aggressive cleanup before final attempts try { - Write-Host "DEBUG: Performing aggressive file handle cleanup..." -ForegroundColor Yellow # Force close any PowerShell handles [System.GC]::Collect() @@ -1335,56 +894,38 @@ public class PrivilegeManager { # Wait longer for file handles to be released Start-Sleep -Seconds 10 - Write-Host "DEBUG: Aggressive cleanup completed" -ForegroundColor Green } catch { - Write-Host "DEBUG: Aggressive cleanup failed: $($_.Exception.Message)" -ForegroundColor Yellow } # Last attempt - try multiple fallback strategies - Write-Host "DEBUG: Final attempt - trying multiple fallback strategies..." # Try DISM discard try { - Write-Host "DEBUG: Trying DISM discard..." $dismResult = & dism /english /unmount-image /mountdir:"$scratchDir" /discard /loglevel:1 if ($LASTEXITCODE -eq 0) { - Write-Host "DEBUG: DISM discard successful" $dismountSuccess = $true } else { - Write-Host "DEBUG: DISM discard failed with exit code $LASTEXITCODE" } } catch { - Write-Host "DEBUG: DISM discard failed: $($_.Exception.Message)" } # Try PowerShell discard if DISM failed if (-not $dismountSuccess) { try { - Write-Host "DEBUG: Trying PowerShell discard..." Dismount-WindowsImage -Path "$scratchDir" -Discard - Write-Host "DEBUG: PowerShell discard completed (changes discarded)" $dismountSuccess = $true } catch { - Write-Host "DEBUG: PowerShell discard failed: $($_.Exception.Message)" } } # Final fallback: cleanup mountpoints if (-not $dismountSuccess) { try { - Write-Host "DEBUG: Trying DISM cleanup-mountpoints..." & dism /cleanup-mountpoints Start-Sleep -Seconds 3 - Write-Host "DEBUG: Mountpoints cleanup completed" } catch { - Write-Host "DEBUG: Cleanup mountpoints failed: $($_.Exception.Message)" } - Write-Host "DEBUG: CRITICAL ERROR - Could not dismount image with any method" - Write-Host "DEBUG: Manual cleanup commands:" - Write-Host "DEBUG: 1. dism /unmount-image /mountdir:`"$scratchDir`" /discard" - Write-Host "DEBUG: 2. dism /cleanup-mountpoints" - Write-Host "DEBUG: 3. Remove-Item -Recurse -Force `"$scratchDir`"" } } @@ -1399,30 +940,21 @@ public class PrivilegeManager { } try { - Write-Host "DEBUG: ========== EXPORTING INSTALL IMAGE ==========" Write-Host "Exporting image into $mountDir\sources\install2.wim with optimized settings..." try { - Write-Host "DEBUG: Trying PowerShell Export-WindowsImage with optimized compression for speed..." # Use Fast compression for better performance, especially during development/testing # Users can change this to "Max" if they prefer smaller file size over speed Export-WindowsImage -SourceImagePath "$mountDir\sources\install.wim" -SourceIndex $index -DestinationImagePath "$mountDir\sources\install2.wim" -CompressionType "Fast" - Write-Host "DEBUG: PowerShell export with Fast compression completed successfully" } catch { # Fall back to DISM with optimized settings - Write-Host "DEBUG: PowerShell export failed, falling back to DISM with optimized settings..." - Write-Host "DEBUG: Error was: $($_.Exception.Message)" dism /english /export-image /sourceimagefile="$mountDir\sources\install.wim" /sourceindex=$index /destinationimagefile="$mountDir\sources\install2.wim" /compress:fast /checkintegrity /verify /loglevel:1 - Write-Host "DEBUG: DISM export with optimized settings completed" } - Write-Host "DEBUG: ========== REPLACING INSTALL.WIM ==========" 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" - Write-Host "DEBUG: install.wim replaced successfully" } catch { - Write-Host "DEBUG: ERROR replacing install.wim: $($_.Exception.Message)" throw $_ } @@ -1440,12 +972,10 @@ public class PrivilegeManager { if ($esd) { Write-Host "Converting install image to ESD with optimized settings..." try { - Write-Host "DEBUG: Using PowerShell Export with Recovery compression..." 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 { - Write-Host "DEBUG: PowerShell ESD export failed, falling back to DISM with optimized settings..." 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." @@ -1453,7 +983,6 @@ public class PrivilegeManager { } } catch { Write-Error "An unexpected error occurred during image export: $_" - Write-Host "DEBUG: ERROR during image export/processing: $($_.Exception.Message)" throw $_ } @@ -1515,7 +1044,7 @@ public class PrivilegeManager { 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 + $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 @@ -1523,7 +1052,7 @@ public class PrivilegeManager { if ($copyToUSB) { Write-Host "Copying target ISO to the USB drive" - Microwin-CopyToUSB("$($SaveDialog.FileName)") + Microwin-CopyToUSB("$SaveDialogFileName") if ($?) { Write-Host "Done Copying target ISO to USB drive!" } else { Write-Host "ISO copy failed." } } @@ -1539,7 +1068,7 @@ public class PrivilegeManager { Write-Host "`n`nPerforming Cleanup..." Remove-Item -Recurse -Force "$($scratchDir)" Remove-Item -Recurse -Force "$($mountDir)" - $msg = "Done. ISO image is located here: $($SaveDialog.FileName)" + $msg = "Done. ISO image is located here: $SaveDialogFileName" Write-Host $msg $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" @@ -1565,7 +1094,7 @@ public class PrivilegeManager { $sync.form.Dispatcher.Invoke([action]{ Toggle-MicrowinPanel 1 - $sync.MicrowinFinalIsoLocation.Text = "$($SaveDialog.FileName)" + $sync.MicrowinFinalIsoLocation.Text = "$SaveDialogFileName" }) # Allow the machine to sleep again (optional) @@ -1578,23 +1107,12 @@ public class PrivilegeManager { # Invoke-MicrowinBusyInfo -action "warning" -message "Critical error occurred: $_" }) } finally { - Write-Host "DEBUG: ========== PERFORMANCE OPTIMIZATIONS APPLIED ==========" - Write-Host "DEBUG: - Process priority set to High" - Write-Host "DEBUG: - Memory optimization with garbage collection" - Write-Host "DEBUG: - Multi-core processing support detected ($optimalThreads threads available)" - Write-Host "DEBUG: - Fast compression used instead of Max for better performance" - Write-Host "DEBUG: - Optimized DISM settings with reduced logging" - Write-Host "DEBUG: - Enhanced error handling and retry mechanisms" - Write-Host "DEBUG: - Streamlined registry and cleanup operations" - Write-Host "DEBUG: Performance optimizations complete. Process should be significantly faster." # Reset process priority to normal try { $currentProcess = Get-Process -Id $PID $currentProcess.PriorityClass = [System.Diagnostics.ProcessPriorityClass]::Normal - Write-Host "DEBUG: Process priority reset to Normal" } catch { - Write-Host "DEBUG: Could not reset process priority: $($_.Exception.Message)" } $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 index 8c41a41aeb..acdc8e0bc3 100644 --- a/functions/microwin/Set-ScratchFolderPermissions.ps1 +++ b/functions/microwin/Set-ScratchFolderPermissions.ps1 @@ -1,244 +1,32 @@ function Set-ScratchFolderPermissions { <# .SYNOPSIS - Sets DISM-compatible permissions on any directory + Creates a scratch directory for DISM operations .DESCRIPTION - This function sets proper permissions on a directory to make it compatible with DISM operations + 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 set permissions on - - .PARAMETER ShowPermissions - Switch to display the current permissions after setting them + The path to the directory to prepare #> param( [Parameter(Mandatory = $true)] - [string]$Path, - - [switch]$ShowPermissions + [string]$Path ) -function Set-DismCompatiblePermissions { - param([string]$Path) - - # Log which user is setting permissions and which folder - $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name - Write-Host "Current user setting permissions: $currentUser for folder: $Path" -ForegroundColor Magenta - - # Remove ReadOnly attribute from install.wim and boot.wim in the scratch folder - $wimFiles = @('install.wim', 'boot.wim') - Write-Host "Removing ReadOnly attributes from WIM files before mount..." -ForegroundColor Yellow - $wimFilesProcessed = 0 - $wimFilesFound = 0 - - foreach ($wim in $wimFiles) { - $wimPath = Join-Path $Path $wim - if (Test-Path $wimPath) { - $wimFilesFound++ - $item = Get-Item -Path $wimPath -Force - if ($item.Attributes -band [System.IO.FileAttributes]::ReadOnly) { - try { - $item.Attributes = $item.Attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly) - # Verify the change was successful - $item.Refresh() - if ($item.Attributes -band [System.IO.FileAttributes]::ReadOnly) { - Write-Host "CRITICAL: Failed to remove ReadOnly attribute from $wimPath - mount will fail" -ForegroundColor Red - Write-Host "Cannot proceed with mount operation. Exiting." -ForegroundColor Red - return $false - } - Write-Host "Removed ReadOnly attribute from $wimPath" -ForegroundColor Green - $wimFilesProcessed++ - } catch { - Write-Host "CRITICAL: Unable to modify ReadOnly attribute on $wimPath - $($_.Exception.Message)" -ForegroundColor Red - Write-Host "Cannot proceed with mount operation. Exiting." -ForegroundColor Red - return $false - } - } else { - Write-Host "$wimPath is already writable" -ForegroundColor Yellow - $wimFilesProcessed++ - } - Write-Host ("Current attributes for {0}: {1}" -f $wimPath, $item.Attributes) -ForegroundColor Cyan - } else { - Write-Host "$wimPath not found in scratch folder" -ForegroundColor Red - } - } - - # If we found WIM files but couldn't process them all, exit - if ($wimFilesFound -gt 0 -and $wimFilesProcessed -ne $wimFilesFound) { - Write-Host "CRITICAL: Could not make all WIM files writable. Mount operation aborted." -ForegroundColor Red - return $false - } - - if ($wimFilesFound -eq 0) { - Write-Host "No WIM files found in scratch folder - proceeding with folder permissions only" -ForegroundColor Yellow - } else { - Write-Host "Successfully processed $wimFilesProcessed WIM file(s) - ready for mount" -ForegroundColor Green - } try { - Write-Host "Setting DISM-compatible permissions on: $Path" -ForegroundColor Green - + # Create directory if it doesn't exist if (-not (Test-Path $Path)) { New-Item -Path $Path -ItemType Directory -Force | Out-Null - Write-Host "Created directory: $Path" -ForegroundColor Yellow - } - - # Remove read-only attribute from the directory and all subdirectories - try { - Write-Host "Removing read-only attributes from directory and contents..." -ForegroundColor Yellow - - # Remove read-only from the main directory - $item = Get-Item -Path $Path -Force - if ($item.Attributes -band [System.IO.FileAttributes]::ReadOnly) { - $item.Attributes = $item.Attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly) - Write-Host "Removed read-only attribute from directory: $Path" -ForegroundColor Yellow - } - - # Use attrib command to remove read-only from folder and all contents recursively - # This is more reliable than PowerShell for folder attributes - try { - & attrib -R "$Path" /S /D 2>$null - Write-Host "Successfully removed read-only attributes using attrib command" -ForegroundColor Green - } catch { - Write-Host "Warning: attrib command failed, using PowerShell fallback" -ForegroundColor Yellow - - # PowerShell fallback - remove read-only from any existing subdirectories and files - Get-ChildItem -Path $Path -Recurse -Force -ErrorAction SilentlyContinue | ForEach-Object { - if ($_.Attributes -band [System.IO.FileAttributes]::ReadOnly) { - $_.Attributes = $_.Attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly) - } - } - } - } catch { - Write-Host "Warning: Could not modify read-only attributes: $($_.Exception.Message)" -ForegroundColor Yellow - } $acl = Get-Acl -Path $Path - - # Remove inherited permissions and set explicit ones - $acl.SetAccessRuleProtection($true, $false) - - # Clear all existing access rules by creating a new ACL with only essential rules - # This is more reliable than trying to remove individual rules - try { - # Get the current owner - $owner = $acl.Owner - - # Create a fresh ACL object - $newAcl = New-Object System.Security.AccessControl.DirectorySecurity - $newAcl.SetOwner($acl.Owner) - $newAcl.SetAccessRuleProtection($true, $false) - - # Use the fresh ACL instead of trying to modify the existing one - $acl = $newAcl - Write-Host "Created fresh ACL for directory" -ForegroundColor Green - } catch { - Write-Host "Warning: Could not create fresh ACL, attempting manual rule removal" -ForegroundColor Yellow - - # Fallback: Try to remove rules one by one with better error handling - $accessRules = @($acl.Access) # Create array copy to avoid collection modification issues - foreach ($rule in $accessRules) { - if ($rule -ne $null -and $rule.GetType().Name -eq "FileSystemAccessRule") { - try { - $acl.RemoveAccessRule($rule) | Out-Null - } catch { - # Ignore individual rule removal failures - Write-Host "Skipped removing rule for: $($rule.IdentityReference)" -ForegroundColor Gray - } - } - } } - # Administrators - Full Control - $adminRule = New-Object System.Security.AccessControl.FileSystemAccessRule( - "Administrators", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" - ) - $acl.SetAccessRule($adminRule) - - # SYSTEM - Full Control - $systemRule = New-Object System.Security.AccessControl.FileSystemAccessRule( - "SYSTEM", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" - ) - $acl.SetAccessRule($systemRule) - - # Current User - Full Control - $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name - Write-Host "Setting Full Control permissions for current user: $currentUser" -ForegroundColor Cyan - $userRule = New-Object System.Security.AccessControl.FileSystemAccessRule( - $currentUser, "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" - ) - $acl.SetAccessRule($userRule) + # Remove read-only attributes (this is the only thing that actually matters) + & attrib -R "$Path" /S /D 2>$null - # Current User - Full Control - $everyoneRule = New-Object System.Security.AccessControl.FileSystemAccessRule( - "Everyone", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" - ) - $acl.SetAccessRule($everyoneRule) - - # Authenticated Users - Modify (subfolders and files only) - $authUsersRule = New-Object System.Security.AccessControl.FileSystemAccessRule( - "Authenticated Users", "Modify", "ContainerInherit,ObjectInherit", "InheritOnly", "Allow" - ) - $acl.SetAccessRule($authUsersRule) - - # Authenticated Users - Create folders/append data (this folder only) - $authUsersThisFolderRule = New-Object System.Security.AccessControl.FileSystemAccessRule( - "Authenticated Users", "CreateDirectories,AppendData", "None", "None", "Allow" - ) - $acl.SetAccessRule($authUsersThisFolderRule) - - Set-Acl -Path $Path -AclObject $acl - Write-Host "Successfully applied DISM-compatible permissions to: $Path" -ForegroundColor Green return $true } catch { - Write-Host "Failed to set permissions on $Path`: $($_.Exception.Message)" -ForegroundColor Red + Write-Host "Failed to prepare directory $Path`: $($_.Exception.Message)" -ForegroundColor Red return $false } } - -# Main execution -Write-Host "=== DISM-Compatible Permission Setter ===" -ForegroundColor Cyan -Write-Host "" - -# Show who is running the commands -$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name -$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator") -Write-Host "Running as user: $currentUser" -ForegroundColor Yellow -if ($isAdmin) { - Write-Host "Administrator privileges: Yes" -ForegroundColor Green -} else { - Write-Host "Administrator privileges: No" -ForegroundColor Red -} -Write-Host "" - -# Apply permissions -$success = Set-DismCompatiblePermissions -Path $Path - -if ($success -and $ShowPermissions) { - Write-Host "" - Write-Host "Current permissions on $Path`:" -ForegroundColor Cyan - $acl = Get-Acl -Path $Path - foreach ($access in $acl.Access) { - $color = switch ($access.AccessControlType) { - "Allow" { "Green" } - "Deny" { "Red" } - default { "White" } - } - Write-Host " $($access.IdentityReference): $($access.FileSystemRights) ($($access.AccessControlType))" -ForegroundColor $color - } -} - -Write-Host "" -if ($success) { - Write-Host "✅ Permissions applied successfully!" -ForegroundColor Green - Write-Host "" - Write-Host "This directory now has the same permissions as a working DISM scratch folder:" -ForegroundColor Cyan - Write-Host "• Administrators: Full control" -ForegroundColor White - Write-Host "• SYSTEM: Full control" -ForegroundColor White - Write-Host "• Current User ($([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)): Full control" -ForegroundColor White - Write-Host "• Authenticated Users: Modify (subfolders and files only)" -ForegroundColor White - Write-Host "• Authenticated Users: Create folders/append data (this folder only)" -ForegroundColor White -} else { - Write-Host "❌ Failed to apply permissions. Check the error messages above." -ForegroundColor Red -} - - return $success -} From 1a5f3fd0b283688f1e0610e5d1db64a025e585cc Mon Sep 17 00:00:00 2001 From: CodingWonders <101426328+CodingWonders@users.noreply.github.com> Date: Wed, 6 Aug 2025 08:31:11 +0200 Subject: [PATCH 03/10] Delete functions/microwin/Set-WimFilesWritable.ps1 It's empty and not needed --- functions/microwin/Set-WimFilesWritable.ps1 | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 functions/microwin/Set-WimFilesWritable.ps1 diff --git a/functions/microwin/Set-WimFilesWritable.ps1 b/functions/microwin/Set-WimFilesWritable.ps1 deleted file mode 100644 index e69de29bb2..0000000000 From 54c9d4e1e837b94c3a7667fd04dfc83f9d64235f Mon Sep 17 00:00:00 2001 From: Real-MullaC Date: Wed, 6 Aug 2025 08:20:43 +0100 Subject: [PATCH 04/10] Update functions/microwin/Get-ProcessesUsingPath.ps1 Co-authored-by: CodingWonders <101426328+CodingWonders@users.noreply.github.com> --- functions/microwin/Get-ProcessesUsingPath.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/microwin/Get-ProcessesUsingPath.ps1 b/functions/microwin/Get-ProcessesUsingPath.ps1 index c69a322885..a1bd725b1e 100644 --- a/functions/microwin/Get-ProcessesUsingPath.ps1 +++ b/functions/microwin/Get-ProcessesUsingPath.ps1 @@ -27,7 +27,7 @@ function Get-ProcessesUsingPath { $allProcesses = Get-Process -ErrorAction SilentlyContinue foreach ($process in $allProcesses) { try { - if ($process.ProcessName -match "^(System|Idle)$") { + if ($process.ProcessName -match "^(System|Registry|Idle)$") { continue } From 73db0d075b8bf61e578fc9e4c4e24c0805d9d5f9 Mon Sep 17 00:00:00 2001 From: Real-MullaC Date: Wed, 6 Aug 2025 08:29:45 +0100 Subject: [PATCH 05/10] Update functions/microwin/Force-CleanupMountDirectory.ps1 Co-authored-by: CodingWonders <101426328+CodingWonders@users.noreply.github.com> --- functions/microwin/Force-CleanupMountDirectory.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/microwin/Force-CleanupMountDirectory.ps1 b/functions/microwin/Force-CleanupMountDirectory.ps1 index 3d52f5debb..0fb3ca620b 100644 --- a/functions/microwin/Force-CleanupMountDirectory.ps1 +++ b/functions/microwin/Force-CleanupMountDirectory.ps1 @@ -44,7 +44,7 @@ function Force-CleanupMountDirectory { # Try to set the mount directory and its contents to not readonly try { - if (Test-Path $MountPath) { + if (Test-Path "$MountPath") { & attrib -R "$MountPath\*" /S /D 2>$null } } catch { From 6e66d63f9afa1d4693807c1618d7cdc4a4b369df Mon Sep 17 00:00:00 2001 From: Real-MullaC Date: Wed, 6 Aug 2025 09:49:24 +0100 Subject: [PATCH 06/10] Update --- .../microwin/Force-CleanupMountDirectory.ps1 | 22 +++-- .../Invoke-WPFMicroWinGetIsoRunspace.ps1 | 39 +------- .../microwin/Invoke-WPFMicroWinRunspace.ps1 | 98 +++++-------------- functions/private/Copy-Files.ps1 | 12 +-- .../private/Get-LocalGroupNameFromSid.ps1 | 7 ++ .../private/Invoke-GarbageCollection.ps1 | 34 +++++++ 6 files changed, 89 insertions(+), 123 deletions(-) create mode 100644 functions/private/Get-LocalGroupNameFromSid.ps1 create mode 100644 functions/private/Invoke-GarbageCollection.ps1 diff --git a/functions/microwin/Force-CleanupMountDirectory.ps1 b/functions/microwin/Force-CleanupMountDirectory.ps1 index 0fb3ca620b..cdc2fd2dbb 100644 --- a/functions/microwin/Force-CleanupMountDirectory.ps1 +++ b/functions/microwin/Force-CleanupMountDirectory.ps1 @@ -27,7 +27,17 @@ function Force-CleanupMountDirectory { try { $null = reg query $hiveName 2>$null if ($LASTEXITCODE -eq 0) { - reg unload $hiveName 2>$null + # Registry hive is loaded, try to unload it with retries + $attempts = 0 + $maxAttempts = 10 + do { + $attempts++ + reg unload $hiveName 2>$null + if ($LASTEXITCODE -eq 0) { + break + } + Start-Sleep -Milliseconds 100 + } until ($attempts -ge $maxAttempts) } } catch { # Hive not loaded or error checking - continue @@ -35,12 +45,7 @@ function Force-CleanupMountDirectory { } # Force garbage collection to release any PowerShell file handles - [System.GC]::Collect() - [System.GC]::WaitForPendingFinalizers() - [System.GC]::Collect() - - # Wait a moment for handles to be released - Start-Sleep -Seconds 2 + Invoke-GarbageCollection -WaitSeconds 2 # Try to set the mount directory and its contents to not readonly try { @@ -64,8 +69,7 @@ function Force-CleanupMountDirectory { } # Final cleanup - [System.GC]::Collect() - [System.GC]::WaitForPendingFinalizers() + Invoke-GarbageCollection return $true diff --git a/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 b/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 index 4290ea2e53..12f7bb2de8 100644 --- a/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 +++ b/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 @@ -358,27 +358,8 @@ function Invoke-WPFMicroWinGetIsoRunspace { try { $totalTime = Measure-Command { - # Use native Copy-Item instead of Copy-Files function - Write-Host "Starting copy operation with robocopy for better performance..." - $robocopyArgs = @( - "$($driveLetter):", - "$mountDir", - "/E", # Copy subdirectories, including empty ones - "/R:3", # Retry 3 times on failed copies - "/W:1", # Wait 1 second between retries - "/MT:8", # Multi-threaded copying with 8 threads - "/XJ" # Exclude junction points - ) - - $robocopyResult = Start-Process -FilePath "robocopy" -ArgumentList $robocopyArgs -Wait -PassThru -NoNewWindow - - # Robocopy exit codes: 0-7 are success, 8+ are errors - if ($robocopyResult.ExitCode -gt 7) { - throw "Robocopy failed with exit code: $($robocopyResult.ExitCode)" - } - - Write-Host "Robocopy completed with exit code: $($robocopyResult.ExitCode)" - + Copy-Files -Path "$($driveLetter):" -Destination "$mountDir" -Recurse -Force + # Force UI update during long operation $sync.form.Dispatcher.Invoke([action]{ [System.Windows.Forms.Application]::DoEvents() @@ -386,20 +367,7 @@ function Invoke-WPFMicroWinGetIsoRunspace { } Write-Host "Copy complete! Total Time: $($totalTime.Minutes) minutes, $($totalTime.Seconds) seconds" } catch { - - # Fallback to PowerShell Copy-Item if robocopy fails - try { - $totalTime = Measure-Command { - Copy-Item -Path "$($driveLetter):*" -Destination "$mountDir" -Recurse -Force - # Force UI update during long operation - $sync.form.Dispatcher.Invoke([action]{ - [System.Windows.Forms.Application]::DoEvents() - }) - } - Write-Host "Fallback copy complete! Total Time: $($totalTime.Minutes) minutes, $($totalTime.Seconds) seconds" - } catch { - throw $_ - } + 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 @@ -460,6 +428,7 @@ function Invoke-WPFMicroWinGetIsoRunspace { $sync.form.Dispatcher.Invoke([action]{ $sync.MicrowinWindowsFlavors.SelectedIndex = $_.ImageIndex - 1 }) + break # Exit the loop since we found the Pro edition } # Allow UI updates during this loop $sync.form.Dispatcher.Invoke([action]{ diff --git a/functions/microwin/Invoke-WPFMicroWinRunspace.ps1 b/functions/microwin/Invoke-WPFMicroWinRunspace.ps1 index 4cbf3c445f..9c63138b74 100644 --- a/functions/microwin/Invoke-WPFMicroWinRunspace.ps1 +++ b/functions/microwin/Invoke-WPFMicroWinRunspace.ps1 @@ -30,29 +30,7 @@ function Invoke-WPFMicroWinRunspace { param($MicroWinSettings, $DebugPreference) # Function to set DISM-compatible permissions on a directory - function Set-DismCompatiblePermissions { - param([string]$Path) - try { - # Use icacls for reliable permission setting with language-independent SIDs - # Grant full control to Administrators (S-1-5-32-544) - & icacls "$Path" /grant "*S-1-5-32-544:(OI)(CI)F" /T /C | Out-Null - - # Grant full control to SYSTEM (S-1-5-18) - & icacls "$Path" /grant "*S-1-5-18:(OI)(CI)F" /T /C | Out-Null - - # Grant full control to current user - $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name - & icacls "$Path" /grant "${currentUser}:(OI)(CI)F" /T /C | Out-Null - - # Grant modify to Authenticated Users (S-1-5-11) for subfolders and files - & icacls "$Path" /grant "*S-1-5-11:(OI)(CI)M" /T /C | Out-Null - - return $true - } catch { - return $false - } - } $sync.ProcessRunning = $true @@ -117,12 +95,6 @@ function Invoke-WPFMicroWinRunspace { Write-Host "Target ISO location: $SaveDialogFileName" - # Performance optimization: Determine optimal thread count - $coreCount = (Get-WmiObject -Class Win32_Processor | Measure-Object -Property NumberOfCores -Sum).Sum - $logicalProcessors = (Get-WmiObject -Class Win32_ComputerSystem).NumberOfLogicalProcessors - $optimalThreads = [Math]::Min($logicalProcessors, [Math]::Max(2, $coreCount)) - Write-Host "System has $coreCount cores, $logicalProcessors logical processors. Using $optimalThreads threads for optimal performance." - # Extract settings from hashtable $index = $MicroWinSettings.selectedIndex $mountDir = $MicroWinSettings.mountDir @@ -213,10 +185,7 @@ function Invoke-WPFMicroWinRunspace { } # Check if running as administrator - $currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) - $isAdmin = $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) - - if (-not $isAdmin) { + 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]{ @@ -286,12 +255,6 @@ function Invoke-WPFMicroWinRunspace { # Pre-mount system checks - # Check if DISM is available and working - try { - $dismCheck = & dism /? 2>&1 - } catch { - } - # Check available disk space try { $scratchDrive = Split-Path $scratchDir -Qualifier @@ -303,23 +266,12 @@ function Invoke-WPFMicroWinRunspace { } catch { } - # Check if scratch directory is accessible and set proper permissions + # Check if scratch directory is accessible try { if (-not (Test-Path $scratchDir)) { New-Item -Path $scratchDir -ItemType Directory -Force | Out-Null } - # Set proper permissions for DISM operations using the helper function - $permissionsSet = Set-DismCompatiblePermissions -Path $scratchDir - - if ($permissionsSet) { - # Verify permissions were set correctly - $newAcl = Get-Acl -Path $scratchDir - foreach ($access in $newAcl.Access) { - } - } else { - } - # Test write access $testFile = Join-Path $scratchDir "test_access.tmp" "test" | Out-File -FilePath $testFile -Force @@ -357,22 +309,14 @@ function Invoke-WPFMicroWinRunspace { foreach ($wimFilePath in $wimFilePaths) { if (Test-Path $wimFilePath) { try { - $wimItem = Get-Item -Path $wimFilePath -Force - if ($wimItem.Attributes -band [System.IO.FileAttributes]::ReadOnly) { - $wimItem.Attributes = $wimItem.Attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly) - - # Verify the change was successful - $wimItem.Refresh() - if ($wimItem.Attributes -band [System.IO.FileAttributes]::ReadOnly) { - $criticalWimError = $true - } else { - } - } else { + # Remove ReadOnly attribute using attrib command + & attrib -R "$wimFilePath" 2>$null + if ($LASTEXITCODE -ne 0) { + $criticalWimError = $true } } catch { $criticalWimError = $true } - } else { } } @@ -899,14 +843,25 @@ function Invoke-WPFMicroWinRunspace { # Last attempt - try multiple fallback strategies - # Try DISM discard + + # First, commit the image try { - $dismResult = & dism /english /unmount-image /mountdir:"$scratchDir" /discard /loglevel:1 - if ($LASTEXITCODE -eq 0) { - $dismountSuccess = $true - } else { - } - } catch { + & 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 @@ -942,9 +897,8 @@ function Invoke-WPFMicroWinRunspace { try { Write-Host "Exporting image into $mountDir\sources\install2.wim with optimized settings..." try { - # Use Fast compression for better performance, especially during development/testing - # Users can change this to "Max" if they prefer smaller file size over speed - Export-WindowsImage -SourceImagePath "$mountDir\sources\install.wim" -SourceIndex $index -DestinationImagePath "$mountDir\sources\install2.wim" -CompressionType "Fast" + # 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 diff --git a/functions/private/Copy-Files.ps1 b/functions/private/Copy-Files.ps1 index f9a36ed259..2b5166ff07 100644 --- a/functions/private/Copy-Files.ps1 +++ b/functions/private/Copy-Files.ps1 @@ -40,11 +40,9 @@ function Copy-Files { try { Copy-Item $file.FullName ($destination+$restpath) -ErrorAction Stop -Force:$force - # Use more robust method to remove ReadOnly attribute - $copiedFile = Get-Item -Path ($destination+$restpath) -Force -ErrorAction SilentlyContinue - if ($copiedFile -and ($copiedFile.Attributes -band [System.IO.FileAttributes]::ReadOnly)) { - $copiedFile.Attributes = $copiedFile.Attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly) - } + + # Remove ReadOnly attribute using attrib for consistency + & attrib -R ($destination+$restpath) 2>$null # Force garbage collection to release file handles $copiedFile = $null @@ -53,8 +51,8 @@ function Copy-Files { # Try alternative method if standard copy fails try { [System.IO.File]::Copy($file.FullName, ($destination+$restpath), $force) - # Remove ReadOnly using .NET method - [System.IO.File]::SetAttributes(($destination+$restpath), [System.IO.File]::GetAttributes(($destination+$restpath)) -band (-bnot [System.IO.FileAttributes]::ReadOnly)) + # Remove ReadOnly attribute using attrib for consistency + & attrib -R ($destination+$restpath) 2>$null } catch { Write-Debug "Alternative copy method also failed: $($_.Exception.Message)" } diff --git a/functions/private/Get-LocalGroupNameFromSid.ps1 b/functions/private/Get-LocalGroupNameFromSid.ps1 new file mode 100644 index 0000000000..83774a2fc0 --- /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..1d0f82e9b0 --- /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 + } +} From 46fdd8205633d1a5b758eac5b2e371d42c9f9aa0 Mon Sep 17 00:00:00 2001 From: Real-MullaC Date: Wed, 6 Aug 2025 09:51:56 +0100 Subject: [PATCH 07/10] Update --- functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 | 4 +--- functions/microwin/Invoke-WPFMicroWinRunspace.ps1 | 7 +------ 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 b/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 index 12f7bb2de8..66fcb3ef17 100644 --- a/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 +++ b/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 @@ -402,10 +402,8 @@ function Invoke-WPFMicroWinGetIsoRunspace { $images = Get-WindowsImage -ImagePath $wimFile $images | ForEach-Object { - $imageIdx = $_.ImageIndex - $imageName = $_.ImageName $sync.form.Dispatcher.Invoke([action]{ - $sync.MicrowinWindowsFlavors.Items.Add("$imageIdx : $imageName") + $sync.MicrowinWindowsFlavors.Items.Add("$_.ImageIndex : $_.ImageName") }) } } catch { diff --git a/functions/microwin/Invoke-WPFMicroWinRunspace.ps1 b/functions/microwin/Invoke-WPFMicroWinRunspace.ps1 index 9c63138b74..9a74aaa900 100644 --- a/functions/microwin/Invoke-WPFMicroWinRunspace.ps1 +++ b/functions/microwin/Invoke-WPFMicroWinRunspace.ps1 @@ -395,11 +395,6 @@ function Invoke-WPFMicroWinRunspace { $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object System.Security.Principal.WindowsPrincipal($currentUser) $isAdmin = $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - - # Show privilege status again - $tokenPrivs = whoami /priv | Out-String - $tokenPrivs -split "`n" | Where-Object { $_ -match "Se(Backup|Restore|Security|TakeOwnership|ManageVolume)Privilege" } | ForEach-Object { - } } catch { } @@ -460,7 +455,7 @@ function Invoke-WPFMicroWinRunspace { 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 /loglevel:1 | Out-Host + dism /English /image:$scratchDir /add-driver /driver:$driverPath /recurse /forceunsigned | Out-Host } else { Write-Host "Path to drivers is invalid continuing without driver injection" } From 0366f3ef3d1900c0d0e90ebb1c6b3a38e875b362 Mon Sep 17 00:00:00 2001 From: Real-MullaC Date: Wed, 6 Aug 2025 10:03:12 +0100 Subject: [PATCH 08/10] Update --- .../microwin/Force-CleanupMountDirectory.ps1 | 7 ++----- .../Invoke-WPFMicroWinGetIsoRunspace.ps1 | 2 +- functions/private/Copy-Files.ps1 | 19 ++++++++++--------- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/functions/microwin/Force-CleanupMountDirectory.ps1 b/functions/microwin/Force-CleanupMountDirectory.ps1 index cdc2fd2dbb..8f223aea40 100644 --- a/functions/microwin/Force-CleanupMountDirectory.ps1 +++ b/functions/microwin/Force-CleanupMountDirectory.ps1 @@ -28,16 +28,13 @@ function Force-CleanupMountDirectory { $null = reg query $hiveName 2>$null if ($LASTEXITCODE -eq 0) { # Registry hive is loaded, try to unload it with retries - $attempts = 0 - $maxAttempts = 10 - do { - $attempts++ + while ($true) { reg unload $hiveName 2>$null if ($LASTEXITCODE -eq 0) { break } Start-Sleep -Milliseconds 100 - } until ($attempts -ge $maxAttempts) + } } } catch { # Hive not loaded or error checking - continue diff --git a/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 b/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 index 66fcb3ef17..e720ebf527 100644 --- a/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 +++ b/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 @@ -426,7 +426,7 @@ function Invoke-WPFMicroWinGetIsoRunspace { $sync.form.Dispatcher.Invoke([action]{ $sync.MicrowinWindowsFlavors.SelectedIndex = $_.ImageIndex - 1 }) - break # Exit the loop since we found the Pro edition + break } # Allow UI updates during this loop $sync.form.Dispatcher.Invoke([action]{ diff --git a/functions/private/Copy-Files.ps1 b/functions/private/Copy-Files.ps1 index 2b5166ff07..79993f003d 100644 --- a/functions/private/Copy-Files.ps1 +++ b/functions/private/Copy-Files.ps1 @@ -30,19 +30,20 @@ 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)" + $targetPath = Join-Path $destination $restpath + Write-Debug "Copy from $($file.FullName) to $targetPath" try { - Copy-Item $file.FullName ($destination+$restpath) -ErrorAction Stop -Force:$force - + Copy-Item $file.FullName $targetPath -ErrorAction Stop -Force:$force # Remove ReadOnly attribute using attrib for consistency - & attrib -R ($destination+$restpath) 2>$null + & attrib -R $targetPath 2>$null # Force garbage collection to release file handles $copiedFile = $null @@ -50,9 +51,9 @@ function Copy-Files { Write-Debug "Failed to copy $($file.FullName): $($_.Exception.Message)" # Try alternative method if standard copy fails try { - [System.IO.File]::Copy($file.FullName, ($destination+$restpath), $force) + [System.IO.File]::Copy($file.FullName, $targetPath, $force) # Remove ReadOnly attribute using attrib for consistency - & attrib -R ($destination+$restpath) 2>$null + & attrib -R ($destination\$restpath) 2>$null } catch { Write-Debug "Alternative copy method also failed: $($_.Exception.Message)" } From 5f5815c0535d51856d5a281e0c1dff42f08b4f3b Mon Sep 17 00:00:00 2001 From: Real-MullaC Date: Wed, 6 Aug 2025 10:06:19 +0100 Subject: [PATCH 09/10] Update --- functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 | 2 +- functions/microwin/Invoke-WPFMicroWinRunspace.ps1 | 2 +- functions/private/Get-LocalGroupNameFromSid.ps1 | 2 +- functions/private/Invoke-GarbageCollection.ps1 | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 b/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 index e720ebf527..26ddeb8276 100644 --- a/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 +++ b/functions/microwin/Invoke-WPFMicroWinGetIsoRunspace.ps1 @@ -359,7 +359,7 @@ function Invoke-WPFMicroWinGetIsoRunspace { $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() diff --git a/functions/microwin/Invoke-WPFMicroWinRunspace.ps1 b/functions/microwin/Invoke-WPFMicroWinRunspace.ps1 index 9a74aaa900..5103085881 100644 --- a/functions/microwin/Invoke-WPFMicroWinRunspace.ps1 +++ b/functions/microwin/Invoke-WPFMicroWinRunspace.ps1 @@ -346,7 +346,7 @@ function Invoke-WPFMicroWinRunspace { } 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 diff --git a/functions/private/Get-LocalGroupNameFromSid.ps1 b/functions/private/Get-LocalGroupNameFromSid.ps1 index 83774a2fc0..a31542e0db 100644 --- a/functions/private/Get-LocalGroupNameFromSid.ps1 +++ b/functions/private/Get-LocalGroupNameFromSid.ps1 @@ -1,6 +1,6 @@ function Get-LocalGroupNameFromSid { param ( - [Parameter(Mandatory, Position = 0)] [string] $sid + [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 index 1d0f82e9b0..9ae8d76f56 100644 --- a/functions/private/Invoke-GarbageCollection.ps1 +++ b/functions/private/Invoke-GarbageCollection.ps1 @@ -12,7 +12,7 @@ function Invoke-GarbageCollection { .EXAMPLE Invoke-GarbageCollection - + .EXAMPLE Invoke-GarbageCollection -WaitSeconds 2 #> From 6785b2ac7ff7c299392bae6b285608ff6da8df28 Mon Sep 17 00:00:00 2001 From: Real-MullaC Date: Wed, 6 Aug 2025 10:07:27 +0100 Subject: [PATCH 10/10] Update Copy-Files.ps1 --- functions/private/Copy-Files.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/private/Copy-Files.ps1 b/functions/private/Copy-Files.ps1 index 79993f003d..cdd9436386 100644 --- a/functions/private/Copy-Files.ps1 +++ b/functions/private/Copy-Files.ps1 @@ -53,7 +53,7 @@ function Copy-Files { try { [System.IO.File]::Copy($file.FullName, $targetPath, $force) # Remove ReadOnly attribute using attrib for consistency - & attrib -R ($destination\$restpath) 2>$null + & attrib -R $targetPath 2>$null } catch { Write-Debug "Alternative copy method also failed: $($_.Exception.Message)" }