A few months ago, Microsoft backported the DISM cleanup functionality of Windows 8 and 8.1 into Windows 7 SP1 using the disk cleanup manager.
I replied to members of the patchmanagement.org mailing list that there’s also a way to automate this on the following link:
http://blogs.technet.com/b/askpfeplat/archive/2013/10/07/breaking-news-reduce-the-size-of-the-winsxs-directory-and-free-up-disk-space-with-a-new-update-for-windows-7-sp1-clients.aspx
If you wonder how much freespace you can gain, here’s a snapshop of my W7 box at home:
Nice isn’t it đŸ˜€
I first followed the above technet article and used procmon to see what happens when I configure a set
Then I wrote some helper functions to help administrators automate the configuration in their corporate environment.
#Requires -Version 3.0 Function Get-DiskCleanupSet { [CmdletBinding()] Param( [Parameter(Mandatory)] [ValidateRange(0,9999)] [int32]$SetNumber = 9999 ) Begin { $key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches" $ar = @() } Process { (Get-Item -Path $key -ErrorAction Stop).GetSubKeyNames() | ForEach-Object -Process { $obj = New-Object -TypeName PSObject -Property @{ Name = $_ Enabled = $false } Try { $i = Get-ItemProperty -Path (Join-Path -Path $key -ChildPath $_) -ErrorAction Stop Switch ($i."StateFlags$($SetNumber)") { 0 { $obj.Enabled = $false ; break } 2 { $obj.Enabled = $true ; break } default {$obj.Enabled = $false } } } Catch { } $ar += $obj } $ar } } # endof function Function Select-DiskCleanUpItem { $key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches" Try { (Get-Item -Path $key -ErrorAction Stop).GetSubKeyNames() | Out-GridView -Title "Select items to cleanup" -PassThru } Catch { Write-Warning -Message "Failed to read the registry because $($_.Exception.Message)" } } Function Set-DiskCleanUpItemState { [CmdletBinding()] Param( [Parameter()] [ValidateRange(0,9999)] [int32]$SetNumber = 9999, [Parameter(Mandatory)] [switch]$Enabled, [Parameter(Mandatory,ValueFromPipeline)] [string[]]$Item ) Begin { # Make sure we run as admin $usercontext = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() $IsAdmin = $usercontext.IsInRole(544) if (-not($IsAdmin)) { Write-Warning -Message "Must run powerShell as Administrator to perform these actions" break } $key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches" Try { $validitems = (Get-Item -Path $key -ErrorAction Stop).GetSubKeyNames() } Catch { Throw $_ } if ($Enabled) { $Action = 'Enabling' $Value = 2 } else { $Action = 'Disabling' $Value = 0 } } Process { $Item | ForEach-Object -Process { if ($_ -notin $validitems) { Write-Warning -Message "Ignoring $_ as it isn't recognised as a valid item" } else { Write-Verbose -Message "$Action flag $_ in set number $SetNumber" Try { Set-ItemProperty -Path (Join-Path -Path $key -ChildPath $_) -Name "StateFlags$($SetNumber)" -Value $Value -Type DWORD -Force -ErrorAction Stop } Catch { Write-Warning -Message "Failed to set flag because $($_.Exception.Message)" } } } } } # endof function
Now let’s see how to use the above functions and how to configure a new set of cleanup options without firing up the cleanup manager utility.
# Select disk cleanup items to enable $DCItemsToEnable = Select-DiskCleanUpItem # Define a new random set $setx = Get-Random -Minimum 2 -Maximum 9999 # Enable the selected items $DCItemsToEnable | Set-DiskCleanUpItemState -SetNumber $setx -Enabled -Verbose # Disable all the others in that set Try { (Get-Item -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches" -ErrorAction Stop).GetSubKeyNames() | ForEach-Object -Process { if ($_ -notin $DCItemsToEnable) { $_ | Set-DiskCleanUpItemState -SetNumber $setx -Enabled:$false } } } Catch { Write-Warning -Message "Failed to read the registry because $($_.Exception.Message)" } # Let us know what was configured Write-Verbose -Message "Get item in set number: $setx" -Verbose Get-DiskCleanupSet -SetNumber $setx
If I execute the above code, I’ll first get a window asking me to select some items and click Ok.
and I’ll get at the end what was configured for the set number 5911
At this step, the cleanup isn’t performed yet. To launch it, I need to run
& (Get-Command "$($env:systemroot)\system32\cleanmgr.exe") @("/sagerun:$setx")
If I had to automate the Windows Update cleanup process on Windows 7 SP1 computers that only have Powershell 2.0, I’d do the following to have a fully silent execution of cleanmgr.exe
$script = @' #Requires -Version 2.0 if (Get-HotFix | Where-Object { $_.HotfixID -match "2852386"}) { # Configure that set to perform only the Windows Update cleanup $key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches" (Get-Item -Path $key).GetSubKeyNames() | ForEach-Object -Process { Try { if ($_ -match "Update\sCleanup") { Set-ItemProperty -Path (Join-Path -Path $key -ChildPath $_) -Name "StateFlags9999" -Value 2 -Type DWORD -Force -ErrorAction Stop } else { Set-ItemProperty -Path (Join-Path -Path $key -ChildPath $_) -Name "StateFlags9999" -Value 0 -Type DWORD -Force -ErrorAction Stop } } Catch { Write-Warning -Message "Failed to set flag because $($_.Exception.Message)" } } # Run that set & (Get-Command "$($env:systemroot)\system32\cleanmgr.exe") @("/sagerun:9999") } else { Write-Warning -Message "Required hotfix KB2852386 not found" } '@ $XMLDef = @" <?xml version="1.0" encoding="UTF-16"?> <Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"> <RegistrationInfo> <Date>2013-12-25T00:00:00.0000000</Date> <Author>NT AUTHORITY\SYSTEM</Author> </RegistrationInfo> <Triggers> <TimeTrigger> <StartBoundary>2013-12-25T00:00:00.0000000</StartBoundary> <Enabled>true</Enabled> </TimeTrigger> </Triggers> <Principals> <Principal id="Author"> <RunLevel>HighestAvailable</RunLevel> <GroupId>NT AUTHORITY\SYSTEM</GroupId> </Principal> </Principals> <Settings> <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries> <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries> <AllowHardTerminate>true</AllowHardTerminate> <StartWhenAvailable>false</StartWhenAvailable> <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable> <IdleSettings> <StopOnIdleEnd>true</StopOnIdleEnd> <RestartOnIdle>false</RestartOnIdle> </IdleSettings> <AllowStartOnDemand>true</AllowStartOnDemand> <Enabled>true</Enabled> <Hidden>true</Hidden> <RunOnlyIfIdle>false</RunOnlyIfIdle> <DisallowStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession> <UseUnifiedSchedulingEngine>false</UseUnifiedSchedulingEngine> <WakeToRun>false</WakeToRun> <ExecutionTimeLimit>PT2H</ExecutionTimeLimit> <Priority>7</Priority> </Settings> <Actions Context="Author"> <Exec> <Command>C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe</Command> <Arguments>-ExecutionPolicy Bypass -File C:\windows\temp\WUClean.ps1</Arguments> </Exec> </Actions> </Task> "@ # Create a local powershell script $script | Out-File -FilePath "$($env:systemroot)\Temp\WUClean.ps1" -Encoding ascii -Force # Create a task to process the script $TaskService = New-Object -ComObject schedule.service Try { $TaskService.Connect() | Out-Null $TaskDef = (New-Object -ComObject schedule.service).NewTask($null) $TaskDef.xmlText = $XMLDef $TaskService.GetFolder('\').RegisterTaskDefinition( 'Windows Update Cleanup', $TaskDef, 0x6, $null,$null,$null ) | Out-Null } Catch { Write-Warning "Failed to register task because $($_.Exception.Message)" } # Run the task $TaskService.GetFolder('\').GetTask('Windows Update Cleanup').Run(0)
Now the task can be run on demand for any further use and the file C:\windows\WUClean.ps1 would be executed.
schtasks /run /tn "Windows Update Cleanup"