#The below lines will remove the unattend.xml that gets the machine into audit mode. If not removed, the OS will get stuck booting to audit mode each time. #Also kills the sysprep process in order to automate sysprep generalize function Stop-SysprepProcess { $sysprepProcesses = @(Get-Process -Name 'sysprep' -ErrorAction SilentlyContinue) if (0 -eq $sysprepProcesses.Count) { return } Write-Host 'Stopping existing Sysprep process before continuing...' $sysprepProcesses | Stop-Process -Force -ErrorAction SilentlyContinue $deadline = (Get-Date).AddSeconds(30) do { Start-Sleep -Seconds 1 $sysprepProcesses = @(Get-Process -Name 'sysprep' -ErrorAction SilentlyContinue) } while ($sysprepProcesses.Count -gt 0 -and (Get-Date) -lt $deadline) if ($sysprepProcesses.Count -gt 0) { throw 'Unable to stop the existing Sysprep process before continuing.' } } function Stop-CompetingSysprepProcess { param( [Parameter(Mandatory)] [int]$SysprepProcessId ) $competingSysprepProcesses = @(Get-Process -Name 'sysprep' -ErrorAction SilentlyContinue | Where-Object { $_.Id -ne $SysprepProcessId }) foreach ($competingSysprepProcess in $competingSysprepProcesses) { Write-Host "Stopping competing Sysprep process $($competingSysprepProcess.Id)." Stop-Process -Id $competingSysprepProcess.Id -Force -ErrorAction SilentlyContinue } } function Join-ProcessArgumentList { param( [Parameter(Mandatory)] [string[]]$ArgumentList ) $escapedArguments = foreach ($argument in $ArgumentList) { if ($argument -match '[\s"]') { '"{0}"' -f ($argument -replace '"', '\"') } else { $argument } } return ($escapedArguments -join ' ') } function Invoke-SysprepProcess { param( [Parameter(Mandatory)] [string[]]$ArgumentList ) $sysprepPath = 'C:\windows\system32\sysprep\sysprep.exe' $sysprepArgumentString = Join-ProcessArgumentList -ArgumentList $ArgumentList Write-Host "Starting Sysprep with arguments: $sysprepArgumentString" $sysprepProcess = Start-Process -FilePath $sysprepPath -ArgumentList $sysprepArgumentString -PassThru -ErrorAction Stop do { Stop-CompetingSysprepProcess -SysprepProcessId $sysprepProcess.Id Start-Sleep -Seconds 1 $sysprepProcess.Refresh() } while (-not $sysprepProcess.HasExited) return $sysprepProcess.ExitCode } $orchestrationBootstrapPath = 'C:\Windows\Setup\Scripts\Start-FFUOrchestration.ps1' if (Test-Path -LiteralPath $orchestrationBootstrapPath -PathType Leaf) { Write-Host "Removing build-only orchestration bootstrap: $orchestrationBootstrapPath" try { Remove-Item -LiteralPath $orchestrationBootstrapPath -Force -ErrorAction Stop } catch { Write-Warning "Unable to remove build-only orchestration bootstrap '$orchestrationBootstrapPath': $($_.Exception.Message)" } } Write-Host "Removing existing unattend.xml files and stopping sysprep process if running..." Remove-Item -Path "C:\windows\panther\unattend\unattend.xml" -Force -ErrorAction SilentlyContinue Remove-Item -Path "C:\windows\panther\unattend.xml" -Force -ErrorAction SilentlyContinue Stop-SysprepProcess $sysprepRetryTaskName = 'FFU-Sysprep-Retry' $sysprepRetryRunOncePath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce' $sysprepRetryRunOnceName = '!FFU-Sysprep-Retry' $sysprepRetryStatePath = 'C:\Windows\Setup\Scripts\Logs\SysprepRetry.state' $sysprepRetryScriptPath = 'C:\Windows\Setup\Scripts\Run-FFUSysprepRetry.ps1' $maxSysprepRebootRetries = 1 function Disable-AuditModeSysprepStartup { $startupRegistryPaths = @( 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run', 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce', 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run', 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce' ) foreach ($startupRegistryPath in $startupRegistryPaths) { $startupRegistryKey = Get-Item -Path $startupRegistryPath -ErrorAction SilentlyContinue if ($null -eq $startupRegistryKey) { continue } foreach ($valueName in $startupRegistryKey.GetValueNames()) { if ($valueName -eq $sysprepRetryRunOnceName) { continue } $valueData = [string]$startupRegistryKey.GetValue($valueName) if ($valueData -notmatch '(?i)sysprep\.exe') { continue } Write-Host "Removing Audit mode Sysprep startup value '$valueName' from '$startupRegistryPath'." Remove-ItemProperty -Path $startupRegistryPath -Name $valueName -ErrorAction SilentlyContinue } } } Disable-AuditModeSysprepStartup function Resolve-FFUAppsMediaRoot { $appsRoot = ([string]$env:FFUAppsRoot).Trim().TrimEnd('\') if (-not [string]::IsNullOrWhiteSpace($appsRoot) -and (Test-Path -Path $appsRoot)) { return $appsRoot } foreach ($driveInfo in Get-PSDrive -PSProvider FileSystem) { if ([string]::IsNullOrWhiteSpace([string]$driveInfo.Root)) { continue } $candidateOrchestrator = Join-Path -Path $driveInfo.Root -ChildPath 'Orchestration\Orchestrator.ps1' $candidateSysprep = Join-Path -Path $driveInfo.Root -ChildPath 'Orchestration\Run-Sysprep.ps1' if ((Test-Path -Path $candidateOrchestrator -PathType Leaf) -and (Test-Path -Path $candidateSysprep -PathType Leaf)) { return $driveInfo.Root.TrimEnd('\') } } return 'D:' } function Get-SysprepRetryCount { if (-not (Test-Path -Path $sysprepRetryStatePath)) { return 0 } try { $retryCount = [int](Get-Content -Path $sysprepRetryStatePath -Raw -ErrorAction Stop).Trim() return $retryCount } catch { return 0 } } function Set-SysprepRetryCount { param( [Parameter(Mandatory)] [int]$RetryCount ) $retryStateFolder = Split-Path -Path $sysprepRetryStatePath -Parent New-Item -Path $retryStateFolder -ItemType Directory -Force | Out-Null Set-Content -Path $sysprepRetryStatePath -Value ([string]$RetryCount) -Encoding ASCII -Force } function Clear-SysprepRetryResume { Unregister-ScheduledTask -TaskName $sysprepRetryTaskName -Confirm:$false -ErrorAction SilentlyContinue Remove-ItemProperty -Path $sysprepRetryRunOncePath -Name $sysprepRetryRunOnceName -ErrorAction SilentlyContinue Remove-ItemProperty -Path $sysprepRetryRunOncePath -Name $sysprepRetryTaskName -ErrorAction SilentlyContinue Remove-Item -Path $sysprepRetryScriptPath -Force -ErrorAction SilentlyContinue } function Clear-SysprepRetryState { Clear-SysprepRetryResume Remove-Item -Path $sysprepRetryStatePath -Force -ErrorAction SilentlyContinue } function Test-PendingReboot { return ((Get-PendingRebootReasons).Count -gt 0) } function Get-RegistryChildSummary { param( [Parameter(Mandatory)] [string]$Path ) $childNames = @(Get-ChildItem -Path $Path -ErrorAction SilentlyContinue | Select-Object -First 8 -ExpandProperty PSChildName) if (0 -eq $childNames.Count) { return '' } return ' Child keys: {0}' -f ($childNames -join ', ') } function Get-PendingRebootReasons { $pendingRebootReasons = @() $cbsRebootPendingPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending' if (Test-Path -Path $cbsRebootPendingPath) { $pendingRebootReasons += "CBS RebootPending marker found at $cbsRebootPendingPath.$(Get-RegistryChildSummary -Path $cbsRebootPendingPath)" } $cbsPackagesPendingPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\PackagesPending' if (Test-Path -Path $cbsPackagesPendingPath) { $pendingRebootReasons += "CBS PackagesPending marker found at $cbsPackagesPendingPath.$(Get-RegistryChildSummary -Path $cbsPackagesPendingPath)" } $windowsUpdateRebootRequiredPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired' if (Test-Path -Path $windowsUpdateRebootRequiredPath) { $pendingRebootReasons += "Windows Update RebootRequired marker found at $windowsUpdateRebootRequiredPath.$(Get-RegistryChildSummary -Path $windowsUpdateRebootRequiredPath)" } $pendingXmlPath = 'C:\Windows\WinSxS\pending.xml' if (Test-Path -Path $pendingXmlPath) { $pendingXml = Get-Item -Path $pendingXmlPath -ErrorAction SilentlyContinue if ($null -ne $pendingXml) { $pendingRebootReasons += "WinSxS pending.xml exists at $pendingXmlPath. LastWriteTime: $($pendingXml.LastWriteTime); Size: $($pendingXml.Length) bytes." } else { $pendingRebootReasons += "WinSxS pending.xml exists at $pendingXmlPath." } } $sessionManagerPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' foreach ($pendingRenameValueName in @('PendingFileRenameOperations', 'PendingFileRenameOperations2')) { $pendingFileRename = (Get-ItemProperty -Path $sessionManagerPath -Name $pendingRenameValueName -ErrorAction SilentlyContinue).$pendingRenameValueName if ($null -eq $pendingFileRename) { continue } $pendingFileRenameEntries = @($pendingFileRename | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) }) $pendingRebootReasons += "Session Manager $pendingRenameValueName contains $($pendingFileRenameEntries.Count) pending file rename/delete entries:" $pendingFileRenameIndex = 1 foreach ($pendingFileRenameEntry in $pendingFileRenameEntries) { $pendingRebootReasons += 'Session Manager {0} entry {1}: {2}' -f $pendingRenameValueName, $pendingFileRenameIndex, $pendingFileRenameEntry $pendingFileRenameIndex++ } } $updatesPath = 'HKLM:\SOFTWARE\Microsoft\Updates' $updateExeVolatile = (Get-ItemProperty -Path $updatesPath -Name 'UpdateExeVolatile' -ErrorAction SilentlyContinue).UpdateExeVolatile if ($null -ne $updateExeVolatile -and 0 -ne $updateExeVolatile) { $pendingRebootReasons += "Microsoft Updates UpdateExeVolatile is $updateExeVolatile at $updatesPath." } return @($pendingRebootReasons) } function Write-PendingRebootReasons { param( [string[]]$PendingRebootReasons ) if ($null -eq $PendingRebootReasons -or 0 -eq $PendingRebootReasons.Count) { Write-Host 'Pending reboot detected, but no specific pending reboot marker details were collected.' return } Write-Host 'Pending reboot reason(s) detected:' foreach ($pendingRebootReason in $PendingRebootReasons) { Write-Host " - $pendingRebootReason" } } function Test-SysprepPendingRebootFailure { $sysprepLogPaths = @( 'C:\Windows\System32\Sysprep\Panther\setuperr.log', 'C:\Windows\System32\Sysprep\Panther\setupact.log' ) foreach ($sysprepLogPath in $sysprepLogPaths) { if (-not (Test-Path -Path $sysprepLogPath)) { continue } $sysprepLogText = Get-Content -Path $sysprepLogPath -Tail 200 -ErrorAction SilentlyContinue | Out-String if ($sysprepLogText -match '0x8007139f|pending\s+reboot|reboot\s+required|required\s+reboot|Windows Updates?.*reboot|pre-validate sysprep cleanup internal providers') { return $true } } return (Test-PendingReboot) } function New-SysprepRetryLauncher { $retryScriptFolder = Split-Path -Path $sysprepRetryScriptPath -Parent New-Item -Path $retryScriptFolder -ItemType Directory -Force | Out-Null $retryScript = @' $ErrorActionPreference = 'Stop' $logPath = 'C:\Windows\Temp\FFUSysprepRetry.log' Start-Transcript -Path $logPath -Append -Force | Out-Null function Disable-AuditModeSysprepStartup { $startupRegistryPaths = @( 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run', 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce', 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run', 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce' ) foreach ($startupRegistryPath in $startupRegistryPaths) { $startupRegistryKey = Get-Item -Path $startupRegistryPath -ErrorAction SilentlyContinue if ($null -eq $startupRegistryKey) { continue } foreach ($valueName in $startupRegistryKey.GetValueNames()) { $valueData = [string]$startupRegistryKey.GetValue($valueName) if ($valueData -notmatch '(?i)sysprep\.exe') { continue } Write-Host "Removing Audit mode Sysprep startup value '$valueName' from '$startupRegistryPath'." Remove-ItemProperty -Path $startupRegistryPath -Name $valueName -ErrorAction SilentlyContinue } } } try { Write-Host 'Starting FFU sysprep retry.' Disable-AuditModeSysprepStartup Write-Host 'Waiting for the Audit mode desktop shell to load before retrying.' $explorerProcesses = @() $shellDeadline = (Get-Date).AddMinutes(2) do { $explorerProcesses = @(Get-Process -Name 'explorer' -ErrorAction SilentlyContinue) if ($explorerProcesses.Count -gt 0) { Write-Host 'Detected Explorer shell. Continuing retry.' break } Start-Sleep -Seconds 2 } while ((Get-Date) -lt $shellDeadline) if (0 -eq $explorerProcesses.Count) { Write-Host 'Explorer shell was not detected before timeout. Continuing retry.' } Write-Host 'Waiting briefly for the Audit mode Sysprep UI to launch, then closing it before retrying.' $sysprepUiSeen = $false $minimumSysprepUiWaitDeadline = (Get-Date).AddSeconds(15) $sysprepUiQuietDeadline = (Get-Date).AddSeconds(15) $sysprepUiDeadline = (Get-Date).AddMinutes(2) do { $sysprepProcesses = @(Get-Process -Name 'sysprep' -ErrorAction SilentlyContinue) if ($sysprepProcesses.Count -gt 0) { $sysprepUiSeen = $true foreach ($sysprepProcess in $sysprepProcesses) { Write-Host "Stopping Audit mode Sysprep process $($sysprepProcess.Id) before retrying." Stop-Process -Id $sysprepProcess.Id -Force -ErrorAction SilentlyContinue } $sysprepUiQuietDeadline = (Get-Date).AddSeconds(10) } elseif ((Get-Date) -ge $minimumSysprepUiWaitDeadline -and (Get-Date) -ge $sysprepUiQuietDeadline) { break } Start-Sleep -Seconds 1 } while ((Get-Date) -lt $sysprepUiDeadline) if ($sysprepUiSeen) { Write-Host 'Audit mode Sysprep UI is closed.' } else { Write-Host 'Audit mode Sysprep UI was not detected before retrying.' } $deadline = (Get-Date).AddMinutes(10) $sysprepScript = $null do { $fileSystemDrives = @(Get-PSDrive -PSProvider FileSystem | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.Root) }) foreach ($driveInfo in $fileSystemDrives) { $candidatePath = Join-Path -Path $driveInfo.Root -ChildPath 'Orchestration\Run-Sysprep.ps1' Write-Host "Checking for sysprep script at $candidatePath" if (Test-Path -Path $candidatePath -PathType Leaf) { $sysprepScript = $candidatePath break } } if (-not [string]::IsNullOrWhiteSpace($sysprepScript)) { break } Write-Host 'Apps ISO sysprep script was not found yet. Waiting before retry.' Start-Sleep -Seconds 5 } while ((Get-Date) -lt $deadline) if ([string]::IsNullOrWhiteSpace($sysprepScript)) { throw 'Unable to locate Apps ISO Run-Sysprep.ps1 after waiting for Apps media.' } $env:FFUAppsRoot = Split-Path -Parent (Split-Path -Parent $sysprepScript) Write-Host "Using Apps media root: $env:FFUAppsRoot" Write-Host "Launching sysprep script: $sysprepScript" & $sysprepScript } catch { Write-Error "FFU sysprep retry failed: $($_.Exception.Message)" throw } finally { Stop-Transcript -ErrorAction SilentlyContinue | Out-Null } '@ Set-Content -Path $sysprepRetryScriptPath -Value $retryScript -Encoding ASCII -Force } function Register-SysprepRetryResume { Disable-AuditModeSysprepStartup New-SysprepRetryLauncher $cmdPath = Join-Path -Path $env:SystemRoot -ChildPath 'System32\cmd.exe' $powerShellPath = Join-Path -Path $env:SystemRoot -ChildPath 'System32\WindowsPowerShell\v1.0\powershell.exe' $runOnceCommand = "`"$cmdPath`" /c start `"FFU Sysprep Retry`" `"$powerShellPath`" -NoProfile -ExecutionPolicy Bypass -File `"$sysprepRetryScriptPath`"" New-Item -Path $sysprepRetryRunOncePath -Force | Out-Null New-ItemProperty -Path $sysprepRetryRunOncePath -Name $sysprepRetryRunOnceName -Value $runOnceCommand -PropertyType String -Force | Out-Null } function Restart-SysprepRetry { param( [Parameter(Mandatory)] [string]$Reason, [string[]]$PendingRebootReasons ) $retryCount = Get-SysprepRetryCount if ($retryCount -ge $maxSysprepRebootRetries) { return $false } Set-SysprepRetryCount -RetryCount ($retryCount + 1) Register-SysprepRetryResume Write-Host $Reason Write-PendingRebootReasons -PendingRebootReasons $PendingRebootReasons Write-Host 'Restarting now. Sysprep will resume during the next Audit mode logon.' Restart-Computer -Force -ErrorAction Stop return $true } # Detect and remediate per-user, non-provisioned Appx packages that would block Sysprep. Write-Host "Checking for per-user Appx packages not provisioned for all users (potential Sysprep blockers)..." # Build hash set of provisioned package families (DisplayName_PublisherId). $provFamilies = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase) Get-AppxProvisionedPackage -Online | ForEach-Object { $family = '{0}_{1}' -f $_.DisplayName, $_.PublisherId [void]$provFamilies.Add($family) } # Collect current user Appx packages excluding frameworks, resource packs, and non-removable packages. $userApps = Get-AppxPackage -User $env:USERNAME | Where-Object { $_.Status -eq 'Ok' -and -not $_.IsFramework -and -not $_.IsResourcePackage -and -not $_.NonRemovable } # Identify packages not provisioned (per-user only). $notProvisioned = foreach ($pkg in $userApps) { if (-not $provFamilies.Contains($pkg.PackageFamilyName)) { [PSCustomObject]@{ Name = $pkg.Name PackageFamilyName = $pkg.PackageFamilyName Version = $pkg.Version SignatureKind = $pkg.SignatureKind PackageFullName = $pkg.PackageFullName } } } if ($notProvisioned) { Write-Host "Found $($notProvisioned.Count) per-user Appx package(s) not provisioned for all users:" $notProvisioned | Sort-Object PackageFamilyName | Format-Table -AutoSize -Property Name,PackageFamilyName,Version Write-Host "Attempting removal of per-user, non-provisioned Appx packages..." foreach ($pkg in $notProvisioned) { try { Write-Host "Removing $($pkg.PackageFullName)..." Remove-AppxPackage -Package $pkg.PackageFullName -ErrorAction Stop } catch { Write-Warning "Failed to remove $($pkg.PackageFullName): $($_.Exception.Message)" } } # Re-check after attempted removals. $remaining = @() $currentUserApps = Get-AppxPackage -User $env:USERNAME | Where-Object { $_.Status -eq 'Ok' -and -not $_.IsFramework -and -not $_.IsResourcePackage -and -not $_.NonRemovable } foreach ($pkg in $currentUserApps) { if (-not $provFamilies.Contains($pkg.PackageFamilyName)) { $remaining += $pkg } } if ($remaining.Count -gt 0) { Write-Error "Unable to remove all per-user, non-provisioned Appx packages. Sysprep cannot continue." $remaining | Sort-Object PackageFamilyName | Format-Table -AutoSize -Property Name,PackageFamilyName,Version throw "Sysprep aborted due to unresolved per-user Appx packages. Resolve manually and re-run." } else { Write-Host "All per-user, non-provisioned Appx packages were successfully removed." } } else { Write-Host "No per-user, non-provisioned Appx packages detected." } # If an Unattend.xml has been provided on the mounted Apps ISO, # pass it to sysprep; otherwise, run without /unattend. $appsMediaRoot = Resolve-FFUAppsMediaRoot $env:FFUAppsRoot = $appsMediaRoot $unattendOnAppsIso = Join-Path -Path $appsMediaRoot -ChildPath "Unattend\Unattend.xml" $sysprepArguments = @('/quiet', '/generalize', '/oobe') if (Test-Path -Path $unattendOnAppsIso) { Write-Host "Using $unattendOnAppsIso from Apps ISO..." $sysprepArguments += "/unattend:$unattendOnAppsIso" } $pendingRebootReasons = @(Get-PendingRebootReasons) if ($pendingRebootReasons.Count -gt 0) { if (Restart-SysprepRetry -Reason 'Pending reboot detected immediately before sysprep. Restarting before continuing sysprep...' -PendingRebootReasons $pendingRebootReasons) { return } Clear-SysprepRetryState throw "Pending reboot is still detected after $maxSysprepRebootRetries sysprep retry reboot(s). Sysprep cannot continue." } Clear-SysprepRetryResume Disable-AuditModeSysprepStartup Stop-SysprepProcess $sysprepExitCode = Invoke-SysprepProcess -ArgumentList $sysprepArguments if (0 -eq $sysprepExitCode) { Clear-SysprepRetryState return } if (Test-SysprepPendingRebootFailure) { if (Restart-SysprepRetry -Reason 'Sysprep reported a pending-reboot condition. Restarting and retrying sysprep once...' -PendingRebootReasons (Get-PendingRebootReasons)) { return } } Clear-SysprepRetryState throw "Sysprep failed with exit code $sysprepExitCode. Review C:\Windows\System32\Sysprep\Panther\setuperr.log and setupact.log."