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.

 

 

 

Add Custom Notifications to a ConfigMgr Task Sequence

One feature I would really like to see added to a Configuration Manager task sequence is the ability to natively provide notification messages to the logged-on user. Previously, to accomplish this, I have used simple pop-up notifications like the Wscript Shell Popup method in a PowerShell script, together with the handy ServiceUI utility in MDT to display the notification in the logged-on users’ session. This has worked well enough for simple messages, and has been useful in several scenarios. For example, see my blog post about prompting for input during a task sequence.

Recently I wrote a PowerShell function to display my own custom notifications using WPF, called New-WPFMessageBox. This allows for much greater customisation of the message box, including adding your own WPF content. So I decided to revisit displaying notifications during a task sequence using this new function instead. In this post I will show you how to add a “Restart Required” notification to run at the end of a task sequence. This can be used to advise the user that a restart needs to take place after the installation of some software for example, and give them the option to restart immediately, or restart later.

RestartRequired

Instead of using the ServiceUI utility – which works well, but it still runs in SYSTEM context even though it will allow you to display in the logged-on users’ session – I decided on a different method that allows you to truly run a process in the users’ context. Thanks to a tip from Roger Zander I found some C# sharp code by a guy named Justin Murray that can be used in PowerShell to make this possible.

Invoke-PSScriptAsUser

Create a new PowerShell script containing the following code. In the $Source variable, copy and paste the C# code from https://github.com/murrayju/CreateProcessAsUser/blob/master/ProcessExtensions/ProcessExtensions.cs. I have renamed the namespace (line 4 in the C# code) from namespace murrayju.ProcessExtensions to namespace Runasuser.


Param($File)

$Source = @"

"@

# Load the custom type
Add-Type -ReferencedAssemblies 'System', 'System.Runtime.InteropServices' -TypeDefinition $Source -Language CSharp -ErrorAction Stop

# Run PS as user to display the message box
[Runasuser.ProcessExtensions]::StartProcessAsCurrentUser("$env:windir\System32\WindowsPowerShell\v1.0\Powershell.exe"," -ExecutionPolicy Bypass -WindowStyle Hidden -File $PSScriptRoot\$File")

Save this script as Invoke-PSScriptAsUser.ps1

Display-RestartNotification

Create a new PowerShell script containing the following code. At the top paste in my New-WPFMessageBox function from https://gist.github.com/SMSAgentSoftware/0c0eee98a673b6ac34f5215ea6841beb. You can, of course, customise the notification as you wish.


# Paste here New-WPFMessageBox function from https://gist.github.com/SMSAgentSoftware/0c0eee98a673b6ac34f5215ea6841beb

$Params = @{
    Content = "You must restart your computer before using Software X."
    Title = "Computer Restart Required!"
    TitleFontSize = 20
    TitleFontWeight = "Bold"
    TitleBackground = "OrangeRed"
    ButtonType = "None"
    CustomButtons = "RESTART NOW","RESTART LATER"
    Sound = 'Windows Notify'
}

New-WPFMessageBox @Params
If ($WPFMessageBoxOutput -eq "RESTART NOW")
{
    Restart-Computer
}

The function saves the content of the button you click to the variable $WPFMessageBoxOutput, so you can use this to perform certain actions depending on which button the user clicks, in this case simply restarting the computer. This variable is only available in the script scope however.

Save this script as Display-RestartNotification.ps1.

Create a Package

Now create a standard package in ConfigMgr containing both of these scripts in the same directory, and distribute the content. No program is required for the package.

Configure Task Sequence

In your task sequence, add a Run Powershell Script step. Reference the package you created and enter the script name and parameters:

Script name: Invoke-PSScriptAsUser.ps1

Parameters: -File Display-RestartNotification.ps1

TS

When the task sequence executes, it will run the Invoke-PSScriptAsUser.ps1 in SYSTEM context, which will in turn run PowerShell in the logged-on users’ context and run the Display-RestartNotification.ps1 script, which displays the notification to the user.

The task sequence will not wait for the user to respond to the message; it will simply finish up in the background and the notification will remain on screen until the user responds to it.

If you enabled the option to Show task sequence progress then the notification will display behind the task sequence progress UI. Since this is the last step in the sequence it doesn’t matter, but if you have other steps running after the notification, you should hide the task sequence progress UI at that point. Since ConfigMgr 1706 we have the TSDisableProgressUI task sequence variable that can do that for us, so simply place a step before the notification step disabling the progress UI:

tsui

The ability to run a process in the user context during a task sequence is quite useful, not just for displaying notifications, but for running any code or process that must run in the user context, for example setting HKCU registry keys, or triggering a baseline evaluation that has user-based settings.

ConfigMgr OS Upgrade TS W10 1709 Does Not Care About Windows Edition

Today I ran a ConfigMgr OS Upgrade task sequence configured to use the Enterprise edition of Windows 10 1709 on a workstation that had Windows 10 Pro 1703 installed. Since the VLC media for 1709 contains the various editions in different indexes, you are supposed to choose the relevant one in the Upgrade Operating System step.

OSUpgrade

Of course, I expected the TS to fail because the configured edition is different to the edition that TS was being run on – but to my surprise, it didn’t care and the TS succeeded! Windows 10 Pro 1703 was upgraded to Windows 10 Pro 1709.

Not sure if that’s a bug or a feature, but it’s actually quite convenient!

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.

New tool: ConfigMgr PXE Boot Log

Today I’m releasing a new tool for ConfigMgr admins and IT support staff!  This tool displays PXE boot events in an easy-to-view format and provides a history of PXE boot attempts on a distribution point during a selected time period. The tool can also display any records that exist in ConfigMgr associated with a device based on its SMBIOS GUID. The ‘log’ entries returned come from the status messages sent by the distribution point and not from the SMSPXE.log.

PXEBoot

The tool can help in troubleshooting PXE boot failures such as devices not being targeted with a deployment, duplicate records in the ConfigMgr database, or mismatched unique identifiers because of hardware changes.

Requirements

  • Minimum PowerShell 5
  • Minimum .Net Framework 4.5
  • Minimum read-only access to the Configuration Manager database (db_datareader role)

Installation

The tool can be downloaded and installed from the Technet Gallery.

Configuration

  • For first time use, click the “More Options” menu (3 dots, top-right) and choose Settings
  • Enter your Configuration Manager SQL Server and Database
  • Optionally select to view dates and times in your local timezone instead of the default UTC.
  • Click Save.

Using the Tool

  • Select a PXE-Enabled distribution point from the drop-down list.
  • Select a time period to view results
  • Click Retrieve Log
  • Double-click any log entry to retrieve the associated record/s for the device in ConfigMgr

Source Code

Source code is available on GitHub

Downloading the Latest Dell Driver Packs with PowerShell

It was a regular Tuesday morning and I hadn’t yet had my ‘PowerShell fix’ for the week, so when I realised I needed to download a new driver pack from Dell for my ConfigMgr OS deployments, I could hear a faint voice calling out to me: ‘Dude, I can make your life easier! Work smarter, not harder!

Of course, that’s only ever partially true, because with PowerShell you must work harder today in order to work smarter tomorrow, but in the interest of long-term benefit I proceeded to fire up the ISE.

Suddenly, a thought arose from my subconscious: ‘Wait just a second. Don’t re-invent the wheel.  Aren’t there already some good solutions out there for this?

Well yes, that’s true,‘ my internal musings continued. ‘Most notably, we have a very cool tool by Maurice Daly – the Driver Automation Tool. With this we can just click buttons and go get coffee while the tool does all the hard work. It’ll even import the driver packs into SCCM. I like that!

Yes that is awesome.‘ I responded to myself, ‘Problem is, I still need a PowerShell fix. So maybe I can find a different way of downloading driver packs. What do you suggest?

Well, we have the Dell Driver Pack Catalog. Dell even provide examples of how to use that with PowerShell to find the URLs you need to download the relevant cab files.

Yes, this is cool too. But I think there is still another way. Doesn’t Dell’s TechCenter wiki contain the download URLs for the most recent driver packs?

Yes, it does. But you want to use PowerShell, right?

Correct.’

So what are you thinking?

Web-scraping.

Ah, you bad boy! Let’s do it!

Dell maintains a wiki page containing links to the latest driver packs which can be found here:

http://en.community.dell.com/techcenter/enterprise-client/w/wiki/2065.dell-command-deploy-driver-packs-for-enterprise-client-os-deployment

You simply find the model and operating system version you want and click the link, which leads you to another wiki page containing a download URL.

Rolling up the sleeves, I whipped up some code that will scrape these web pages to find the download URL for the current driver pack version and download it using a BITS transfer. You can then import or add the driver pack into ConfigMgr for OSD using your favourite method (which is PowerShell, right?!)

The resulting script is quite simple to use and works reliably in my testing, although it takes a few seconds to filter the HTML in order to find the appropriate download URL.

You can download a driver pack for a single model, for example:


Download-LatestDellDriverPack -Model "Latitude E7470" -OperatingSystem 'Windows 10' -DownloadDirectory C:\DriverPacks -Verbose

Just provide the model name, operating system version and a location to save the downloaded file to. Support is provided for verbose output.

You can also pass a list of models to the script and it will download each one in turn, for example:


"M4800","Optiplex 9020","E6420","E5250" | Download-LatestDellDriverPack -OperatingSystem 'Windows 7' -DownloadDirectory C:\DriverPacks -Verbose

drivers

The script in action

The script will work for any driver pack with an operating system Windows 7 or higher (are you really deploying anything older than that?!), and there is no proxy support currently.

Here’s the full script:

[CmdletBinding()]
Param
(
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
ValueFromPipeline=$true,
Position=0)]
[String[]]$Model,
[Parameter(Mandatory=$true,
Position=1)]
[ValidateSet("Windows 10","Windows 8.1","Windows 8","Windows 7")]
$OperatingSystem,
[Parameter(Mandatory=$true,
Position=2)]
[String]$DownloadDirectory
)
Begin
{
function Get-LatestDellDriverPackURL {
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
[String]$Model,
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=1)]
[ValidateSet("Windows 10","Windows 8.1","Windows 8","Windows 7")]
$OperatingSystem
)
# Remove the "E" prefix character from Latitude models due to some dodgy Dell URLs…
If ($Model.ToCharArray()[0] -eq "E" -and $Model -notmatch "Embedded")
{
$Model = $Model.Replace("E","")
}
# Find the specific wiki page for the model from the main wiki page
$URI = "http://en.community.dell.com/techcenter/enterprise-client/w/wiki/2065.dell-command-deploy-driver-packs-for-enterprise-client-os-deployment"
Try
{
$HTML = Invoke-WebRequest Uri $URI ErrorAction Stop
If ($OperatingSystem -eq "Windows 8")
{
# Filter out Windows 8.1 from the results if it's just Windows 8
$Href = $HTML.AllElements | Where {$_.innerText -match ("$Model" + " W") -and $_.innerText -match $OperatingSystem -and $_.innerText -notmatch "8.1" -and $_.innerText -match "Driver" -and $_.tagName -eq "A"} | Select ExpandProperty href
}
Else
{
$Href = $HTML.AllElements | Where {$_.innerText -match ("$Model" + " W") -and $_.innerText -match $OperatingSystem -and $_.innerText -match "Driver" -and $_.tagName -eq "A"} | Select ExpandProperty href
}
}
Catch
{
$_
Return
}
If (!$Href)
{
Write-Error "No Wiki page found for $Model and $OperatingSystem."
Return
}
# Find the download URL from the model
$URI = "http://en.community.dell.com/$Href"
Try
{
$HTML = Invoke-WebRequest Uri $URI ErrorAction Stop
$CabDownloadLink = $HTML.AllElements | Where {$_.innerHTML -match "Download Now" -and $_.tagName -eq "A"} | Select ExpandProperty href
Return $CabDownloadLink
}
Catch
{
$_
Return
}
Write-Error "No download URL found for $Model."
}
# Start a timer
$Stopwatch = New-Object System.Diagnostics.Stopwatch
$Stopwatch.Start()
}
Process
{
Foreach ($System in $Model)
{
Write-Verbose "Finding the download URL for '$System' and '$OperatingSystem'"
Try
{
# Get the download URL
$URL = Get-LatestDellDriverPackURL Model $System OperatingSystem $OperatingSystem ErrorAction Stop
}
Catch
{
$Stopwatch.Stop()
$_
Return
}
Write-Verbose "Initiating download of URL '$URL' with BITS"
Try
{
# Begin the download
Start-BitsTransfer Source $URL Destination $DownloadDirectory
}
Catch
{
$Stopwatch.Stop()
$_
Return
}
}
}
End
{
$Stopwatch.Stop()
Write-Verbose "Completed in $($Stopwatch.Elapsed.Minutes) minutes and $($Stopwatch.Elapsed.Seconds) seconds"
}

New Free App – ConfigMgr Deployment Reporter

Just released a new free application for ConfigMgr admins – ConfigMgr Deployment Reporter.  I developed this app for use in the organisation I currently work for, and it turned out quite well, so I decided to release a public version to the community!

capture

I developed this app as an alternative (and IMO easier) way to report on ConfigMgr deployments than using the ConfigMgr console. It uses a little different format than the console node allowing you to select which deployment you wish to view data for based on the “feature type” (ie application, package etc) and report on only that deployment.  It also introduces a separation of results between all applicable systems for a deployment, and only those systems which have currently reported status, which allows for a more accurate view of the success of a deployment as it progresses.

The app allows the creation of charts and HTML-format reports to give a nice graphical snapshot of a deployment.

I also added the capability to report per-device for Software Update and Task Sequence deployments.  For Software Updates, this allows you to see which updates from the deployment are applicable to the machine and the status of each update, and for Task Sequences it allows viewing the execution status of each step in the task sequence for the selected device.

As usual, I code purely in PowerShell using WPF for the UI.  This time I added metro styling using the excellent MahApps.Metro project 🙂

Download the app from here.

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.

Send a Weekly OS Deployment Summary Report with PowerShell

Here’s a script I wrote that sends a simple summary of ConfigMgr OS Deployments in the last week as an HTML-formatted email.

It gives you the start and finish date/time, duration and model for each computer deployed (where the information is available in SCCM), and a list of any steps in an error state for any of the deployments.

You can use a scheduled task to send this report to yourself each week for a nice deployment overview 🙂

OSDreport

Note: to generate ad-hoc reports for specific time-periods for any ConfigMgr Task Sequence you can use my tool ConfigMgr Task Sequence Monitor

Generate-OSDReport

At the top of the script, enter the following:

  • SQLServer name (and instance name where applicable)
  • ConfigMgr Database name
  • The time in hours past that the report will display data for
  • The name/s of your OSD tasks sequences
  • Email parameters

You need db_datareader permission to the ConfigMgr database with the account running the script.


#requires -Version 2

# Database info
$dataSource = 'mysqlserver\myinstance' # SQLServer\Instance
$database = 'CM_ABC' # Database name
$TimeInHours = '168' # 168 = 7 days
$OSDTaskSequences = "
    'Windows OS Deployment x86',
    'Windows OS Deployment x64'
    " # Name/s of your OSD Task Sequences

#Email params
$EmailParams = @{
    To         = 'Joe.Bloggs@contoso.com'
    From       = 'PowerShell@contoso.com'
    Smtpserver = 'myexchangeserver'
    Subject    = "Operating System Deployment Weekly Report $(Get-Date -Format dd-MMM-yyyy)"
}

$results = @()

$connectionString = "Server=$dataSource;Database=$database;Integrated Security=SSPI;"
$connection = New-Object -TypeName System.Data.SqlClient.SqlConnection
$connection.ConnectionString = $connectionString
$connection.Open()

$Query = "
    select distinct tes.ResourceID
    from vSMS_TaskSequenceExecutionStatus tes
    inner join v_TaskSequencePackage tsp on tes.PackageID = tsp.PackageID
    where tsp.Name in ($OSDTaskSequences)
    and DATEDIFF(hour,ExecutionTime,GETDATE()) < $TimeInHours
"

$command = $connection.CreateCommand()
$command.CommandText = $Query
$reader = $command.ExecuteReader()
$table = New-Object -TypeName 'System.Data.DataTable'
$table.Load($reader)

foreach ($ResourceID in $table.Rows.ResourceID)
{
    $Query = "
        Select (select top(1) convert(datetime,ExecutionTime,121)
        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 ($OSDTaskSequences)
        and DATEDIFF(hour,ExecutionTime,GETDATE()) < $TimeInHours
        and LastStatusMsgName = 'The task sequence execution engine started execution of a task sequence'
        and Step = 0
        and tes.ResourceID = $ResourceID
        order by ExecutionTime desc) as 'Start',
        (select top(1) convert(datetime,ExecutionTime,121)
        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 ($OSDTaskSequences)
        and DATEDIFF(hour,ExecutionTime,GETDATE()) < $TimeInHours
        and LastStatusMsgName = 'The task sequence execution engine successfully completed a task sequence'
        and tes.ResourceID = $ResourceID
        order by ExecutionTime desc) as 'Finish',
        (Select name0 from v_R_System sys where sys.ResourceID = $ResourceID) as 'ComputerName',
        (select Model0 from v_GS_Computer_System comp where comp.ResourceID = $ResourceID) as 'Model'
    "
    $command = $connection.CreateCommand()
    $command.CommandText = $Query
    $reader = $command.ExecuteReader()
    $table = New-Object -TypeName 'System.Data.DataTable'
    $table.Load($reader)

    if ($table.rows[0].Start.GetType().Name -eq 'DBNull')
    {
        $Start = ''
    }
    Else
    {
        $Start = $table.rows[0].Start
    }

    if ($table.rows[0].Finish.GetType().Name -eq 'DBNull')
    {
        $Finish = ''
    }
    Else
    {
        $Finish = $table.rows[0].Finish
    }

    #$table
    if ($Start -eq '' -or $Finish -eq '')
    {
        $diff = $null
    }
    else
    {
        $diff = $Finish-$Start
    }

    $PC = New-Object -TypeName psobject
    Add-Member -InputObject $PC -MemberType NoteProperty -Name ComputerName -Value $table.rows[0].ComputerName
    Add-Member -InputObject $PC -MemberType NoteProperty -Name StartTime -Value $table.rows[0].Start
    Add-Member -InputObject $PC -MemberType NoteProperty -Name FinishTime -Value $table.rows[0].Finish
    if ($Start -eq '' -or $Finish -eq '')
    {
        Add-Member -InputObject $PC -MemberType NoteProperty -Name DeploymentTime -Value ''
    }
    else
    {
        Add-Member -InputObject $PC -MemberType NoteProperty -Name DeploymentTime -Value $("$($diff.hours)" + ' hours ' + "$($diff.minutes)" + ' minutes')
    }
    Add-Member -InputObject $PC -MemberType NoteProperty -Name Model -Value $table.rows[0].Model
    $results += $PC
}

$results = $results | Sort-Object -Property ComputerName

$Query = "
    select sys.Name0 as 'ComputerName',
    tsp.Name 'Task Sequence',
    comp.Model0 as Model,
    tes.ExecutionTime,
    tes.Step,
    tes.GroupName,
    tes.ActionName,
    tes.LastStatusMsgName,
    tes.ExitCode,
    tes.ActionOutput
    from vSMS_TaskSequenceExecutionStatus tes
    left join v_R_System sys on tes.ResourceID = sys.ResourceID
    left join v_TaskSequencePackage tsp on tes.PackageID = tsp.PackageID
    left join v_GS_COMPUTER_SYSTEM comp on tes.ResourceID = comp.ResourceID
    where tsp.Name in ($OSDTaskSequences)
    and DATEDIFF(hour,ExecutionTime,GETDATE()) < $TimeInHours
    and tes.ExitCode not in (0,-2147467259)
    Order by tes.ExecutionTime desc
"

$command = $connection.CreateCommand()
$command.CommandText = $Query
$reader = $command.ExecuteReader()
$table = New-Object -TypeName 'System.Data.DataTable'
$table.Load($reader)

# Send html email
$style = "
<style>
body {
    color:#333333;
    font-family: ""Trebuchet MS"", Arial, Helvetica, sans-serif;}
    font-size: 10pt;
}
h1 {
    text-align:center;
}
h2 {
    border-top:1px solid #666666;
}
table {
    border-collapse: collapse;
    font-family: ""Trebuchet MS"", Arial, Helvetica, sans-serif;
}
th {
    font-size: 1.2em;
    text-align: left;
    padding-top: 5px;
    padding-bottom: 4px;
    background-color: #1FE093;
    color: #ffffff;
}
th, td {
    font-size: 1em;
    border: 1px solid #1FE093;
    padding: 3px 7px 2px 7px;
}
<style>
"

$body1 = $results |
Select-Object -Property ComputerName, StartTime, FinishTime , DeploymentTime, Model |
ConvertTo-Html -Head $style -Body "
<H2>OS Deployments This Week ($($results.Count))</H2>

" |
Out-String

$body2 = $table |
Select-Object -Property ComputerName, 'Task Sequence', Model, ExecutionTime, Step, GroupName, ActionName, LastStatusMsgName, ExitCode, ActionOutput |
ConvertTo-Html -Head $style -Body "
<H2>OS Deployment Errors This Week ($($table.Rows.Count))</H2>

" |
Out-String

$Body = $body1 + $body2

Send-MailMessage @EmailParams -Body $Body -BodyAsHtml

# Close the connection
$connection.Close()