Public/Edit-VMConnectConfig.ps1

#.ExternalHelp VMConnectConfig-help.xml
function Edit-VMConnectConfig
{
    [CmdletBinding(ConfirmImpact = 'Medium', HelpURI='https://thegraffix.github.io/VMConnectConfig/edit-vmconnectconfig.html', SupportsShouldProcess)]
    [Alias('edvmc')]
    [OutputType([System.Management.Automation.PSObject[]])]
    param (
        [Parameter(Mandatory, ParameterSetName = 'Id', Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Parameter(Mandatory, ParameterSetName = 'IdGUI', Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('VMId')]
        [ValidateNotNullOrEmpty()]
        [System.Guid[]]$Id,

        [Parameter(Mandatory, ParameterSetName = 'InputObject', Position = 0, ValueFromPipeline)]
        [Parameter(Mandatory, ParameterSetName = 'InputObjectGUI', Position = 0, ValueFromPipeline)]
        [System.Management.Automation.PSTypeName('VMConnectConfig')]$InputObject,

        [Parameter(Mandatory, ParameterSetName = 'LiteralPath', ValueFromPipelineByPropertyName)]
        [Parameter(Mandatory, ParameterSetName = 'LiteralPathGUI', ValueFromPipelineByPropertyName)]
        [Alias('FullName')]
        [ValidateNotNullOrEmpty()]
        [System.String[]]$LiteralPath,

        [Parameter(Mandatory, ParameterSetName = 'Name', Position = 0, ValueFromPipeline)]
        [Parameter(Mandatory, ParameterSetName = 'NameGUI', Position = 0, ValueFromPipeline)]
        [SupportsWildcards()]
        [ValidateNotNullOrEmpty()]
        [System.String[]]$Name,

        [Parameter(ParameterSetName = 'Id')]
        [Parameter(ParameterSetName = 'InputObject')]
        [Parameter(ParameterSetName = 'LiteralPath')]
        [Parameter(ParameterSetName = 'Name')]
        [Parameter(ParameterSetName = 'Path')]
        [switch]$PassThru,

        [Parameter(Mandatory, ParameterSetName = 'Path')]
        [Parameter(Mandatory, ParameterSetName = 'PathGUI')]
        [SupportsWildcards()]
        [ValidateNotNullOrEmpty()]
        [System.String[]]$Path,

        [Parameter(ParameterSetName = 'LiteralPathGUI')]
        [Parameter(ParameterSetName = 'PathGUI')]
        [Parameter(ParameterSetName = 'IdGUI')]
        [Parameter(ParameterSetName = 'InputObjectGUI')]
        [Parameter(ParameterSetName = 'NameGUI')]
        [switch]$ShowEnhancedConnectionDialog,

        [Parameter(ParameterSetName = 'Id')]
        [Parameter(ParameterSetName = 'InputObject')]
        [Parameter(ParameterSetName = 'LiteralPath')]
        [Parameter(ParameterSetName = 'Name')]
        [Parameter(ParameterSetName = 'Path')]
        [System.Boolean]$AudioCaptureRedirectionMode,

        [Parameter(ParameterSetName = 'Id')]
        [Parameter(ParameterSetName = 'InputObject')]
        [Parameter(ParameterSetName = 'LiteralPath')]
        [Parameter(ParameterSetName = 'Name')]
        [Parameter(ParameterSetName = 'Path')]
        [ValidateSet('AUDIO_MODE_NONE', 'AUDIO_MODE_REDIRECT', 'AUDIO_MODE_PLAY_ON_SERVER')]
        [System.String]$AudioPlaybackRedirectionMode,

        [Parameter(ParameterSetName = 'Id')]
        [Parameter(ParameterSetName = 'InputObject')]
        [Parameter(ParameterSetName = 'LiteralPath')]
        [Parameter(ParameterSetName = 'Name')]
        [Parameter(ParameterSetName = 'Path')]
        [System.Boolean]$ClipboardRedirection,

        [Parameter(ParameterSetName = 'Id')]
        [Parameter(ParameterSetName = 'InputObject')]
        [Parameter(ParameterSetName = 'LiteralPath')]
        [Parameter(ParameterSetName = 'Name')]
        [Parameter(ParameterSetName = 'Path')]
        [ValidateNotNullOrEmpty()]
        [System.Drawing.Size]$DesktopSize,

        [Parameter(ParameterSetName = 'Id')]
        [Parameter(ParameterSetName = 'InputObject')]
        [Parameter(ParameterSetName = 'LiteralPath')]
        [Parameter(ParameterSetName = 'Name')]
        [Parameter(ParameterSetName = 'Path')]
        [System.Boolean]$EnablePrinterRedirection,

        [Parameter(ParameterSetName = 'Id')]
        [Parameter(ParameterSetName = 'InputObject')]
        [Parameter(ParameterSetName = 'LiteralPath')]
        [Parameter(ParameterSetName = 'Name')]
        [Parameter(ParameterSetName = 'Path')]
        [System.Boolean]$FullScreen,

        [Parameter(ParameterSetName = 'Id')]
        [Parameter(ParameterSetName = 'InputObject')]
        [Parameter(ParameterSetName = 'LiteralPath')]
        [Parameter(ParameterSetName = 'Name')]
        [Parameter(ParameterSetName = 'Path')]
        [System.Boolean]$PrinterRedirection,

        [Parameter(ParameterSetName = 'Id')]
        [Parameter(ParameterSetName = 'InputObject')]
        [Parameter(ParameterSetName = 'LiteralPath')]
        [Parameter(ParameterSetName = 'Name')]
        [Parameter(ParameterSetName = 'Path')]
        [System.String[]]$RedirectedDrives,

        [Parameter(ParameterSetName = 'Id')]
        [Parameter(ParameterSetName = 'InputObject')]
        [Parameter(ParameterSetName = 'LiteralPath')]
        [Parameter(ParameterSetName = 'Name')]
        [Parameter(ParameterSetName = 'Path')]
        [System.String[]]$RedirectedPnpDevices,

        [Parameter(ParameterSetName = 'Id')]
        [Parameter(ParameterSetName = 'InputObject')]
        [Parameter(ParameterSetName = 'LiteralPath')]
        [Parameter(ParameterSetName = 'Name')]
        [Parameter(ParameterSetName = 'Path')]
        [System.String[]]$RedirectedUsbDevices,

        [Parameter(ParameterSetName = 'Id')]
        [Parameter(ParameterSetName = 'InputObject')]
        [Parameter(ParameterSetName = 'LiteralPath')]
        [Parameter(ParameterSetName = 'Name')]
        [Parameter(ParameterSetName = 'Path')]
        [System.Boolean]$SaveButtonChecked,

        [Parameter(ParameterSetName = 'Id')]
        [Parameter(ParameterSetName = 'InputObject')]
        [Parameter(ParameterSetName = 'LiteralPath')]
        [Parameter(ParameterSetName = 'Name')]
        [Parameter(ParameterSetName = 'Path')]
        [System.Boolean]$SmartCardsRedirection,

        [Parameter(ParameterSetName = 'Id')]
        [Parameter(ParameterSetName = 'InputObject')]
        [Parameter(ParameterSetName = 'LiteralPath')]
        [Parameter(ParameterSetName = 'Name')]
        [Parameter(ParameterSetName = 'Path')]
        [System.Boolean]$UseAllMonitors,

        [Parameter(ParameterSetName = 'Id')]
        [Parameter(ParameterSetName = 'InputObject')]
        [Parameter(ParameterSetName = 'LiteralPath')]
        [Parameter(ParameterSetName = 'Name')]
        [Parameter(ParameterSetName = 'Path')]
        [System.Boolean]$WebAuthnRedirection
    )

    begin
    {
        # This is a workaround for a bug where advanced functions can output an error if "-ErrorAction/-WarningAction Ignore" are used.
        if ($ErrorActionPreference -eq 'Ignore') {$ErrorActionPreference = 'Ignore'}
        if ($WarningPreference -eq 'Ignore') {$WarningPreference = 'Ignore'}

        # Validate $RedirectedDrives arguments if necessary.
        if ($PSBoundParameters.ContainsKey('RedirectedDrives'))
        {
            # Have to do this in the begin{} block instead of using ValidateSet() because $RedirectedDrives needs to be able to accept
            # $null/empty strings (in addition to drive letters, "*" and "dynamicdrives").
            $allowedRedirectedDrivesValues = @($null, [System.String]::Empty, '*', 'dynamicdrives', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z')

            foreach ($redirectedDriveItem in $RedirectedDrives)
            {
                if ($redirectedDriveItem -inotin $allowedRedirectedDrivesValues)
                {
                    $ex = New-Object -TypeName 'System.ArgumentException' -ArgumentList ($MsgTable.InvalidRedirectedDriveFormatError -f $redirectedDriveItem)
                    throw $ex
                }
            }
        }

        if ($ShowEnhancedConnectionDialog)
        {
            # Win 6.1 (Win7/Server2008 R2) uses "%ProgramFiles%\Hyper-V\vmconnect.exe".
            # Win 6.2+ (Win8+/Server 2012+) uses "%windir%\System32\vmconnect.exe".
            $vmconnectExePath = Join-Path -Path $SystemDirectory -ChildPath 'vmconnect.exe'
            if ((Test-Path -Path $vmconnectExePath) -eq $false)
            {
                $vmconnectExePath = [System.IO.Path]::Combine($env:ProgramFiles, 'Hyper-V', 'vmconnect.exe')
            }

            $cimParamFilter = $null
            $os = (Get-CimInstance -ClassName 'Win32_OperatingSystem' -Property ProductType, Version -Verbose:$false)

            # (1=Workstation 2=DomainController 3=Server)
            $osProductType = $os.ProductType
            $isServer = (($osProductType -eq 2) -or ($osProductType -eq 3))
            $osVersion = $os.Version

            # Win10+
            $wmiFilterClient = 'Caption = "Hyper-V GUI Management Tools"'
            # Server 2016+
            $wmiFilterServer = 'Name = "RSAT-Hyper-V-Tools-Feature"'
            # Win7, Server 2008 R2
            $wmiFilterClientLegacy = 'Caption = "Hyper-V Tools"'
            # Win8/8.1, Server 2012/2012 R2
            $wmiFilterServerLegacy = 'Name = "Microsoft-Hyper-V-Management-Clients"'

            if ($osVersion.StartsWith('6.1'))
            {
                # Windows Feature name for Win7/Server 2008 R2 is: "Hyper-V Tools"
                $windowsFeatureName = ($MsgTable.HyperVGuiMgmtToolsFeatureNameLegacy)
            }
            else
            {
                # Windows Feature for Win8+/Server 2012+ is: "Hyper-V GUI Management Tools"
                $windowsFeatureName = ($MsgTable.HyperVGuiMgmtToolsFeatureName)
            }

            if ($osVersion.StartsWith('10.'))
            {
                if ($isServer)
                {
                    $cimParamFilter = $wmiFilterServer
                }
                else
                {
                    $cimParamFilter = $wmiFilterClient
                }
            }
            elseif ($osVersion.StartsWith('6.2') -or $osVersion.StartsWith('6.3'))
            {
                $cimParamFilter = $wmiFilterServerLegacy
            }
            elseif ($osVersion.StartsWith('6.1'))
            {
                if ($isServer)
                {
                    $cimParamFilter = $wmiFilterServerLegacy
                }
                else
                {
                    $cimParamFilter = $wmiFilterClientLegacy
                }
            }

            $cimParams = @{
                ClassName = 'Win32_OptionalFeature'
                ErrorAction = 'Ignore'
                Filter = $cimParamFilter
                Property = 'InstallState'
                Verbose = $false
            }

            if ([System.String]::IsNullOrEmpty($cimParamFilter))
            {
                $ex = New-Object -TypeName 'System.Exception' -ArgumentList ($MsgTable.HyperVGuiToolsWmiError)
                throw $ex
            }
            elseif ((Get-CimInstance @cimParams).InstallState -ne 1)
            {
                $ex = New-Object -TypeName 'System.ComponentModel.Win32Exception' -ArgumentList 0x80370114, ($MsgTable.HyperVGuiMgmtToolsError -f $windowsFeatureName)
                throw $ex
            }
            elseif ((Test-Path -Path $vmconnectExePath) -eq $false)
            {
                # Check %windir%\System32\vmconnect.exe first, then %programfiles%\Hyper-V\vmconnect.exe -- throw an exception if neither are found.
                $ex = New-Object -TypeName 'System.IO.FileNotFoundException' -ArgumentList ($MsgTable.VmconnectExeNotFoundError -f $vmconnectExePath, $windowsFeatureName)
                throw $ex
            }
        }

        # This is for checking if at least 1x "settings" parameter is used.
        $settingsArray = @('AudioCaptureRedirectionMode',
                           'AudioPlaybackRedirectionMode',
                           'ClipboardRedirection',
                           'DesktopSize',
                           'EnablePrinterRedirection',
                           'FullScreen',
                           'PrinterRedirection',
                           'RedirectedDrives',
                           'RedirectedPnpDevices',
                           'RedirectedUsbDevices',
                           'SaveButtonChecked',
                           'SmartCardsRedirection',
                           'UseAllMonitors',
                           'WebAuthnRedirection')

        [System.String[]]$configFilePathList = @()
    } #begin

    process
    {
        if ($PSCmdlet.ParameterSetName -notlike '*Path')
        {
            $null = Test-VMConfigFolder
            $allConfigFiles = Get-AllConfigFiles
        }

        $configSettingParamsList = $null
        $params = $null

        if ($ShowEnhancedConnectionDialog -eq $false)
        {
            [System.String[]]$params = $PSBoundParameters.Keys
            $configSettingParamsList = (Compare-Object -ReferenceObject $params -DifferenceObject $settingsArray -ExcludeDifferent -IncludeEqual).InputObject

            # Throw an exception if not using the "-ShowEnhancedConnectionDialog" switch && no "settings" parameters are used.
            if ([System.String]::IsNullOrEmpty($configSettingParamsList))
            {
                $paramMissingString = ($settingsArray | Sort-Object) -join ', '
                $exceptionMsg = ($MsgTable.ParameterMissingError -f $paramMissingString)
                $ex = New-Object -TypeName 'System.Management.Automation.ParameterBindingException' -ArgumentList $exceptionMsg
                throw $ex
            }
        }

        switch ($PSCmdlet.ParameterSetName)
        {
            {($_ -eq 'Id') -or ($_ -eq 'IdGUI')}
            {
                foreach ($vmGuid in $Id)
                {
                    $files = @()
                    $files = [System.String[]](($allConfigFiles | Where-Object {$_.Name -match "vmconnect\.rdp\.$vmGuid.*\.config"}).FullName)

                    if (($files.Count -eq 0) -or ([System.String]::IsNullOrEmpty($files)))
                    {
                        $errMsg = ($MsgTable.ConfigFileNotFoundIdError -f $VMConfigFolder, $vmGuid)

                        $errParams = @{
                            Category = $ErrorCatObjectNotFound
                            Exception = New-Object -TypeName 'System.ArgumentException' -ArgumentList $errMsg
                            Message = $errMsg
                        }

                        Write-Error @errParams
                    } #if no .config file(s) found.
                    else
                    {
                        foreach ($file in $files)
                        {
                            $configFilePathList += $file
                        }
                    } #else
                } #foreach $vmGuid

                break
            } # -Id [-ShowEnhancedConnectionDialog]

            {($_ -eq 'InputObject') -or ($_ -eq 'InputObjectGUI')}
            {
                foreach ($file in $InputObject.Path)
                {
                    $configFilePathList += $file
                }

                break
            } # -InputObject [-ShowEnhancedConnectionDialog]

            {($_ -eq 'LiteralPath') -or ($_ -eq 'LiteralPathGUI')}
            {
                foreach ($literalItem in $LiteralPath)
                {
                    $literalPathError = $null
                    $resolvedLiteralPaths = @()
                    $resolvedLiteralPaths = [System.String[]]((Resolve-Path -LiteralPath $literalItem -ErrorVariable 'literalPathError').Path | Where-Object {[System.IO.File]::Exists($_)})

                    if ((($resolvedLiteralPaths.Count -eq 0) -or ([System.String]::IsNullOrEmpty($resolvedLiteralPaths))) -and [System.String]::IsNullOrEmpty($literalPathError))
                    {
                        # Output an error message when Resolve-Path returns 0 items and doesn't output an error message.
                        $resolvedLiteralItemPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($literalItem)
                        $errMsg = ($MsgTable.ConfigFileNotFoundPathError -f $resolvedLiteralItemPath)

                        $errParams = @{
                            Category = $ErrorCatObjectNotFound
                            Exception = New-Object -TypeName 'System.IO.FileNotFoundException' -ArgumentList $errMsg, $resolvedLiteralItemPath
                            Message = $errMsg
                            TargetObject = $resolvedLiteralItemPath
                        }

                        Write-Error @errParams
                    } #if
                    else
                    {
                        foreach ($resolvedLiteralPath in $resolvedLiteralPaths)
                        {
                            $configFilePathList += $resolvedLiteralPath
                        }
                    } #else
                } #foreach $literalItem

                break
            } # -LiteralPath [-ShowEnhancedConnectionDialog]

            {($_ -eq 'Name') -or ($_ -eq 'NameGUI')}
            {
                foreach ($vmName in $Name)
                {
                    $files = @()

                    try
                    {
                        $files = [System.String[]]((Select-Xml -Path $allConfigFiles.FullName -XPath $VMNameXPath | Where-Object {$_.Node.Value -like $vmName}).Path)
                    } #try
                    catch
                    {
                        # Do nothing. This is to fully suppress terminating errors from the Select-Xml cmdlet when the 1.0 directory exists but is empty.
                    } #catch

                    if (($files.Count -eq 0) -or ([System.String]::IsNullOrEmpty($files)))
                    {
                        $errMsg = ($MsgTable.ConfigFileNotFoundNameError -f $VMConfigFolder, $vmName)

                        $errParams = @{
                            Category = $ErrorCatObjectNotFound
                            Exception = New-Object -TypeName 'System.ArgumentException' -ArgumentList $errMsg
                            Message = $errMsg
                        }

                        Write-Error @errParams
                    } #if no .config file(s) found.
                    else
                    {
                        foreach ($file in $files)
                        {
                            $configFilePathList += $file
                        }
                    } #else
                } #foreach $vmName

                break
            } # -Name [-ShowEnhancedConnectionDialog]

            {($_ -eq 'Path') -or ($_ -eq 'PathGUI')}
            {
                foreach ($item in $Path)
                {
                    $pathError = $null
                    $resolvedPaths = @()
                    $resolvedPaths = [System.String[]]((Resolve-Path -Path $item -ErrorVariable 'pathError').Path | Where-Object {[System.IO.File]::Exists($_)})

                    if ((($resolvedPaths.Count -eq 0) -or ([System.String]::IsNullOrEmpty($resolvedPaths))) -and [System.String]::IsNullOrEmpty($pathError))
                    {
                        # Output an error message when Resolve-Path returns 0 items and doesn't output an error message.
                        $resolvedItemPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($item)
                        $errMsg = ($MsgTable.ConfigFileNotFoundPathError -f $resolvedItemPath)

                        $errParams = @{
                            Category = $ErrorCatObjectNotFound
                            Exception = New-Object -TypeName 'System.IO.FileNotFoundException' -ArgumentList $errMsg, $resolvedItemPath
                            Message = $errMsg
                            TargetObject = $resolvedItemPath
                        }

                        Write-Error @errParams
                    } #if
                    else
                    {
                        foreach ($resolvedPath in $resolvedPaths)
                        {
                            $configFilePathList += $resolvedPath
                        }
                    } #else
                } #foreach $item

                break
            } # -Path [-ShowEnhancedConnectionDialog]
        } #switch ($PSCmdlet.ParameterSetName)
    } #process

    end
    {
        $configFilePathList = $configFilePathList | Select-Object -Unique
        [System.String[]]$passThruList = @()
        [System.String[]]$validConfigFilePathList = @()

        foreach ($configFile in $configFilePathList)
        {
            if ((Test-VMConnectConfig -LiteralPath $configFile -ErrorAction SilentlyContinue) -eq $false)
            {
                $errMsg = ($MsgTable.InvalidConfigFileError -f $configFile)

                $errParams = @{
                    Category = $ErrorCatInvalidArgument
                    Exception = New-Object -TypeName 'System.ArgumentException' -ArgumentList $errMsg
                    Message = $errMsg
                    TargetObject = $configFile
                }

                Write-Error @errParams
            }
            else
            {
                $validConfigFilePathList += $configFile
            }
        } #foreach ($configFile in $configFilePathList)

        if ($ShowEnhancedConnectionDialog -and $validConfigFilePathList.Count -gt 0)
        {
            # The vmconnect.exe -C switch's value is an [Int32], but $vmconnectExeLaunchCount is a [UInt32] because:
            # - The -C command line option/switch does not accept negative values.
            # - If the -C parameter value is greater than [Int32]::MaxValue, vmconnect.exe will still use $vmconnectExeLaunchCount as an argument for the -C parameter and vmconnect.exe will output its own (friendlier and more useful) error messages rather than PowerShell generating an ArgumentTransformationMetadataException exception.
            [System.UInt32]$vmconnectExeLaunchCount = 0
            $vmObjArray = @()

            foreach ($configFile in $validConfigFilePathList)
            {
                try
                {
                    $xmlFile = New-Object -TypeName 'System.Xml.XmlDocument'
                    $xmlFile.Load($configFile)

                    $configFileName = Split-Path -Path $configFile -Leaf
                    $vmPsObjId = ($configFileName | Select-String -Pattern $GuidRegexPattern).Matches.Value
                    $vmPsObjName = $xmlFile.SelectSingleNode($VMNameXPath).value
                    $vmPsObjServerName = $xmlFile.SelectSingleNode($VMServerNameXPath).value

                    $vmObjArray += [pscustomobject] @{
                        VMId = $vmPsObjId
                        VMName = $vmPsObjName
                        VMServerName = $vmPsObjServerName
                    }
                } #try
                catch
                {
                    $PSCmdlet.WriteError($_)
                } #catch
            } #foreach ($configFile in $validConfigFilePathList)

            # Remove duplicate objects from vmObjArray so we don't call vmconnect.exe multiple times for the same VM Names/IDs.
            $vmObjArray = $vmObjArray | Select-Object -Property VMId, VMName, VMServerName -Unique

            foreach ($vmObj in $vmObjArray)
            {
                $vmId = $vmObj.VMId
                $vmName = $vmObj.VMName
                $vmServerName = $vmObj.VMServerName

                # Only run vmconnect.exe if there's a valid VM ID in the filename.
                if ([System.String]::IsNullOrEmpty($vmId))
                {
                    $errMsg = ($MsgTable.GuidNotFoundInFilenameError -f $configFile)

                    $errParams = @{
                        Category = $ErrorCatInvalidArgument
                        Exception = New-Object -TypeName 'System.ArgumentException' -ArgumentList $errMsg
                        Message = $errMsg
                        TargetObject = $configFile
                    }

                    Write-Error @errParams
                } #if no VMId.
                else
                {
                    $actionString = ($MsgTable.ActionMsgStartingVmconnectExe -f $vmconnectExePath)
                    $targetString = ($MsgTable.TargetMsgStartingVmconnectExe -f $vmName, $vmId, $vmServerName)

                    try
                    {
                        if ($PSCmdlet.ShouldProcess($targetString, $actionString))
                        {
                            if ((Get-VMEnhancedSessionModeState -Id $vmId -VMServerName $vmServerName -Verbose:$false) -ne 2)
                            {
                                $errMsg = ($MsgTable.EnhancedRdpSessionError -f $vmName, $vmId, $vmServerName)

                                $errParams = @{
                                    Category = $ErrorCatInvalidOperation
                                    Exception = New-Object -TypeName 'System.InvalidOperationException' -ArgumentList $errMsg
                                    Message = $errMsg
                                }

                                Write-Error @errParams
                            } #if VM can't establish an enhanced RDP session.
                            else
                            {
                                $vmconnectExeArgs = @(
                                    "`"$vmServerName`"",
                                    "`"$vmName`"",
                                    "-G `"$vmId`"",
                                    "-C `"$vmconnectExeLaunchCount`"",
                                    '/edit'
                                )

                                $vmconnectExeParams = @{
                                    FilePath = $vmconnectExePath
                                    ArgumentList = $vmconnectExeArgs
                                    Confirm = $false
                                }

                                Start-Process @vmconnectExeParams

                                $vmconnectExeLaunchCount++
                            } #else
                        } #ShouldProcess
                    } #try
                    catch
                    {
                        $PSCmdlet.WriteError($_)
                    } #catch
                } #else
            } #foreach ($vmObj in $vmObjArray)
        } # if -ShowEnhancedConnectionDialog
        else
        {
            foreach ($configFile in $validConfigFilePathList)
            {
                Write-VMVerbose -FunctionName Edit-VMConnectConfig -Category Reading -Message ($MsgTable.LoadingFileMsg -f $configFile)
                try
                {
                    $xmlFile = New-Object -TypeName 'System.Xml.XmlDocument'
                    $xmlFile.Load($configFile)
                    $configFileName = Split-Path -Path $configFile -Leaf
                    $vmName = $xmlFile.SelectSingleNode($VMNameXPath).value
                    $vmNameActionString = $vmName
                    $vmId = ($configFileName | Select-String -Pattern $GuidRegexPattern).Matches.Value

                    if ([System.String]::IsNullOrEmpty($vmId))
                    {
                        $vmIdErrorMsgString = $null
                    }
                    else
                    {
                        $vmIdErrorMsgString = " ($vmId)"
                    }

                    $vmIdActionString = $null
                    if ([System.String]::IsNullOrEmpty($vmId) -eq $false)
                    {
                        $vmIdActionString = " ($vmId)"
                    }

                    $actionString = ($MsgTable.ActionMsgModifyConfigFile -f $vmNameActionString, $vmIdActionString)

                    if ($PSCmdlet.ShouldProcess($configFile, $actionString))
                    {
                        $xmlElementListCount = $xmlFile.SelectNodes($XPath).Count
                        $flagChangesMade = $false

                        switch ($configSettingParamsList)
                        {
                            'AudioCaptureRedirectionMode'
                            {
                                $xmlFile.SelectSingleNode("//setting[@name='AudioCaptureRedirectionMode']").Value = [System.String]$AudioCaptureRedirectionMode
                                $flagChangesMade = $true
                            }

                            'AudioPlaybackRedirectionMode'
                            {
                                $xmlFile.SelectSingleNode("//setting[@name='AudioPlaybackRedirectionMode']").Value = $AudioPlaybackRedirectionMode
                                $flagChangesMade = $true
                            }

                            'ClipboardRedirection'
                            {
                                $xmlFile.SelectSingleNode("//setting[@name='ClipboardRedirection']").Value = [System.String]$ClipboardRedirection
                                $flagChangesMade = $true
                            }

                            'DesktopSize'
                            {
                                $xmlFile.SelectSingleNode("//setting[@name='DesktopSize']").Value = ((New-Object -TypeName 'System.Drawing.SizeConverter').ConvertToString($DesktopSize))
                                $flagChangesMade = $true
                            }

                            'EnablePrinterRedirection'
                            {
                                if ($xmlElementListCount -eq 15)
                                {
                                    Write-Warning ($MsgTable.UnsupportedConfigurationSettingError -f $configFile, $vmName, $vmIdErrorMsgString, 'EnablePrinterRedirection')
                                }
                                else
                                {
                                    $xmlFile.SelectSingleNode("//setting[@name='EnablePrinterRedirection']").Value = [System.String]$EnablePrinterRedirection
                                    $flagChangesMade = $true
                                }
                            }

                            'FullScreen'
                            {
                                $xmlFile.SelectSingleNode("//setting[@name='FullScreen']").Value = [System.String]$FullScreen
                                $flagChangesMade = $true
                            }

                            'PrinterRedirection'
                            {
                                $xmlFile.SelectSingleNode("//setting[@name='PrinterRedirection']").Value = [System.String]$PrinterRedirection
                                $flagChangesMade = $true
                            }

                            'RedirectedDrives'
                            {
                                if ([System.String]::IsNullOrEmpty($RedirectedDrives))
                                {
                                    $xmlFile.SelectSingleNode("//setting[@name='RedirectedDrives']/value").IsEmpty = $true
                                }
                                else
                                {
                                    $redirectedDrivesString = ($RedirectedDrives -join ';').Trim().ToLower()
                                    $xmlFile.SelectSingleNode("//setting[@name='RedirectedDrives']").Value = $redirectedDrivesString
                                }

                                $flagChangesMade = $true
                            }

                            'RedirectedPnpDevices'
                            {
                                if ([System.String]::IsNullOrEmpty($RedirectedPnpDevices))
                                {
                                    $xmlFile.SelectSingleNode("//setting[@name='RedirectedPnpDevices']/value").IsEmpty = $true
                                }
                                else
                                {
                                    $redirectedPnpDevicesString = ($RedirectedPnpDevices -join ';').Trim().ToLower()
                                    $xmlFile.SelectSingleNode("//setting[@name='RedirectedPnpDevices']").Value = $redirectedPnpDevicesString
                                }

                                $flagChangesMade = $true
                            }

                            'RedirectedUsbDevices'
                            {
                                if ([System.String]::IsNullOrEmpty($RedirectedUsbDevices))
                                {
                                    $xmlFile.SelectSingleNode("//setting[@name='RedirectedUsbDevices']/value").IsEmpty = $true
                                }
                                else
                                {
                                    $redirectedUsbDevicesString = ($RedirectedUsbDevices -join ';').Trim().ToLower()
                                    $xmlFile.SelectSingleNode("//setting[@name='RedirectedUsbDevices']").Value = $redirectedUsbDevicesString
                                }

                                $flagChangesMade = $true
                            }

                            'SaveButtonChecked'
                            {
                                $xmlFile.SelectSingleNode("//setting[@name='SaveButtonChecked']").Value = [System.String]$SaveButtonChecked
                                $flagChangesMade = $true
                            }

                            'SmartCardsRedirection'
                            {
                                $xmlFile.SelectSingleNode("//setting[@name='SmartCardsRedirection']").Value = [System.String]$SmartCardsRedirection
                                $flagChangesMade = $true
                            }

                            'UseAllMonitors'
                            {
                                $xmlFile.SelectSingleNode("//setting[@name='UseAllMonitors']").Value = [System.String]$UseAllMonitors

                                if ($UseAllMonitors)
                                {
                                    # If the UseAllMonitors setting is enabled, the FullScreen setting will be automatically enabled.
                                    $xmlFile.SelectSingleNode("//setting[@name='FullScreen']").Value = [System.String]$UseAllMonitors
                                }
                                $flagChangesMade = $true
                            }

                            'WebAuthnRedirection'
                            {
                                if ($xmlElementListCount.Count -lt 17)
                                {
                                    Write-Warning ($MsgTable.UnsupportedConfigurationSettingError -f $configFile, $vmName, $vmIdErrorMsgString, 'WebAuthnRedirection')
                                }
                                else
                                {
                                    $xmlFile.SelectSingleNode("//setting[@name='WebAuthnRedirection']").Value = [System.String]$WebAuthnRedirection
                                    $flagChangesMade = $true
                                }
                            }
                        } #switch ($configSettingParamsList)

                        if ($flagChangesMade)
                        {
                            $xmlWriterSettings = New-Object -TypeName 'System.Xml.XmlWriterSettings'
                            $xmlWriterSettings.Indent = $true
                            # Indent 4x spaces.
                            $xmlWriterSettings.IndentChars = New-Object -TypeName 'System.String' -ArgumentList ' ', 4
                            $xmlWriterSettings.Encoding = $XmlEncoding

                            $tmpConfigFile = [System.IO.Path]::GetTempFileName()

                            # This throws a terminating error on purpose.
                            $tmpConfigFilePath = (Get-Item -Path $tmpConfigFile -ErrorAction Stop).FullName

                            Write-VMVerbose -FunctionName Edit-VMConnectConfig -Category Writing -Message ($MsgTable.SavingToTempPathMsg -f $tmpConfigFilePath)
                            $xmlWriter = [System.Xml.XmlWriter]::Create($tmpConfigFilePath, $xmlWriterSettings)
                            $xmlFile.Save($xmlWriter)
                            $xmlWriter.Dispose()

                            Write-VMVerbose -FunctionName Edit-VMConnectConfig -Category Writing -Message ($MsgTable.CopyingToTempPathMsg -f $tmpConfigFilePath, $configFile)
                            Copy-Item -LiteralPath $tmpConfigFilePath -Destination $configFile -Force:$true -Confirm:$false
                        } #if .config file was modified and saved.
                        else
                        {
                            Write-VMVerbose -FunctionName Edit-VMConnectConfig -Category Writing -Message ($MsgTable.NoChangesMadeMsg -f $configFile)
                        }

                        if ($PassThru)
                        {
                            $passThruList += $configFile
                        } #if -PassThru
                    } #ShouldProcess
                } #try
                catch
                {
                    $PSCmdlet.WriteError($_)
                } #catch
                finally
                {
                    if ($null -ne $xmlWriter)
                    {
                        $xmlWriter.Dispose()
                    }

                    if ([System.IO.File]::Exists($tmpConfigFilePath))
                    {
                        $null = Remove-Item -LiteralPath $tmpConfigFilePath -Force:$true -Confirm:$false -ErrorAction SilentlyContinue
                    }
                } #finally
            } #foreach ($configFile in $validConfigFilePathList)

            if ($passThruList.Count -gt 0)
            {
                Get-VMConnectConfig -LiteralPath $passThruList
            }
        } #else (-ShowEnhancedConnectionDialog NOT used)

        # Perform garbage collection.
        [System.GC]::Collect()
        [System.GC]::WaitForPendingFinalizers()
    } #end
} #function Edit-VMConnectConfig