Reduce the size of WinSXS on Windows 7 SP1

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"
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s