mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-08-12 21:53:05 -06:00
Prevent cleanmgr hangs in disk cleanup orchestration
Run cleanmgr.exe without blocking on Start-Process -Wait, pump WM_NULL to its threads while enforcing a timeout, and always remove StateFlags0000 registry values in a finally block.
This commit is contained in:
@@ -3,19 +3,125 @@
|
||||
|
||||
$rootKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches"
|
||||
|
||||
# Set StateFlags0000 to 2 for all subkeys except "Offline Pages Files"
|
||||
Get-ChildItem -Path $rootKey | ForEach-Object {
|
||||
if ($_.PSChildName -ne "Offline Pages Files") {
|
||||
Set-ItemProperty -Path $_.PSPath -Name "StateFlags0000" -Type DWord -Value 2 -Force
|
||||
$timeoutMinutes = 30
|
||||
$pumpIntervalSeconds = 5
|
||||
$wmNull = 0x0000
|
||||
|
||||
if (-not ("DiskCleanup.NativeMethods" -as [type])) {
|
||||
Add-Type -TypeDefinition @'
|
||||
namespace DiskCleanup
|
||||
{
|
||||
public static class NativeMethods
|
||||
{
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll", EntryPoint = "PostThreadMessageW", SetLastError = true)]
|
||||
[return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
|
||||
public static extern bool PostThreadMessage(
|
||||
uint idThread,
|
||||
uint Msg,
|
||||
System.UIntPtr wParam,
|
||||
System.IntPtr lParam
|
||||
);
|
||||
}
|
||||
}
|
||||
'@
|
||||
}
|
||||
|
||||
try {
|
||||
# Set StateFlags0000 to 2 for all subkeys except "Offline Pages Files"
|
||||
Get-ChildItem -Path $rootKey | ForEach-Object {
|
||||
if ($_.PSChildName -ne "Offline Pages Files") {
|
||||
Set-ItemProperty -Path $_.PSPath -Name "StateFlags0000" -Type DWord -Value 2 -Force
|
||||
}
|
||||
}
|
||||
|
||||
# Run the disk cleanup tool with the specified flags.
|
||||
# Do not use Start-Process -Wait here; cleanmgr.exe can hang waiting for its own UI message thread.
|
||||
$cleanMgrPath = Join-Path $env:SystemRoot "System32\cleanmgr.exe"
|
||||
|
||||
$process = Start-Process -FilePath $cleanMgrPath `
|
||||
-ArgumentList "/sagerun:0" `
|
||||
-WindowStyle Normal `
|
||||
-PassThru
|
||||
|
||||
$deadline = (Get-Date).AddMinutes($timeoutMinutes)
|
||||
|
||||
Write-Host "Started cleanmgr.exe. PID: $($process.Id)"
|
||||
Write-Host "Posting WM_NULL to cleanmgr.exe threads every $pumpIntervalSeconds seconds while waiting for exit."
|
||||
|
||||
$pumpCount = 0
|
||||
|
||||
while (-not $process.HasExited) {
|
||||
if ((Get-Date) -ge $deadline) {
|
||||
throw "cleanmgr.exe did not exit within $timeoutMinutes minutes. PID: $($process.Id)"
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $pumpIntervalSeconds
|
||||
|
||||
try {
|
||||
$liveProcess = Get-Process -Id $process.Id -ErrorAction Stop
|
||||
|
||||
$pumpCount++
|
||||
$postedCount = 0
|
||||
$failedCount = 0
|
||||
$threadIdsPosted = New-Object System.Collections.Generic.List[int]
|
||||
|
||||
foreach ($thread in $liveProcess.Threads) {
|
||||
# Wake threads blocked in GetMessageW, such as cleanmgr!PurgeAbortThread.
|
||||
# Failures are expected for threads that do not have message queues.
|
||||
$posted = [DiskCleanup.NativeMethods]::PostThreadMessage(
|
||||
[uint32]$thread.Id,
|
||||
[uint32]$wmNull,
|
||||
[System.UIntPtr]::Zero,
|
||||
[System.IntPtr]::Zero
|
||||
)
|
||||
|
||||
if ($posted) {
|
||||
$postedCount++
|
||||
$threadIdsPosted.Add([int]$thread.Id)
|
||||
}
|
||||
else {
|
||||
$failedCount++
|
||||
}
|
||||
}
|
||||
|
||||
$elapsed = New-TimeSpan -Start $process.StartTime -End (Get-Date)
|
||||
|
||||
Write-Host ("[{0:yyyy-MM-dd HH:mm:ss}] cleanmgr.exe still running. Pump #{1}. Posted WM_NULL to {2}/{3} threads. Successful TIDs: {4}. Elapsed: {5:n1}s" -f `
|
||||
(Get-Date),
|
||||
$pumpCount,
|
||||
$postedCount,
|
||||
$liveProcess.Threads.Count,
|
||||
($(if ($threadIdsPosted.Count -gt 0) { ($threadIdsPosted -join ',') } else { 'none' })),
|
||||
$elapsed.TotalSeconds
|
||||
)
|
||||
|
||||
$process.Refresh()
|
||||
}
|
||||
catch [System.ArgumentException] {
|
||||
# Process exited between checks.
|
||||
break
|
||||
}
|
||||
catch [Microsoft.PowerShell.Commands.ProcessCommandException] {
|
||||
# Process exited between checks.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# Run the disk cleanup tool with the specified flags
|
||||
Start-Process -FilePath "cleanmgr.exe" -ArgumentList "/sagerun:0" -Wait
|
||||
$process.Refresh()
|
||||
|
||||
# Remove the StateFlags0000 registry values that were added
|
||||
Get-ChildItem -Path $rootKey | ForEach-Object {
|
||||
if ($_.PSChildName -ne "Offline Pages Files") {
|
||||
Remove-ItemProperty -Path $_.PSPath -Name "StateFlags0000" -Force
|
||||
if ($process.HasExited) {
|
||||
Write-Host "cleanmgr.exe exited. ExitCode: $($process.ExitCode)"
|
||||
}
|
||||
|
||||
if ($process.HasExited -and $process.ExitCode -ne 0) {
|
||||
throw "cleanmgr.exe exited with code $($process.ExitCode)."
|
||||
}
|
||||
}
|
||||
finally {
|
||||
# Remove the StateFlags0000 registry values that were added
|
||||
Get-ChildItem -Path $rootKey | ForEach-Object {
|
||||
if ($_.PSChildName -ne "Offline Pages Files") {
|
||||
Remove-ItemProperty -Path $_.PSPath -Name "StateFlags0000" -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user