Importing Azure RDC Files into RDCMan.exe’s RDG

This is really over-engineering.  Azure’s “connect” link sends you a .rdc file to download or open.  The registered handler for .rdc files is mstsc.exe, a.k.a. Remote Desktop Connection.

The key line in the .rdc file is “Full Address:s:FQDN:PORT”.  The ‘s’ column in the colon-delimited value is short for ‘string,’ nothing more.  What we want are the FQDN, the port, and the .rdc file’s name.  Why the filename?  It turns out that the Azure Cloud Service is the FQDN in the .rdc file.  The machine name is just the filename.  In other words, if all your VMs are in the same Azure Cloud Service, then the FQDN for each .rdc will be identical.  Only the port will differentiate one VM from the other.

Anyhow, all this does is look for the Full Address line, extract out the FQDN and port data, then create a server element under the specified group in the RDG file.

 function Import-AzureRdpToRdg
{
    <#
    .Synopsis
    Import a saved Remote Desktop Connection configuration file (RDC) into a Remote Desktop Connection Manager (RDCMan) configuration file (RDG)

    .description
    The name says 'Azure', but this will import the FQDN, the displayed name, and the port from any RDC file.  It's useful for Azure because the ports in their RDCs are not consistent.  

    If an existing server shares the same FQDN and port, regardless of the displayed name, it is marked redundant and will be removed after a warning and delay prompt.

    .parameter Path
    Location of one or more RDC files to import.

    .parameter Group
    Name of RDG group into which to import the RDC servers.  If multiple groups with the same name are found, the first one is used.  Defaults to 'Azure'.  If specified group does not exist, it will be created after a warning and delay prompt.

    .parameter RdgPath
    Name of RDG file into which to import the RDC servers.  If not specified, it defaults to the value stored in $Host.Rdg.XML.  If that is not set, script will error out.  If file does not exist, it will be created after a warning and delay prompt.

    .parameter Force
    Do not warn nor delay prompt when creating missing RdgPath, -Group group value, nor removing redundant hosts.

    #>

    param (
        [parameter(ValueFromPipeline=$true)][string[]]$Path,
        [string]$Group = 'Azure',
        [string]$RdgPath = $Host.Rdg.Path,
        [switch]$Force
    );

    begin
    {
        #region header
        ##########

        $ErrorActionPreference = 'SilentlyContinue';

        trap { Write-Warning $_.Exception.Message; return; }

        ##########
        #endregion
        #region validate $RdgPath
        ##########

        if ($RdgPath)
        {
            if (!($host.Rdg.Path = $RdgPath))
            { 
                Add-Member -InputObject $Host -MemberType NoteProperty -Name Rdg -Value @{ 
                    Path = $RdgPath; 
                } -Force -ErrorVariable errorVariable; 
                if ($errorVariable) { return ( Write-Warning $errorVariable.Exception.Message ); }
    
            } # if (!($host.Rdg.Path = $RdgPath))

        } # if (!$RdgPath)
        else
        {
            $message = "-RdgPath not specified and `$Host.Rdg.Path is not set.";
            Write-Error -Message $message -ErrorAction SilentlyContinue
            return ( Write-Warning -Message $message );

        } # if (!$RdgPath) ... else

        if (Test-Path -Path $RdgPath)
        { # if we can find the file. back it up
            $backupPath = ($RdgPath -replace "\.rdg$") + " ($(Get-Date -Format 'yyyy-MM-dd_HH-mm-ss')).rdg"
            Copy-Item $RdgPath $backupPath;
        }
        else
        { # if we can't find the file, try to create it
            
            if (!$Force)
            {
                Write-Warning "-RdgPath $Rdgpath not found.  Creating in 5 seconds.";
                Start-Sleep -Seconds 5;

            } # if (!$Force)

            Set-Content -Path $RdgPath -Value "
                <?xml version='1.0' encoding='utf-8'?>
                <RDCMan schemaVersion='1'>
                <version>2.2</version>
                <file>
                    <properties>
                        <name>AzureRDG</name>
                    </properties>
                    <group>
                        <properties>
                            <name>$group</name>
                        </properties>
                        </group>
                    </file>
                </RDCMan>
            " -ErrorAction SilentlyContinue 

        } # if (Test-Path -Path $RdgPath) ... else

        if (!($Host.Rdg.XML = (Get-Content -Path $RdgPath -ErrorVariable errorVariable) -as [xml]))
        {
            $message = "-RdgPath $RdgPath cannot be parsed as XML.";
            Write-Error -Message $message -ErrorAction SilentlyContinue
            return ( Write-Warning -Message $message );

        } # if (!($Host.Rdg.XML = (Get-Content -Path $RdgPath) -as [xml]))
        if ($errorVariable) { return ( Write-Warning $errorVariable.Exception.Message ); }

        ##########
        #endregion
        #region validate $Group
        ##########

        $groupNameHash = @{}; # need a [HashTable] to make the group name handling case-insensitive
        $Host.Rdg.XML.SelectNodes("//group/properties/name") | % { $groupNameHash.($_.'#text') = $_.'#text'; }

        if ($groupNameHash.$Group)
        { # if the group name is in the [HashTable], correct the case

            $Group = $groupNameHash.$Group; 
    
        } # if ($groupNameHash.$Group) ... else
        elseif (!$Force)
        { # otherwise, the group name, no matter the case, is not in the list of groups in the RDG file

            Write-Warning -Message "-RdgPath $RdgPath -Group $Group cannot be found.  Creating in 5 seconds.";
            Start-Sleep -Seconds 5;

        } # if ($groupNameHash.$Group) ... else

        if (!($azureGroup = $Host.Rdg.XML.SelectSingleNode("//group/properties/name[text()='Azure']/../..") |
            Select-Object -First 1
        ))
        { # if we can't find the Azure group, create it

            $azureGroup = $Host.Rdg.XML.CreateElement("group");
            $azureGroup.InnerXml = "
                <properties>
                    <name>$group</name>
                </properties>
            ";
            $Host.Rdg.XML.SelectSingleNode("//file").AppendChild($azureGroup) | Out-Null;
    
        } # if (($azureGroup = !$Host.Rdg.XML.SelectSingleNode(...

        #endregion
        ##########

    } # begin
    process
    {
        #region validate each file in $Path
        ##########

        foreach ($file in $Path)
        {
            if (Test-Path -Path $Path -ErrorVariable errorVariable)
            {
                $fqdn, $port = (
                    (
                        Get-Content -Path $Path |
                        ? { $_ -match '^full address:' } |
                        Select-Object -First 1
                    ) -split ":"
                )[2,3]

            } # if (Test-Path -Path $Path -ErrorVariable errorVariable)
            elseif ($errorVariable) 
            { 
                Write-Warning $errorVariable.Exception.Message;
                continue;

            } # if (Test-Path -Path $Path -ErrorVariable errorVariable) ... else
            
        } # foreach ($file in $Path)

        if (!$fqdn -or !$port -or !($port -as [int]))
        {
            $message = "-Path $file is not a valid RDP file.";
            Write-Error -Message $message -ErrorAction SilentlyContinue
            Write-Warning -Message $message;
            continue;

        } # if (!$fqdn -or !$port -or !($port -as [int]))

        ##########
        #endregion
        #region create server node
        ##########

        $Host.Rdg.XML.SelectNodes("//server/name[text()='$fqdn']/../connectionSettings/port[text()='$port']/../..") |
        % {
            $serverNode = $_;

            if (!$Force)
            {
                $groupNode = $serverNode

                while ($groupNode = $sgroupNode.ParentNode)
                { $groupString = "/" + $serverNode.ParentNode.Properties.Name.'#text'; }

                Write-Warning "$groupString/$displayname points to ${fqdn}:$port.  Overwriting in 5 seconds.";

                Start-sleep -Seconds 5;

            } # if (!$Force)

            $serverNode.ParentNode.RemoveChild($serverNode) | Out-Null;
        }

        $displayName = (Split-Path -Path $file -Leaf) -replace '\.rdp$';
        $serverNode = $Host.Rdg.XML.CreateElement("server");
        $serverNode.InnerXml = "
             <name>$fqdn</name>
             <displayName>$displayName</displayName>
             <comment />
             <connectionSettings inherit='None'>
                 <port>$port</port>
             </connectionSettings>
        ";

        $azureGroup.AppendChild($serverNode) | Out-Null;
        $Host.Rdg.XML.SelectNodes("//server/name[text()='$fqdn']/../connectionsettings/port[text()='$port']/../..")
        Write-Verbose -Verbose "$displayName (${fqdn}:$port) added.";

        ##########
        #endregion

    } # process
    end
    { $host.Rdg.XML.Save($Host.Rdg.Path); }

} # function Import-AzureRdpToRdg