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.

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