About keyboard layouts available at logon

  • Context

During a Windows 10 migration, as long as you use the following wizard, you can set the logon keyboards’ layouts available before a user logs on.

Let’s say now that you want to do it remotely and that you’ve a huge number of target computers to modify.
You’d typically need automation and a tool like PowerShell to do this peacefully.

  • Problem

For the last 20 years, all you needed to do was to modify the keyboard layouts under the following registry HKEY_USERS\.DEFAULT\Keyboard Layout\Preload.
(40c is French, 409 is English US,…)
To be more accurate, the input language equals the keyboard layout when there’s only a value in the Preload key and nothing under the Substitutes key.
If there’s a Substitute, the value in the Preload key is the input language and the keyboard layout is under the Substitutes key.

You could for most simple cases (having the same pair input language/keyboard layout) just populate the Preload key with the following function provided by Jakub Jareš on this page that uses the *-ItemProperty cmdlets:

Unfortunately, you cannot just only modify these registry keys on recent Windows 10 versions. The above is no longer valid 😦
If you do, the LogonUI.exe process (being run by the SYSTEM account) uses other registry keys and will actually overwrite what you did.
It appears that HKEY_USERS\S-1-5-18\ and HKEY_USERS\.DEFAULT are the same registry hives.
The LogonUI.exe process uses what’s stored in this registry key ‘HKEY_USERS\S-1-5-18\Control Panel\International\User Profile’.
Do not delete this content as suggested on some websites.

I just want a simple fully supported native way to set the keyboards layouts available at logon.
I don’t want to hijack anything, alter the security or write more that 2 values in the registry.

  • Solution

My solution uses the native cmdlets from the International module to validate what was entered as input but that’s not the only reason.

There’s a huge issue with the International cmdlets. They can only be used in the Current User context.
To work around this caveat, my solution creates a scheduled task that runs as SYSTEM.
In other words, Set-WinUserLanguageList is launched by the SYSTEM account in a scheduled task.

I also wanted to be able to interact remotely with computers over WinRM and find the shortest way to create the scheduled tasks without using the XML schema or binaries.

Here’s my solution:

#Requires -Version 3.0
#Requires -RunAsAdministrator
Function Install-DefaultKeyboardLayout {
Begin {
# Validate entered languages once
"$($FirstLanguage)","$($SecondLanguage)" |
ForEach-Object {
$l = "$($_)"
Write-Verbose -Message "Dealing with language $($l)"
try {
$test = New-WinUserLanguageList -Language "$($_)" -Verbose -ErrorAction Stop
if($null -eq (($test).InputMethodTips)) {
Throw "Input language $($l) isn't recognized as a valid language"
} else {
Write-Verbose -Message "Valid input language detected: $($_)"
} catch {
Throw "Testing Input language $($l) went wrong because $($_.Exception.Message)"
# Intialize a scriptblock
$s = {
if ($null -eq $Verbose) { $Verbose = $false }
$lar = (
(New-WinUserLanguageList -Language $using:FirstLanguage)+
(New-WinUserLanguageList -Language $using:SecondLanguage)
$mHT = @{
Message = 'About to set {0} and {1} on computer {2}' -f "$($using:FirstLanguage)",
Write-Verbose @mHT -Verbose:$Verbose
# Write-Verbose -Message "Verbose param is to: -$($Verbose)-" -Verbose
try {
$errHT = @{ ErrorAction = 'Stop' }
$aHT = @{
Execute = 'C:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe'
Argument = '-Command "Set-WinUserLanguageList -LanguageList {0} -Force"' -f $lar
$HT = @{
TaskName = 'Set-Logon-KeyboardLayout'
User = 'S-1-5-18' #'nt authority\system'
Force = [switch]::Present
Action = (New-ScheduledTaskAction @aHT @errHT)
Register-ScheduledTask @HT @errHT |
Start-ScheduledTask @errHT
Write-Verbose -Message "Successfully set default keyboard layout on computer $($using:c)" -Verbose:$Verbose
} catch {
Write-Warning -Message "Failed because $($_.Exception.Message)"
Process {
$v = ($PSBoundParameters['Verbose'])
$ComputerName |
ForEach-Object {
$c = $_
Write-Verbose -Message "Dealing with computer $($c)"
try {
if ($v) {
Invoke-Command -ComputerName "$($c)" -ScriptBlock $s -ErrorAction Stop -AsJob:$AsJob -ArgumentList @($v)
} else {
Invoke-Command -ComputerName "$($c)" -ScriptBlock $s -ErrorAction Stop -AsJob:$AsJob
} catch {
Write-Warning -Message "Failed to invoke command on computer: $($c) because $($_.Exception.Message)"
End {}
Set the keyobard layout at logon
Can set two keyboard layouts at logon (available before a user logs on).
.PARAMETER ComputerName
The list of remote computers to target and modify
.PARAMETER FirstLanguage
The first keyboard layout to set
.PARAMETER SecondLanguage
The second keyboard layout to set
Set the keyboard layouts on the local computer to the default values: 1rst: fr-FR, 2nd en-GB
Install-DefaultKeyboardLayout -Verbose
Set the keyboard layouts on the local computer to the default values: 1rst: fr-FR, 2nd en-GB
and shows a verbose stream of what happens
Install-DefaultKeyboardLayout -FirstLanguage fr-FR -Verbose
Set the keyboard layouts on the local computer explicitly to french for the first keyboard layout,
uses the default value for the 2nd one (en-GB) and shows a verbose stream of what happens
Install-DefaultKeyboardLayout -SecondLanguage en-GB -Verbose
Set the keyboard layouts on the local computer explicitly to english for the second keyboard layout,
uses the default value for the 1rst one (fr-FR) and shows a verbose stream of what happens
Install-DefaultKeyboardLayout -ComputerName 'localhost' -Verbose
Set the keyboard layouts targeting the explicitely specified target computer
using the the default values: 1rst: fr-FR, 2nd en-GB
and shows a verbose stream of what happens
$HT = @{ FirstLanguage = 'en-GB' ; SecondLanguage = 'fr-FR'}
Install-DefaultKeyboardLayout -ComputerName 'target1','target2' @HT -Verbose
Create a hashtable to define the first and second keyboard layouts to set
Set the keyboard layouts on the 2 remote computers and shows a verbose stream
$HT = @{ FirstLanguage = 'en-GB' ; SecondLanguage = 'fr-FR'}
'target1','target2' |
Install-DefaultKeyboardLayout @HT -Verbose -AsJob
Get-Job | Receive-Job
Create a hashtable to define the first and second keyboard layouts to set
Set the keyboard layouts on the 2 remote computers as a job and shows a verbose stream
Export-ModuleMember -Function 'Install-DefaultKeyboardLayout'

Let’s see it in action:

# Import the module
Import-Module KeyboardLayoutsAtLogon.psm1 -Verbose

# Have a look at the help
Get-Help Install-DefaultKeyboardLayout

# Yes, it can be invoked w/o parameters

# Use the verbose stream to see what it does by default
# when you don't specify any parameter
Install-DefaultKeyboardLayout -Verbose

# Let's do the opposite
# set en-GB as 1rst keyboard layout
# fr-FR as the 2nd keyboard layout
# and target 2 remote computers over PSRemoting
$HT = @{ FirstLanguage = 'en-GB' ; SecondLanguage = 'fr-FR'}
Install-DefaultKeyboardLayout -ComputerName 'target1','localhost' @HT -Verbose

# Use the -AsJob parameter
$HT = @{ FirstLanguage = 'en-GB' ; SecondLanguage = 'fr-FR'}
'localhost','target2' |
Install-DefaultKeyboardLayout @HT -Verbose -AsJob
Get-Job | Receive-Job

About Appx packages not working in the Start menu

  • Context

We have upgraded a Windows 10 1803 to 1809 back in June.
The end-user complained that he couldn’t launch calc anymore and his start menu looked like this:

He should have had this instead:

Microsoft just released some fixes in this October 15, 2019—KB4520062 (OS Build 17763.832) where it says:

Prevents blank tiles from appearing in the Start menu when you upgrade to Windows 10, version 1809 from any previous version of Windows 10. However, if you have already upgraded to Windows 10, version 1809, installing this update will not remove existing blank tiles.

I’m not sure if this KB would really help because it doesn’t match what we see in the “broken” Tiles in his Start menu.

  • Problem

Here’s the result of the

Get-AppxProvisionedPackage -Online

You can notice that there’s something wrong with the InstallLocation property.
That %systemdrive% is an environment variable.

I ran a procmon trace to find out what registry keys and values were being read when I run the Get-AppxProvisionedPackage cmdlet.

Then I queried what appears in these keys on his computer:

You can see that the environment variable %systemdrive% isn’t expanded because the value type is wrong.
It’s currently a REG_SZ whereas it should be a REG_EXPAND_SZ to be able to work with the environment variable.

  • Solution

There are 2 ways to fix this. Either replace %systemdrive% by C: or change the type of the value from REG_SZ to REG_EXPAND_SZ.
Here’s a example using the REG_EXPAND_SZ way of fixing this:

$k = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Appx\AppxAllUserStore\Applications'
Get-ChildItem -Path $k -Recurse |
Where-Object { $_.PSIsContainer} |
ForEach-Object {
$p = $null
if ($p=Get-ItemProperty -Path "$($_.PsPath)" -Name 'Path' -ErrorAction SilentlyContinue) {
$App = Split-Path $_.Name -Leaf
Write-Verbose -Message "Dealing with $($App)" -Verbose
if ($p.'Path' -match '%SYSTEMDRIVE%\\') {
Write-Verbose "$($App) has a wrong path set in the registry" -Verbose
$v = $p.Path
try {
Set-ItemProperty -Path "$($_.PsPath)" -Name 'Path' -ErrorAction Stop -Value $v -Type ExpandString
} catch {
Write-Warning -Message "Failed to fix $($App) because $($_.Exception.Message)"


Last Saturday, I’ve been to the PowerShell Saturday Paris 2019

It wouldn’t have been possible without:

    • the sponsors
    • the speakers:
    • speakers who delivered lightening session during the lunch break.
    • the PowerBeer at the end
    • the help and work of organizers (here are only 3 of them busy at work)

I enjoyed all the sessions and every minute of this event, this community is just awesome.
I ❤ the PowerShell community.

I’ve also won a Learn dbatools in a Month of Lunches book
written by Chrissy LeMaire and Rob Sewell and published by Manning Publications

Thanks again to anybody involved in this event who made it a great success.

Split strings

  • Context

I worked recently on some Active Directory properties and needed to split a path.

$h = (
Get-ADUser UserName -Properties HomeDirectory
$h = '\\servername\homepath.username$'

The HomeDirectory is a System.String and my variable $h contains something like \\servername\homepath.username$ (there’s a dollar at the end to hide the share).

  • Problem

I’d like to split on the \ character and to do so, you need to escape it with another backslash character because it’s a special character for regular expressions.

The official documentation About Split says that the delimiter is evaluated as a regular expression if I use the highlighted 3rd syntax:
The Options parameter is optional in the syntax and defaults to the value RegexMatch
The doc says:

RegexMatch: Use regular expression matching to evaluate the delimiter.

If I use the split operator like this:

$h -split '\\'

$h -split '\\' | measure

[string]::Empty -eq ($h -split '\\')[0]
[string]::Empty -eq ($h -split '\\')[1]
[string]::Empty -eq ($h -split '\\')[2]
[string]::Empty -eq ($h -split '\\')[3]

I get 4 results. The first 2 results of the split operation are an empty system.string.
This is expected.

I want to capture the server name and the share name, not the backslashes and not the empty strings.

  • Solution(s)

My first solution followed the idea “just ignore and discard empty strings”:

# capture
$null,$null,$server,$share=$h -split '\\'
# display result

My second solution is based on the idea that “there’s probably a better way to skip these empty strings”:

# display result

If you’ve another great idea and want to share another (better?) way to skin the cat, please post it in the comments 🙂 .

[Quick post] Exchange (Online) team’s changes to the default policy about blocked file types

The Exchange (Online) team is about to change the default policy about blocked file types, meaning that OWA (a.k.a Outlook on the web) users will be impacted by this change.

Their message is quite clear and was published on this page https://techcommunity.microsoft.com/t5/Exchange-Team-Blog/Changes-to-File-Types-Blocked-in-Outlook-on-the-web/ba-p/874451

Almost all the most common extensions related to PowerShell are concerned: .ps1, .ps1xml, .ps2, .ps2xml, .psc1, .psc2, .psd1, .psdm1, .cdxml, .pssc. That said, neither .ps2, ps2xml nor .psc2 are used and recognized by PowerShell or pwsh. We just wonder where they got this list of extensions? 🙄 (from telemetry?)

If you’ve the habit to send these files by email, your recipient may not be able to download and open the sent files if he uses Exchange Online (EXO) (i.e. OWA / Outlook on the web).

If you are an EXO admin, you can override the default policy and set your own policy using the guidance in the above article mentioned

As a user, you can zip the files before sending them. Or, if you’ve a OneDrive (or a similar online storage) you can store these blocked files there.

The kind of change doesn’t add anything to the security of your endpoints. On any Windows, these file types have always been associated by default with notepad and not PowerShell .

If any user gets a PowerShell script (a .ps1) by email, when he downloads and opens it, the .ps1 script doesn’t get executed but it’s instead opened by notepad.exe and its content displayed in notepad.exe.

The attack surface of notepad is extremely tiny. As far as I can remember, there’s only 1 known vulnerability that was discovered by a Google Project Zero security researcher over the last few years. It isn’t notepad.exe that was actually vulnerable but a more subtle sub-component CTF (a.k.a. Windows Text Services Framework) used by notepad.