Frequently Asked Question
One of the many issues with WIndows RSDH is that users are lazy, leave sessions logged in, leave applications open, and that prevents proper shutdown of the server because sessions are stuck with 'save your work?' etc.
This can prevent stable backups or interfere with snapshots, so many IT managers want to report on WHO is leaving sessions open at the end of the day so they can tackle it with training.
The following script will do that just, report on who is currently connected and what is left open. It stores this as a text report that will be saved in a specific folder, from which it can be emailed (genmail) or otherwise.
#
# ░░░░░░░░ ░░░░░░░ ░░░░░░░░░
# ▒▒ ▒▒ ▒▒
# ▒▒ ▒▒ ▒▒▒▒▒▒▒ ▒▒ ▒▒
# ▓▓ ▓▓ ▓▓ ▓▓ ▓▓
# ████████ ███████ ██ ██
#
# GEN Software Systems Development Team
#├─────────────────────────────────────────────
#│ Copyright (c) 2026 Global Enterprise Networks
#│ Website: www.gensoftware.dev / www.gen.uk
#│ License: https://www.gen.uk/?page=license
#│ Author: Richard Taylor (109)
#│ Project Name: RDSH-Sessions
#│ Languages: Powershell
#├─────────────────────────────────────────────
#│ Revision History
#│ 1.0.1 2026-02-10 Fix issue with null returns on sessions
#├─────────────────────────────────────────────
#
#.SYNOPSIS
# Daily RDSH Session & Application Report
#.DESCRIPTION
# Lists all active/disconnected RDS sessions and non-system user processes,
# then writes the output to a timestamped text file.
#>
# --- Configuration ---
$OutputDir = "C:\GEN\RDSH_Reports"
$Timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm"
$OutputFile = "$OutputDir\RDSH_Report_$Timestamp.txt"
# System processes to exclude (standard Windows background tasks)
$ExcludedProcesses = @(
'explorer', 'csrss', 'wininit', 'winlogon', 'services', 'lsass',
'svchost', 'dwm', 'taskhostw', 'sihost', 'ctfmon', 'fontdrvhost',
'RuntimeBroker', 'ShellExperienceHost', 'SearchIndexer', 'SearchHost',
'StartMenuExperienceHost', 'TextInputHost', 'conhost', 'dllhost',
'spoolsv', 'msdtc', 'WmiPrvSE', 'audiodg', 'rdpclip', 'rdpinput',
'TabTip', 'userinit', 'LogonUI', 'smss', 'Registry', 'System',
'Idle', 'SecurityHealthSystray', 'SecurityHealthService', 'SgrmBroker',
'sppsvc', 'TiWorker', 'TrustedInstaller', 'MsMpEng', 'NisSrv',
'uhssvc', 'wlms', 'AgentService', 'RDSTaskManager'
)
# --- Ensure output directory exists ---
if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
}
function Write-Section ($Title) {
$line = "=" * 70
return "`n$line`n $Title`n$line"
}
$Report = [System.Text.StringBuilder]::new()
# ============================================================
# HEADER
# ============================================================
[void]$Report.AppendLine("RDSH Daily Session & Application Report")
[void]$Report.AppendLine("Generated : $(Get-Date -Format 'dddd, dd MMMM yyyy HH:mm:ss')")
[void]$Report.AppendLine("Server : $($env:COMPUTERNAME)")
[void]$Report.AppendLine("=" * 70)
# ============================================================
# SECTION 1 - ACTIVE RDS SESSIONS
# ============================================================
[void]$Report.AppendLine((Write-Section "ACTIVE / DISCONNECTED RDS SESSIONS"))
try {
$RawSessions = & qwinsta 2>&1
foreach ($line in $RawSessions) {
[void]$Report.AppendLine($line)
}
} catch {
[void]$Report.AppendLine(" [ERROR] $_")
}
# ============================================================
# SECTION 2 - USER PROCESSES PER SESSION
# ============================================================
[void]$Report.AppendLine((Write-Section "USER-LAUNCHED APPLICATIONS (Non-System Processes)"))
$AllProcesses = $null
try {
# Filter out NT AUTHORITY, WINDOW MANAGER, and all NT SERVICE accounts
$AllProcesses = Get-Process -IncludeUserName -ErrorAction SilentlyContinue |
Where-Object {
$_.UserName -ne $null -and
$_.UserName -notmatch '^NT AUTHORITY\\' -and
$_.UserName -notmatch '^WINDOW MANAGER\\' -and
$_.UserName -notmatch '^NT SERVICE\\' -and
$ExcludedProcesses -notcontains $_.ProcessName
} |
Sort-Object UserName, ProcessName
if ($AllProcesses) {
$CurrentUser = ""
foreach ($proc in $AllProcesses) {
if ($proc.UserName -ne $CurrentUser) {
$CurrentUser = $proc.UserName
[void]$Report.AppendLine("")
[void]$Report.AppendLine(" User: $CurrentUser")
[void]$Report.AppendLine(" " + ("-" * 60))
[void]$Report.AppendLine(" " + ("Process Name").PadRight(35) + ("PID").PadRight(8) + "Window Title")
[void]$Report.AppendLine(" " + ("-----------").PadRight(35) + ("---").PadRight(8) + "------------")
}
$rawTitle = $proc.MainWindowTitle
$title = if ($rawTitle -and $rawTitle.Trim() -ne "") { $rawTitle.Trim() } else { "(no window)" }
$line = " " + $proc.ProcessName.PadRight(35) + ($proc.Id.ToString()).PadRight(8) + $title
[void]$Report.AppendLine($line)
}
} else {
[void]$Report.AppendLine(" No user-launched processes found.")
}
} catch {
[void]$Report.AppendLine(" [ERROR] $_")
}
# ============================================================
# SECTION 3 - SUMMARY
# ============================================================
[void]$Report.AppendLine((Write-Section "SUMMARY"))
$SessionLines = & qwinsta 2>$null | Where-Object { $_ -match '\s+(Active|Disc)\s*$' }
$SessionCount = ($SessionLines | Measure-Object).Count
$ProcessCount = if ($AllProcesses) { ($AllProcesses | Measure-Object).Count } else { 0 }
[void]$Report.AppendLine(" Active/Disconnected Sessions : $SessionCount")
[void]$Report.AppendLine(" User Processes Found : $ProcessCount")
[void]$Report.AppendLine(" Report saved to : $OutputFile")
[void]$Report.AppendLine("`n" + "=" * 70)
# ============================================================
# WRITE TO FILE & CONSOLE
# ============================================================
$ReportText = $Report.ToString()
$ReportText | Out-File -FilePath $OutputFile -Encoding UTF8
Write-Host $ReportText
Write-Host "`n[OK] Report written to: $OutputFile" -ForegroundColor Green Change $OutputDir to be your chosen directory, then set this up as a scheduled task by running the following as an elevated powershell
$TaskName = "GEN - RDSH Check Sessions" $Script = "C:\GEN\GEN_Check_Sessions.ps1" $Action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$Script`"" $Trigger = New-ScheduledTaskTrigger -Daily -At 22:00 $Principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest $Settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $Trigger -Principal $Principal -Settings $Settings
Be sure to change the $Script to point to your actual script file, and change $Trigger for whatever schedule you require.
For more support or information, contact GEN.
