Windows 10 Upgrade Splash Screen – Take 2

Recently I tweeted a picture of the custom Windows 10-style splash screen I’m using in an implementation of Windows as a Service with SCCM (aka in-place upgrade), and a couple of people asked for the code, so here it is!

A while ago a blogged about a custom splash screen I created to use during the Windows 10 upgrade process. Since then, I’ve seen some modifications of it out there, including that of Gary Blok, where he added the Windows Setup percent complete which I quite liked. So I made a few changes to the original code as follows:

  • Added a progress bar and percentage for the Windows Setup percent complete
  • Added a timer so the user knows how long the upgrade has been running
  • Prevent the monitors from going to sleep while the splash screen is displayed
  • Added a simple way to close the splash screen in a failure scenario by setting a task sequence variable
  • Re-wrote the WPF part into XAML code

Another change is that I call the script with ServiceUI.exe from the MDT toolkit instead of via the Invoke-PSScriptasUser.ps1 as this version needs to read task sequence variables so must run in the same context as the task sequence.

I haven’t added things like looping the text, or adding TS step names as I prefer not to do that, but check out Gary’s blog if you want to know how.

To use this version, download the files from my Github repo. Make sure you download the v2 edition. Grab the ServiceUI.exe from an MDT installation and add it at top-level (use the x64 version of ServiceUI.exe if you are deploying 64-bit OS). Package these files in a package in SCCM – no program needed.

To call the splash screen, add a Run Command Line step to your upgrade task sequence and call the main script via Service UI, referencing the package:

ServiceUI.exe -process:Explorer.exe %SYSTEMROOT%\System32\WindowsPowershell\v1.0\powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "Show-OSUpgradeBackground.ps1"

To close the screen in a failure scenario, I add 3 steps as follows:

The first step kills the splash screen simply by setting the task sequence variable QuitSplashing to True. The splash screen code will check for this variable and initiate closure of the window when set to True.

The second step just runs a PowerShell script to wait 5 seconds for the splash screen to close

The last step restores the taskbar to the screen

For that step, run the following PowerShell code:

# Thanks to https://stackoverflow.com/questions/25499393/make-my-wpf-application-full-screen-cover-taskbar-and-title-bar-of-window
$Source = @"
using System;
using System.Runtime.InteropServices;

public class Taskbar
{
    [DllImport("user32.dll")]
    private static extern int FindWindow(string className, string windowText);
    [DllImport("user32.dll")]
    private static extern int ShowWindow(int hwnd, int command);

    private const int SW_HIDE = 0;
    private const int SW_SHOW = 1;

    protected static int Handle
    {
        get
        {
            return FindWindow("Shell_TrayWnd", "");
        }
    }

    private Taskbar()
    {
        // hide ctor
    }

    public static void Show()
    {
        ShowWindow(Handle, SW_SHOW);
    }

    public static void Hide()
    {
        ShowWindow(Handle, SW_HIDE);
    }
}
"@
Add-Type -ReferencedAssemblies 'System', 'System.Runtime.InteropServices' -TypeDefinition $Source -Language CSharp

# Restore the taskbar
[Taskbar]::Show()

Forcing Installation of the MDT ConfigMgr Integration WMI Classes

Today I encountered an unexpected issue installing the ConfigMgr Integration for MDT. The scenario was an environment with several SMS providers and 2 site servers in a high availability configuration (active / passive). The MDT ConfigMgr Integrations ran successfully on each of the SMS Provider servers, but on the passive site server the BDD_* WMI classes were not created under ROOT\sms\site_XYZ, even though the ConfigMgr Integration wizard completed successfully and reported no error. I ran the wizard with the option to install the task sequence actions to the local server in each case.

Without the WMI classes in place, you get the error “Failed to load class properties and qualifiers for class BDD_*** in task sequence.” when viewing or editing a task sequence containing MDT steps:

The solution was simply to manually compile the MOF file that comes with MDT, which is called Microsoft.BDD.CM12Actions.mof. After the Integration wizard has run, the MOF file be found in Program Files\Microsoft Configuration Manager\AdminConsole\bin. It can also be found in the MDT installation directory Program Files\Microsoft Deployment Toolkit\SCCM.

You need to edit the first line of the MOF file so that it is pointing to the local server, and contains the correct WMI location to install the classes to, eg:

#pragma namespace("\\SCCM001.CONTOSO.COM\root\sms\site_XYZ")

Then compile the MOF file from an admin CMD:

mofcomp <path>\Microsoft.BDD.CM12Actions.mof
BDD_* classes in WMI

Setting the Default Wallpaper for Windows 10 during ConfigMgr OSD

So finally I got around to doing some Windows 10 deployments (better late than never :)) and I wanted to set the default wallpaper during Operating System deployment with Configuration Manager. I’ve seen a few posts out there about how to do that, but they all run during the OS phase of the deployment, which means you have to take ownership and set permissions on all the image files. However,  you can bypass those permission changes by setting the wallpaper during the WinPE phase of the deployment instead.

I wrote a post about that before, but it was only tested on Windows 7. In Windows 10, the process has changed a bit. In addition to the \Windows\Web\Wallpaper\Windows directory that contains the “img0.jpg” default wallpaper image, there is now also a \Windows\Web\4K\Wallpaper\Windows directory, which contains images of various resolutions. It seems that if the screen resolution is set to the resolution of one of the images in the 4K directory, it will use that one as the default.

This means that we need to provide image files of equivalent resolutions in this directory as well as the standard wallpaper directory. The following image files and resolutions are required:

img0.jpg
img0_768x1024.jpg
img0_768x1366.jpg
img0_1024x768.jpg
img0_1200x1920.jpg
img0_1366x768.jpg
img0_1600x2560.jpg
img0_2160x3840.jpg
img0_2560x1600.jpg
img0_3840x2160.jpg

The “img0.jpg” image file can be around 1920×1200 to match the default size.

Since I am using MDT-integrated ConfigMgr (and you are too, right?!), I’m simply going to place all these image files in the Scripts directory in my MDT toolkit package, together with the following PowerShell script which will replace the existing image files with the new ones.

# Get the TS variables
$tsenv = New-Object COMObject Microsoft.SMS.TSEnvironment
$ScriptRoot = $tsenv.Value("ScriptRoot")
$OSDTargetSystemRoot = $tsenv.Value("OSDTargetSystemRoot")
# Rename default wallpaper
Rename-Item $OSDTargetSystemRoot\Web\Wallpaper\Windows\img0.jpg img1.jpg Force
# Copy new default wallpaper
Copy-Item $ScriptRoot\img0.jpg $OSDTargetSystemRoot\Web\Wallpaper\Windows Force
# Remove old wallpaper
Remove-Item $OSDTargetSystemRoot\Web\Wallpaper\Windows\img1.jpg Force Confirm:$false
# Define image file list
$Images = @(
'img0_1024x768.jpg'
'img0_1200x1920.jpg'
'img0_1366x768.jpg'
'img0_1600x2560.jpg'
'img0_2160x3840.jpg'
'img0_2560x1600.jpg'
'img0_3840x2160.jpg'
'img0_768x1024.jpg'
'img0_768x1366.jpg'
)
# Copy each 4K image
Foreach ($Image in $Images)
{
Copy-Item "$ScriptRoot\$Image" $OSDTargetSystemRoot\Web\4K\Wallpaper\Windows Force
}

If you are using standalone ConfigMgr, then you will need to create a package containing the image files and PowerShell script, scrap the TS variable section at the top of the script, and change the variables used:

$ScriptRoot > $PSScriptRoot

$OSDTargetSystemRoot > $env:windir

After updating the MDT toolkit package to my distribution points (or distributing the package for standalone ConfigMgr), I can add a step to my OSD task sequence to run a PowerShell script. For MDT-integrated, I can use the MDT step Run PowerShell Script:

ts

If standalone ConfigMgr, I can use the ConfigMgr step Run PowerShell Script and reference the package:

ts2

I am adding this step during the Post Install phase, just after laying the OS image on the disk.  At this point, we are still in WinPE, so we are not changing the online OS and therefore no permission changes are required.

And that’s it!  Now all the default wallpaper images will be replaced with my custom ones.  The beauty of doing this during OSD is that the end user can still change the default image if he wants to. If you prefer to lock the default wallpaper, then consider using group policy instead.

Get OSD Info Post-Deployment with PowerShell

In MDT-integrated Configuration Manager, a UDI task sequence contains a couple of steps called Branding to Reg that brands OSD variables to the registry.  This can be useful for reporting, and they can be inventoried with Configuration Manager using the handy RegKeyToMOF utility.

Capture

These steps can also be added manually to a ZTI task sequence as Kenneth van Surksum describes in his blog series.

The steps run a script called “OSDBranding.vbs” which defines which variables will be stamped to the following registry location:

HKLM\Software\Microsoft\MPSD\OSD

You can edit this script to add / remove any variables you want to save in the registry.  If you have custom variables that begin with “OSD” for example, these will be saved to the registry by default.

I wrote the following PowerShell script to retrieve these OSD registry values post-deployment from any remote computer.  The script includes a calculation of the deployment duration if the OSDStartTime and OSDEndTime variables are populated and adds it as OSDDuration.  It only returns properties that actually have populated values.

Example:


Get-OSDInfo -Computername PC001

Capture
 

Get-OSDInfo


function Get-OSDInfo
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $true,
                ValueFromPipelineByPropertyName = $true,
                ValueFromPipeline = $true,
        Position = 0)]
        [ValidateScript({
                    Test-Connection -ComputerName $_ -Count 2 -Quiet
        })]
        [string]$ComputerName
    )

    # Define code to run
    $Code = {
        $results = @()

        # Check if the registry key exists, and get the property list if it does
        try
        {
            $Properties = Get-Item -Path 'HKLM:\SOFTWARE\Microsoft\MPSD\OSD' | Select-Object -ExpandProperty Property -ErrorAction Stop
        }
        catch
        {
            Write-Host -Object "$_" -ForegroundColor Red
            continue
        }

        # Get the property values for each key
        $Properties | ForEach-Object -Process {
            $value = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\MPSD\OSD' -Name $_ | Select-Object -ExpandProperty $_
            if ($value)
            {
                $obj = New-Object -TypeName psobject
                Add-Member -InputObject $obj -Name Property -Value $_ -MemberType NoteProperty
                Add-Member -InputObject $obj -Name Value -Value $value -MemberType NoteProperty
                $results += $obj
            }
        }

        # Calculate OSD duration if start time and end time exists
        $start = $results | Where-Object -FilterScript {
            $_.Property -eq 'OSDStartTime'
        }
        $end = $results | Where-Object -FilterScript {
            $_.Property -eq 'OSDEndTime'
        }
        if (($start.Value -ne $null) -and ($end.Value -ne $null))
        {
            $Hours = (($end.Value | Get-Date) - ($start.Value | Get-Date)).Hours
            $Minutes = (($end.Value  | Get-Date) - ($start.Value | Get-Date)).Minutes
            $Duration = "$Hours hours $Minutes minutes"
            $obj = New-Object -TypeName psobject
            Add-Member -InputObject $obj -Name Property -Value 'OSDDuration' -MemberType NoteProperty
            Add-Member -InputObject $obj -Name Value -Value $Duration -MemberType NoteProperty
            $results += $obj
        }

        # Sort and return results
        $results = $results | Sort-Object -Property Property
        if ($results)
        {
            return $results
        }
    }

    # Invoke the code remotely
    try
    {
        Invoke-Command -ComputerName $ComputerName -ScriptBlock $Code -ErrorAction Stop |
        Select-Object -Property Property, Value |
        Format-Table -AutoSize
    }
    catch
    {
        Write-Host -Object "$_" -ForegroundColor Red
    }
}

 

 

Prompting the End-User during ConfigMgr Application Installs

As a Configuration Manager administrator, from time to time I have to deploy an application where I need to notify the end-user of something before the installation begins. A recent example was a plugin for IE that would fail to install if Internet Explorer was running at the time. I can force-ably kill the running process of course, but that’s not necessarily a nice experience for the user – without warning their browser and any open tabs get closed. So better to notify them first, and give them a chance to close the application themselves and save any work. Rather than email each targeted user and warn them to close Internet Explorer before the plugin installs (which they probably ignore or forget anyway), I wanted the installation process to handle that by some kind of prompt.

I could create a script wrapper for the plugin but that would necessitate running in the user context to display interactively. An easier way is simply to install it using a task sequence with some additional steps that will prompt the user first, kill the process if necessary, then install the plugin. A task sequence also gives me better logging.

The problem with a task sequence is that it runs in the system context, so I cannot interact with the end user who is effectively working in a different session. This can be solved however by using the ServiceUI.exe that comes with MDT. Sometime ago I wrote a post about how to prompt for input during a task sequence, but in this case I don’t want input, I simply want to use a message box.  I also want something reusable – so I don’t have to create a new package for each custom prompt.

I have a nice PowerShell function that will create a message box for me using the Wscript.shell “popup” method, so I added this function to a script, where I have also defined the message parameters I want to use at the bottom.


function New-PopupMessage {
# Return values for reference (https://msdn.microsoft.com/en-us/library/x83z1d9f(v=vs.84).aspx)

# Decimal value    Description  
# -----------------------------
# -1               The user did not click a button before nSecondsToWait seconds elapsed.
# 1                OK button
# 2                Cancel button
# 3                Abort button
# 4                Retry button
# 5                Ignore button
# 6                Yes button
# 7                No button
# 10               Try Again button
# 11               Continue button

# Define Parameters
[CmdletBinding()]
    [OutputType([int])]
    Param
    (
        # The popup message
        [Parameter(Mandatory=$true,Position=0)]
        [string]$Message,

        # The number of seconds to wait before closing the popup.  Default is 0, which leaves the popup open until a button is clicked.
        [Parameter(Mandatory=$false,Position=1)]
        [int]$SecondsToWait = 0,

        # The window title
        [Parameter(Mandatory=$true,Position=2)]
        [string]$Title,

        # The buttons to add
        [Parameter(Mandatory=$true,Position=3)]
        [ValidateSet('Ok','Ok-Cancel','Abort-Retry-Ignore','Yes-No-Cancel','Yes-No','Retry-Cancel','Cancel-TryAgain-Continue')]
        [array]$ButtonType,

        # The icon type
        [Parameter(Mandatory=$true,Position=4)]
        [ValidateSet('Stop','Question','Exclamation','Information')]
        $IconType
    )

# Convert button types
switch($ButtonType)
    {
        "Ok" { $Button = 0 }
        "Ok-Cancel" { $Button = 1 }
        "Abort-Retry-Ignore" { $Button = 2 }
        "Yes-No-Cancel" { $Button = 3 }
        "Yes-No" { $Button = 4 }
        "Retry-Cancel" { $Button = 5 }
        "Cancel-TryAgain-Continue" { $Button = 6 }
    }

# Convert Icon types
Switch($IconType)
    {
        "Stop" { $Icon = 16 }
        "Question" { $Icon = 32 }
        "Exclamation" { $Icon = 48 }
        "Information" { $Icon = 64 }
    }

# Create the popup
(New-Object -ComObject Wscript.Shell).popup($Message,$SecondsToWait,$Title,$Button + $Icon)
}

# Close the Task Sequence Progress UI temporarily (if it is running) so the popup is not hidden behind
try
    {
        $TSProgressUI = New-Object -COMObject Microsoft.SMS.TSProgressUI
        $TSProgressUI.CloseProgressDialog()
    }
Catch {}

# Define the parameters.  View the function parameters above for other options.
$Params = @(
    "The software 'Custom IE Plugin' is being installed to your computer. Please close Internet Explorer then click OK to continue." # Popup message
    0                           # Seconds to wait till the popup window is closed
    "Contoso IT: Custom IE Plugin" # title
    "Ok"                        # Button type
    "Exclamation"               # Icon type
    )

# Run the function
New-PopupMessage @Params

I place this script in a network share that everyone can access, and then simply call it during the task sequence using ServiceUI.exe.

How to Do It

Firstly, I need to create a package in SCCM containing the ServiceUI.exe for x86 and x64 architectures.  This package has no program, but simply contains the exe files, which I have renamed per architecture.  You can find the ServiceUI.exe in the following locations in your MDT install:

C:\Program Files\Microsoft Deployment Toolkit\Templates\Distribution\Tools\x64, or
C:\Program Files\Microsoft Deployment Toolkit\Templates\Distribution\Tools\x86

Capture

Once I have created and distributed the package, I create a new task sequence and add two “Run command line” steps at the beginning where I will prompt the user, one for x86 OS and one for x64.

Capture

The following things are needed in this step:

  • Use the package you created that contains the ServiceUI executables
  • Call ServiceUI using a process that the end user is running.  This enables ServiceUI to detect the session of the end user and interact with it.  If you are using a task sequence deployment with the option “Show task sequence progress” enabled, then you can use the tsprogressui.exe process, however if you are hiding the task sequence progress from the user, then this process will not exist, so you can call Explorer.exe which is certain to be running in the user session.
    • Eg, ServiceUI_x86 -process:Explorer.exe
  • You must specify the full path to powershell.exe
    • Eg, %SYSTEMROOT%\System32\WindowsPowershell\v1.0\powershell.exe
  • Use the “-File” parameter to call the powershell script that displays the popup.
  • Do NOT use the “timeout” option in the step, as this will cause ServiceUI to give an access denied error.
  • On the Option tab of the step, I use a couple of WMI queries so that the step only runs if the correct OS architecture is detected, and the Internet Explorer process is actually running.  I don’t want to prompt the user to close IE if it’s not actually open.
    • Eg, Select * from win32_OperatingSystem where OSArchitecture = ’32-bit’
    • Select * from Win32_Process where Name = ‘iexplore.exe’

Capture

A couple of things to note:

  • You could include the PowerShell script in the package with the ServiceUI executables, then you can call it locally instead of from a network share.  But the advantage of keeping the script and the executables separate is that you don’t need to create a new package each time you want to add a prompt – you simply reuse the ServiceUI package and create a new PowerShell script in the network share by copying and updating and existing script.
  • If you are using the “Show task sequence progress” option, the script includes some code that will hide the progress UI temporarily while the popup is displayed, otherwise it may appear behind the progress UI.
  • Don’t try to pass parameters when calling the PowerShell script, ServiceUI doesn’t seem to like that.
  • The script function includes a “SecondsToWait” parameter – this is set to 0 by default, which means the popup will stay on the screen indefinitely until a button is clicked.  In some cases this may not be desirable, so you can set a value here such that the task sequence will continue if no button has been clicked for some time.

Next, in case the user ignored the prompt or it timed-out, we add another “Run command line” step to kill the process forcefully using taskkill, if it is still running.

  • Eg, cmd /c taskkill /F /IM iexplore.exe

Capture

Make sure to add the same WMI process query to this step:

Capture

Then in the last step, we install the application itself.

Now, when the application is deployed to the end user’s machine, the first thing that happens is they get a popup on the screen warning them to close Internet Explorer.

Capture

Sweet 🙂

You could customise this further by adding some code to the script that will set a task sequence variable based on the exit code of the popup function, which will tell you what button was pressed, for example Yes, No, Ok, Cancel, Abort, Retry etc.  Then you could perform different activities in the task sequence based on the value of the variable.

Free ConfigMgr Task Sequence Monitoring Tool

Update! (Nov 19 2015 – If you experienced the issue with the app appearing and immediately disappearing again, this has been fixed in the latest release v1.2.1)

Today I am pleased to release a new free application for System Center Configuration Manager users: ConfigMgr Task Sequence Monitor 🙂

capture

It is a tool for monitoring or reviewing task sequence executions in Configuration Manager and is particularly useful for monitoring Operating System deployments.  Where integration with MDT is available and the MDT monitoring web service has been enabled, the app can include the data from MDT for an enhanced monitoring experience of your ZTI OS deployments.

Action output can be displayed for each step of the task sequence, giving more detail about the execution of that step, and is useful for quickly identifying the cause of any errors in the task sequence execution.

The app can also generate an HTML deployment summary report for any task sequence.

Check it out here: https://smsagent.wordpress.com/tools/configmgr-task-sequence-monitor/

The tool is my first publicly released WPF application coded entirely in PowerShell, so I welcome any feedback!

Translating Error Codes for Windows and Configuration Manager

As a Windows and Configuration Manager administrator, I often come across error codes that need translating into their more friendly descriptions.  In Configuration Manager, sometimes these codes are translated for you in the log files, reports and the ConfigMgr console, but sometimes they are not.  Sometimes they will be in decimal format, and sometimes hexadecimal.  For Windows error codes, there are a number of methods to return the friendly descriptions, for example the “net helpmsg”:

Capture

But it can only handle decimal codes:

Capture1

In PowerShell, there is the .Net namespace ComponentModel.Win32Exception, which can handle both decimal and hex:

Capture3

Common Windows error codes are also documented in MSDN:

https://msdn.microsoft.com/en-us/library/windows/desktop/ms681381(v=vs.85).aspx
https://msdn.microsoft.com/en-us/library/cc231199.aspx

However, for error codes that are specific to Configuration Manager, you can use the handy CMTRACE utility in the Configuration Manager toolkit, which has an error lookup.  This returns error descriptions for both Windows and Configuration Manager, supports decimal and hex, and supports error codes from more sources too, including WMI and Windows Update Agent:

Capture2

Capture4

Capture5

But if you are scripting and want to translate an error code, how can you do that?  Well there is a handy little dll file called SrsResources.dll that comes with the installation of the Configuration Manager Console, and can be found here: %ProgramFiles(x86)%\Microsoft Configuration Manager\AdminConsole\bin\SrsResources.dll.  Using this dll, we can translate error codes for Windows, Configuration Manager, WMI etc, and even translate status message IDs.  It will call other dll files when it needs to, to find the error string.

Using PowerShell, we can create the following simple function which will use the SrsResources.dll to translate a decimal or hex error code for us:


function Get-CMErrorMessage {
[CmdletBinding()]
    param
        (
        [Parameter(Mandatory=$True,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
            [int64]$ErrorCode
        )

[void][System.Reflection.Assembly]::LoadFrom("C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\SrsResources.dll")
[SrsResources.Localization]::GetErrorMessage($ErrorCode,"en-US")
}

Capture6

To take it further, we can export a list of error codes, for example here we will use the same function to enumerate all decimal codes between 0 and 50, and also output the equivalent hex codes:


$errorcodes = @()
$i = -1
Do
    {
        $i ++
        $description = Get-CMErrorMessage -ErrorCode $i
        if ($description -notlike "Unknown Error*")
            {
                $hex = '{0:x}' -f $i
                $errorcode = New-Object psobject
                Add-Member -InputObject $errorcode -MemberType NoteProperty -Name DecimalErrorCode -Value $i
                Add-Member -InputObject $errorcode -MemberType NoteProperty -Name HexErrorCode -Value ("0x" + $hex)
                Add-Member -InputObject $errorcode -MemberType NoteProperty -Name ErrorDescription -Value $description
                $errorcodes += $errorcode
            }

    }
Until ($i -eq 50)
$errorcodes | ft -AutoSize

Capture7Pretty cool 🙂  Using this SrsResources.dll creates a log file in your %TEMP% directory called SCCMReporting.log, and this log quickly increases in size, so if you use it a lot check the size of this log file from time to time.  The logging can be useful for identifying which dll was used to find the error string.

To convert between decimal and hexadecimal and vice-versa, we can use this simple function. With PowerShell, you can convert to decimal natively in the console just by entering the hexadecimal code,  but using this function allows us to convert both ways, and is more useful for scripts.


function Convert-ErrorCode {
[CmdletBinding()]
    param
        (
        [Parameter(Mandatory=$True,ParameterSetName='Decimal')]
            [int64]$DecimalErrorCode,
        [Parameter(Mandatory=$True,ParameterSetName='Hex')]
            $HexErrorCode
        )
if ($DecimalErrorCode)
    {
        $hex = '{0:x}' -f $DecimalErrorCode
        $hex = "0x" + $hex
        $hex
    }

if ($HexErrorCode)
    {
        $DecErrorCode = $HexErrorCode.ToString()
        $DecErrorCode
    }
}

Capture8Finally, wrapping all this together, here is a script that uses both functions we have created earlier, and will return all the machines that are in an error state for a ConfigMgr application deployment, with the error code and description.  Because we filter using the current application revision, this actually returns more accurate results than the ConfigMgr console > Deployments node, as that data will include previous application revisions where no data is available for the current revision, which produces misleading results.

First, we query WMI on the ConfigMgr site server for the list of applications and choose the one we want:

Capture

Then we query for the deployments and deployment types for that application, and choose the one we want.  The numbers of errors is returned, but as previously mentioned, this may not be completely accurate at this stage.

Capture2Then we return the results translating the error codes into their descriptions.

Capture3

Cool 🙂

Note that WMI stores the error codes as unsigned integers, but the ConfigMgr console displays errors as signed integers, so we do a conversion and include both in our results.

In the next blog, I describe how to create a SQL database of these error codes for easy referencing in SQL queries: Create a database of error codes and descriptions for Windows and Configmgr

Here’s the complete script:


<#

.SYNOPSIS
    Returns the error code and error descriptions for all computers in an error state for an application deployment

.DESCRIPTION
    This script asks you to choose a ConfigrMgr application, then choose a deployment / deployment type for that application, then returns all the computers that are in an error state for that
    deployment, with the error code and error description.
    Requires to be run on a computer with the ConfigMgr console installed, and the path to the SrsResources.dll needs to be specified in the "Get-CMErrorMessage" function.  You may also
    need to change the localization in this function to your region, eg "en-US".

.PARAMETER SiteServer
    The name of the ConfigMgr Site server

.PARAMETER SiteCode
    The ConfigMgr Site Code

.NOTES
    Script name: Get-CMAppDeploymentErrors.ps1
    Author:      Trevor Jones
    Contact:     @trevor_smsagent
    DateCreated: 2015-06-17
    Link:        https://smsagent.wordpress.com

#>

[CmdletBinding(SupportsShouldProcess=$True)]
    param
        (
        [Parameter(Mandatory=$False)]
            [string]$SiteServer="sccmserver-01",
        [Parameter(Mandatory=$False)]
            [string]$SiteCode="ABC"
        )

function Get-CMErrorMessage {
[CmdletBinding()]
    param
        (
        [Parameter(Mandatory=$True,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
            [int64]$ErrorCode
        )

[void][System.Reflection.Assembly]::LoadFrom("C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\SrsResources.dll")
[SrsResources.Localization]::GetErrorMessage($ErrorCode,"en-US")
}

function Convert-ErrorCode {
[CmdletBinding()]
    param
        (
        [Parameter(Mandatory=$True,ParameterSetName='Decimal')]
            [int64]$DecimalErrorCode,
        [Parameter(Mandatory=$True,ParameterSetName='Hex')]
            $HexErrorCode
        )
if ($DecimalErrorCode)
    {
        $hex = '{0:x}' -f $DecimalErrorCode
        $hex = "0x" + $hex
        $hex
    }

if ($HexErrorCode)
    {
        $DecErrorCode = $HexErrorCode.ToString()
        $DecErrorCode
    }
}

# Get Application
$App = Get-WmiObject -ComputerName $SiteServer -Namespace ROOT\sms\Site_$SiteCode -Class SMS_ApplicationLatest |
    Sort LocalizedDisplayName |
    Select LocalizedDisplayName,SDMPackageVersion,ModelName |
    Out-GridView -Title "Choose an Application" -OutputMode Single

# Get Deployment Types and Deployments for Application
$DT = Get-WmiObject -ComputerName $SiteServer -Namespace ROOT\sms\Site_$SiteCode -query "Select * from SMS_AppDTDeploymentSummary where AppModelName = '$($App.ModelName)'" |
    Select Description,CollectionName,CollectionID,NumberErrors,AssignmentID |
    Out-GridView -Title "Choose a Deployment / Deployment Type" -OutputMode Single

# Get Errors
$Errors = Get-WmiObject -ComputerName $SiteServer -Namespace ROOT\sms\Site_$SiteCode -query "Select * from SMS_AppDeploymentErrorAssetDetails where AssignmentID = '$($DT.AssignmentID)' and DTName = '$($DT.Description)' and Revision = '$($App.SDMPackageVersion)' and Errorcode <> 0" |
    Sort Machinename |
    Select MachineName,Username,Starttime,Errorcode

if ($Errors -ne $null)
{
    # Create new object with error descriptions in
    $AllErrors = @()
    foreach ($item in $Errors)
        {
            $errordescription = Get-CMErrorMessage -ErrorCode $item.Errorcode
            $hex = Convert-ErrorCode -DecimalErrorCode $item.Errorcode
            $int = [int]$hex
            $obj = New-Object psobject
            Add-Member -InputObject $obj -MemberType NoteProperty -Name ComputerName -Value $item.MachineName
            Add-Member -InputObject $obj -MemberType NoteProperty -Name UserName -Value $item.Username
            Add-Member -InputObject $obj -MemberType NoteProperty -Name StartTime -Value $([management.managementDateTimeConverter]::ToDateTime($item.Starttime))
            Add-Member -InputObject $obj -MemberType NoteProperty -Name UnsignedIntErrorCode -Value $item.Errorcode
            Add-Member -InputObject $obj -MemberType NoteProperty -Name SignedIntErrorCode -Value $int
            Add-Member -InputObject $obj -MemberType NoteProperty -Name HexErrorCode -Value $hex
            Add-Member -InputObject $obj -MemberType NoteProperty -Name ErrorDescription -Value $errordescription
            $AllErrors += $obj
        }
    # Return results
    write-host "Application: $($App.LocalizedDisplayName)"
    write-host "DeploymentType: $($DT.Description)"
    write-host "TargetedCollection: $($DT.CollectionName)"
    $AllErrors | ft -AutoSize
}
Else {Write-host "No results returned."}

 

Setting the Default Windows Wallpaper during OS Deployment

Note: For a Windows 10 Version, see this blog instead: https://smsagent.wordpress.com/2017/07/06/setting-the-default-wallpaper-for-windows-10-during-configmgr-osd/

Recently I was given an interesting task – set the default wallpaper on new computer builds with ConfigMgr OSD, but don’t lock it such that users can’t change it.  It turns out it is simple enough to do, but it requires changing the default wallpaper that comes with windows, which can be found at C:\Windows\Web\Wallpaper\Windows\img0.jpg.  If you check the security on that file however, you’ll notice that only the “TrustedInstaller” has full permissions to it, so to change it in the online OS requires messing with permissions.  Instead, you can change offline during the WinPE phase of the deployment, which bypasses the permissions problem.

This procedure requires MDT-integrated ConfigMgr 2012, and also requires Windows PowerShell to be added to your boot image.  I’ve tested on Windows 7 but should also work with Windows 8.1 as it uses the same location for the default wallpaper.

Create a Wallpaper

First, create your new default wallpaper.  The recommended resolution is 1920×1200, which is the same as the built-in default wallpaper.  Save it as “img0.jpg”

Create a PowerShell Script

Use the following code in a new PowerShell Script, and save it as “Set-DefaultWallpaper.ps1”

The script will load some task sequence variables, rename the existing default wallpaper to “img1.jpg”, and copy the new “img0.jpg” into the same directory.


# Get the TS variables
$tsenv = New-Object -COMObject Microsoft.SMS.TSEnvironment
$ScriptRoot = $tsenv.Value('ScriptRoot')
$OSDTargetSystemRoot =  $tsenv.Value('OSDTargetSystemRoot')

# Rename default wallpaper
Rename-Item $OSDTargetSystemRoot\Web\Wallpaper\Windows\img0.jpg img1.jpg -Force

# Copy new default wallpaper
Copy-Item $ScriptRoot\img0.jpg $OSDTargetSystemRoot\Web\Wallpaper\Windows -Force

Copy the files to your MDT Package directory

Copy the new wallpaper and the PowerShell script to the Scripts directory of your MDT package source files, and update the MDT Package to your distribution points.

Capture1

Capture2

Add a Task Sequence Step

Now edit your OSD task sequence and after the “Apply Operating System Image” Step and before the “Apply Windows Settings” step, add a “Run PowerShell Script” step from the MDT menu.

Capture3

Capture4

Add the path to the script file in the step, eg %SCRIPTROOT%\Set-DefaultWallpaper.ps1

Capture5
That’s it!  After OSD, your computer will have two files in the location C:\Windows\Web\Wallpaper\Windows, the new default wallpaper, and the old one renamed to “img1.jpg”.  Anyone who logs into the computer will get the new default wallpaper, and they are free to change it if they wish.

If you want to also change the colour scheme in Windows 8.1, there’s a nice post on the Coretech blog.

How to Quickly Retrieve Errors from OS Deployment Logs with PowerShell

I saw an interesting post yesterday by Keith Garner on using PowerShell’s “Select-string” to search OS deployment logs and find errors by searching on the entry type instead of the usual way that the CMTrace utility does it – by highlighting keywords.  I decided to take the idea further and create a script that will search my deployment logging shares, allow me to choose which deployed computer to search on, which log file to search, and then return the errors into CMTrace for easy viewing.

Here’a a quick demo:

I run the script and it searches my OSD logging directory (“SLShare” variable in the customsettings.ini) and returns all deployments in the last 10 days:

osd1

I select the deployment I want and click OK.  Then it searches recursively for all the log files in that directory, and asks me to choose one:

osd2

I want to search the smsts.log for errors, so I select it and click OK.  It then puts all the errors into a temporary log file and invokes it.  As .log files open with CMTrace by default, I can read through the errors more easily.

osd3

The script will then watch the CMTrace process, wait until you exit it, and delete the temporary log.

Configure the Script

The script has comment-based help, and there are some parameters you can use.  You will need to set the defaults in the script as the parameters are not mandatory.

$NumberOfDays – The script will search the logging share for deployments in the last x number of days

$LogDirectory – The location of your OSD logging share, eg your MDT “SLShare” directory

$Dynamic – Use this switch to search the dynamic logging share instead, eg your MDT “SLShareDynamicLogging” directory

$DynamicLogDirectory – the location of the  “SLShareDynamicLogging” directory

The Script


<#

.SYNOPSIS
    Retrieves error entries from logs used by MDT and ConfigMgr during OSD

.DESCRIPTION
    This script searches the OSD logs for error entries. You can:
    - specify the location of the log files, for example your deployment logging share
    - choose which computer's log files to search
    - choose which log file to search
    The errors will be added to a temporary log file, which will open with CMTrace (you need to set that as the default viewer for log files).
    This script is based on an idea from Keith Garner: https://keithga.wordpress.com/2015/05/04/find-errors-quickly-in-a-sccm-or-mdt-log-file/

.PARAMETER LogDirectory
    The location of your OSD logging directory, for example your "SLShare"

.PARAMETER DynamicLogDirectory
    The location of your OSD dynamic logging directory, for example your "SLShareDynamicLogging".  Use with the -Dynamic switch.

.PARAMETER NumberOfDays
    The number of days of deployment logs to retrieve, for example all deployments in the last 5 days

.PARAMETER Dynamic
    Use this switch to search the dynamic logging directory

.EXAMPLE
    .\Get-OSDLogErrors.ps1
    Searches the default logging directory for all deployments in the default number of days past, prompts you to choose the computer, then the log, then displays the error entries
    in a temporary log file

.EXAMPLE
    .\Get-OSDLogErrors.ps1 -LogDirectory \\mymdtserver\MDT_Logs$ -NumberOfDays 10
    Searches the logging directory specified for all deployments in the last 10 days, prompts you to choose the computer, then the log, then displays the error entries in a temporary
    log file

.EXAMPLE
    .\Get-OSDLogErrors.ps1 -Dynamic
    Searches the default dynamic logging directory for all deployments in the default number of days past, prompts you to choose the computer, then the log, then displays the error
    entries in a temporary log file

.NOTES
    Script name: Get-OSDLogErrors.ps1
    Author:      Trevor Jones
    Contact:     @trevor_smsagent
    DateCreated: 2015-05-11
    Link:        https://smsagent.wordpress.com

#>

[CmdletBinding()]
    param
        (
        [parameter(Mandatory=$False, HelpMessage="The number of days of deployment log files to check")]
            [string]$NumberOfDays = 5,
        [Parameter(Mandatory=$False, HelpMessage="The location of the OSD logging directory")]
            [string]$LogDirectory = "\\sccmserver01\MDT_Logs$",
        [parameter(Mandatory=$False)]
            [switch]$Dynamic,
        [Parameter(Mandatory=$False, HelpMessage="The location of the OSD dynamic logging directory")]
            [string]$DynamicLogDirectory = "\\sccmserver01\MDT_Logs$\Dynamic"
        )

if ($Dynamic)
    {$LogDirectory = $DynamicLogDirectory}

# Get the directory listing of deployed computers, and prompt to choose
if (test-path $LogDirectory)
    {
        $Computer = Get-ChildItem $LogDirectory |
            Where-Object {$_.LastWriteTime -ge (Get-Date).AddDays(-$NumberOfDays) -and $_.Name -notin ('Dynamic','Variables')} |
            Sort LastWriteTime -Descending |
            Select Name, LastWriteTime |
            Out-GridView -Title "Choose a deployed computer" -OutputMode Single |
            Select -ExpandProperty Name
    }
Else {Write-Warning "Could not access the log directory"; break}

# Get the list of log files for that computer, and prompt to choose
if ($Computer -ne $null)
    {
        $LogFile = Get-ChildItem "$LogDirectory\$Computer" -Recurse |
            Where-Object {$_.Mode -ne "d----"} |
            Select Name, @{N='Size (KB)'; E={[math]::Round(($_.Length / 1KB), 2)}}, LastWriteTime, @{N='Location'; E={$_.FullName}} |
            Sort Name |
            Out-GridView -Title "Choose a log file" -OutputMode Single |
            Select -ExpandProperty Location

        # Search the log file for entries of type 2 or 3
        $Entries = Select-String -Path "$LogFile" -Pattern "type=""(2|3)"""
    }
Else {break}

if ($Entries -eq $Null)
    {Write-Warning "No error entries found."; break}
Else
    {
        # Output each log entry to a temporary log file and invoke it
        $x = -1
        foreach ($Line in $Entries)
            {
                $x ++
                $Entries[$x].Line | Out-File "$env:TEMP\OSDerrors.log" -Append
            }
        Invoke-Item "$env:TEMP\OSDerrors.log"

        # Wait for CMTrace to start
        do
            {Start-Sleep -Seconds 2}
        until ((Get-Process CMTrace -ErrorAction Ignore) -ne $null)

        # wait until CMTrace is closed, then delete the temporary log file
        do
            {
                start-sleep -Seconds 2
                $process = Get-process -name CMTrace -ErrorAction Ignore
            }
        until ($process -eq $null)
        Remove-Item "$env:TEMP\OSDerrors.log"
    }

An Easy Way to Monitor Your ConfigMgr OS Deployments

There are several ways to monitor your OS deployments, since the monitoring service feature was introduced in MDT 2012 and above.  But if you are using ConfigMgr, or MDT-Integrated ConfigMgr, then it’s very easy to monitor your OSDs using a connection to the ConfigMgr database.  I made a post about this before, using a Powershell script to get a ‘snapshot’ of OSD status from the database.  In this post, I will use Microsoft Excel which allows me to set a refresh period on the SQL query, meaning I can get almost real-time information about my deployments as they happen, in step-by-step detail, even if my deployments are running on another geographical site.  This can be especially useful if a deployment fails (in a controlled way) as the error information is usually available in the database, and therefore visible in my Excel document.

Capture4

Create the Excel Document

First, we need to create a new Excel workbook and create a data connection to the ConfigMgr SQL database.  This is described in more detail in my post: Creating dynamic reports for configuration manager with microsoft excel

SQL Query

When you create the data connection, paste the following SQL query


Select Name0 as 'Computer Name'
,Name as 'Task Sequence'
,ExecutionTime
,Step
,ActionName
,GroupName
,tes.LastStatusMsgName
,ExitCode
,ActionOutput

from vSMS_TaskSequenceExecutionStatus tes
inner join v_R_System sys on tes.ResourceID = sys.ResourceID
inner join v_TaskSequencePackage tsp on tes.PackageID = tsp.PackageID
where tsp.Name in ('Windows OS Deployment x64', 'Windows OS Deployment x86')
and DATEDIFF(hour,ExecutionTime,GETDATE()) < 8
ORDER BY Step Desc

In line 14 of the query, I have added the names of my OSD task sequences, you will need to add your own.  You could, of course, create more than one data connection in the same Excel workbook, using a new worksheet for each OSD task sequence.

In line 15, I am returning data from the database from the last 8 hours.  You can change this to your preferred setting.

Set the Refresh Period

When creating the data connection, you have the option to refresh the data periodically.  Set this to refresh every minute.  This will simply run the SQL query every minute, keeping the workbook up-to-date.

Capture

Using the OSD Monitoring Workbook

Filtering

The workbook will display the computer name, task sequence name, step number and execution time, the name of the action and the group it belongs to, the last status message for that action, the exit code, and the action output, which is basically a snippet from the smsts.log log file for that action.

You can use Excel’s filters to filter the information you want to see.  For example, if you want to monitor OSD for a specific machine, simply filter for the machine name in the ‘Computer Name‘ column.

Capture2

Note that the computer name will only appear if the machine is already known to ConfigMgr, ie it is being rebuilt.  If it is bare metal, it will display ‘Unknown’ in the computer name.

You can also use the ‘ExecutionTime‘ column to filter for OSDs during a specific time period.  For example, here I filter for all OSD actions after 10:00:

Capture3

Finding errors

Because the exit code is reported for each step, you can simply filter that column to discover any steps that failed by selecting the non-zero exit codes.  Then you can check the ‘ActionOutput‘ column for a snippet from the smsts.log to find more about why it failed.

For example, one of my OSDs failed to apply the OS image:

PC003 Windows OS Deployment x64 16/01/2015 08:28 89 Apply x64 Operating System Image (Partition 3) OS Image x64 The task sequence execution engine failed executing an action -2147024751

I’ve seen that before, but let’s check the ActionOutput column for more details:


... ,721)
ApplyImage(), HRESULT=80070091 (e:\nts_sccm_release\sms\client\osdeployment\applyos\installimage.cpp,1830)
Apply(), HRESULT=80070091 (e:\nts_sccm_release\sms\client\osdeployment\applyos\installimage.cpp,2019)
installer.install(), HRESULT=80070091 (e:\nts_sccm_release\sms\client\osdeployment\applyos\installimage.cpp,2094)
Closing image file \\sccmsrv-01.testlab.com\SMSPKGC$\ABC00116\W7-X64-001.wim
ReleaseSource() for \\sccmsrv-01-testlab\SMSPKGC$\ABC00116\.
reference count 1 for the source \\sccmsrv-01.diasemi.com\SMSPKGC$\ABC00116\ before releasing
Released the resolved source \\sccmsrv-01.diasemi.com\SMSPKGC$\ABC00116InstallImage( g_InstallPackageID, g_ImageIndex, targetVolume, ImageType_OS, g_ConfigPackageID, g_ConfigFileName, bOEMMedia, g_RunFromNet ), HRESULT=80070091 (e:\nts_sccm_release\sms\client\osdeployment\applyos\applyos.cpp,509)
Installation of image 1 in package ABC00116 failed to complete..
The directory is not empty. (Error: 80070091; Source: Windows)

As I suspected, it could not wipe the partition of all the files, so I need to do it manually with diskpart.

A handy solution for easy monitoring and troubleshooting of your ConfigMgr OSDs 🙂