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

Be prepared for the Winter Scripting Games, 3, 2, 1, Go!

  1. Read the 2014 Winter SG Players Guide
  2. Sharpen your skills by:
  3. Be timely informed by
  4. Prepare your toolbox
  5. Last but not least be on time
    • Review the games schedule
    • As it’s up to you to translate UTC times in your local time, have some nifty pieces of code ready
    • # When will event 1 start in my local time zone?            
      (Get-Date '2014-01-19 01:00:00').ToLocalTime()            
                  
      # How long until event 1 is due?            
      New-TimeSpan -End ([datetime]'2014-01-26 01:00:00').ToLocalTime()

Don’t forget to have fun 😎

PS: …and if you don’t have time and/or aren’t on holidays, do at least what’s written in green (point 1. and 3.1).

Testing Bitlocker drive encryption

Early December, I attented a Microsoft training on Windows 8 (22688A) in a local training center. I had the opportunity to test Bitlocker in module 11.

In the lab we were first asked to encrypt the volume E: on the first computer named Computer01A

Get-BitLockerVolume

After encrypting the volume, we had to dismount the encrypted drive from the VM Computer01A and attach it to the second computer named LON-CL2.
We had to look at the recovery key that was stored in Active Directory and use it to unlock the drive mounted on a second computer named LON-CL2

I decided to use PowerShell to unlock the drive 😛

Let’s first look at the properties of the newly attched encrypted drive. We can see that it’s locked and what “key protectors” were used to encrypt it.

 Get-BitLockerVolume -MountPoint F: | fl *

Using the key stored in Active Directory, I did the following to unlock the drive:

 Unlock-BitLockerVolume -MountPoint F: -RecoveryPassword "036212-568502..."

As I had some spare time during the lab, I explored other bitlocker cmdlets.
I started first to disable bitlocker on the F: drive on the computer named LON-CL2 as it was previously unlocked.

 Disable-BitLockerVolume -MountPoint F:

Now, I wanted to be able to encrypt the drive and have its recovery key stored in Active Directory and I did:

Enable-Bitlocker -MountPoint F: -EncryptionMethod AES128 -UsedSpaceOnly:$true -RecoveryProtector

Why does my Get-WinEvent command fail?

This morning I wanted to audit computers to assess whether the certificate revocation list has been updated or not as Microsoft published the following advisory: Security Advisory (2916652) Improperly Issued Digital Certificates Could Allow Spoofing

As stated in the FAQ, I just needed to check the Application log for event ID 4112

So, I quickly did

Get-WinEvent -FilterHashtable @{ LogName = 'Application' ; ProviderName = "Microsoft-Windows-CAPI2" ; Id = 4112 } -MaxEvents 1 | Select -ExpandProperty Message

(same code as above but with splatting for a better readability)

$HT= @{ FilterHashtable =            
 @{            
    LogName = 'Application';            
    ProviderName = "Microsoft-Windows-CAPI2";            
    Id = 4112             
 }            
}            
Get-WinEvent @HT -MaxEvents 1 |            
Select -ExpandProperty Message

The above worked perfectly well on computers that had their $host current culture set to English-US or French.

But when I tried on computers that had another culture, it failed with the following message:
Get-WinEvent : Could not retrieve information about the Microsoft-Windows-CAPI2 provider. Error: The locale specific resource for the desired message is not present.

Get-WinEvent -ProviderName "Microsoft-Windows-CAPI2"

I got the same error with the following command:

Get-WinEvent -ProviderName "Microsoft-Windows-CAPI2"

But it partially worked with the following command. Notice that the Message property is empty

(Get-WinEvent -FilterHashtable @{ LogName = 'Application' ; Id = 4112 } -MaxEvents 1)

My two workarounds in this case were:

$a = (Get-WinEvent -FilterHashtable @{ LogName = 'Application' ; Id = 4112 } -MaxEvents 1)
'Successful auto update of disallowed certificate list with effective date: {0}' -f @(([xml]$a.ToXml()).Event.EventData.Data)[0]
'Successful auto update of disallowed certificate list with effective date: {0}' -f $a.Properties[0].Value


Having workarounds without understanding what’s going on under the hood was quite frustrating 😦

I tried to list the MetaData associated with the provider with the following command (I used a tip I saw on this page):

(Get-WinEvent -ListProvider "Microsoft-Windows-CAPI2").Events

It worked on English-US or French culture but failed silently on the en-GB.

This time, I used the following MSDN page to create the System.Diagnostics.Eventing.Reader.ProviderMetadata object.

$ar = @(            
 "Microsoft-Windows-CAPI2",            
 $null,            
 ([System.Globalization.CultureInfo]'en-GB')            
)            
(New-Object System.Diagnostics.Eventing.Reader.ProviderMetadata -ArgumentList $ar).get_Events()            
            
$ar = @(            
 "Microsoft-Windows-CAPI2",            
 $null,            
 ([System.Globalization.CultureInfo]'en-US')            
)            
(New-Object System.Diagnostics.Eventing.Reader.ProviderMetadata -ArgumentList $ar).get_Events() |             
ft Id,Description -AutoSize

My Get-Winevent commnand failed because my user locale name ($host.CurrentCulture) is set to en-GB.

Hyper-V 2012 R2: copy file from host into guest VM

I wanted to move some policy definitions files from 2012 R2 to a domain joined VM.

I made a zip of the missing files and directly tried to use the new Copy-VMFile cmdlet of the Hyper-V module of Windows 2012 Server R2.

I got a little bit disappointed as my first attempt ended with the following error message:

Copy-VMFile : ‘GuestVMName’ failed to copy file. (Virtual machine ID a4391904-ba6c-462f-99e0-7abd1d90b0a5)
‘PRA406’ failed to initiate copying files to the guest: The device is not ready. (0x80070015). (Virtual machine ID
a4391904-ba6c-462f-99e0-7abd1d90b0a5)
‘GuestVMName’: The ‘Guest Service Interface’ integration service is either not enabled, not running or not initialized.
(Virtual machine ID a4391904-ba6c-462f-99e0-7abd1d90b0a5)
The system cannot process the request at this time.

I thought the integration service weren’t upgraded, so I checked with the following command:

Get-VM | ? State -eq "Running" |            
Select Name,State,Integrationservicesversion |            
ft -AutoSize

If I had read more carefully the above message, I’d have found immediately that the ‘Guest Services’ weren’t enabled (confirmed by the UI)

Get-VM -Name GuestVMName |  Get-VMIntegrationService

Let’s enable it:

Get-VM -Name GuestVMName |             
Get-VMIntegrationService | ? {-not($_.Enabled)} |             
Enable-VMIntegrationService -Verbose

My second attempt with Copy-VMFile ran like a charm 😎

Copy-VMFile -Name GuestVMName -SourcePath $home\documents\policydefinitions.zip -DestinationPath C:\Users\administrator\documents -FileSource Host

About Update-Help first prompt

As you may already know, as of Powershell version 3.0, you get a prompt to update help files the first time you invoke the Get-Help cmdlet.
You can read about it in the Remark section of the Get-Help cmdlet.

Get-Help Get-Help -Full

As I was doing a lab last week, I tried to use Get-help inside a virtual machine without realizing that it was the VM and not the host. D’oh!
I answered No as I realized that the VM wasn’t wasn’t connected to the internet. 😦

Now, instead of using the Update-Help, how do I restore that first prompt.

As you can see in the above screenshot, PowerShell writes a REG_DWORD value in the above registry locations.
Removing the two values makes it:

gp HKLM:\SOFTWARE\Microsoft\PowerShell | rp -Name "DisablePromptToUpdateHelp"            
gp HKLM:\SOFTWARE\Wow6432Node\Microsoft\PowerShell | rp -Name "DisablePromptToUpdateHelp"            

Here’s another way (with error handling) to perform the removal of the 2 registry values:

$null,"Wow6432Node" | ForEach-Object {
    try {
        Remove-ItemProperty -Name "DisablePromptToUpdateHelp" -Path "HKLM:\SOFTWARE\$($_)\Microsoft\PowerShell" -ErrorAction Stop
    } catch {
        Write-Warning -Message "Failed because $($_.Exception.Message)"
    }
}

How to revert virtual machines in your lab

Last week I attented a Microsoft training on Windows 8 (22688A) in a local training center.

I wrote a few lines of code at the beginning of the session to revert a specific VM to its initial state as we had to repeat this task at the end of each lab.

Get-VM -Name 22688A-LON-CL1 |
Get-VMSnapshot | Sort CreationTime |
Out-GridView -PassThru |
Restore-VMSnapshot -Confirm:$false -Passthru -Verbose |
ForEach-Object -Process {
	Start-VM -Name $_.VMName -Verbose
}


When you revert a VM to its initial state, the trust relationship with its domain is broken. The password of the computer needs just to be reset.

I didn’t use the Test-ComputerSecureChannel although it was quickly mentioned at the end of chapter 3.

Test-ComputerSecureChannel -Repair:$true

As we log on the client computer with domain admin credentials (a very bad practice), I just did:

Notice that no reboot is required 😀

Both Test-ComputerSecureChannel and Reset-ComputerMachinePassword exist in Powershell 2.0 but as of version 3.0, you’ve now the ability to specify credentials used to perform the reset. Very handy, isn’t it?

At the end of the week to revert all the virtual machines, I used the following code 😎

Get-VM | Out-GridView -PassThru -Title "Select Virtual Machines" | % {            
    $_ | Get-VMSnapshot | Sort CreationTime |             
    Out-GridView -PassThru -Title "Select one snapshot for $($_.Name)"            
} | Restore-VMSnapshot -Confirm:$false -PassThru -Verbose