Public/New-VMConnectConfig.ps1

#.ExternalHelp VMConnectConfig-help.xml
function New-VMConnectConfig
{
    [CmdletBinding(HelpURI='https://thegraffix.github.io/VMConnectConfig/new-vmconnectconfig.html', SupportsShouldProcess)]
    [Alias('nvmc')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param (
        [Parameter(Position = 2)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('Legacy', 'Modern', 'WebAuthn')]
        [System.String]$ConfigFileType,

        [switch]$Force,

        [Parameter(Mandatory, ParameterSetName = 'Id')]
        [ValidateNotNullOrEmpty()]
        [System.Guid]$Id,

        [Parameter(Mandatory, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Name,

        [switch]$PassThru,

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

        [Parameter(Position = 1)]
        [System.String]$VMServerName
    )

    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'}

        if ($PSBoundParameters.ContainsKey('ConfigFileType') -eq $false)
        {
            $osVersion = [System.Version](Get-CimInstance -ClassName 'Win32_OperatingSystem' -Property Version -Verbose:$false).Version

            if ($osVersion.Major -eq 10)
            {
                $ConfigFileType = 'Modern'

                if ($osVersion.Build -ge 22000)
                {
                    $ConfigFileType = 'WebAuthn'
                }
            }
            else
            {
                $ConfigFileType = 'Legacy'
            }
        } #if -ConfigFileType not specified
    } #begin

    process
    {
        # Nothing to do here because there's no pipeline input to process. Future releases might implement pipeline input here.
    } #process

    end
    {
        $newConfigFileDesktopSize = $null

        if (Test-Path -Path $ClientSettingsConfigFile -PathType Leaf)
        {
            try
            {
                $clientSettingsXmlFile = New-Object -TypeName 'System.Xml.XmlDocument'
                $clientSettingsXmlFile.Load($ClientSettingsConfigFile)
                $newConfigFileDesktopSize = $clientSettingsXmlFile.SelectSingleNode("//setting[@name='DesktopSize']").value
            } #try
            catch {}
        }

        if ([System.String]::IsNullOrEmpty($newConfigFileDesktopSize))
        {
            Write-VMVerbose -FunctionName New-VMConnectConfig -Category Error -Message ($MsgTable.ClientSettingsFileDesktopSizeError -f $ClientSettingsConfigFile, $DefaultDesktopSize)
            $newConfigFileDesktopSize = $DefaultDesktopSize
        }

        try
        {
            switch ($PSCmdlet.ParameterSetName)
            {
                'Id'
                {
                    if ($PSBoundParameters.ContainsKey('Path'))
                    {
                        # -Id and -Path were used, so $Path must be a path to a container, not an item.
                        if (Test-Path -Path $Path -PathType Container)
                        {
                            $childPath = ($VMConnectConfigFilenamePattern -f $Id.ToString())
                            $unresolvedConfigFilePath = Join-Path -Path $Path -ChildPath $childPath
                        }
                        else
                        {
                            $exMsg = ($MsgTable.DirectoryNotFoundError -f $Path)
                            $ex = New-Object 'System.IO.DirectoryNotFoundException' -ArgumentList $exMsg
                            throw $ex
                        }
                    }
                    else
                    {
                        # Throw an exception if no -Path is specified && the "Hyper-V\Client\1.0" directory does not exist.
                        $null = Test-VMConfigFolder

                        # Name the file "vmconnect.rdp.$Id.config" and save it in the "Hyper-V\Client\1.0" directory since -Path wasn't specified.
                        $childPath = ($VMConnectConfigFilenamePattern -f $Id.ToString())
                        $unresolvedConfigFilePath = Join-Path -Path $VMConfigFolder -ChildPath $childPath
                    }

                    break
                }

                'Path'
                {
                    $unresolvedConfigFilePath = $Path
                    break
                }
            }

            $configFilePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($unresolvedConfigFilePath)

            if ([System.IO.Path]::GetExtension($configFilePath) -ne '.config')
            {
                $errMsg = ($MsgTable.InvalidConfigFileExtensionError -f $configFilePath)

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

                Write-Error @errParams
                return
            }

            # Below is to accommodate cases where Reset-VMConnectConfig retrieves an empty string/$null value from the .config file's VMServerName element content and
            # ends up passing an empty string/$null argument to "New-VMConnectConfig -VMServerName $null".
            # - The $VMServerName parameter should therefor accept empty string/$null values.
            # - Since the $VMServerName parameter can accept empty string/$null arguments, a default value of $HostName is assigned here if $VMServerName IsNullOrEmpty()
            # rather than assigning the parameter's default value in the param() block.
            if ([System.String]::IsNullOrEmpty($VMServerName))
            {
                $VMServerName = $HostName
                Write-VMVerbose -FunctionName New-VMConnectConfig -Category Warning -Message ($MsgTable.UsingHostNameForVmServerName -f $HostName)
            }

            switch ($ConfigFileType)
            {
                'Legacy'
                {
                    $newConfigFileXml = ($LegacyConfigFileXml -f $newConfigFileDesktopSize, $VMServerName, $Name)
                    break
                }

                'Modern'
                {
                    $newConfigFileXml = ($ModernConfigFileXml -f $newConfigFileDesktopSize, $VMServerName, $Name)
                    break
                }

                'WebAuthn'
                {
                    $newConfigFileXml = ($WebAuthnConfigFileXml -f $newConfigFileDesktopSize, $VMServerName, $Name)
                    break
                }
            } #switch ($ConfigFileType)
        } #try
        catch
        {
            $PSCmdlet.WriteError($_)
            return
        } #catch

        $actionVmId = $null
        $vmId = $null
        $vmId = (Split-Path -Path $configFilePath -Leaf | Select-String -Pattern $GuidRegexPattern).Matches.Value

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

        $actionString = ($MsgTable.ActionMsgCreateConfigFile -f $Name, $actionVmId)

        if (($Force) -or ($PSCmdlet.ShouldProcess($configFilePath, $actionString)))
        {
            try
            {
                Write-VMVerbose -FunctionName New-VMConnectConfig -Category Constructing -Message ($MsgTable.CreatingConfigFileMsg -f $Name, $actionVmId)
                $newConfigFile = New-Object -TypeName 'System.Xml.XmlDocument'
                $newConfigFile.LoadXml($newConfigFileXml)
                $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. Using a tmp file helps to avoid corrupting the original .config file should the write operation fail and result in corrupted data.
                $tmpConfigFilePath = (Get-Item -Path $tmpConfigFile -ErrorAction Stop).FullName

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

                Write-VMVerbose -FunctionName New-VMConnectConfig -Category Writing -Message ($MsgTable.CopyingToTempPathMsg -f $tmpConfigFilePath, $configFilePath)
                # Using Copy() instead of Copy-Item because System.IO exception messages are more descriptive and helpful compared to Copy-Item error messages.
                [System.IO.File]::Copy($tmpConfigFilePath, $configFilePath, $Force)

                if ($PassThru)
                {
                    Write-VMVerbose -FunctionName New-VMConnectConfig -Category Constructing -Message ($MsgTable.ConstructingOutputObjMsg -f $configFilePath)
                    Get-VMConnectConfig -LiteralPath $configFilePath
                }
            } #try
            catch
            {
                $PSCmdlet.WriteError($_)
                return
            } #catch
            finally
            {
                if ($xmlWriter)
                {
                    $xmlWriter.Dispose()
                }

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

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