Windows 10 Upgrade Blocked

Tags

On trying to upgrade a PC to Windows 10 build 1903 I received the following error:

1903UpgradeBlock

There is a link to the following article: KB4500988

The underlying problem is the presence of a USB mass storage device – in this case, a USB Flash Drive acting as a Bitlocker Startup key.  In the absence of a fix from Microsoft yet, the only obvious option would be to disable Bitlocker.  I wasn’t willing to decrypt the entire PC just to upgrade.

My solution was to use PowerShell to add an extra key protector based on a Password.  That way, the PC can be started using a password instead of the USB startup key.  If, like me, you’re not used to using Bitlocker cmdlets, have no fear, PowerShell makes things easy to work out:

Firstly, confirm the name of the module containing Bitlocker cmdlets:
1903-BL1

Then check what cmdlets are in it:
1903-BL2

And a quick way to check the syntax of the likely cmdlet, Add-BitlockerKeyProtector:
1903-BL3
So the syntax overload we want is the one with the -PasswordProtector parameter which takes a SecureString in the -Password parameter.  We can construct one easily from plaintext:
1903-BL4

Now the PC can be booted without the USB stick in it.

After upgrading Windows, to remove the KeyProtector, we can use the Remove-BitlockerKeyProtector cmdlet:
1903-BL5
As you can see, that requires a KeyProtectorId string.  I tried “password” but that didn’t work.  The Get-BitlockerVolume cmdlet includes a multivalued property called KeyProtector.  I had a closer look:
1903-BL6

I cheated and copied and pasted the ID:
1903-BL7

Of course I could have let PowerShell find it for me:
1903-BL8

Advertisements

Setting SCCM OSD OU by DP – Revisited

Further to yesterday’s post on this subject, I’ve seen some material online where I discovered there was a Task Sequence variable which can be used to take some of the guess work out of things.

It’s called _SMSTSBootImageID and it contains the Package ID of the Boot Image used to boot into Windows PE.  As a result, it’s not necessary to search through all the variables for those which have values that contain a package ID by means of a wildcard search.  Indeed, it’s no longer even required to parse all the variables at all.

Continuing on from previously, where $vars contains an object collection of the Task Sequence variables with Name and Value properties, here’s a list of those containing the SMS site code.  Here I’m using the simpler Where-Object filter format which can filter on a single property without reference to $_ or $PSItem and without enclosing the filter script in a scriptblock { … }:

TSEnv-10-SiteVars

Notice the three _SMSTSHTTP variables; one of them contains a Package ID in its name which matches the value of the _SMSTSBootImageID variable.  In a bare-metal situation, we can be sure this variable will be present as the boot image will have to have been downloaded.  That then is the variable whose value we will use to obtain the Distribution Point’s fully qualified hostname.  Once again, I’ll use string substitution and a sub-expression:

TSEnv-11-ObtainURL

In the previous post, I extracted the hostname from the URL using some .NET methods to search through the string for forward slashes.  I realised afterwards it would have been easier to Split the string on the slashes.  Here is the result if we split using the PowerShell -split operator:

TSEnv-12-SplitResults

As you can see the third value is the one we went.  The second, empty, value is the zero-length string between the adjacent forward slashes in http://.  Here’s how we extract it.  Arrays are zero-indexed, so the third item is number 2:

TSEnv-13-Splitted

Please beware that the -split operator uses Regular Expressions, so if you try splitting on what they consider to be special characters such as “.” or “\”, you will get unexpected results.  For example, the backslash is the escape character, so to split on it you would need to escape it itself by splitting on “\\”.

For ease of reading and comprehension, breaking the script over a number of lines can make things clearer, but sometimes, one liners are more satisfying:

TSEnv-14-FinalOperator

That operator doesn’t look tidy (!) in there. so you could use the .NET String Split() method instead.  This is in some ways easier as it doesn’t use Regular Expressions, but beware, it is case-sensitive!

TSEnv-15-FinalNET

Speaking of case, you might want to add a .ToUpper() or .ToLower() to the end of this to make your URL matching definitely incapable of being tripped up by case.

The final PowerShell script is much shorter than the one in the previous post:

$TSEnv = New-Object -ComObject 'Microsoft.SMS.TSEnvironment'
try {
    $TSEnv.Value('DPName') = $TSEnv.Value("_SMSTSHTTP$($TSEnv.Value('_SMSTSBootImageID'))").Split('/')[2].ToLower()
} catch {
    $TSEnv.Value('DPName') = 'ERROR'
}

 

Setting SCCM OSD Computer OU by DP

Tags

, ,

There is a part two to this post here.

I’m currently involved with the setting up of a new SCCM Site that spans a number of physical sites.  Each site will have at least one Distribution Point (DP).  During Operating System Deployment (OSD), we wanted to be able to put the new computer account into a specific OU depending on its location.

One way of doing this I was shown would be a script in the Task Sequence that looks at the Default Gateway of the computer and then sets the OU based on that.  Unfortunately this would mean many entries for all our subnets and it would take a lot of maintenance.

Another possibility could be to let the computer account be created in a default OU and then use a script later in the Task Sequence – after the computer becomes domain-joined – to move it to a final OU location based on the AD Site detected.

Instead I found a way to determine the Distribution Point involved in OSD at the start of the Task Sequence and then use that to set the Target OU for the domain join step.

As is usually the case, I decided to tackle this with PowerShell, so first the Boot Image had to have the PowerShell and .NET components added to it and the F8 command prompt feature needed to be enabled too (a checkbox).  Once done, I PXE booted a new VM; started the OSD going; pressed F8 to get a CMD window up; and typed “PowerShell” to change the CMD console into a PowerShell console.
My intention was to have a look to see what Task Sequence variables were present at this early stage of OSD to see if any referenced the DP hostname.  A quick look online showed that to read and write Task Sequence variables, you need to use the COM Object “Microsoft.SMS.TSEnvironment“.  A COM Object is instantiated by the -ComObject parameter on the New-Object cmdlet.  Once instantiated, you can of course pipe it to Get-Member to get an idea of what it can do:

TSEnv-01-Members

Not much to see there.  Let’s have a look at the GetVariables() method and see what Type of objects the variables actually are:

TSEnv-02-VarsCount

As you can see, a whopping 194 variables were present – surely the Distribution Point name has to be in there somewhere.  Looking at the first variable returned shows it to be of Type [String].  You can access the name of one by its numerical index and then query the associated Value using the Value() method we saw above:

TSEnv-03-Value

To get those variables into a form you can easily look through or filter in some way, you can turn them into a collection of Objects with a Name property set to the variable name and a Value property obtained by using the Value() method.  This is easily done by creating Hashtables using @{} and then casting them to PSCustomObjects, one for each variable name present in the collection.

$TSEnv = New-Object -ComObject 'Microsoft.SMS.TSEnvironment'
$vars = $TSEnv.GetVariables() | ForEach-Object {
	[pscustomobject]@{
		Name = $_
		Value = $TSEnv.Value($_)
	}
}

I entered the script on one line (which requires semi-colons between Hashtable elements) and then selected the first 25 values:

TSEnv-04-VarsList

You soon find that a lot of values are empty, or are very short or (as I saw later in the list are very long).  With the variables all now a collection of objects, it’s easy to filter them to ones that might be of interest by looking at the length of the variable’s value property:

$vars |	Where-Object { ($_.Value.Length -gt 3) -and ($_.Value.Length -lt 30) } | Select-Object -First 25<span id="mce_SELREST_start" style="overflow:hidden;line-height:0;"></span>

This gives more interesting values (note the Default Gateway I mentioned earlier).  There’s also a variable called _SMSTSMP which is the Management Point:

TSEnv-05-VarsCandidates

I did eventually spot a whole group of variables whose value included a URL on the Distribution Point.  These variable names all included the ID of a Package in them (and in the value URLs).  Here is an example one.  The name is in the form _SMSTSHTTPxxxnnnnn where xxxnnnn is the Package ID, of which xxx is the SCCM Site Code:

TSEnv-06-DPString

Armed with this information, it’s now straightforward to slice out the DP’s hostname from one of these values using some .NET string methods and then put it into a new variable to return.  In the unlikely event that something should go wrong, I’ll return the value “ERROR”.

param (
    [ValidateLength(3,3)]
    [string]$SiteCode = 'CC1'
)

$TSEnv = New-Object -ComObject 'Microsoft.SMS.TSEnvironment'
try {
    $allVars = $TSEnv.GetVariables() | ForEach-Object {
        [pscustomobject]@{
            Name = $_
            Value = $TSEnv.Value($_)
        }
    }
    $DPvalue = @($allVars | Where-Object { $_.Name -like "_SMSTSHTTP$($SiteCode)*" })[0].Value
    $startIndex = $DPValue.IndexOf('://') + 3
    $endIndex = $DPValue.IndexOf('/', $startIndex)
    $DPName = $DPvalue.Substring($startIndex, $endIndex-$startIndex)
    $TSEnv.Value('DPName') = $DPName
} catch {
    $TSEnv.Value('DPName') = 'ERROR'
}

I’ve added an optional SiteCode parameter (which is validated as being exactly 3 characters long) and for which I’ve added a default value for my own fictitious environment.

The relevant variable name is found by filtering the collection of constructed variable objects by looking for those whose name matches (i.e. are “-like“) _SMSTSHTTPxxx* where xxx is replaced by the $SiteCode variable by means of the Sub-Expression Operator $().  Note the need for double-quotes to allow this substitution to take place.  Since the result of the Where-Object cmdlet may or may not return more than one item, the filter is wrapped inside the array sub-expression @() to guarantee an array is created, even if it is only of one element.  That makes it safe to grab the first returned value by indexing into the array with [0].  Finally, it is necessary to get just the actual Value string property using the .Value at the end of the line!  If no variable names match, the line will throw an exception as the array will have no elements – the exception will be caught by the catch block.

Chopping out the DP name is simply a case of starting with the first character after the position of “://” in the string and then chopping up to the first “/” after this point.  Positions in the string are found using the IndexOf() method overloads on the .NET string class.  The first time I use it I give it just the string to look for but the second time I supply a second parameter which is the position at which to start searching (the first character being position 0).  Without that, the “/” would match the first one in http:// and not the one delineating the end of the hostname I’m trying to extract.  The substring is chopped out using the .NET SubString() method where you tell it which character to start at and how many characters you want.

The script ends by assigning the located value to a Task Sequence variable called “DPName”.  If any exception was caught, then the value “ERROR” is returned in the same variable which will later be used to set a default OU location rather than error.  Note that the instantiation of the COM Object was not protected by  the try{} block.  If that fails, I’d really like to know!

The code is then put into a program-less package, distributed to the PXE-capable DPs and then referenced at the top of the OSD Task Sequence as a “Run PowerShell Script” task.  (The following screenshots are from a lab setup.)  The script is not signed so I needed to tell it to Bypass the Execution Policy:

TSEnv-07-GetDPName

After this, you insert a “Set Dynamic Variables” task containing a number of “Add Rule – Task Sequence Variable” rules which compare the DPName variable the PowerShell script created with your known DP Fully Qualified Domain Names.  Under each match, you use the “Add Variable” button to make it create a new OUName variable with the Distinguished Name of the target OU you wish to use for the location that DP is in.  Note the final rule for if the script returned “ERROR” where you choose a default location:

TSEnv-08-SetOUName

Maintaining this list of DPs is far easier than a massive list of Default Gateways would be.  It also doesn’t require anyone to touch the PowerShell code.

The final step is to pass the OUName variable to the Task Sequence step that configures how to join the machine to the domain.  This is the “Apply Networking Settings” step.  The variable name has to be inside % signs and preceded with the string “LDAP://”:

TSEnv-09-ApplyNW

And that’s it!

Subsequently, I found a much easier way of getting what I wanted in PowerShell.  Please see the follow-up post here.

Automated Java Uninstaller v2

If you’ve not read it, please read my original post on this subject before continuing.

The java uninstaller I posted works a treat most of the time, but not always.  We have a number of machines with Java v1.8 Update 74 on that refuse to uninstall it – even via Programs & Features the Windows installer progress box just sits there.

Process Explorer shows something interesting at this point:
Java Issue

The Windows Installer process, MsiExec.exe, has launched Java Web Start which has then opened a process called JP2Launcher.exe.  I’ve no idea what that is trying to do.  I tried a Fiddler trace of it and it caught nothing.  Whatever it is doing, killing the process allows the uninstall of Java to then continue successfully.

As a result, I’ve made an extended version of my Java Uninstaller script that will kill any instances of this process shortly after they appear.  Here it is:

Rem Remove all versions of Java x86 and x64

On Error Resume Next

Const HKEY_LOCAL_MACHINE = &H80000002
Const WINDOW_HIDDEN = 0
Const WAIT_ON_RETURN = True
Const wbemFlagReturnWhenComplete = 0

Const intJP2KillDelay = 5

Dim objSWbemServices

Set objWshShell = WScript.CreateObject("WScript.Shell")
Set objStdReg = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\default:StdRegProv")
Rem Need Debug privileges to terminate processes we do not own.
Set objSWbemServices = GetObject("winmgmts:{impersonationLevel=impersonate,(Debug)}!\\.\root\cimv2")

Rem MsiExec.exe sometimes launches JavaWS.exe which launches JP2Launcher.exe and blocks uninstall until the latter is killed.
Set objWMISink = WScript.CreateObject("WbemScripting.SWbemSink","SINK_")
strWQL = "SELECT * FROM __InstanceCreationEvent WITHIN 3 WHERE TargetInstance ISA 'Win32_Process' AND TargetInstance.Name = 'JP2Launcher.exe'"
objSWbemServices.ExecNotificationQueryAsync objWMISink, strWQL

For i = 1 To 2
	If i = 1 Then
		strKeyPath = "Software\Microsoft\Windows\CurrentVersion\Uninstall"
	Else
		strKeyPath = "Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
	End If

	objStdReg.EnumKey HKEY_LOCAL_MACHINE, strKeyPath, arrSubKeys
	If Not IsNull(arrSubKeys) Then
		For Each strSubKey In arrSubKeys
			Rem Ensure is a GUID key
			If Left(strSubKey, 1) = "{" Then
				strSubKeyPath = strKeyPath & "\" & strSubKey
				objStdReg.GetStringValue HKEY_LOCAL_MACHINE, strSubKeyPath, "DisplayName", strDisplayName
				If Not IsNull(strDisplayName) Then
					intJavaIndex = InStr(LCase(strDisplayName), "java")
					If intJavaIndex > 0 Then
						intUpdateIndex = InStr(LCase(strDisplayName), "update")
						Rem Effectively make the check to be for "*java*update*"
						If intUpdateIndex > intJavaIndex Then
							strCmd = "MsiExec.exe /X " & strSubKey & " /quiet /norestart"
							intRC = objWshShell.Run(strCmd, WINDOW_HIDDEN, WAIT_ON_RETURN)
						End If
					End If
				End If
			End If
		Next
	End If
Next

objWMISink.Cancel
Set objWMISink = Nothing
WScript.Quit

Sub SINK_OnObjectReady(objNextObject, objWbemAsyncContext)
	Rem Kill JP2Launcher.exe processes after a delay (in case it's meant to launch)

	On Error Resume Next

	intPID = objNextObject.TargetInstance.ProcessID
	strPath = objNextObject.TargetInstance.ExecutablePath
	WScript.Sleep intJP2KillDelay * 1000

	Rem Check that process is still running but can't assume the PID has not been reused
	strWQL = "SELECT * FROM Win32_Process WHERE ProcessID = " & intPID
	Set colProcesses = objSWbemServices.ExecQuery(strWQL,, wbemFlagReturnWhenComplete)
	If colProcesses.Count = 0 Then
		rem Already closed
		Exit Sub
	End If
	For Each objProcess In colProcesses
		Rem Will only ever be one match with a given PID!
		If objProcess.ExecutablePath <> strPath Then
			rem Is a different process
			Exit Sub
		End If
	Next

	objNextObject.TargetInstance.Terminate
End Sub

The extra code watches for the creation of a process called JP2Launcher.exe and then kills it after a short delay.  For a comprehensive explanation of how this works, I direct you to this excellent article here.

Firstly, an SWbemSink object is created to determine what code is called when events we’re interested in are raised.  You’ll see I specify the prefix “SINK_” that is to be used in the names of the subroutines called:

Set objWMISink = WScript.CreateObject("WbemScripting.SWbemSink","SINK_")

Next I construct a WQL query that every 3 seconds, looks to see if an event has been raised for the creation of a new instance of a Win32_Process whose name is JP2Launcher.exe:

strWQL = "SELECT * FROM __InstanceCreationEvent WITHIN 3 WHERE TargetInstance ISA 'Win32_Process' AND TargetInstance.Name = 'JP2Launcher.exe'"

Given that description, you can work out how to alter the syntax of that query to meet your requirements.  The query and the Sink object are then passed as parameters to a special WMI call called an Asynchronous (i.e. Runs in the background) Notification Query:

objSWbemServices.ExecNotificationQueryAsync objWMISink, strWQL

If and when a matching process is instantiated, this generates an event known as “OnObjectReady” and this, along with the prefix I specified earlier, causes the sink object to call the subroutine SINK_OnObjectReady.

Note the syntax for getting at the properties of the new Win32_Process from inside that subroutine:

intPID = objNextObject.TargetInstance.ProcessID
intPID = objNextObject.TargetInstance.ProcessID
strPath = objNextObject.TargetInstance.ExecutablePath

After a short delay (in case there are circumstances where this process should legitimately appear and then soon disappear during uninstall), the process is killed by calling its Terminate method:

objNextObject.TargetInstance.Terminate

You’ll see in the code I’ve added in an extra check first to see if the process with the same ProcessID still exists and whether it’s pointing at the same executable before I kill it.  This is in case the process had closed and then by chance a new process had been created with the same ID.  I suspect that even if this were the case, that the call to Terminate would just throw an error as the original Process object would be gone and it wouldn’t try and touch the new one, but since I can’t really test this, I’m just being careful!

One last thing to note is that the WMI object used has to have Debug privileges to be able to kill processes launched under a different user context:

Set objSWbemServices = GetObject("winmgmts:{impersonationLevel=impersonate,(Debug)}!\\.\root\cimv2")

Automated Java Uninstall

Tags

,

I’ve recently had to face the thorny issue of upgrading Java in our environment and wanting to be able to do this remotely and silently.  The main problem was working out how to remove the existing version(s) first.

There are MANY scripts online that offer to do this, ranging from huge BAT files to short PowerShell scripts.  There was one particular solution I looked at that was a mammoth batch file which looked really promising and incredibly thorough.  I went through it line by line and then I came across lines like the following that do the uninstall:

wmic.exe product where filter clause call uninstall /nointeractive

I would assume that “product” is the alias for Win32_Product and I’ve read many articles (e.g. This one) about how you should not query that class as it triggers a repair of all your MSIs!  I’ve seen that happen when I tried to use that class in the past and sometimes it was not pleasant and broke things.  I decided to roll my own script instead.

Our environment is such that I can’t use PowerShell [on enough of our kit] and so I’ve gone for VBScript.  It’s not designed to be able to remove all possible versions, but removes the ones we have scattered around.  As long as it can be uninstalled by MsiExec and is labelled under Programs & Features with a name that matches “*Java*Update*”, then this ought to remove it.  Use entirely at your own risk!  I hope you find it helpful.

rem Remove all versions of Java x86 and x64

Const HKEY_LOCAL_MACHINE = &H80000002
Const WINDOW_HIDDEN = 0
Const WAIT_ON_RETURN = True

Set objWshShell = WScript.CreateObject("WScript.Shell")
Set objStdReg = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\default:StdRegProv")

For i = 1 To 2
	If i = 1 Then
		strKeyPath = "Software\Microsoft\Windows\CurrentVersion\Uninstall"
	Else
		strKeyPath = "Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
	End If

	objStdReg.EnumKey HKEY_LOCAL_MACHINE, strKeyPath, arrSubKeys
	If Not IsNull(arrSubKeys) Then
		For Each strSubKey In arrSubKeys
			Rem Ensure is a GUID key
			If Left(strSubKey, 1) = "{" Then
				strSubKeyPath = strKeyPath & "\" & strSubKey
				objStdReg.GetStringValue HKEY_LOCAL_MACHINE, strSubKeyPath, "DisplayName", strDisplayName
				If Not IsNull(strDisplayName) Then
					intJavaIndex = InStr(LCase(strDisplayName), "java")
					If intJavaIndex > 0 Then
						intUpdateIndex = InStr(LCase(strDisplayName), "update")
						rem Effectively make the check to be for "*java*update*"
						If intUpdateIndex > intJavaIndex Then
							strCmd = "MsiExec.exe /X " & strSubKey & " /quiet /norestart"
							intRC = objWshShell.Run(strCmd, WINDOW_HIDDEN, WAIT_ON_RETURN)
						End If
					End If
				End If
			End If
		Next
	End If
Next

WScript.Quit

Conexant Keylogger

Tags

,

You may have heard that HP have been found to have some laptops that were shipping with what is in effect a keylogger!  The affected models have a Conexant audio chip whose driver includes hotkey functionality for microphone muting.  Unfortunately this was left in debugging mode and as a result logged all keypresses to a file locally!  HP’s official security bulletin is here.

Some of our laptops were affected and our antivirus started detecting it as “Conexant MicTray Keylogger”.  Just the thing to scare our users with after the recent news over the WannaCry v2 ransomware…

Rather than push a 200MB driver update to them all, I shut them up remotely with the following little batch file I pushed out over SCCM.

@echo off
taskkill.exe /im:MicTray.exe /f >nul 2>&1
taskkill.exe /im:MicTray64.exe /f >nul 2>&1
del /F c:\Windows\System32\MicTray.exe >nul 2>&1
del /F c:\Windows\System32\MicTray64.exe >nul 2>&1
del /F "c:\Program Files\Conexant\Install\Audio\MicTray\MicTray\MicTray.exe" >nul 2>&1
del /F "c:\Program Files\Conexant\Install\Audio\MicTray\MicTray\MicTray64.exe" >nul 2>&1
del /F c:\Users\Public\MicTray.log >nul 2>&1
exit

In retrospect, I’m glad that’s the solution I took, as the first update HP released to fix the issue apparently didn’t even remove the functionality, it just turned it off – but it could then be re-enabled with a registry value change!  Since our users don’t use the mics on their laptops, I’m leaving the thing deleted as above.

Interesting PowerShell Remoting Issue

Tags

,

I saw an odd error message today whilst testing PSRemoting on a couple of PCs, a process which involved my disabling and re-enabling it.  It brought up an interesting problem I thought worth sharing.

I have a Group Policy Object with a Computer Startup script that looks for missing PowerShell Remoting Endpoints (i.e. PSSession Configurations such as “Microsoft.PowerShell”) and re-runs Enable-PSRemoting if required to fix them.  If you look at my earlier post, you’ll see why I ended up with such a config.  I thought this would ensure remoting would always work for PCs in this particular Organisational Unit.

Here’s the error I got trying to remote to one PC that had had Remoting expressly disabled but had then been put back in the OU that should have re-enabled it again but clearly hadn’t done so successfully:

1-EnterPsSession

Testing WSMan connectivity showed that side of things was working:

2-TestWSMan

Both expected Session Configurations were also present but I decided to investigate them closer with the following command.

Get-PSSessionConfiguration | Format-List -Property *

Going down each property in turn on the two PCs I spotted a difference in the rather unfriendly SecurityDescriptorSddl property.  Fortunately this is interpreted into something more meaningful in the Permission property which of course was the last one of over 30 in the list!  Picking that one out, here’s how it looked on a working PC:

3-Permissions-Working

And here’s how it looked on the faulty PC:

4-Permissions-Failing

This Denied Network access entry could also be seen using the GUI equivalent:

5-Permissions-UI

I thought it odd that there was a Deny entry in there but I fixed it with an Enable-PSRemoting again (which would actually would have been the first fix I’d have tried, except I wanted to try and work out exactly why it was broken).

The explanation of course is that I hadn’t looked into the details of exactly what Disable-PSRemoting might do.  From the documentation comes the phrase:

Disable-PSRemoting blocks remote access to all session configurations on the local computer. This prevents remote users from creating temporary or persistent sessions to the local computer. Disable-PSRemoting does not prevent users of the local computer from creating sessions (“PSSessions”) on the local computer or remote computers.

If you look at the Examples in that cmdlet’s help, you will see how the Network Deny we’ve seen gets put in.  It’s more of a Deny-PSRemoting than a Disable-PSRemoting!

Looks as though I need to alter my Startup script to make a check for these denies as well as just a check to see if the endpoints exist, as a Disable-PSRemoting makes them still exist, just no longer work remotely…!

Windows 10 build 1703 RSAT Fail

Tags

,

I upgraded my Windows 10 Pro Build 1607 work PC to Build 1703 this week.  At the start of the install process, it warned me that it was going to remove a display language and I’d have to reinstall it afterwards.  I’m using en-GB.

Once the upgrade was done, I found that RSAT was no longer installed and its pinned shortcuts on Start were blank with odd text across them.  I reinstalled RSAT (incidentally I’ve read that there will not be a Build 1703 of RSAT and that you just use Build 1607).  It reinstalled suspiciously quickly and the problem remained; including after reboot.

I remember similar issues with previous builds involving having to get the en-US display language installed again as a fix.  Unfortunately I could not persuade it to do so – the relevant part of the GUI kept saying that I needed to connect to the Internet – despite Windows Update working fine!  I think our web filtering proxy software was probably blocking its access in some way.

I eventually had to give up and leave it for another day and so rolled back to Build 1607 (which was quite a quick and painless process).

Later, back at home, I installed the en-US Display Language into my home PC and used Process Monitor to see exactly what got downloaded and installed.  The file was:

C:\Windows\SoftwareDistribution\Download\4b5f7f550123f7e8a15edef6d60b110f\lp.cab

I took that CAB file to work today and re-upgraded my PC to Build 1703.  I then ran LPKSETUP.exe which you use to install Display Languages:

LPKSetup1

After browsing to the CAB file it happily reinstalled the en-US pack, though this was quite a slow process:

LPKSetup2

Once done, the problem with my shortcuts remained, so I reinstalled RSAT and this time all my pinned shortcuts came back to life (without a further reboot).

I did try and reproduce all this again in a VM at home, hoping to get some more screenshots and to sniff out the URL too but the problem did not occur.  I’m guessing it must be something to do with differences in the locale of the ISO used between work and home.

I hope the above helps someone else!