If you’ve ever used the Remote Desktop Connection Manager (RDCMan), you might be familiar with how obnoxious it can be trying to configure groups and servers manually. This is especially true if you often need to change servers between groups, or create temporary groupings of servers for any reason.
Are you tired of saving these RDCMan Group files (.RDG) by hand, or needing to wade through the clunky UI to accomplish changes?!
I’ve come up with a way to simplify this process by generating these files with PowerShell. Once generated, you can use these files to spawn multiple RDP sessions with the appropriate credentials configured for each computer.
Credit to the overall design of this function and its help functions go here.
Generating .RDG files with PowerShell
Since this is going to be a fairly large blog, I’m going to try my best to block this off into (logical?) sections.
So, bear with me…and off we go!
1. Helper Functions
These are going to help me set the stage for the real action to come. Here, I’m defining helper functions to list our RDCMan groups, add machines to our group, and generate passwords in the format that RDCMan expects.
Get-Group (5 parameters)
element – the full XML object
groupName – what the group will be labeled in RDCMan
userName – the username that will be used for the group
password – the password that will be used for the group
domain– the domain that the username/password belong to
This will return the group.
function Get-Group($element, $groupName, $userName, $password, $domain){
$group = $Template.RDCMan.file.group | Where-Object { $_.properties.name -eq $groupName} | Select-Object -First 1
if ($group -eq $null){
$group = $groupTemplateElement.Clone()
$group.properties.name = $groupName
$group.logonCredentials.userName = $userName
$group.logonCredentials.password = $password
$group.logonCredentials.domain = $domain
$group.RemoveChild($group.server)
$element.AppendChild($group) | out-null
}
return $group
}
Add-ServerToGroup (2 parameters)
group – The name of the group that the server will be added to
serverName – The name of the server
This will add the specified server to the group.
function Add-ServerToGroup($group, $serverName) {
$serverElement = $serverTemplateElement.clone()
$serverElement.properties.name = $serverName
[void]$group.AppendChild($serverElement)
}
Secure-Password
RDCManFile – Location of RDCMan
password – The password that we’ll convert to RDCMan’s encryption format
This will create the password encrypted in a way that RDCMan requires. As part of this, I prompt for the credentials of each group. As such, a new prompt will need to be added for each new group that is created. (See Copy-Item)
function Secure-Password ([string]$RDCManFile,[string]$password) {
if (-not(test-path "$($env:temp)\RDCMan.dll")) {
copy-item "$RDCManFile" "$($env:temp)\RDCMan.dll"
}
Import-Module "$($env:temp)\RDCMan.dll"
$EncryptionSettings = New-Object -TypeName RdcMan.EncryptionSettings
[RdcMan.Encryption]::EncryptString($password, $EncryptionSettings)
}
2. Template XML file
In order to generate our files, it’s going to be easiest to start with a template. So, for reference, I present (drum roll please) The Template.
I chose to store this in a separate XML file under template\template.rdg, which I pass in as the Path parameter to Get-Content when I store the XML in a variable.
<?xml version="1.0" encoding="utf-8"?>
<RDCMan programVersion="2.7" schemaVersion="3">
<file>
<credentialsProfiles />
<properties>
<expanded>True</expanded>
<name>Template</name>
</properties>
<remoteDesktop inherit="None">
<sameSizeAsClientArea>True</sameSizeAsClientArea>
<fullScreen>False</fullScreen>
<colorDepth>24</colorDepth>
</remoteDesktop>
<group>
<properties>
<expanded>True</expanded>
<name>Template</name>
</properties>
<logonCredentials inherit="None">
<profileName scope="Local">Custom</profileName>
<userName>UserName</userName>
<password />
<domain>DomainHereIfRelevant</domain>
</logonCredentials>
<server>
<properties>
<name>Template</name>
</properties>
</server>
</group>
</file>
<connected />
<favorites />
<recentlyUsed />
</RDCMan>
3. Main Function
Here is where the magic happens.
Get-RDCManFile
Computers – A list of computers
My parameter Computers needs to be defined as a custom object with the following properties
Name – The name of the computer
OS – The operating system version (7, 8.1, 10, etc)
Architecture – Bitness of the processes. Possible values: x86 or x64
Group – The group within RDCMan where I want this computer stored
I could create these manually or import them from another source (file, database, etc).
For this function, I am utilizing the power of a PowerShell advanced function. A discussion of this is out of the scope of the blog, but please check out more info about advanced functions and the begin, process, and end script blocks.
I start the definition of my function with the following:
function Get-RDCManFile {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True,
Position=1)]
[PSObject[]]$Computers
)
This starts our Begin{} block. I’ll define the location of RDCMan and our XML template in addition to doing a bit of preprocessing prior to jumping into our main body of the function.
begin {
# Store the location of the Remote Desktop Connection Manager file
$RDCMan = "E:\Remote Desktop Connection Manager\RDCMan.exe"
# Store the XML template into a variable for more sensible parsing / operating
# Be aware that you may need to update the path based on where you store the file
[xml]$Template = Get-Content -Path .\Template\Template.rdg
# Root XML element that objects will append to
$fileElement = $template.RDCMan.file
# Group template, for cloning
$groupTemplateElement = $fileElement.group
# Server template, for cloning
$serverTemplateElement = $groupTemplateElement.server
# Name your root element in the RDCMan connection window. Everybody needs a Name!
$fileElement.properties.name = 'Workstations'
$adminGroup = Get-Group $fileElement 'Admin-group' $env:USERNAME (Secure-Password $RDCMan ((New-Object PSCredential "user",$credentialHash[$env:USERNAME]).GetNetworkCredential().password)) $env:USERDOMAIN
$ExampleGroup = Get-Group $fileElement 'ExampleUser1-group' 'exampleUser1' (Secure-Password $RDCMan ((New-Object PSCredential "user",$credentialHash['ExampleUser1']).GetNetworkCredential().password)) ‘ExampleDomain’
}
Next up, the process{} block:
Since we’re using an advanced function, it processes each computer object that is passed down the pipeline from the $Computers parameter utilizing the Process{} block. It checks the “Group” property of our $Computers object to determine which group it is adding this computer to, and then uses the Add-ServerToGroup helper function. This could have been done with a Switch statement instead of multiple Where-Object parses, but… that’s not how I did it.
process {
$computers | Where-Object {$_.group -match "Admin-group"} | Foreach-Object {Add-ServerToGroup $adminGroup "$($_.name)"}
$computers | Where-Object {$_.group -match "ExampleUser1-group"} | Foreach-Object {Add-ServerToGroup $regGroup "$($_.name)"}
}
And, finally, the End{} block:
end {
# Remove the Group template object, as it is just a blank stub at this point.
[void]$fileElement.RemoveChild($groupTemplateElement)
# Create a temporary file to hold the XML
# This is our RDG file for launching RDCMan, although it is not required that it be provided a .RDG extension
$TempFile = New-TemporaryFile
$Template.save($TempFile)
# Launch RDCMan.exe using our temporary RDG file
& $RDCMan $TempFile
}
4. Defining the Data
Bear in mind that all this script has done, so far, is to define the functions. I still need to define which machines I’m going to use and then I still need to call the main function in order to get our process rolling.
First, I’ll make a hashtable with credential information. I’ll use the username as the key and the password (obtained from the user at run-time and stored in a secure string) as the value.
$credentialHash = @{}
$credentialHash['ExampleUser1'] = Read-Host "What is the password for ExampleUser1?" -AsSecureString
$credentialHash[$env:username] = Read-Host "What is the password for $($env:username)?" -AsSecureString
And finally, the action! Note that I have created a couple of mock PCs and stored them in separate variables as hashtables, and then pass them to the function as an array of hashtable objects.
In a real scenario, these PCs would be obtained from an external source and then formatted appropriately before being passed to this function. Please refer to the comments in the full script for more details.
$PC1 = @{"Name"='D14X86'
"OS"=10
"Group"="Admin-group"}
$PC2 = @{"Name"='D14'
"OS"=10
"Group"="ExampleUser1-group"}
5. Running the script
Now I’m finally getting to the good stuff. Are you ready? With all this build up, you’d think it would be more complex to run our script.
Surprisingly, it is this single line of code:
Get-RDCManFile @($PC1,$PC2)
Which should present you with the following output in RDCMan. With the root node selected, just hit enter, and you’re off to the races!
Amazing!
Wrapping Up
I realize this was quite the read, but I’m glad that you could stick around to the not-so bitter end! For your convenience, I’ve included the entire script for you below. In the immortal words of one Kris Powell, Happy PowerShelling!
# Create an RDG (XML formatted) file for RDCMan to import so that all workstations are in relevant groups.
# Add (via AppendChild method) a single Server object to a Group object
function Add-ServerToGroup($group, $serverName) {
$serverElement = $serverTemplateElement.clone()
$serverElement.properties.name = $serverName
[void]$group.AppendChild($serverElement)
}
# Return the requested Group object. If a Group object by the defined name does not already exist,
# a new Group object will be created.
# A Group object matching the defined name will still be returned, even if the other properties do not match
# the arguments passed to this function.
function Get-Group($element, $groupName, $userName, $password, $domain){
$group = $Template.RDCMan.file.group | Where-Object { $_.properties.name -eq $groupName} | Select-Object -First 1
if ($group -eq $null){
$group = $groupTemplateElement.Clone()
$group.properties.name = $groupName
$group.logonCredentials.userName = $userName
$group.logonCredentials.password = $password
$group.logonCredentials.domain = $domain
$group.RemoveChild($group.server)
$element.AppendChild($group) | out-null
}
return $group
}
# Convert the provided SecureString password to encrypted text usable by the RDCMan executable.
function Get-SecurePassword ([string]$RDCManFile,[string]$password) {
if (-not(test-path "$($env:temp)\RDCMan.dll")) {
copy-item "$RDCManFile" "$($env:temp)\RDCMan.dll"
}
Import-Module "$($env:temp)\RDCMan.dll"
$EncryptionSettings = New-Object -TypeName RdcMan.EncryptionSettings
[RdcMan.Encryption]::EncryptString($password, $EncryptionSettings)
}
function Get-RDCManFile {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True,
Position=1)]
[PSObject[]]$Computers
)
begin {
# Store the location of the Remote Desktop Connection Manager file
$RDCMan = "E:\Remote Desktop Connection Manager\RDCMan.exe"
# Store the XML template into a variable for more sensible parsing / operating
[xml]$Template = Get-Content -Path .\Template\Template.rdg
# Root XML element that objects will append to
$fileElement = $template.RDCMan.file
# Group template, for cloning
$groupTemplateElement = $fileElement.group
# Server template, for cloning
$serverTemplateElement = $groupTemplateElement.server
# Name your root element in the RDCMan connection window. Everybody needs a Name!
$fileElement.properties.name = 'Workstations'
$adminGroup = Get-Group $fileElement 'Admin-group' $env:USERNAME (Get-SecurePassword $RDCMan ((New-Object PSCredential "user",$credentialHash[$env:USERNAME]).GetNetworkCredential().password)) $env:USERDOMAIN
$regGroup = Get-Group $fileElement 'ExampleUser1-group' 'exampleUser1' (Get-SecurePassword $RDCMan ((New-Object PSCredential "user",$credentialHash['ExampleUser1']).GetNetworkCredential().password)) 'DEV'
}
process {
$computers | Where-Object {$_.group -match "Admin-group"} | Foreach-Object {Add-ServerToGroup $adminGroup "$($_.name)"}
$computers | Where-Object {$_.group -match "ExampleUser1-group"} | Foreach-Object {Add-ServerToGroup $regGroup "$($_.name)"}
}
end {
# Remove the Group template object, as it is just a blank stub at this point.
[void]$fileElement.RemoveChild($groupTemplateElement)
# Create a temporary file to hold the XML
# This is our RDG file for launching RDCMan, although it is not required that it be provided a .RDG extension
$TempFile = New-TemporaryFile
$Template.save($TempFile)
# Launch RDCMan.exe using our temporary RDG file
& $RDCMan $TempFile
}
}
# Collect the SecureString password from the user:
$credentialHash = @{}
$credentialHash['ExampleUser1'] = Read-Host "What is the password for ExampleUser1?" -AsSecureString
$credentialHash[$env:username] = Read-Host "What is the password for $($env:username)?" -AsSecureString
# Mock a couple of computer objects using a hash
# In a real scenario, you can obtain this information from whatever source you desire
# For instance, I query the relevant workstations out of the PDQ Inventory database
# based on what PDQ Inventory Collection I care about at the moment, but with a little
# more work you could acquire this information from Active Directory, a stored CSV
# file or spreadsheet, or a separate JSON / XML file.
$PC1 = @{"Name"='D14X86'
"OS"=10
"Group"="Admin-group"}
$PC2 = @{"Name"='D14'
"OS"=10
"Group"="ExampleUser1-group"}
Get-RDCManFile @($PC1,$PC2)