VMConnectConfig.psm1

#Requires -RunAsAdministrator
#.ExternalHelp VMConnectConfig-help.xml

data MsgTable
{
    # culture="en-US"
    ConvertFrom-StringData @'
####################################################################################################################################################################################
## Error Message Strings
####################################################################################################################################################################################
CimSessionError=Unable to establish a CIM session to computer '{0}' (unknown error).
ClientSettingsFileDesktopSizeError=Unable to read the DesktopSize value from '{0}'. Using a default value of '{1}'.
ConfigFileNotFoundIdError=Unable to find a valid vmconnect configuration file in '{0}' for a virtual machine with ID '{1}'.
ConfigFileNotFoundNameError=Unable to find a valid vmconnect configuration file in '{0}' for a virtual machine with name '{1}'.
ConfigFileNotFoundPathError=A valid vmconnect configuration file could not be found at the specified path '{0}'.
ConnectionError=Unable to connect to computer '{0}'.
DirectoryNotFoundError=Cannot find directory '{0}' because it does not exist.
EnhancedRdpSessionError=Cannot establish an enhanced RDP session for the virtual machine '{0}' ({1}) on computer '{2}'. Please check configuration and try again.
FilenameGuidUndefinedError=The 'Id' property cannot be determined for the virtual machine '{0}' because: {1}
GuidNotFoundInFilenameError=A valid GUID was not detected in the filename '{0}'.
HyperVGuiMgmtToolsError=Error 0x80370114 - The operation could not be started. Please verify that the Windows feature '{0}' is installed and try again.
HyperVGuiToolsWmiError=An unknown error occurred while trying to detect the installation of the Hyper-V GUI Management Tools.
InvalidConfigFileError='{0}' is not a valid vmconnect configuration file.
InvalidConfigFileExtensionError=The file '{0}' does not have a valid '.config' extension.
InvalidRedirectedDriveFormatError='{0}' is not a valid redirected drive value. Please specify a single drive letter(s). The values "*" and "dynamicdrives" are also allowed.
InvalidXmlElementError=The file '{0}' contains one or more invalid vmconnect configuration XML elements.
InvalidXmlFileError='{0}' is not a valid XML file.
ParameterMissingError=Please include at least one additional parameter from the set '{0}'.
PathNotFoundError=Cannot find path '{0}' because it does not exist.
UnknownError=Unknown error.
UnsupportedConfigurationSettingError=The vmconnect configuration file '{0}' for the virtual machine '{1}'{2} does not support the '{3}' configuration setting.
VmconnectExeNotFoundError=Unable to locate '{0}'. Please verify that the '{1}' Windows feature is installed and try again.
VmExistsAndVmVersionUndefinedError=The properties 'VMExists' and 'VMVersion' cannot be determined for the virtual machine '{0}'{1} on computer '{2}' because: {3}
VmExistsAndVmVersionAndVmServerNameUndefinedError=The properties 'VMExists' and 'VMVersion' cannot be determined for the virtual machine '{0}'{1} because: {2}
VmExistsUndefinedError=The property 'VMExists' cannot be determined for the virtual machine '{0}'{1} because: {2}
VmHostHyperVRoleError=The Hyper-V role is not installed on the destination host '{0}'.
VmIdNeededToCalculatePropertiesError=A valid virtual machine ID is needed to determine the properties 'VMExists' and 'VMVersion'.
VmNotFoundError={0} was unable to find the virtual machine '{1}'{2} on computer '{3}'.
VmServerNameNotFound=The VmServerName value is empty or missing in the file '{0}'.
VmVersionUndefinedError=The property 'VMVersion' cannot be determined for the virtual machine '{0}'{1} because: {2}
 
####################################################################################################################################################################################
## Non-Error Status Message Strings
####################################################################################################################################################################################
ActionMsgCreateConfigFile=Create vmconnect configuration file for the virtual machine '{0}'{1}
ActionMsgDeletePermanently=Remove File
ActionMsgModifyConfigFile=Modify enhanced session configuration for the virtual machine '{0}'{1}
ActionMsgMoveToRecycleBin=Send to Recycle Bin
ActionMsgRemoveDirectory=Remove Directory
ActionMsgResetVmConfigFile=Reset vmconnect configuration for the virtual machine '{0}'{1}
ActionMsgStartingVmconnectExe=Start '{0}'
ConnectionSuccessMsg=Successful connection test to computer '{0}'.
ConstructingOutputObjMsg=Constructing VMConnectConfig output object for '{0}'.
CopyingToTempPathMsg=Copying temp file '{0}' to '{1}'.
CreatingCimSessionMsg=Attempting to create a new CIM session connection to computer '{0}'.
CreatingConfigFileMsg=Creating vmconnect configuration file for the virtual machine '{0}'{1}
DeletingMsg=Moving '{0}' to the Recycle Bin.
DeletingPermanentlyMsg=Permanently deleting '{0}'.
GettingConfigFilesForDeletedVirtualMachines=Getting a list of .config files in '{0}' whose corresponding virtual machines no longer exist.
HyperVGuiMgmtToolsFeatureName=Hyper-V GUI Management Tools
HyperVGuiMgmtToolsFeatureNameLegacy=Hyper-V Tools
LoadingFileMsg=Loading '{0}'.
NoChangesMadeMsg=No changes were made to file '{0}'.
NoFilesToDelete=Operation canceled. No files to delete.
ResetVmConfigFileMsg=Resetting vmconnect configuration file '{0}' to default settings.
SavingToTempPathMsg=Saving to temp file '{0}'.
TargetMsgStartingVmconnectExe=Virtual machine '{0}' ({1}) hosted on computer '{2}'
TestingConnectionMsg=Testing connection to '{0}'.
TestingConnectionWaitingMsg=Waiting for connection test to finish.
TestingConnectionWaitingParallelMsg=Waiting for connection tests to finish running in parallel.
UsingHostNameForVmServerName=VMServerName was not specified. By default, the local host name '{0}' will be used.
XmlSchemaValidatingFileMsg=Validating '{0}' against XML schema.
'@

}

Import-LocalizedData -BindingVariable 'MsgTable' -FileName 'VMConnectConfigResources'

$GuidRegexPattern = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
$ModuleRoot = $PSScriptRoot
$VMConfigFolder = [System.IO.Path]::Combine([System.String[]]@($env:APPDATA, 'Microsoft', 'Windows', 'Hyper-V', 'Client', '1.0'))
$ClientSettingsConfigFile = (Join-Path -Path $VMConfigFolder -ChildPath 'clientsettings.config')
$ConfigFilePattern = "^vmconnect\.rdp\.$GuidRegexPattern.*\.config$"
$ErrorCatInvalidArgument = [System.Management.Automation.ErrorCategory]::InvalidArgument
$ErrorCatInvalidOperation = [System.Management.Automation.ErrorCategory]::InvalidOperation
$ErrorCatObjectNotFound = [System.Management.Automation.ErrorCategory]::ObjectNotFound
# The [DefaultSettingValue] of [System.Drawing.Size]DesktopSize is hardcoded in Microsoft.Virtualization.Client.dll and is usually 1366 x 768.
$DefaultDesktopSize = '1366, 768'
$HostName = [System.Net.Dns]::GetHostName()
$LegacySettingsList = @('AudioCaptureRedirectionMode',
                        'SaveButtonChecked',
                        'FullScreen',
                        'SmartCardsRedirection',
                        'RedirectedPnpDevices',
                        'ClipboardRedirection',
                        'DesktopSize',
                        'VmServerName',
                        'RedirectedUsbDevices',
                        'SavedConfigExists',
                        'UseAllMonitors',
                        'AudioPlaybackRedirectionMode',
                        'PrinterRedirection',
                        'RedirectedDrives',
                        'VmName'
)
$ModernSettingsList = @('AudioCaptureRedirectionMode',
                        'EnablePrinterRedirection',
                        'FullScreen',
                        'SmartCardsRedirection',
                        'RedirectedPnpDevices',
                        'ClipboardRedirection',
                        'DesktopSize',
                        'VmServerName',
                        'RedirectedUsbDevices',
                        'SavedConfigExists',
                        'UseAllMonitors',
                        'AudioPlaybackRedirectionMode',
                        'PrinterRedirection',
                        'RedirectedDrives',
                        'VmName',
                        'SaveButtonChecked'
)
$WebAuthnSettingsList = @('AudioCaptureRedirectionMode',
                        'EnablePrinterRedirection',
                        'FullScreen',
                        'SmartCardsRedirection',
                        'RedirectedPnpDevices',
                        'ClipboardRedirection',
                        'DesktopSize',
                        'VmServerName',
                        'RedirectedUsbDevices',
                        'SavedConfigExists',
                        'UseAllMonitors',
                        'AudioPlaybackRedirectionMode',
                        'PrinterRedirection',
                        'WebAuthnRedirection',
                        'RedirectedDrives',
                        'VmName',
                        'SaveButtonChecked'
)
$VMConnectConfigXmlSchema = @"
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="configuration">
<xs:complexType>
<xs:sequence>
<xs:element name="Microsoft.Virtualization.Client.RdpOptions">
<xs:complexType>
<xs:sequence>
<xs:element name="setting" minOccurs="15" maxOccurs="17">
<xs:complexType>
  <xs:sequence>
    <xs:element type="xs:string" name="value"/>
  </xs:sequence>
  <xs:attribute type="xs:string" name="name" use="required"/>
  <xs:attribute type="xs:string" name="type" use="required"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
"@

$LegacyConfigFileXml = @'
<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <Microsoft.Virtualization.Client.RdpOptions>
        <setting name="AudioCaptureRedirectionMode" type="System.Boolean">
            <value>False</value>
        </setting>
        <setting name="SaveButtonChecked" type="System.Boolean">
            <value>True</value>
        </setting>
        <setting name="FullScreen" type="System.Boolean">
            <value>False</value>
        </setting>
        <setting name="SmartCardsRedirection" type="System.Boolean">
            <value>True</value>
        </setting>
        <setting name="RedirectedPnpDevices" type="System.String">
            <value />
        </setting>
        <setting name="ClipboardRedirection" type="System.Boolean">
            <value>True</value>
        </setting>
        <setting name="DesktopSize" type="System.Drawing.Size">
            <value>{0}</value>
        </setting>
        <setting name="VmServerName" type="System.String">
            <value>{1}</value>
        </setting>
        <setting name="RedirectedUsbDevices" type="System.String">
            <value />
        </setting>
        <setting name="SavedConfigExists" type="System.Boolean">
            <value>True</value>
        </setting>
        <setting name="UseAllMonitors" type="System.Boolean">
            <value>False</value>
        </setting>
        <setting name="AudioPlaybackRedirectionMode" type="Microsoft.Virtualization.Client.RdpOptions+AudioPlaybackRedirectionType">
            <value>AUDIO_MODE_REDIRECT</value>
        </setting>
        <setting name="PrinterRedirection" type="System.Boolean">
            <value>True</value>
        </setting>
        <setting name="RedirectedDrives" type="System.String">
            <value />
        </setting>
        <setting name="VmName" type="System.String">
            <value>{2}</value>
        </setting>
    </Microsoft.Virtualization.Client.RdpOptions>
</configuration>
'@

$ModernConfigFileXml = @'
<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <Microsoft.Virtualization.Client.RdpOptions>
        <setting name="AudioCaptureRedirectionMode" type="System.Boolean">
            <value>False</value>
        </setting>
        <setting name="EnablePrinterRedirection" type="System.Boolean">
            <value>True</value>
        </setting>
        <setting name="FullScreen" type="System.Boolean">
            <value>False</value>
        </setting>
        <setting name="SmartCardsRedirection" type="System.Boolean">
            <value>True</value>
        </setting>
        <setting name="RedirectedPnpDevices" type="System.String">
            <value />
        </setting>
        <setting name="ClipboardRedirection" type="System.Boolean">
            <value>True</value>
        </setting>
        <setting name="DesktopSize" type="System.Drawing.Size">
            <value>{0}</value>
        </setting>
        <setting name="VmServerName" type="System.String">
            <value>{1}</value>
        </setting>
        <setting name="RedirectedUsbDevices" type="System.String">
            <value />
        </setting>
        <setting name="SavedConfigExists" type="System.Boolean">
            <value>True</value>
        </setting>
        <setting name="UseAllMonitors" type="System.Boolean">
            <value>False</value>
        </setting>
        <setting name="AudioPlaybackRedirectionMode" type="Microsoft.Virtualization.Client.RdpOptions+AudioPlaybackRedirectionType">
            <value>AUDIO_MODE_REDIRECT</value>
        </setting>
        <setting name="PrinterRedirection" type="System.Boolean">
            <value>True</value>
        </setting>
        <setting name="RedirectedDrives" type="System.String">
            <value />
        </setting>
        <setting name="VmName" type="System.String">
            <value>{2}</value>
        </setting>
        <setting name="SaveButtonChecked" type="System.Boolean">
            <value>True</value>
        </setting>
    </Microsoft.Virtualization.Client.RdpOptions>
</configuration>
'@

$RedirectedUsbDeviceCSCode = @'
using System;
using System.Management.Automation;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
 
public enum DeviceType
{
    PnP,
    USB
}
 
public class RedirectedUsbDevice
{
    public string Name {get; set;}
    public string Description { get; set; }
    public string InstanceId { get; set; }
    public DeviceType DeviceType { get; set; }
}
 
internal class RdpClientInterop
{
    [ComImport]
    [Guid("B3378D90-0728-45C7-8ED7-B6159FB92219")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IMsRdpClientNonScriptable3
    {
        [DispId(1)]
        string ClearTextPassword { set; }
 
        [DispId(2)]
        string PortablePassword { get; set; }
 
        [DispId(3)]
        string PortableSalt { get; set; }
 
        [DispId(4)]
        string BinaryPassword { get; set; }
 
        [DispId(5)]
        string BinarySalt { get; set; }
 
        void ResetPassword();
 
        void NotifyRedirectDeviceChange([In][ComAliasName("MSTSCLib.UINT_PTR")] uint wParam, [In][ComAliasName("MSTSCLib.LONG_PTR")] int lParam);
 
        void SendKeys([In] int numKeys, [In] ref bool pbArrayKeyUp, [In] ref int plKeyData);
 
        [DispId(13)]
        [ComAliasName("MSTSCLib.wireHWND")]
        IntPtr UIParentWindowHandle
        {
            [return: ComAliasName("MSTSCLib.wireHWND")]
            get;
 
            [param: ComAliasName("MSTSCLib.wireHWND")]
            set;
        }
 
        [DispId(14)]
        bool ShowRedirectionWarningDialog { get; set; }
 
        [DispId(15)]
        bool PromptForCredentials { get; set; }
 
        [DispId(16)]
        bool NegotiateSecurityLayer { get; set; }
 
        [DispId(17)]
        bool EnableCredSspSupport { get; set; }
 
        [DispId(21)]
        bool RedirectDynamicDrives { get; set; }
 
        [DispId(20)]
        bool RedirectDynamicDevices { get; set; }
 
        [DispId(18)]
        IMsRdpDeviceCollection DeviceCollection
        {
            [return: MarshalAs(UnmanagedType.Interface)]
            get;
        }
 
        [DispId(23)]
        bool WarnAboutSendingCredentials { get; set; }
 
        [DispId(22)]
        bool WarnAboutClipboardRedirection { get; set; }
 
        [DispId(24)]
        string ConnectionBarText { get; set; }
    }
 
    [Guid("56540617-D281-488C-8738-6A8FDF64A118")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IMsRdpDeviceCollection
    {
        void RescanDevices(bool vboolDynRedir);
 
        IMsRdpDevice get_DeviceByIndex(uint index);
 
        IMsRdpDevice get_DeviceById(string devInstanceId);
 
        [DispId(225)]
        uint DeviceCount { get; }
    }
 
    [Guid("60C3B9C8-9E92-4F5E-A3E7-604A912093EA")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IMsRdpDevice
    {
        [DispId(222)]
        string DeviceInstanceId { get; }
 
        [DispId(220)]
        string FriendlyName { get; }
 
        [DispId(221)]
        string DeviceDescription { get; }
 
        [DispId(223)]
        bool RedirectionState { get; set; }
    }
 
    [ComImport]
    [Guid("5fb94466-7661-42a8-98b7-01904c11668f")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IMsRdpDeviceV2
    {
        string DeviceInstanceId();
        string FriendlyName();
        string DeviceDescription();
        void RedirectionState([In][MarshalAs(UnmanagedType.Bool)] bool RedirState);
        bool RedirectionState();
        string DeviceText();
        bool IsUSBDevice();
        bool IsCompositeDevice();
        uint DriveLetterBitmap();
    }
 
    [ComImport]
    [Guid("B2A5B5CE-3461-444A-91D4-ADD26D070638")]
    [TypeLibType(4160)]
    internal interface IMsRdpClient7
    {
    }
 
    internal static string GetDeviceName(IMsRdpDeviceV2 device)
    {
        try
        {
            string friendlyName = device.FriendlyName();
            if (!string.IsNullOrEmpty(friendlyName))
            {
                return friendlyName;
            }
        }
        catch { }
 
        try
        {
            string deviceText = device.DeviceText();
            if (!string.IsNullOrEmpty(deviceText))
            {
                return deviceText;
            }
        }
        catch { }
 
        try
        {
            string deviceDescription = device.DeviceDescription();
            if (!string.IsNullOrEmpty(deviceDescription))
            {
                return deviceDescription;
            }
        }
        catch { }
        return "Unknown device name";
    }
}
 
[Cmdlet(VerbsCommon.Get, "RedirectedUsbDevice")]
public class GetRedirectedUSBDeviceCommand : Cmdlet
{
    protected override void ProcessRecord()
    {
        try
        {
            Type mstscaxType = Type.GetTypeFromProgID("mstscax.mstscax");
            var rdpClient = (RdpClientInterop.IMsRdpClient7)Activator.CreateInstance(mstscaxType);
            RdpClientInterop.IMsRdpClientNonScriptable3 msRdpClientNonScriptable = (RdpClientInterop.IMsRdpClientNonScriptable3)(object)rdpClient;
            RdpClientInterop.IMsRdpDeviceCollection deviceCollection = msRdpClientNonScriptable.DeviceCollection;
 
            for (int i = 0; i < deviceCollection.DeviceCount; i++)
            {
                RdpClientInterop.IMsRdpDevice msRdpDevice = deviceCollection.get_DeviceByIndex((uint)i);
                RdpClientInterop.IMsRdpDeviceV2 msRdpDeviceV2 = (RdpClientInterop.IMsRdpDeviceV2)msRdpDevice;
 
                RedirectedUsbDevice redirectedUsbDevice = new RedirectedUsbDevice {
                    Name = RdpClientInterop.GetDeviceName(msRdpDeviceV2).Trim().Replace("\0", ""),
                    Description = msRdpDeviceV2.DeviceDescription().Trim().Replace("\0", ""),
                    InstanceId = msRdpDeviceV2.DeviceInstanceId().Trim().Replace("\0", ""),
                    DeviceType = msRdpDeviceV2.IsUSBDevice() ? DeviceType.USB : DeviceType.PnP
                };
 
                WriteObject(redirectedUsbDevice);
            }
 
            DeviceType devType;
 
            foreach (string dType in Enum.GetNames(typeof(DeviceType)))
            {
                if (dType == "PnP")
                {
                    devType = DeviceType.PnP;
                }
                else
                {
                    devType = DeviceType.USB;
                }
 
                RedirectedUsbDevice laterDevices = new RedirectedUsbDevice
                {
                    Name = "Redirect all supported devices that are connected later.",
                    Description = "Redirect all supported devices that are connected later.",
                    InstanceId = "dynamicdevices",
                    DeviceType = devType
                };
 
                RedirectedUsbDevice allDevices = new RedirectedUsbDevice
                {
                    Name = "Redirect all supported devices, including ones that are connected later.",
                    Description = "Redirect all supported devices, including ones that are connected later.",
                    InstanceId = "*",
                    DeviceType = devType
                };
 
                WriteObject(laterDevices);
                WriteObject(allDevices);
            }
        }
        catch
        {
            // Since this is being used for tab/menu completion for Edit-VMConnectConfig's -RedirectedPnpDevices and -RedirectedUsbDevices parameters
            // don't output an error record object if an exception occurs.
            // WriteError(new ErrorRecord(ex, "USB device enumeration failed", ErrorCategory.InvalidOperation, null));
        }
    }
}
'@

$WebAuthnConfigFileXml = @'
<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <Microsoft.Virtualization.Client.RdpOptions>
        <setting name="AudioCaptureRedirectionMode" type="System.Boolean">
            <value>False</value>
        </setting>
        <setting name="EnablePrinterRedirection" type="System.Boolean">
            <value>True</value>
        </setting>
        <setting name="FullScreen" type="System.Boolean">
            <value>False</value>
        </setting>
        <setting name="SmartCardsRedirection" type="System.Boolean">
            <value>True</value>
        </setting>
        <setting name="RedirectedPnpDevices" type="System.String">
            <value />
        </setting>
        <setting name="ClipboardRedirection" type="System.Boolean">
            <value>True</value>
        </setting>
        <setting name="DesktopSize" type="System.Drawing.Size">
            <value>{0}</value>
        </setting>
        <setting name="VmServerName" type="System.String">
            <value>{1}</value>
        </setting>
        <setting name="RedirectedUsbDevices" type="System.String">
            <value />
        </setting>
        <setting name="SavedConfigExists" type="System.Boolean">
            <value>True</value>
        </setting>
        <setting name="UseAllMonitors" type="System.Boolean">
            <value>False</value>
        </setting>
        <setting name="AudioPlaybackRedirectionMode" type="Microsoft.Virtualization.Client.RdpOptions+AudioPlaybackRedirectionType">
            <value>AUDIO_MODE_REDIRECT</value>
        </setting>
        <setting name="PrinterRedirection" type="System.Boolean">
            <value>True</value>
        </setting>
        <setting name="WebAuthnRedirection" type="System.Boolean">
            <value>True</value>
        </setting>
        <setting name="RedirectedDrives" type="System.String">
            <value />
        </setting>
        <setting name="VmName" type="System.String">
            <value>{2}</value>
        </setting>
        <setting name="SaveButtonChecked" type="System.Boolean">
            <value>True</value>
        </setting>
    </Microsoft.Virtualization.Client.RdpOptions>
</configuration>
'@


$SystemDirectory = [System.Environment]::SystemDirectory
$VMConnectConfigFilenamePattern = 'vmconnect.rdp.{0}.config'
$VMNameXPath = "//setting[@name='VmName']"
$VMServerNameXPath = "//setting[@name='VmServerName']"
$XmlEncoding = New-Object -TypeName 'System.Text.UTF8Encoding' -ArgumentList $false
$XPath = 'configuration/Microsoft.Virtualization.Client.RdpOptions/setting'

$privateFunctionsPath = Join-Path -Path $ModuleRoot -ChildPath 'Private'
$publicFunctionsPath = Join-Path -Path $ModuleRoot -ChildPath 'Public'

foreach ($privateFunction in (Get-ChildItem -Path $privateFunctionsPath -Filter '*.ps1' -ErrorAction Ignore))
{
  . $privateFunction.FullName
}

foreach ($publicFunction in (Get-ChildItem -Path $publicFunctionsPath -Filter '*.ps1' -ErrorAction Ignore))
{
  . $publicFunction.FullName
}

if ($PSVersionTable.PSVersion.Major -ge 5)
{
    $argCompleterFunctionList = @('Edit-VMConnectConfig', 'Get-VMConnectConfig', 'Remove-VMConnectConfig', 'Reset-VMConnectConfig')

    $nameArgCompleterScriptBlock = {
        param ($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams)

        $files = Get-AllConfigFiles | Where-Object {Test-VMConnectConfig -LiteralPath $_.FullName -Verbose:$false}
        $vmNameList = @()

        $files | ForEach-Object {
            ($xmlFile = [System.Xml.XmlDocument]::new()).Load($_.FullName)
            $vmName = $xmlFile.SelectSingleNode($VMNameXPath).value
            $vmNameList += $vmName
        } #ForEach-Object

        $vmNameList | Sort-Object -Unique | Where-Object {$_ -like "*$WordToComplete*"} | ForEach-Object {
            $resultName = $_

            if ($resultName -match '\s')
            {
                $resultName = "'$resultName'"
            }

            [System.Management.Automation.CompletionResult]::new($resultName, $_, 'ParameterValue', $_)
        } #ForEach-Object
    }

    $idArgCompleterScriptBlock = {
        param ($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams)

        $files = Get-AllConfigFiles | Where-Object {Test-VMConnectConfig -LiteralPath $_.FullName -Verbose:$false}
        $vmObjects = @()

        $files | ForEach-Object {
            ($xmlFile = [System.Xml.XmlDocument]::new()).Load($_.FullName)
            $vmName = $xmlFile.SelectSingleNode($VMNameXPath).value
            $vmId = ($_.Name | Select-String -Pattern $GuidRegexPattern).Matches.Value
            $vmObjects += [pscustomobject] @{Name = $vmName; Id = $vmId}
        } #ForEach-Object

        $vmObjects | Sort-Object -Property Id | Where-Object {$_.Id -like "*$WordToComplete*"} | ForEach-Object {
            $resultId = "'" + $_.Id + "'"
            $toolTip = "Id:`t`t$($_.Id)`nName:`t`t$($_.Name)"
            [System.Management.Automation.CompletionResult]::new($resultId, $_.Id, 'ParameterValue', $toolTip)
        } #ForEach-Object
    }

    $redirectedUsbDevicesArgCompleterScriptBlock = {
        param ($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams)
        $devices = Get-RedirectedUsbDevice -DeviceType 'USB'
        if ($devices.Count -gt 0)
        {
            $devices | Where-Object {$_.InstanceId -like "*$WordToComplete*"} | ForEach-Object {
                $result = "'" + $_.InstanceId + "'"
                $toolTip = "InstanceId: $($_.InstanceId)`nDevice Name: $($_.Name)"
                [System.Management.Automation.CompletionResult]::new($result, $_.InstanceId, 'ParameterValue', $toolTip)
            }
        }
    }

    $redirectedPnPDevicesArgCompleterScriptBlock = {
        param ($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams)
        $devices = @()
        $devices = Get-RedirectedUsbDevice -DeviceType 'PnP'
        $devices = $devices | Sort-Object -Property 'Name' -Unique
        if ($devices.Count -gt 0)
        {
            $devices | Where-Object {$_.InstanceId -like "*$WordToComplete*"} | ForEach-Object {
                $result = "'" + $_.InstanceId + "'"
                $toolTip = "InstanceId: $($_.InstanceId)`nDevice Name: $($_.Name)"
                [System.Management.Automation.CompletionResult]::new($result, $_.InstanceId, 'ParameterValue', $toolTip)
            }
        }
    }

  Register-ArgumentCompleter -CommandName $argCompleterFunctionList -ParameterName 'Id' -ScriptBlock $idArgCompleterScriptBlock
  Register-ArgumentCompleter -CommandName $argCompleterFunctionList -ParameterName 'Name' -ScriptBlock $nameArgCompleterScriptBlock
  Register-ArgumentCompleter -CommandName 'Edit-VMConnectConfig' -ParameterName 'RedirectedPnpDevices' -ScriptBlock $redirectedPnPDevicesArgCompleterScriptBlock
  Register-ArgumentCompleter -CommandName 'Edit-VMConnectConfig' -ParameterName 'RedirectedUsbDevices' -ScriptBlock $redirectedUsbDevicesArgCompleterScriptBlock
} # if ($PSVersionTable.PSVersion.Major -ge 5)

# In PowerShell 4.0 sometimes the module manifest doesn't export aliases and sometimes the function [Alias()] attribute doesn't work reliably.
if ($PSVersionTable.PSVersion.Major -lt 5)
{
    $functionAliasList = @()
    $functionAliasList += [pscustomobject] @{FunctionName = 'Edit-VMConnectConfig'; AliasName = 'edvmc'}
    $functionAliasList += [pscustomobject] @{FunctionName = 'Get-VMConnectConfig'; AliasName = 'gvmc'}
    $functionAliasList += [pscustomobject] @{FunctionName = 'New-VMConnectConfig'; AliasName = 'nvmc'}
    $functionAliasList += [pscustomobject] @{FunctionName = 'Remove-VMConnectConfig'; AliasName = 'rvmc'}
    $functionAliasList += [pscustomobject] @{FunctionName = 'Reset-VMConnectConfig'; AliasName = 'rsvmc'}
    $functionAliasList += [pscustomobject] @{FunctionName = 'Test-VMConnectConfig'; AliasName = 'tvmc'}

    foreach ($function in $functionAliasList)
    {
        New-Alias -Name $function.AliasName -Value $function.FunctionName -Force
        Export-ModuleMember -Function $function.FunctionName -Alias $function.AliasName
    }
}