Configuring a custom shell launcher with VMware Horizon View Client on a Dell Wyse 7020 Windows 10 IoT device

I’ve recently had to assist a client with configuring their Dell Wyse 7020 Windows 10 IoT thin clients with a custom shell launcher mimicking a kiosk type of mode where only the VMware Horizon View Client is available to the user but the local admin account should still have the regular explorer shell.  These thin clients will not be joined to the Active Directory domain in the organization so users should not have to log into the thin client and the thin client OS should never lock.

The Dell Wyse 7020 (Z90QQ10) thin client I was working with is the following:

Prerequisites

You must ensure that the Shell Launcher feature is installed before beginning the configuration and although the OS on this thin client indicates it is Windows 10 Enterprise 2015 LTSB, there are some subtle differences with a full desktop and one of them is that the Shell Launcher is labeled as the following in the Windows Features:

Embedded Shell Launcher

This is different than where you would find the feature on a regular Windows 10 OS:

It is also possible to add the feature with the following command:

Dism /online /Enable-Feature /all /FeatureName:Client-EmbeddedShellLauncher

Configuring the customized shell with a PowerShell Script

Microsoft provides the following 2 articles with a PowerShell script to configure the custom shell:

Shell Launcher
https://docs.microsoft.com/en-us/windows-hardware/customize/enterprise/shell-launcher

Use Shell Launcher to create a Windows 10 kiosk
https://docs.microsoft.com/en-us/windows/configuration/kiosk-shelllauncher

However, those who may not be familiar with the behavior of customizing the shell on a Windows OS may be confused so here is a breakdown of the script and the changes required.

Original Script:

# Check if shell launcher license is enabled

function Check-ShellLauncherLicenseEnabled

{

[string]$source = @”

using System;

using System.Runtime.InteropServices;

static class CheckShellLauncherLicense

{

    const int S_OK = 0;

    public static bool IsShellLauncherLicenseEnabled()

    {

        int enabled = 0;

        if (NativeMethods.SLGetWindowsInformationDWORD(“EmbeddedFeature-ShellLauncher-Enabled”, out enabled) != S_OK) {

            enabled = 0;

        }

        return (enabled != 0);

    }

    static class NativeMethods

    {

        [DllImport(“Slc.dll”)]

        internal static extern int SLGetWindowsInformationDWORD([MarshalAs(UnmanagedType.LPWStr)]string valueName, out int value);

    }

}

“@

$type = Add-Type -TypeDefinition $source -PassThru

return $type[0]::IsShellLauncherLicenseEnabled()

}

[bool]$result = $false

$result = Check-ShellLauncherLicenseEnabled

“`nShell Launcher license enabled is set to ” + $result

if (-not($result))

{

“`nThis device doesn't have required license to use Shell Launcher”

exit

}

$COMPUTER = “localhost”

$NAMESPACE = “rootstandardcimv2embedded”

# Create a handle to the class instance so we can call the static methods.

try {

$ShellLauncherClass = [wmiclass]”\$COMPUTER${NAMESPACE}:WESL_UserSetting”

    } catch [Exception] {

write-host $_.Exception.Message;

write-host “Make sure Shell Launcher feature is enabled”

exit

    }

# This well-known security identifier (SID) corresponds to the BUILTINAdministrators group.

$Admins_SID = “S-1-5-32-544”

# Create a function to retrieve the SID for a user account on a machine.

function Get-UsernameSID($AccountName) {

$NTUserObject = New-Object System.Security.Principal.NTAccount($AccountName)

$NTUserSID = $NTUserObject.Translate([System.Security.Principal.SecurityIdentifier])

return $NTUserSID.Value

}

# Get the SID for a user account named “Cashier”. Rename “Cashier” to an existing account on your system to test this script.

$Cashier_SID = Get-UsernameSID(“Cashier”)

# Define actions to take when the shell program exits.

$restart_shell = 0

$restart_device = 1

$shutdown_device = 2

# Examples. You can change these examples to use the program that you want to use as the shell.

# This example sets the command prompt as the default shell, and restarts the device if the command prompt is closed.

$ShellLauncherClass.SetDefaultShell(“cmd.exe”, $restart_device)

# Display the default shell to verify that it was added correctly.

$DefaultShellObject = $ShellLauncherClass.GetDefaultShell()

“`nDefault Shell is set to ” + $DefaultShellObject.Shell + ” and the default action is set to ” + $DefaultShellObject.defaultaction

# Set Internet Explorer as the shell for “Cashier”, and restart the machine if Internet Explorer is closed.

$ShellLauncherClass.SetCustomShell($Cashier_SID, “c:program filesinternet exploreriexplore.exe www.microsoft.com”, ($null), ($null), $restart_shell)

# Set Explorer as the shell for administrators.

$ShellLauncherClass.SetCustomShell($Admins_SID, “explorer.exe”)

# View all the custom shells defined.

“`nCurrent settings for custom shells:”

Get-WmiObject -namespace $NAMESPACE -computer $COMPUTER -class WESL_UserSetting | Select Sid, Shell, DefaultAction

# Enable Shell Launcher

$ShellLauncherClass.SetEnabled($TRUE)

$IsShellLauncherEnabled = $ShellLauncherClass.IsEnabled()

“`nEnabled is set to ” + $IsShellLauncherEnabled.Enabled

# Remove the new custom shells.

$ShellLauncherClass.RemoveCustomShell($Admins_SID)

$ShellLauncherClass.RemoveCustomShell($Cashier_SID)

# Disable Shell Launcher

$ShellLauncherClass.SetEnabled($FALSE)

$IsShellLauncherEnabled = $ShellLauncherClass.IsEnabled()

“`nEnabled is set to ” + $IsShellLauncherEnabled.Enabled

Breakdown:

Check to see if the Shell Launcher is enabled:

# Check if shell launcher license is enabled

function Check-ShellLauncherLicenseEnabled

{

[string]$source = @”

using System;

using System.Runtime.InteropServices;

static class CheckShellLauncherLicense

{

    const int S_OK = 0;

    public static bool IsShellLauncherLicenseEnabled()

    {

        int enabled = 0;

        if (NativeMethods.SLGetWindowsInformationDWORD(“EmbeddedFeature-ShellLauncher-Enabled”, out enabled) != S_OK) {

            enabled = 0;

        }

        return (enabled != 0);

    }

    static class NativeMethods

    {

        [DllImport(“Slc.dll”)]

        internal static extern int SLGetWindowsInformationDWORD([MarshalAs(UnmanagedType.LPWStr)]string valueName, out int value);

    }

}

“@

$type = Add-Type -TypeDefinition $source -PassThru

return $type[0]::IsShellLauncherLicenseEnabled()

}

[bool]$result = $false

$result = Check-ShellLauncherLicenseEnabled

“`nShell Launcher license enabled is set to ” + $result

if (-not($result))

{

“`nThis device doesn’t have required license to use Shell Launcher”

exit

}

Create a handle to the class instance to call the static methods (pretty much what the comment says):

$COMPUTER = “localhost”

$NAMESPACE = “rootstandardcimv2embedded”

# Create a handle to the class instance so we can call the static methods.

try {

$ShellLauncherClass = [wmiclass]”\$COMPUTER${NAMESPACE}:WESL_UserSetting”

    } catch [Exception] {

write-host $_.Exception.Message;

write-host “Make sure Shell Launcher feature is enabled”

exit

    }

Assigning a variable with the SID of the Windows 10 local admin account:

# This well-known security identifier (SID) corresponds to the BUILTINAdministrators group.

$Admins_SID = “S-1-5-32-544”

Create a function to retrieve the SID for a local account on the thin client then assigns a variable with the SID of the Windows 10 local user account:

# Create a function to retrieve the SID for a user account on a machine.

function Get-UsernameSID($AccountName) {

$NTUserObject = New-Object System.Security.Principal.NTAccount($AccountName)

$NTUserSID = $NTUserObject.Translate([System.Security.Principal.SecurityIdentifier])

return $NTUserSID.Value

}

# Get the SID for a user account named “User”. Rename “User” to an existing account on your system to test this script.

$Cashier_SID = Get-UsernameSID(“Cashier”)

Defining actions to take when the shell program exits (pretty much what the comment says):

# Define actions to take when the shell program exits.

$restart_shell = 0

$restart_device = 1

$shutdown_device = 2

This section typically confuses most administrators because it indicates that the command prompt is being configured as the default shell and the reason for this is because when Shell Launcher is enabled, the default shell is set to cmd.exe.  It is possible to change this to the regular explorer.exe shell but for this example, we’ll modify the admin account’s shell instead:

# Examples. You can change these examples to use the program that you want to use as the shell.

# This example sets the command prompt as the default shell, and restarts the device if the command prompt is closed.

$ShellLauncherClass.SetDefaultShell(“cmd.exe”, $restart_device)

This section just provides an output indicating what the default shell was configured as:

# Display the default shell to verify that it was added correctly.

$DefaultShellObject = $ShellLauncherClass.GetDefaultShell()

“`nDefault Shell is set to ” + $DefaultShellObject.Shell + ” and the default action is set to ” + $DefaultShellObject.defaultaction

This section is where you would modify the shell you intend on configuring for the locked down user account:

# Set Internet Explorer as the shell for “Cashier”, and restart the machine if Internet Explorer is closed.

$ShellLauncherClass.SetCustomShell($Cashier_SID, “c:program filesinternet exploreriexplore.exe www.microsoft.com”, ($null), ($null), $restart_shell)

This section is where you would modify the shell you intend on configuring for the admin account:

# Set Explorer as the shell for administrators.

$ShellLauncherClass.SetCustomShell($Admins_SID, “explorer.exe”)

This section retrieves the custom shells defined:

# View all the custom shells defined.

“`nCurrent settings for custom shells:”

Get-WmiObject -namespace $NAMESPACE -computer $COMPUTER -class WESL_UserSetting | Select Sid, Shell, DefaultAction

This section enables the shell launcher:

# Enable Shell Launcher

$ShellLauncherClass.SetEnabled($TRUE)

$IsShellLauncherEnabled = $ShellLauncherClass.IsEnabled()

“`nEnabled is set to ” + $IsShellLauncherEnabled.Enabled

This section removes the custom shells and will need to be commented out:

# Remove the new custom shells.

$ShellLauncherClass.RemoveCustomShell($Admins_SID)

$ShellLauncherClass.RemoveCustomShell($Cashier_SID)

This section disables the shell launcher and will need to be commented out:

# Disable Shell Launcher

$ShellLauncherClass.SetEnabled($FALSE)

$IsShellLauncherEnabled = $ShellLauncherClass.IsEnabled()

“`nEnabled is set to ” + $IsShellLauncherEnabled.Enabled

Customized for Dell Wyse 7020 thin client:

The following is the slightly modified version that will:

  1. Configure the VMware Horizon View client as shell launcher for the User account
  2. Configure the explorer.exe (regular shell) as the shell launcher for the Admin account
  3. The default shell will be configured with cmd.exe

Note that I’ve made changes to the Cashier_SID variable name to User_SID so it reflects the current context better.

# Check if shell launcher license is enabled

function Check-ShellLauncherLicenseEnabled

{

[string]$source = @”

using System;

using System.Runtime.InteropServices;

static class CheckShellLauncherLicense

{

    const int S_OK = 0;

    public static bool IsShellLauncherLicenseEnabled()

    {

        int enabled = 0;

        if (NativeMethods.SLGetWindowsInformationDWORD(“EmbeddedFeature-ShellLauncher-Enabled”, out enabled) != S_OK) {

            enabled = 0;

        }

        return (enabled != 0);

    }

    static class NativeMethods

    {

        [DllImport(“Slc.dll”)]

        internal static extern int SLGetWindowsInformationDWORD([MarshalAs(UnmanagedType.LPWStr)]string valueName, out int value);

    }

}

“@

$type = Add-Type -TypeDefinition $source -PassThru

return $type[0]::IsShellLauncherLicenseEnabled()

}

[bool]$result = $false

$result = Check-ShellLauncherLicenseEnabled

“`nShell Launcher license enabled is set to ” + $result

if (-not($result))

{

“`nThis device doesn’t have required license to use Shell Launcher”

exit

}

$COMPUTER = “localhost”

$NAMESPACE = “rootstandardcimv2embedded”

# Create a handle to the class instance so we can call the static methods.

try {

$ShellLauncherClass = [wmiclass]”\$COMPUTER${NAMESPACE}:WESL_UserSetting”

    } catch [Exception] {

write-host $_.Exception.Message;

write-host “Make sure Shell Launcher feature is enabled”

exit

    }

# This well-known security identifier (SID) corresponds to the BUILTINAdministrators group.

$Admins_SID = “S-1-5-32-544”

# Create a function to retrieve the SID for a user account on a machine.

function Get-UsernameSID($AccountName) {

$NTUserObject = New-Object System.Security.Principal.NTAccount($AccountName)

$NTUserSID = $NTUserObject.Translate([System.Security.Principal.SecurityIdentifier])

return $NTUserSID.Value

}

# Get the SID for a user account named “User”. Rename “User” to an existing account on your system to test this script.

$User_SID = Get-UsernameSID(“User”)

# Define actions to take when the shell program exits.

$restart_shell = 0

$restart_device = 1

$shutdown_device = 2

# Examples. You can change these examples to use the program that you want to use as the shell.

# This example sets the command prompt as the default shell, and restarts the device if the command prompt is closed.

$ShellLauncherClass.SetDefaultShell(“cmd.exe”, $restart_device)

# Display the default shell to verify that it was added correctly.

$DefaultShellObject = $ShellLauncherClass.GetDefaultShell()

“`nDefault Shell is set to ” + $DefaultShellObject.Shell + ” and the default action is set to ” + $DefaultShellObject.defaultaction

# Set Internet Explorer as the shell for “User”, and restart the machine if Internet Explorer is closed.

# $ShellLauncherClass.SetCustomShell($User_SID, “c:program filesinternet exploreriexplore.exe www.microsoft.com”, ($null), ($null), $restart_shell)

# Set VMware Horizon View Client as the shell for “User”, and restart the machine if VMware Horizon View Client is closed.

$ShellLauncherClass.SetCustomShell($User_SID, “C:Program Files (x86)VMwareVMware Horizon View Clientvmware-view.exe”, ($null), ($null), $restart_shell)

# Set Explorer as the shell for administrators.

$ShellLauncherClass.SetCustomShell($Admins_SID, “explorer.exe”)

# View all the custom shells defined.

“`nCurrent settings for custom shells:”

Get-WmiObject -namespace $NAMESPACE -computer $COMPUTER -class WESL_UserSetting | Select Sid, Shell, DefaultAction

# Enable Shell Launcher

$ShellLauncherClass.SetEnabled($TRUE)

$IsShellLauncherEnabled = $ShellLauncherClass.IsEnabled()

“`nEnabled is set to ” + $IsShellLauncherEnabled.Enabled

# Remove the new custom shells.

# $ShellLauncherClass.RemoveCustomShell($Admins_SID)

# $ShellLauncherClass.RemoveCustomShell($User_SID)

# Disable Shell Launcher

# $ShellLauncherClass.SetEnabled($FALSE)

# $IsShellLauncherEnabled = $ShellLauncherClass.IsEnabled()

# “`nEnabled is set to ” + $IsShellLauncherEnabled.Enabled

It is important to comment out the lines for removing and disabling the shell launcher or executing the PowerShell script would not change anything.

The output should look similar to the following upon successfully executing it on the thin client:

Note that if you want the VMware Horizon View client to automatically connect to a server then you can append the -serverURL <connection_server> to the shell.

If you ever had to revert the user account’s shell back to the original explorer.exe then uncomment these lines and rerun the PowerShell script under the admin account.

The VMware Horizon View Shell launched under the user account will have a black background and I’ve done a bit of research as to whether it is possible to change it and the short answer is it may not be possible in Windows 10 but please feel free to comment below if you’ve found a solution for this to share with the community.

Additional items and the order in which I configured the thin client are as follows:

1. Disable write filter

2. Configure Power Options to High performance (powercfg -setactive 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c)

3. Configure Turn off the display value to after 15 mins (powercfg -x -monitor-timeout-ac 15)

4. Configure Put the computer to sleep value to Never (powercfg -x -standby-timeout-ac 0)

(preventing the thin client to go to sleep prevents the user from being prompted with the thin client OS login screen where they would need to enter the user password)

5. Uninstall unused applications on the thin client

6. Update TightVNC access password or remove the application completely

7. Edit registry to force Num Lock on:

[HKEY_USERS.DEFAULTControl PanelKeyboard]

“InitialKeyboardIndicators”=”2”

“KeyboardDelay”=”1”

“KeyboardSpeed”=”31”

8. Disable the shade of the VMware Horizon View client via the registry key: HKCUSoftwareVMware, Inc.VMware VDMClientEnableShade

9. Enable firewall

10. Disable remember credentials for Windows which would also cause the Horizon View client to not remember the previous login via the registry key: [HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsCurrentVersionPoliciesSystem] “dontdisplaylastusername”=dword:00000001

11. Change the local administrator and user password (the default DellCCCvdi for both accounts makes it vulnerability)

12. Update auto logon for user account

13. Enable write filter

14. Disable boot from USB in the BIOS

15. Change the BIOS password (the default Fireport should not be used)

If this thin client is going to be captured then run the Build_Master.cmd in the C:WindowsSetup folder on the thin client.

Note that you should select the Enable local account credential changes under the Configure local account credentials heading so you can change the default password of the admin and user account after the preparation:

There will be settings that do not end up getting retained after the image preparation and they are:

  1. The name of the Windows OS does not change
  2. The Power Scheme configuration will be reverted back to defaults (monitor and computer would go to sleep)

More information about the Custom Sysprep tool can be found here: https://www.dell.com/support/manuals/us/en/04/wyse-7020/wie10_th_mr4/running-custom-sysprep-tool?guid=guid-5bd77921-f2e6-4c84-b55f-dbffddc1a89f&lang=en-us

Extra

I’ve been asked several times in the past about the tools I use for capturing from a bootable USB drive and creating ISO files from it then burn it back to a different USB drive so I’ll list the ones I’ve had success with in the past:

Free ISO Creator – For creating an ISO from a bootable USB Drive
http://www.freeisocreator.com/

Rufus – For burning a bootable IOS to a USB Drive
https://rufus.ie/

Please be cautious as to where you download these applications as some download sites contain files that may carry malicious bundled malware.