Frequently Asked Question

Monitor Windows 2025 RDSH for Left Sessions (As part of a backup strategy)
Last Updated 3 days ago

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. 

This website relies on temporary cookies to function, but no personal data is ever stored in the cookies.
OK
Powered by GEN UK CLEAN GREEN ENERGY

Loading ...