Create a Custom Splash Screen for a Windows 10 In-Place Upgrade

A while back I wrote a blog with some scripts that can be used to improve the user experience in a Windows 10 in-place upgrade. The solution included a simple splash screen that runs at the beginning of the upgrade to block the screen to the user and discourage interaction with the computer during the online phase of the upgrade. Since then, I made some improvements to the screen and styled it to look more like the built-in Windows update experience in Windows 10. Using this splash screen not only discourages computer interaction during the upgrade, but also creates a consistent user experience throughout the upgrade process, for a user-initiated upgrade.

The updated screen contains an array of text sentences that you can customise as you wish. Here is an example of what it could look like:

The splash screen is not completely foolproof in that it is still possible to use certain key combinations, like ctrl-alt-del and alt-tab etc, but the mouse cursor is hidden and mouse buttons will do nothing. The intention is simply to discourage the user from using the computer during the online phase. If the computer is locked, it will display the splash screen again when unlocked. If you wish to block user interaction completely, you might consider a more hardcore approach like this or this.

To use the splash screen, download all the files in my GitHub repository here (including the bin directory). Create a standard package in ConfigMgr containing the files (no program needed) and distribute. Then add a Run PowerShell Script step in the beginning of your in-place upgrade task sequence that looks like the following (reference the package you created):

ts

Once the splash screen has been displayed, the task sequence will move on to the next step – the screen will not block the task sequence.

How does it work?

The Invoke-PSScriptAsUser.ps1 simple calls the Show-OSUpgradeBackground.ps1 and runs it in the context of the currently logged-on user so that the splash screen will be visible to the user (task sequences run in SYSTEM context so this is necessary).

The Show-OSUpgradeBackground.ps1 determines your active screens, creates a runspace for each that calls PowerShell.exe and runs the Create-FullScreenBackground.ps1 for each screen.

The Create-FullScreenBackground.ps1  does the main work of displaying the splash screen. It will hide the task bar, hide the mouse cursor and display a full screen window in the Windows 10 update style. I’ve used the excellent MahApps toolkit to create the progress ring. The text displayed in the screen can be defined by placing short sentences in the $TextArray variable. The dispatcher timer will cycle through each of the these every 10 seconds (or whatever value you set) ending with a final sentence “Windows 10 Upgrade in Progress” which will stay on the screen until the computer is restarted into the next phase of the upgrade.

You can test the splash screen before deploying it simply by running the Show-OSUpgradeBackground.ps1 script.

Remember to deselect the option Show task sequence progress in the task sequence deployment to avoid having the task sequence UI show up on top of the window.

Create Disk Usage Reports with PowerShell and WizTree

Recently I discovered a neat little utility called WizTree, which can be used to report on space used by files and folders on a drive. There are many utilities out there that can do that, but this one supports running on the command line which makes it very useful for scripting scenarios. It also works extremely quickly because it uses the Master File Table on disk instead of the slower Windows / .Net methods.

I wanted to create a disk usage report for systems that have less than 20GB of free space – the recommended minimum for doing a Windows 10 in-place upgrade – so that I can easily review it and identify files / folders that could potentially be deleted to free space on the disk. I wanted to script it so that it can be run in the background and deployed via ConfigMgr, and the resulting reports copied to a server share for review.

The following script does just that.

First, it runs WizTree on the command line and generates two CSV reports, one each for all files and folders on the drive. Next, since the generated CSV files contain sizes in bytes, the script imports the CSVs, converts the size data to include KB, MB and GB, then outputs to 2 new CSV files.

The script then generates 2 custom HTML reports that contain a list of the largest 100 files and folders, sorted by size.

Next it generates an HTML summary report that shows visually how much space is used on the disk and tells you how much space you need to free up to drop under the minimum 20GB-free limit.

Finally, it copies those reports to a server share, which will look like this:

fs

The Disk Usage Summary report will look something like this:

dus

And here’s a snippet from the large directories and files reports:

ld

lf

There are also CSV reports which contain the entire list of files and directories on the drive:

csv

To use the script, simply download the WizTree Portable app, extract the WizTree64.exe and place it in the same location as the script (assuming 64-bit OS).  Set the run location in the script (ie $PSScriptRoot if calling the script, or the directory location if running in the ISE), the temporary location where it can create files, and the server share where you want to copy the reports to. Then just run the script in admin context.

# Script to export html and csv reports of file and directory content on the system drive
# Use to identify large files/directories for disk space cleanup
# Uses WizTree portable to quickly retrieve file and directory sizes from the Master File Table on disk
# Download and extract the WizTree64.exe and place in the same directory as this script
# Set the running location
$RunLocation = $PSScriptRoot
#$RunLocation = "C:\temp"
$TempLocation = "C:\temp"
# Set Target share to copy the reports to
$TargetRoot = "\\server-01\sharename\DirectorySizeInfo"
# Free disk space thresholds (percentages) for summary report
$script:Thresholds = @{}
$Thresholds.Warning = 80
$Thresholds.Critical = 90
# Custom function to exit with a specific code
function ExitWithCode
{
    param
    (
        $exitcode
    )
    $host.SetShouldExit($exitcode)
    exit
}
# Function to set the progress bar colour based on the the threshold value in the summary report
function Set-PercentageColour {
param(
[int]$Value
)
If ($Value -lt $Thresholds.Warning)
{
$Hex = "#00ff00" # Green
}
If ($Value -ge $Thresholds.Warning -and $Value -lt $Thresholds.Critical)
{
$Hex = "#ff9900" # Amber
}
If ($Value -ge $Thresholds.Critical)
{
$Hex = "#FF0000" # Red
}
Return $Hex
}
# Define Html CSS style
$Style = @"
<style>
table {
border-collapse: collapse;
}
td, th {
border: 1px solid #ddd;
padding: 8px;
}
th {
padding-top: 12px;
padding-bottom: 12px;
text-align: left;
background-color: #4286f4;
color: white;
}
</style>
"@
# Set the filenames of WizTree csv's
$FilesCSV = "Files_$(Get-Date Format 'yyyyMMdd_hhmmss').csv"
$FoldersCSV = "Folders_$(Get-Date Format 'yyyyMMdd_hhmmss').csv"
# Set the filenames of customised csv's
$ExportedFilesCSV = "Exported_Files_$(Get-Date Format 'yyyyMMdd_hhmmss').csv"
$ExportedFoldersCSV = "Exported_Folders_$(Get-Date Format 'yyyyMMdd_hhmmss').csv"
# Set the filenames of html reports
$ExportedFilesHTML = "Largest_Files_$(Get-Date Format 'yyyyMMdd_hhmmss').html"
$ExportedFoldersHTML = "Largest_Folders_$(Get-Date Format 'yyyyMMdd_hhmmss').html"
$SummaryHTMLReport = "Disk_Usage_Summary_$(Get-Date Format 'yyyyMMdd_hhmmss').html"
# Run the WizTree portable app
Start-Process FilePath "$RunLocation\WizTree64.exe" ArgumentList """$Env:SystemDrive"" /export=""$TempLocation\$FilesCSV"" /admin 1 /sortby=2 /exportfolders=0" Verb runas Wait
Start-Process FilePath "$RunLocation\WizTree64.exe" ArgumentList """$Env:SystemDrive"" /export=""$TempLocation\$FoldersCSV"" /admin 1 /sortby=2 /exportfiles=0" Verb runas Wait
#region Files
# Remove the first 2 rows from the CSVs to leave just the relevant data
$CSVContent = Get-Content Path $TempLocation\$FilesCSV ReadCount 0
$CSVContent = $CSVContent | Select Skip 1
$CSVContent = $CSVContent | Select Skip 1
# Create a table to store the results
$Table = [System.Data.DataTable]::new("Directory Structure")
[void]$Table.Columns.Add([System.Data.DataColumn]::new("Name",[System.String]))
[void]$Table.Columns.Add([System.Data.DataColumn]::new("Size (Bytes)",[System.Int64]))
[void]$Table.Columns.Add([System.Data.DataColumn]::new("Size (KB)",[System.Decimal]))
[void]$Table.Columns.Add([System.Data.DataColumn]::new("Size (MB)",[System.Decimal]))
[void]$Table.Columns.Add([System.Data.DataColumn]::new("Size (GB)",[System.Decimal]))
# Populate the table from the CSV data
Foreach ($csvrow in $CSVContent)
{
$Content = $csvrow.split(',')
[void]$Table.rows.Add(($Content[0].Replace('"','')),$Content[2],([math]::Round(($Content[2] / 1KB),2)),([math]::Round(($Content[2] / 1MB),2)),([math]::Round(($Content[2] / 1GB),2)))
}
# Export the table to a new CSV
$Table | Sort 'Size (Bytes)' Descending | Export-CSV Path $TempLocation\$ExportedFilesCSV NoTypeInformation UseCulture
# Export the largest 100 results into html format
$Table |
Sort 'Size (Bytes)' Descending |
Select First 100 |
ConvertTo-Html Property 'Name','Size (Bytes)','Size (KB)','Size (MB)','Size (GB)' Head $style Body "<h2>100 largest files on $env:COMPUTERNAME</h2>" CssUri "http://www.w3schools.com/lib/w3.css" |
Out-String | Out-File $TempLocation\$ExportedFilesHTML
#endregion
#region Folders
# Remove the first 2 rows from the CSVs to leave just the relevant data
$CSVContent = Get-Content Path $TempLocation\$FoldersCSV ReadCount 0
$CSVContent = $CSVContent | Select Skip 1
$CSVContent = $CSVContent | Select Skip 1
# Create a table to store the results
$Table = [System.Data.DataTable]::new("Directory Structure")
[void]$Table.Columns.Add([System.Data.DataColumn]::new("Name",[System.String]))
[void]$Table.Columns.Add([System.Data.DataColumn]::new("Size (Bytes)",[System.Int64]))
[void]$Table.Columns.Add([System.Data.DataColumn]::new("Size (KB)",[System.Decimal]))
[void]$Table.Columns.Add([System.Data.DataColumn]::new("Size (MB)",[System.Decimal]))
[void]$Table.Columns.Add([System.Data.DataColumn]::new("Size (GB)",[System.Decimal]))
[void]$Table.Columns.Add([System.Data.DataColumn]::new("Files",[System.String]))
[void]$Table.Columns.Add([System.Data.DataColumn]::new("Folders",[System.String]))
# Populate the table from the CSV data
Foreach ($csvrow in $CSVContent)
{
$Content = $csvrow.split(',')
[void]$Table.rows.Add($($Content[0].Replace('"','')),$Content[2],([math]::Round(($Content[2] / 1KB),2)),([math]::Round(($Content[2] / 1MB),2)),([math]::Round(($Content[2] / 1GB),2)),$Content[5],$Content[6])
}
# Export the table to a new CSV
$Table | Sort 'Size (Bytes)' Descending | Export-CSV Path $TempLocation\$ExportedFoldersCSV NoTypeInformation UseCulture
# Export the largest 100 results into html format
$Table |
Sort 'Size (Bytes)' Descending |
Select First 100 |
ConvertTo-Html Property 'Name','Size (Bytes)','Size (KB)','Size (MB)','Size (GB)','Files','Folders' Head $style Body "<h2>100 largest directories on $env:COMPUTERNAME</h2>" CssUri "http://www.w3schools.com/lib/w3.css" |
Out-String | Out-File $TempLocation\$ExportedFoldersHTML
#endregion
#region Create HTML disk usage summary report
# Get system drive data
$WMIDiskInfo = Get-CimInstance ClassName Win32_Volume Property Capacity,FreeSpace,DriveLetter | Where {$_.DriveLetter -eq $env:SystemDrive} | Select Capacity,FreeSpace,DriveLetter
$DiskInfo = [pscustomobject]@{
DriveLetter = $WMIDiskInfo.DriveLetter
'Capacity (GB)' = [math]::Round(($WMIDiskInfo.Capacity / 1GB),2)
'FreeSpace (GB)' = [math]::Round(($WMIDiskInfo.FreeSpace / 1GB),2)
'UsedSpace (GB)' = [math]::Round((($WMIDiskInfo.Capacity / 1GB) ($WMIDiskInfo.FreeSpace / 1GB)),2)
'Percent Free' = [math]::Round(($WMIDiskInfo.FreeSpace * 100 / $WMIDiskInfo.Capacity),2)
'Percent Used' = [math]::Round((($WMIDiskInfo.Capacity $WMIDiskInfo.FreeSpace) * 100 / $WMIDiskInfo.Capacity),2)
}
# Create html header
$html = @"
<!DOCTYPE html>
<html>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="http://www.w3schools.com/lib/w3.css"&gt;
<body>
"@
# Set html
$html = $html + @"
<h2>Disk Space Usage for Drive $($DiskInfo.DriveLetter) on $env:COMPUTERNAME</h2>
<table cellpadding="0" cellspacing="0" width="700">
<tr>
<td style="background-color:$(Set-PercentageColour Value $($DiskInfo.'Percent Used'));padding:10px;color:#ffffff;" width="$($DiskInfo.'Percent Used')%">
$($DiskInfo.'UsedSpace (GB)') GB ($($DiskInfo.'Percent Used') %)
</td>
<td style="background-color:#eeeeee;padding-top:10px;padding-bottom:10px;color:#333333;" width="$($DiskInfo.'Percent Used')%">
</td>
</tr>
</table>
<table cellpadding="0" cellspacing="0" width="700">
<tr>
<td style="padding:5px;" width="80%">
Capacity: $($DiskInfo.'Capacity (GB)') GB
</td>
</tr>
<tr>
<td style="padding:5px;" width="80%">
FreeSpace: $($DiskInfo.'FreeSpace (GB)') GB
</td>
</tr>
<tr>
<td style="padding:5px;" width="80%">
Percent Free: $($DiskInfo.'Percent Free') %
</td>
</tr>
</table>
"@
If ($DiskInfo.'FreeSpace (GB)' -lt 20)
{
$html = $html + @"
<table cellpadding="0" cellspacing="0" width="700">
<tr>
<td style="padding:5px;color:red;font-weight:bold" width="80%">
You need to free $(20 $DiskInfo.'FreeSpace (GB)') GB on this disk to pass the W10 readiness check!
</td>
</tr>
</table>
"@
}
# Close html document
$html = $html + @"
</body>
</html>
"@
# Export to file
$html |
Out-string |
Out-File $TempLocation\$SummaryHTMLReport
#endregion
#region Copy files to share
# Create a subfolder with computername if doesn't exist
If (!(Test-Path $TargetRoot\$env:COMPUTERNAME))
{
$null = New-Item Path $TargetRoot Name $env:COMPUTERNAME ItemType Directory
}
# Create a subdirectory with current date-time
$DateString = ((Get-Date).ToUniversalTime() | get-date Format "yyyy-MM-dd_HH-mm-ss").ToString()
If (!(Test-Path $TargetRoot\$env:COMPUTERNAME\$DateString))
{
$null = New-Item Path $TargetRoot\$env:COMPUTERNAME Name $DateString ItemType Directory
}
# Set final target location
$TargetLocation = "$TargetRoot\$env:COMPUTERNAME\$DateString"
# Copy files
$Files = @(
$ExportedFilesCSV
$ExportedFoldersCSV
$ExportedFilesHTML
$ExportedFoldersHTML
$SummaryHTMLReport
)
Try
{
Robocopy $TempLocation $TargetLocation $Files /R:10 /W:5 /NP
}
Catch {}
#endregion
# Cleanup temp files
$Files = @(
$FilesCSV
$FoldersCSV
$ExportedFilesCSV
$ExportedFoldersCSV
$ExportedFilesHTML
$ExportedFoldersHTML
$SummaryHTMLReport
)
Foreach ($file in $files)
{
Remove-Item Path $TempLocation\$file Force
}
# Force a code 0 on exit, in case of some non-terminating error.
ExitWithCode 0

PowerShell One-liner to Extract a Windows 10 Upgrade Error Code

Short post – here’s a PowerShell one-liner that will extract the upgrade code from the setupact.log generated by a Windows 10 upgrade. It includes both the result code and the extend code. You could include this in an in-place upgrade task sequence with ConfigMgr to stamp the code to the registry, or WMI, or create a task sequence variable etc.


(Get-Content -ReadCount 0 -Path "$env:SystemDrive\`$Windows.~BT\Sources\Panther\setupact.log" |
    Out-String -Stream |
    Select-String -SimpleMatch "MOUPG  SetupHost: Reporting error event").ToString().Split('>')[1].Replace('[','').Replace(']','').Trim()

Here’s an example containing the code for happiness, 0xC1900210, 0x5001B 🙂

errorcode

 

Find Windows 10 Upgrade Blockers with PowerShell

This morning I saw a cool post from Gary Blok about automatically capturing hard blockers in a Windows 10 In-Place Upgrade task sequence. It inspired me to look a bit further at that, and I came up with the following PowerShell code which will search all the compatibility xml files created by Windows 10 setup and look for any hard blockers. These will then be reported either in the console, or you can write them to file where you can copy them to a central location together with your SetupDiag files, or you could stamp the info to the registry or a task sequence variable as Gary describes in his blog post. You could also simply run the script against an online remote computer using Invoke-Command.

The script is not the one-liner that Gary likes, so to use in a task sequence you’ll need to wrap it in a package and call it.

# Searches the Windows 10 Setup Compatibility logs for upgrade hard blockers
# Find all the compatibility xml files
$SearchLocation = 'C:\$WINDOWS.~BT\Sources\Panther'
$CompatibilityXMLs = Get-childitem "$SearchLocation\compat*.xml" | Sort LastWriteTime Descending
# Create an array to hold the results
$Blockers = @()
# Search each file for any hard blockers
Foreach ($item in $CompatibilityXMLs)
{
$xml = [xml]::new()
$xml.Load($item)
$HardBlocks = $xml.CompatReport.Hardware.HardwareItem | Where {$_.InnerXml -match 'BlockingType="Hard"'}
If($HardBlocks)
{
Foreach ($HardBlock in $HardBlocks)
{
$FileAge = (Get-Date).ToUniversalTime() $item.LastWriteTimeUTC
$Blockers += [pscustomobject]@{
ComputerName = $env:COMPUTERNAME
FileName = $item.Name
LastWriteTimeUTC = $item.LastWriteTimeUTC
FileAge = "$($Fileage.Days) days $($Fileage.hours) hours $($fileage.minutes) minutes"
BlockingType = $HardBlock.CompatibilityInfo.BlockingType
Title = $HardBlock.CompatibilityInfo.Title
Message = $HardBlock.CompatibilityInfo.Message
}
}
}
}
# Report results
If ($Blockers)
{
$Blockers
# Export to file
#$Blockers | export-csv -Path "$env:SystemDrive\Windows\CCM\Logs\W10UpgradeHardBlockers.csv" -NoTypeInformation -UseCulture -Force
}
Else
{
Write-host "No hard blockers found"
}

The console output looks like this:

HardBlock

You should remove the FileAge property if using it in a task sequence as that’s a real-time value and is a quick indicator of when the blocker was reported.

If you use my solution here for improving the user experience in an IPU, you could also report this info to the end user by adding a script using my New-WPFMessageBox function, something like this…


$Stack = New-Object System.Windows.Controls.StackPanel
$Stack.Orientation = "Vertical"

$TextBox = New-Object System.Windows.Controls.TextBox
$TextBox.BorderThickness = 0
$TextBox.Margin = 5
$TextBox.FontSize = 14
$TextBox.FontWeight = "Bold"
$TextBox.Text = "The following hard blocks were found that prevent Windows 10 from upgrading:"

$Stack.AddChild($TextBox)

Foreach ($Blocker in $Blockers)
{
    $TextBox = New-Object System.Windows.Controls.TextBox
    $TextBox.BorderThickness = 0
    $TextBox.Margin = 5
    $TextBox.FontSize = 14
    $TextBox.Foreground = "Blue"
    $TextBox.Text = "$($Blocker.Title): $($Blocker.Message)"
    $Stack.AddChild($TextBox)
}

$TextBox = New-Object System.Windows.Controls.TextBox
$TextBox.BorderThickness = 0
$TextBox.Margin = 5
$TextBox.FontSize = 14
$TextBox.Text = "Please contact the Helpdesk for assistance with this issue."

$Stack.AddChild($TextBox)

New-WPFMessageBox -Title "Windows 10 Upgrade Hard Block" -Content $Stack -TitleBackground Red -TitleTextForeground White -TitleFontSize 18

…which creates a message box like this:

wpf

Thanks to Gary and Keith Garner for the inspiration here!

Fix Http 500.19 Error after Removing WSUS

This post is more of a ‘note to self’ for troubleshooting IIS errors.

Recently I decided to remove the WSUS role from an SCCM distribution point as it was previously being used for patching during OSD, but now we patch only the reference image instead. After removing the WSUS role, I also did some cleanup including deleting the WSUS_Updates directory, the %Program Files%\Update Services directory and removing the WsusPool website and application pool from IIS.

After that, clients using that distribution point failed to get content for packages, returning an Http 500 error:

smsts

Checking the IIS log on the distribution point I found the following corresponding entry:

iis

The error code is 500.19 which translates to ‘Internal Server Error / Configuration data is invalid‘ and is documented in more detail in this Microsoft article. A further clue is found in the 126 windows error code, which translates to ‘The specified module could not be found‘.

Browsing to the website directly, ie http://localhost/SMS_DP_SMSPKG$, gave the following information:

siteerror

The error code 0x8007007e is the same as the windows error code 126 and also means ‘The specified module could not be found”. The module referenced in the error is the DynamicCompressionModule.

To get more detailed information on the error, I decided to enable Failed Request Tracing in IIS and log the 500.19 error. The process for enabling and using Failed Request Tracing is nicely summarised here as well as documented by Microsoft here. Using that, I found that a couple of modules were being referenced that were no longer present.

ftrerror

Following a hint from here, I checked the applicationHost.config file and found a reference to a dll in the %Program Files%\Update Services directory that was installed with WSUS and that I deleted after removing the WSUS role:


<scheme name="xpress" doStaticCompression="false" doDynamicCompression="true" dll="C:\Program Files\Update Services\WebServices\suscomp.dll" staticCompressionLevel="10" dynamicCompressionLevel="0" />

To remove the reference, I ran the following command as admin:


%windir%\system32\inetsrv\appcmd.exe set config -section:system.webServer/httpCompression /-[name='xpress']

You can find more info on IIS modules and how to add/remove/disable/enable here.

After removing the reference to the xpress schema and restarting the W3SVC service, everything was back to normal 🙂

Using Windows 10 Toast Notifications with ConfigMgr Application Deployments

When deploying software with ConfigMgr, the ConfigMgr client can create a simple “New software is available” notification to inform the user that something new is available to install from the Software Center. But this notification is not overly descriptive. You might wish to provide a more detailed notification with a description of the software, why the user should install it, the installation deadline etc. For Windows 10, we can do that simply by disabling the inbuilt notifications on the deployment and creating our own custom toast notifications instead.

The Notification

Consider the examples below.

Here I have created a simple toast notification with the name of the software, what it does, what it is needed for, and a simple instruction to close Outlook before installing. The user can then choose to install it now – and clicking on that button will simply open the Software Center to that application via it’s sharing link. If they click Another time… the notification goes away for now, and if they dismiss it, it will move to the Action Center.

Title Only

In this version, I’ve added a logo instead of a title…

Image Only

…and in this version, I’ve added both.

Title and Image

If the deployment has a deadline, you can state the deadline in the notification as well as tell the user how long they have left before the deadline is reached.

Image with Deadline

Clicking Install now opens that app in the Software Center where the user can go ahead and install it…

Software Center

The big gotcha (for now) is that this only works with Application deployments, and you need to be running ConfigMgr 1706 or later. Please, Microsoft, make sharing links possible for other deployments (packages/programs, task sequences) too!

The client machines also need to be running Windows 10 Anniversary Update or later for the notification to work properly.

The Magic

So how does this work? Well, first we need to disable the inbuilt notifications on the application deployment, so set that to Display in Software Center, and only show notifications for computer restarts in the deployment type on the User Experience tab.

Next, we create a compliance item and compliance baseline which will display the notification. Target the compliance baseline at the same collection/s you are targetting your application.

The compliance item will have a PowerShell discovery script and remediation script. The discovery script will simply detect whether the software has been installed and report compliance if it is. The remediation script contains the code that displays the notification, and will only run if the discovery script does not report compliance, ie the software is not yet installed.

The Code

For the discovery script, create some code that will detect whether the software is installed. For my example, I used the code below which simply checks for the existence of a registry key.


## Discovery script for Veritas Enterprise Vault Outlook Add-in (x64) 12.2.1.1485

$RegKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{0DBA46D1-5D49-4888-BC50-D3DF38F85126}"
If (Test-Path $RegKey)
{
    "Compliant"
}
Else
{
    "Not compliant"
}

It’s important that the script outputs a value whether it’s compliant or not, so you don’t get issues with the instance not being found.

For the remediation script, I created the following code to display a toast notification:

## Displays a Windows 10 Toast Notification for a ConfigMgr Application deployment
## To be used in a compliance item
## References
# Options for audio: https://docs.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-audio#attributes-and-elements
# Toast content schema: https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/toast-schema
# Datetime format for deadline: Ref: https://msdn.microsoft.com/en-us/library/system.datetime(v=vs.110).aspx
# Required parameters
$Title = "Enterprise Vault Outlook Add-in"
$SoftwarecenterShortcut= "softwarecenter:SoftwareID=ScopeId_8E25450A-4C7E-4508-B501-B3F0E2C91541/Application_abd1dcbe-275a-4be1-9800-1c1e9a0ce7ff"
$AudioSource = "ms-winsoundevent:Notification.Default"
$SubtitleText = "A new version of the Enterprise Vault Outlook Add-in is now available."
$BodyText = "The add-in provides email archiving functionality. This update is necessary for compatibility with Office 365. Please close Outlook before updating."
$HeaderFormat = "ImageOnly" # Choose from "TitleOnly", "ImageOnly" or "ImageAndTitle"
# Optional parameters
# Base64 string for an image, see Images section below to create the string
$Base64Image = "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"
# Deployment deadline
#[datetime]$Deadline = "21 June 2018 15:00"
# Calculated parameters
If ($Deadline)
{
$TimeSpan = $Deadline [datetime]::Now
}
## Images
# Convert an image file to base64 string
<#
$File = "C:\Users\tjones\Pictures\ICON_EV_LOGO_Resized.png"
$Image = [System.Drawing.Image]::FromFile($File)
$MemoryStream = New-Object System.IO.MemoryStream
$Image.Save($MemoryStream, $Image.RawFormat)
[System.Byte[]]$Bytes = $MemoryStream.ToArray()
$Base64 = [System.Convert]::ToBase64String($Bytes)
$Image.Dispose()
$MemoryStream.Dispose()
$Base64 | out-file "C:\Users\tjones\Pictures\ICON_EV_LOGO_Resized.txt" # Save to text file, copy and paste from there to the $Base64Image variable
#>
# Create an image file from base64 string and save to user temp location
If ($Base64Image)
{
$ImageFile = "$env:TEMP\ToastLogo.png"
[byte[]]$Bytes = [convert]::FromBase64String($Base64Image)
[System.IO.File]::WriteAllBytes($ImageFile,$Bytes)
}
# Load some required namespaces
$null = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
$null = [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime]
# Register the AppID in the registry for use with the Action Center, if required
$app = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe'
$AppID = "{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe"
$RegPath = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Notifications\Settings'
if (!(Test-Path Path "$RegPath\$AppId")) {
$null = New-Item Path "$RegPath\$AppId" Force
$null = New-ItemProperty Path "$RegPath\$AppId" Name 'ShowInActionCenter' Value 1 PropertyType 'DWORD'
}
# Define the toast notification in XML format
[xml]$ToastTemplate = @"
<toast scenario="reminder">
<visual>
<binding template="ToastGeneric">
<text>New Software Notification</text>
<text placement="attribution">from Contoso IT</text>
<group>
<subgroup>
<text hint-style="title" hint-wrap="true" >$Title</text>
</subgroup>
</group>
<group>
<subgroup>
<text hint-style="subtitle" hint-wrap="true" >$SubtitleText</text>
</subgroup>
</group>
<group>
<subgroup>
<text hint-style="body" hint-wrap="true" >$BodyText</text>
</subgroup>
</group>
</binding>
</visual>
<actions>
<action content="Install now" activationType="protocol" arguments="$SoftwarecenterShortcut" />
<action content="Another time…" arguments="" />
</actions>
<audio src="$AudioSource"/>
</toast>
"@
# Change up the headers as required
If ($HeaderFormat -eq "TitleOnly")
{
$ToastTemplate.toast.visual.binding.group[0].subgroup.InnerXml = "<text hint-style=""title"" hint-wrap=""true"" >$Title</text>"
}
If ($HeaderFormat -eq "ImageOnly")
{
$ToastTemplate.toast.visual.binding.group[0].subgroup.InnerXml = "<image src=""$ImageFile""/>"
}
If ($HeaderFormat -eq "ImageAndTitle")
{
$ToastTemplate.toast.visual.binding.group[0].subgroup.InnerXml = "<text hint-style=""title"" hint-wrap=""true"" >$Title</text><image src=""$ImageFile""/>"
}
# Add a deadline if required
If ($Deadline)
{
$DeadlineGroups = @"
<group>
<subgroup>
<text hint-style="base" hint-align="left">Deadline</text>
<text hint-style="caption" hint-align="left">$(Get-Date Date $Deadline Format "dd MMMM yyy HH:mm")</text>
</subgroup>
<subgroup>
<text hint-style="base" hint-align="right">Time Remaining .</text>
<text hint-style="caption" hint-align="right">$($TimeSpan.Days) days $($TimeSpan.Hours) hours $($TimeSpan.Minutes) minutes .</text>
</subgroup>
</group>
"@
$ToastTemplate.toast.visual.binding.InnerXml = $ToastTemplate.toast.visual.binding.InnerXml + $DeadlineGroups
}
# Load the notification into the required format
$ToastXml = New-Object TypeName Windows.Data.Xml.Dom.XmlDocument
$ToastXml.LoadXml($ToastTemplate.OuterXml)
# Display
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($app).Show($ToastXml)

Code Walkthrough

Let’s walk through the code to explain the variables and what it does.

Variables

Title is the notification title that displays more prominently, the name of the software for example.

SoftwareCenterShortcut is the sharing link from your ConfigMgr application. To get this, you simply deploy the application to a machine, go to the Software Center, open the application and in the top-right click the link and copy and paste the link as the variable value.

AudioSource is the sound that displays when the notification appears. There are various options here, see the reference in the script for more info.

SubtitleText and BodyText contain the main wording in the notification.

HeaderFormat is a choice of either:

  1. TitleOnly – this just displays a title in the notification header
  2. ImageOnly – this just displays an image in the notification header
  3. TitleAndImage – this displays both

Base64Image – if you wish to include an image or a logo, use this optional variable. You need to convert an image file to a base64 string first, and code is included in the script for how to do that. You can output the base64 string to a text file and copy and paste it back into the script in this variable.

The reason for encoding the image is simply to avoid any dependencies on files in network locations, setting directory access or requiring internet access. The script will convert the base64 string back to an image file and save it in the user’s temporary directory.

Deadline is an optional parameter. If your deployment has a deadline, you probably want to include that in the notification. Deadline should be a parseable datetime format.

What the Script Does

The script will register PowerShell in the HKCU registry as an application that can display notifications in the Action Center, if it isn’t registered already.

Next it defines the toast notification in XML format. I chose XML to avoid any dependencies on external modules, and it’s actually quite simple to create a notification that way. The schema for toast notification is all documented by Microsoft and you can find a reference in the script.

Next it manipulates the XML a bit depending on whether you chose to display an image or use a deadline etc.

Finally, the notification is displayed.

Duration

The notification uses the reminder scenario so that it stays visible on the screen until the user takes action with it. If this is undesirable, you can change it to a normal notification with either the standard or longer duration. In this case, you need to be sure that the text in the notification can be read in that time frame.

In the toast template XML definition, change the first line from:

<toast scenario=”reminder”>

to either (default duration 5 seconds)

<toast duration=”short”>

or (around 25 seconds)

<toast duration=”long”>

Creating the Compliance Item and Baseline

When creating the compliance item in SCCM, make sure of the following:

  • Supported platforms – should be Windows 10 only. Actually, I have used some features in toast notifications that are only available in the Anniversary Update and later, so don’t target versions less than.
  • User context – make sure the compliance item has the option Run scripts by using the logged on user credentials checked
  • Compliance rule value – the value returned by the script should equal “Compliant
  • Compliance rule remediation – make sure that Run the specified remediation script when this setting is noncompliant is checked

When creating the deployment for the compliance baseline in SCCM, make sure of the following:

  • Remediate noncompliant rules when supported is checked
  • Allow remediation outside the maintenance window is checked (if that is acceptable in your environment)

Conclusion

This is a handy way to create your own notifications for ConfigMgr application deployments in Windows 10 and is fully customizable per application, within the limits of the toast notification schema. If and when Microsoft make sharing links available for task sequences, or packages and programs too, this would become even more useful, for example, sending a custom notification when a Windows 10 version upgrade is available.

Simplify Resolving Windows 10 Upgrade Errors with SetupDiag and ConfigMgr

A few weeks ago Microsoft released a handy tool to help diagnose issues with Windows 10 upgrades called SetupDiag. The tool basically analyzes the Windows Setup logs against known issues and reports it’s findings in a log file. Troubleshooting Windows 10 setup is not the most fun activity, so using this tool certainly makes the process easier. To make it easier still, we can run it using SCCM, either standalone, or as part of a Windows 10 Upgrade task sequence.

Below is a simple PowerShell script wrapper that can be used to run the tool. It checks that the requirement of .Net 4.6 has been met then runs the tool, logging to the location you specify. In this example I am logging the results to the CCM Logs directory for convenience. It creates a file called Setupdiagresults.log and an archive called Logs.zip containing the Windows setup logs used.

# Script to run SetupDiag to troubleshoot Windows 10 Setup
# Download SetupDiag.exe from https://go.microsoft.com/fwlink/?linkid=870142 and place in same directory as this script
# Get the CCM Logs location from registry
$LogLocation = Get-ItemProperty Path "HKLM:\SOFTWARE\Microsoft\CCM\Logging\@Global" Name LogDirectory | Select ExpandProperty LogDirectory
#$LogLocation = "$env:SystemRoot\CCM\Logs"
# Get the location we're running from (or use $PSScriptRoot)
$ScriptPath = Split-Path $MyInvocation.MyCommand.Path Parent
# Check that .Net 4.6 minimum is installed
If (Get-ChildItem "HKLM:SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\" | Get-ItemPropertyValue Name Release | ForEach-Object { $_ -ge 393295 })
{
Try
{
Start-Process FilePath "$ScriptPath\SetupDiag.exe" ArgumentList "/Output:$LogLocation\SetupDiagResults.log" Wait ErrorAction Stop
}
Catch
{
"[ERROR] There was an error starting SetupDiag.exe: $_" | Out-file FilePath "$LogLocation\SetupDiagResults.log" Force
}
}
Else
{
"[ERROR] .Net Framework 4.6 is required to run SetupDiag.exe" | Out-file FilePath "$LogLocation\SetupDiagResults.log" Force
}

view raw
Run-SetupDiag.ps1
hosted with ❤ by GitHub

Download the SetupDiag utility from here, and create a package in SCCM containing both SetupDiag.exe and the PS script in the same directory.

Add a Run PowerShell Script step to your task sequence and reference the package you created.

SetupTS

Here’s an example log file output from a successful upgrade:

SuccessfulUpgrade

 

New tool: ConfigMgr Add2Collection

Today I released a new tool for the community! ConfigMgr Add2Collection is a free tool that allows IT administrators and support staff to add resources to collections in ConfigMgr independently of the ConfigMgr console. It honors role-based access control (RBAC) to limit visibility of collections where appropriate. It can be used either on the Site Server or a remote workstation using PS remoting.

The tool includes a collection explorer so you can browse for collections, view collection details and current membership.

See more info here.

add2-2

add2-1

Improving the User Experience in a ConfigMgr OS Upgrade Task Sequence

Update 24th Nov 2017

  • Fixed the issue where the Upgrade Successful notification does not display for non-admin users. Thanks to a tip from Carl (see comments) I used a somewhat ancient mechanism called ActiveSetup that is still available in Windows 10.
  • The custom background displayed during the online phase of the upgrade now displays on all screens if multiple monitors are being used. Thanks to Ronni Pedersen for the kick 🙂
  • These changes have added a couple more scripts to the download, but the task sequence remains unchanged, so simply update your notifications package in ConfigMgr.

When upgrading to Windows 10 from a ‘down-level’ OS, or to a new version of Windows 10, using installation media, you get a nice UI that guides you through the installation process.

WindowsSetup2

Upgrading using an OS upgrade task sequence in ConfigMgr however, is a comparatively cold experience with no UI except for the TS Progress UI – assuming you enable that. For an IT admin of course, we don’t necessarily care about having a nice UI, we just care that it works and we have log files to check if it doesn’t. But for an end user that can be a different story. It may be a little disconcerting to some that their system is being upgraded yet the upgrade process is providing little feedback about what is happening. Once you get past the online phase of the upgrade however, the experience is more streamlined.

In an OS upgrade task sequence, Windows Setup will be running silently in SYSTEM context so it will not display anything to the logged-on user. Everything is handled by the task sequence. If the task sequence fails, the user might feel panicked and wonder if they have lost any of their data or applications. There is nothing to reassure them otherwise.

We may not be able to reproduce the nice Windows Installer UX, but we could at least add a few custom notifications at different points in the TS to provide some feedback to the end user and improve the overall experience from their perspective.

I experimented with this a bit using my New-WPFMessageBox PowerShell function and the following is what I came up with.

At the start of the upgrade task sequence, I like to check the currently-installed Windows version because – strange but true – the Windows Setup process will not prevent you from ‘upgrading’ to a version you are already running! How’s that for a time-waster?! Of course, you would try to avoid that with correct collection targeting in ConfigMgr, but just as an insurance I check that the system is not already running that version, and if it is, display the following notification to the user, then exit the TS.

AlreadyUpgraded

Next, during the online phase of the Upgrade Operating System step, I display a custom background. This is just to discourage the user from working or rebooting the computer and provides some extra assurance that something is actually happening. This is actually a WPF window that fills the screen, not a desktop wallpaper.

OSUpgrade

I also run the compatibility scan first and if that fails, I notify the user with the error code and description that they can contact IT support with:

CompatScanFail

The same if the upgrade fails, or if a rollback is performed, although no descriptions here as there are many possible result codes.

OSUpgradeFail

OSUpgradeFailRollback

Finally, when the OS upgrade successfully completes, the first user who logs in will see the following notification giving them some hyperlinks to what is new in the upgraded OS:

UpgradeComplete

Using my New-WPFMessageBox function you can customise these notifications as you please.

To make it simple, I have included here an export of an OS upgrade task sequence that you can import into your environment as a basis or an example of how to add such notifications. Here’s a screenshot:

TaskSequence

I’ve also made available all the PowerShell scripts I used as a download. Simply create a standard package in ConfigMgr containing the all the scripts in the same directory and distribute the content (no program required). Update the imported task sequence to reference this package for each of the Run PowerShell script steps, and also reference your OS Upgrade package in the relevant steps.

Some important things to note:

  • The notifications display in the context and session of the logged-on user. This is accomplished by calling the notification scripts via another script – Invoke-PSScriptAsUser.ps1 – that creates a PowerShell process in the user’s context.
  • Where a notification is displayed, I also first hide the TS Progress UI using the TSDisableProgressUI variable, which is available since ConfigMgr Current Branch 1706. This is because the notification will display behind the TS Progress UI, although if there are no further steps to complete after the notification is displayed it doesn’t matter too much because the TS Progress UI will not display for long anyway. The task sequence will not wait for the user to respond to the notification before it continues processing any remaining steps.
  • Where the compatibility scan or OS upgrade fails, the step is set to continue on error so that we can handle the error ourselves. After displaying the error notification, we manually fail the TS using the _SMSTSOSUpgradeActionReturnCode TS variable value as the error code.
  • Where the compatibility scan or OS upgrade fails, we write out the return code to a file so that the custom notification, which runs in the user context, can read in the value. This is because the task sequence variables are only available to query in the SYSTEM context – the user context cannot read them.
  • In handling a failure I set the SMSTSErrorDialogTimeout TS variable to 1 second so that the TS fails quickly and the user is left with our custom error notification instead of the default TS one.
  • The final notification that the upgrade was successful displays for the first user that logs in after the TS has completed. This is because the OS Upgrade TS simply ends at the Windows lock screen where we cannot display anything. Before the TS ends, we copy the notification script to a temp location and set the RunOnce registry key to call it.
  • Pay attention to the step conditions for the groups in the task sequence, as this controls the logical flow of the sequence.
  • Make sure to “Ignore dependency” when importing the task sequence

Pre-caching Content

Another important activity that should be done before making an OS Upgrade task sequence available is to pre-cache as much content as possible on the target systems. Unless the content is already in the ConfigMgr client cache when the TS runs, it’s gonna need to download that content which, for an OS Upgrade TS, is a sizeable amount of data and could add significant time to the execution of the task sequence making for a poorer experience for the end user.

Since ConfigMgr 1702, we have had the ability to pre-download content for a task sequence, and this was improved a bit in 1706, but in my own experience I have not found it to do quite what it says on the tin. Specifically, this line in the documentation – When the client receives the deployment policy, it will start to pre-cache the content. – appears not to be true (at the time of writing with 1706). Even when you have correctly set the OS Architecture and language on the OS Upgrade package, and set the required conditions on the Upgrade Operating System step, no content is actually cached on the client until the date the deployment becomes available. That is, you can target a system with a deployment that has an available date in the future, and theoretically it should start caching content as soon as a machine policy refresh occurs. But in practice, it does not cache any content until the available date of the deployment is reached, then shortly after it will start to download the content. If the user decides to upgrade as soon as the deployment becomes available, they will need to wait for the content to download first. If anyone has a different experience with this, please let me know!

Until that is fixed, we can still pre-cache most of the content by creating a hidden task sequence that uses the Download Package Content step. Make sure to use the Configuration Manager client cache as the location.

TaskSequence2

Check the option to Suppress task sequence notifications on the TS properties, and deploy the TS to the target systems before you deploy the OS Upgrade TS.

SuppressNotifications

Download

Download the PowerShell Scripts and exported Task Sequence here.