How to shadow AVD Sessions using PowerShell

Shadowing Session allows the Administrator to view and control the remote desktop session to which the user is connected. With session shadow, the admin can view the Session, take full control, or view and take complete control with the user’s consent. This free built-in feature can compete with solutions like desk or Teamviewer. 

This post lets us know how to run the PowerShell script to shadow AVD sessions.

Using PowerShell To Shadow AVD Sessions

You need to set up your PowerShell environment and execute the PowerShell Script after setting up the PowerShell module for Azure Virtual Desktop.

There are specific fundamental points to understand that make PowerShell so effective. The most basic concept to understand about PowerShell is that the data returned is not just straightforward text.

Querying data from PowerShell returns data as a rich object with numerous properties unrestricted to explore. PowerShell uses .NET to communicate with the systems it operates, and the data returned includes .NET objects.

Shadowing using PowerShell Script

Cmdlets are grouped in PowerShell Modules. You need to install a module for shadowing Azure virtual machine sessions. 

Requirements

Users need to install The Azure ‘AZ’ PowerShell Modules. Here are the Specific Modules required:

Az.Accounts
    Az.DesktopVirtualization

Setting Up Azure Virtual Desktop

Step 1: Users begin by creating a Microsoft Azure Account. 

image


Step 2: Users need to log into their Azure account and search AVD Session.

Step 3:  Users need to create a host pool. That is the most crucial step, where users must fill in the details in the text fields on the given page. Users will be able to see these text fields under the subsection Basics. To create a host pool, users need to enter Subscription, Resource Group Name, Host Pool Name, Location, Preferred Desktop, Host Pool Type, Assignment Type.

Step 4: Users need to configure virtual machines as the next step for creating a host pool. The default option will be set to “No.” Users need to tick the “Yes” option and enter Windows Edition, number of Virtual Machines, and Virtual Network

Other requirements include the Username & password of the PC they are trying to access remotely. For the Workspace option, users need to select “No”

Step 5: Users need to click on the Review + Create button to create a Host Pool. That will provide the users with the Tenant ID and Subscription ID.

Step 6: Users need to create a powershell file and paste the code. After that, they need to enter the Tenant Id and Subscription ID in the script.

PowerShell Script

The Azure Virtual Desktop PowerShell module is integrated into the Azure PowerShell module.
Here are the Script Actions you need to follow:

  • First, users import the relevant PowerShell Modules
  • Users then need to login into their Azure account and connect to the configured Azure Tenant ID
  • Using the PowerShell GUI:
    • If more than One Azure Subscription has been configured Provide a Drop Down Box so the Administrator
    • Gather a List of the Host Pools within the Subscription and to populate the Host Pool list
    • The required Host Pool is selected
    • Populate the User List with all ACTIVE Users connected to the Host Pool
    • Select the User to be shadowed
    • Enable Remote Control if required
    • Shadow the User

Script

## Variables \ Arrays
# Azure Tenant ID
$AzureTenantId = “<Azure AzureTenantId Here>”

# Subscriptions Hosting AVD Environments
$AVDSubscriptions = @(‘<AVD Subscriptions Here>’)


## Import Required PowerShell Modules
cls
Write-Host
Write-Host $(Get-Date -Format HH:mm:ss:) -ForegroundColor Gray -NoNewLine ; Write-Host ” Importing PowerShell Modules”
$Modules = @(‘Az’)
Foreach ($Module in $Modules) {
if((Get-Module -Name $Module -ErrorAction SilentlyContinue) -eq $false) {
Write-Host $(Get-Date -Format HH:mm:ss:) -ForegroundColor Gray -NoNewLine ; Write-Host ” Importing Module” $Module -ForegroundColor DarkYellow
Import-Module -Name $Module -Verbose -ErrorAction SilentlyContinue
}
Else {
Write-Host $(Get-Date -Format HH:mm:ss:) -ForegroundColor Gray -NoNewLine ; Write-Host ” PowerShell Module ” -NoNewline -ForegroundColor DarkYellow ; Write-Host “‘$Module'” -NoNewline -ForegroundColor DarkCyan ; Write-Host ” already imported” -ForegroundColor DarkYellow
}
}


## Validate Variables
# AzureTenantId
If (!($AzureTenantId -match(“^(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}$”))) {
Write-Host ; Write-Host $(Get-Date -Format HH:mm:ss:) -ForegroundColor Gray -NoNewLine ; Write-Host ” ERROR: ” -NoNewline -ForegroundColor Red ; Write-Host “Azure Tenant Id is not a valid GUID”
Break
}

# Subscriptions
If ($AVDSubscriptions -eq ‘<AVD Subscriptions Here>’) {
Write-Host ; Write-Host $(Get-Date -Format HH:mm:ss:) -ForegroundColor Gray -NoNewLine ; Write-Host ” ERROR: ” -NoNewline -ForegroundColor Red ; Write-Host “AVD Subscription Variable not updated”
Break
}

If ($AVDSubscriptions.Count -eq 0) {
Write-Host ; Write-Host $(Get-Date -Format HH:mm:ss:) -ForegroundColor Gray -NoNewLine ; Write-Host ” ERROR: ” -NoNewline -ForegroundColor Red ; Write-Host “AVD Subscriptions Variable is empty”
Break
}

$WindowPadSubs = 0
If ($AVDSubscriptions.Count -gt 1) {
$WindowPadSubs = 90
}


## Check to see if Remote Assistance is installed
$UseMSTSC = $false
$WindowPadMSTSC = 0
If (!(Test-Path -Path ‘C:\Windows\System32\msra.exe’)) {
Write-Host ; Write-Host $(Get-Date -Format HH:mm:ss:) -ForegroundColor Gray -NoNewLine ; Write-Host ” Remote Assistance not Enabled, Switching the Remote Desktop Shadowing”
$UseMSTSC = $true
$WindowPadMSTSC = 50
}


## Log into Azure
Write-Host ; Write-Host $(Get-Date -Format HH:mm:ss:) -ForegroundColor Gray -NoNewLine ; Write-Host ” Log into Azure”
Write-Host $(Get-Date -Format HH:mm:ss:) -ForegroundColor Gray -NoNewLine ; Write-Host ” Prompting for Log In Credentials for Azure Tenant ID ” -ForegroundColor DarkYellow -NoNewline ; Write-Host $AzureTenantId -ForegroundColor DarkCyan
$HideOutput = Get-AzContext -ListAvailable | Disconnect-AzAccount
$HideOutput = Connect-AzAccount -Tenant $AzureTenantId -WarningAction SilentlyContinue


## Launch Shadow Selector Windows
Write-Host ; Write-Host $(Get-Date -Format HH:mm:ss:) -ForegroundColor Gray -NoNewLine ; Write-Host ” Launching Shadow Selector Screen”
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()

$ShadowUser = New-Object system.Windows.Forms.Form
$ShadowUser.ClientSize = New-Object System.Drawing.Size(700,(270 + $WindowPadSubs + $WindowPadMSTSC))
$ShadowUser.text = “User Shadow Selector”
$ShadowUser.TopMost = $false

$Subscriptions_Label = New-Object system.windows.forms.label
$Subscriptions_Label.text = “Select the Azure Subscription the user is connected too”
$Subscriptions_Label.autosize = $true
$Subscriptions_Label.enabled = $false
$Subscriptions_Label.location = New-Object System.Drawing.Point(30,10)
$Subscriptions_Label.Font = ‘Microsoft Sans Serif,12’

If ($AVDSubscriptions.Count -gt 1) {
$Subscriptions = New-Object system.Windows.Forms.ComboBox
$Subscriptions.text = “”
$Subscriptions.BackColor = “#c2c2c2”
$Subscriptions.width = 640
$Subscriptions.height = 25
$Subscriptions.location = New-Object System.Drawing.Point(30,45)
$Subscriptions.Font = ‘Microsoft Sans Serif,14’

Foreach ($Subs in $AVDSubscriptions) {
$Subscriptions.Items.Add($Subs) | Out-Null
}
}

$HostPool_Label = New-Object system.windows.forms.label
$HostPool_Label.text = “Select the Host Pool the user is connected too”
$HostPool_Label.autosize = $true
$HostPool_Label.enabled = $false
$HostPool_Label.location = New-Object System.Drawing.Point(30,(10 + $WindowPadSubs))
$HostPool_Label.Font = ‘Microsoft Sans Serif,12’

$HostPool = New-Object system.Windows.Forms.ComboBox
$HostPool.text = “Please Select Subscription to Populate”
$HostPool.BackColor = “#c2c2c2”
$HostPool.width = 640
$HostPool.height = 30
$HostPool.location = New-Object System.Drawing.Point(30,(45 + $WindowPadSubs))
$HostPool.Font = ‘Microsoft Sans Serif,14’
$HostPool.enabled = $false

$UserSession_Label = New-Object system.windows.forms.label
$UserSession_Label.text = “Select User to be Shadowed”
$UserSession_Label.autosize = $true
$UserSession_Label.enabled = $false
$UserSession_Label.location = New-Object System.Drawing.Point(30,(100 + $WindowPadSubs))
$UserSession_Label.Font = ‘Microsoft Sans Serif,12’

$UserSession = New-Object system.Windows.Forms.ComboBox
$UserSession.text = “Please Select Host Pool to Populate”
$UserSession.width = 640
$UserSession.height = 30
$UserSession.location = New-Object System.Drawing.Point(30,(135 + $WindowPadSubs))
$UserSession.Font = ‘Microsoft Sans Serif,14’
$UserSession.enabled = $false

#Hidden Value to calculate Session Host the User is connected too
$UserSessionHost = New-Object system.Windows.Forms.ComboBox
$UserSessionHost.enabled = $false
$UserSessionHost.Visible = $false

#Hidden Value to calculate the Session ID of the Users Session
$UserSessionID = New-Object system.Windows.Forms.ComboBox
$UserSessionID.enabled = $false
$UserSessionID.Visible = $false

$TakeControl = New-Object system.Windows.Forms.CheckBox
$TakeControl.text = ” Allow Remote Control of the Users Session”
$TakeControl.width = 600
$TakeControl.height = 30
$TakeControl.location = New-Object System.Drawing.Point(30,(195 + $WindowPadSubs))
$TakeControl.Font = ‘Microsoft Sans Serif,12’

$Shadow = New-Object system.Windows.Forms.Button
$Shadow.BackColor = “#f8e71c”
$Shadow.text = “Shadow User”
$Shadow.width = 500
$Shadow.height = 50
$Shadow.enabled = $false
$Shadow.location = New-Object System.Drawing.Point(100,(195 + $WindowPadSubs + $WindowPadMSTSC))
$Shadow.Font = ‘Microsoft Sans Serif,14’
$Shadow.ForeColor = “#0000ff”

$ShadowUser.controls.Clear()
$ShadowUser.controls.AddRange(@($HostPool_Label,$HostPool,$UserSession_Label,$UserSession,$Shadow))
If ($AVDSubscriptions.Count -gt 1) {
$ShadowUser.controls.AddRange(@($Subscriptions_Label,$Subscriptions))
}
If ($UseMSTSC -eq $true) {
$ShadowUser.controls.AddRange(@($TakeControl))
}

If ($AVDSubscriptions.Count -gt 1) {
$Subscriptions.Add_SelectedValueChanged({ Gather_HostPools })
}
Else {
$Subscriptions.text = $AVDSubscriptions
$HostPool.text = “”
Gather_HostPools
}

$HostPool.Add_SelectedValueChanged({ HostPoolSelected })

$UserSession.Add_SelectedValueChanged({ $Shadow.enabled = $true })

$Shadow.Add_Click({ ShadowUserSession })


## Functions
Function Gather_HostPools {

Write-Host $(Get-Date -Format HH:mm:ss:) -ForegroundColor Gray -NoNewLine ; Write-Host ” Gathering a list of Host Pools in the Subscription ” -ForegroundColor DarkYellow -NoNewLine ; Write-Host $Subscriptions.Text -ForegroundColor DarkCyan

$HostPool.text = “Gathering a list of Host Pools in the Subscription – Please Wait”
$HostPool.Enabled = $false

Select-AzSubscription -Subscription $Subscriptions.Text -WarningAction SilentlyContinue | Out-Null

$SubscriptionInfo = Get-AzSubscription -SubscriptionName $Subscriptions.Text -WarningAction SilentlyContinue
$HostPools = Get-AzWvdHostPool -SubscriptionId $SubscriptionInfo.Id -WarningAction SilentlyContinue

$HostPool.Items.Clear()
Foreach ($HP in $HostPools) {
$HostPool.Items.Add($HP.Name) | Out-Null
}

$HostPool.text = “Please Select the Host Pool required”
$HostPool.Enabled = $true

}


Function HostPoolSelected {

Write-Host $(Get-Date -Format HH:mm:ss:) -ForegroundColor Gray -NoNewLine ; Write-Host ” Gathering a list of Users Connected to ” -ForegroundColor DarkYellow -NoNewLine ; Write-Host $HostPool.Text -ForegroundColor DarkCyan

$SubscriptionInfo = Get-AzSubscription -SubscriptionName $Subscriptions.Text -WarningAction SilentlyContinue
$HostPoolInfo = Get-AzWvdHostPool -SubscriptionId $SubscriptionInfo.Id | Where {$_.Name -eq $HostPool.Text}

$UserSession.Items.Clear()
$UserSessionHost.Items.Clear()
$UserSessionID.Items.Clear()

$ActiveUsers = Get-AzWvdUserSession -HostPoolName $HostPool.Text -ResourceGroupName $HostPoolInfo.id.Split(‘/’)[4] -ErrorAction SilentlyContinue -Filter “SessionState eq ‘active'”

If ($ActiveUsers.Count -eq 0) {
$UserSession.text = “No Active Users logged into Selected Host Pool”
$UserSession.Enabled = $false
}
Else {
$UserSession.BackColor = “white”
$UserSession.text = “Please Select the User to be Shadowed”
$UserSession.Enabled = $true
}

Foreach ($ActiveUser in $ActiveUsers) {
$UserSession.Items.Add($ActiveUser.ActiveDirectoryUserName)
$UserSessionHost.Items.Add($ActiveUser.Name.Split(‘/’)[1])
$UserSessionID.Items.Add($ActiveUser.Name.Split(‘/’)[2])
}

}


Function ShadowUserSession {

$UserUPN = $UserSession.Text
$AVDSessionHost = $UserSessionHost.Items.Item($UserSession.SelectedIndex)
$AVDSessionID = $UserSessionID.Items.Item($UserSession.SelectedIndex)

If ($TakeControl.Checked -eq $True) {
$AllowControl = “Yes”
}
Else {
$AllowControl = “No”
}

$ShadowUser.Dispose()

Write-Host ; Write-Host $(Get-Date -Format HH:mm:ss:) -ForegroundColor Gray -NoNewLine ; Write-Host ” Shadowing User ” -NoNewline
Write-Host $UserUPN -ForegroundColor Cyan -NoNewline ; Write-Host “, Connected to ” -NoNewline
Write-Host $AVDSessionHost -ForegroundColor Green -NoNewline ; Write-Host ” on Session ID ” -NoNewline
Write-Host $AVDSessionID -ForegroundColor Gray -NoNewline

If ($UseMSTSC -eq $true) {
If ($AllowControl -eq “Yes”) {
Write-Host ” with Remote Control ” -NoNewline ; Write-Host “Enabled” -ForegroundColor Green
Start-Process -FilePath “mstsc.exe” -ArgumentList “/v:$AVDSessionHost /shadow:$AVDSessionID /control”
}
Else {
Write-Host ” with Remote Control ” -NoNewline ; Write-Host “Disabled” -ForegroundColor Red
Start-Process -FilePath “mstsc.exe” -ArgumentList “/v:$AVDSessionHost /shadow:$AVDSessionID”
}
}
Else {
Start-Process -FilePath “msra.exe” -ArgumentList “/OfferRa $($AVDSessionHost) $($UserUPN):$($AVDSessionID)”
}
}

[void]$ShadowUser.ShowDialog()

Once you have entered the tenant ID and subscription ID in the script, you can run the PowerShell script to shadow the AVD Session.

Conclusion

The AVD Management platform by Wintellisys is based on Microsoft’s Azure Virtual Desktop service. The platform provides insights into the client’s VDI environment and an extended scope of user management to manage VDI from a single console. AVD Manager offers AVD Sessions Shadowing among several features to reduce the VDI infrastructure cost. WVD Manager leverages WVD PowerShell modules to collect information and perform management tasks. So, users need to install PowerShell modules on their computer.

About the Author Rajeev