Created
January 15, 2026 15:19
-
-
Save WimObiwan/84ebf33e9f5ad90b89e31cf9ca3e9eea to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #Requires -Version 7.0 | |
| <# | |
| .SYNOPSIS | |
| Logs off idle or disconnected users from multiple servers. | |
| .DESCRIPTION | |
| Enumerates all specified servers and forces logoff of users that are idle | |
| or disconnected for longer than the specified timeout periods. | |
| Use 0 for timeout to log off all sessions of that type regardless of idle time. | |
| .PARAMETER ServerNames | |
| Array of server names to check for idle/disconnected sessions. | |
| .PARAMETER IdleTimeoutMinutes | |
| Maximum idle time in minutes before an active session is logged off. | |
| Use 0 to log off all active sessions regardless of idle time. | |
| .PARAMETER DisconnectTimeoutMinutes | |
| Maximum disconnect time in minutes before a disconnected session is logged off. | |
| Use 0 to log off all disconnected sessions regardless of idle time. | |
| .EXAMPLE | |
| .\Force-LogoffIdleSessions.ps1 -ServerNames @('SERVER165') -IdleTimeoutMinutes 60 -DisconnectTimeoutMinutes 30 | |
| .EXAMPLE | |
| .\Force-LogoffIdleSessions.ps1 -ServerNames @('SERVER165') -IdleTimeoutMinutes 0 -DisconnectTimeoutMinutes 30 | |
| Logs off all active sessions regardless of idle time, and disconnected sessions idle for 30+ minutes | |
| .EXAMPLE | |
| .\Force-LogoffIdleSessions.ps1 -ServerNames @('SERVER165') -IdleTimeoutMinutes 60 -DisconnectTimeoutMinutes 30 -Confirm | |
| .EXAMPLE | |
| .\Force-LogoffIdleSessions.ps1 -ServerNames @('SERVER165') -IdleTimeoutMinutes 60 -DisconnectTimeoutMinutes 30 -WhatIf | |
| #> | |
| [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] | |
| param( | |
| [Parameter(Mandatory = $true)] | |
| [string[]]$ServerNames, | |
| [Parameter(Mandatory = $true)] | |
| [ValidateRange(0, [int]::MaxValue)] | |
| [int]$IdleTimeoutMinutes, | |
| [Parameter(Mandatory = $true)] | |
| [ValidateRange(0, [int]::MaxValue)] | |
| [int]$DisconnectTimeoutMinutes | |
| ) | |
| function Parse-QUserOutput { | |
| param( | |
| [string[]]$QUserOutput | |
| ) | |
| $sessions = @() | |
| # Skip the header line | |
| foreach ($line in $QUserOutput | Select-Object -Skip 1) { | |
| if ([string]::IsNullOrWhiteSpace($line)) { continue } | |
| # Remove leading/trailing whitespace and normalize spaces | |
| $line = $line.Trim() -replace '\s+', ' ' | |
| # Split the line | |
| $parts = $line -split ' ' | |
| if ($parts.Count -lt 4) { continue } | |
| # Determine if there's a session name by checking if second field is numeric (ID) or text (session name) | |
| $username = $parts[0] | |
| if ($parts[1] -match '^\d+$') { | |
| # No session name (disconnected) - format: USERNAME ID STATE IDLE_TIME LOGON_TIME | |
| $sessionName = $null | |
| $id = [int]$parts[1] | |
| $state = $parts[2] | |
| $idleTime = $parts[3] | |
| } else { | |
| # Has session name - format: USERNAME SESSIONNAME ID STATE IDLE_TIME LOGON_TIME | |
| $sessionName = $parts[1] | |
| $id = [int]$parts[2] | |
| $state = $parts[3] | |
| $idleTime = if ($parts.Count -gt 4) { $parts[4] } else { '.' } | |
| } | |
| $sessions += [PSCustomObject]@{ | |
| Username = $username | |
| SessionName = $sessionName | |
| Id = $id | |
| State = $state | |
| IdleTime = $idleTime | |
| } | |
| } | |
| return $sessions | |
| } | |
| function Convert-IdleTimeToMinutes { | |
| param( | |
| [string]$IdleTime | |
| ) | |
| if ($IdleTime -eq '.' -or $IdleTime -eq 'none' -or [string]::IsNullOrWhiteSpace($IdleTime)) { | |
| return 0 | |
| } | |
| # Handle formats like "2:30", "52", "17" | |
| if ($IdleTime -match '^(\d+):(\d+)$') { | |
| $hours = [int]$matches[1] | |
| $minutes = [int]$matches[2] | |
| return ($hours * 60) + $minutes | |
| } elseif ($IdleTime -match '^\d+$') { | |
| return [int]$IdleTime | |
| } elseif ($IdleTime -match '^(\d+)\+(\d+):(\d+)$') { | |
| # Handle format like "1+5:30" (days+hours:minutes) | |
| $days = [int]$matches[1] | |
| $hours = [int]$matches[2] | |
| $minutes = [int]$matches[3] | |
| return ($days * 1440) + ($hours * 60) + $minutes | |
| } | |
| return 0 | |
| } | |
| # Main script logic | |
| Write-Host "Starting session cleanup..." -ForegroundColor Cyan | |
| Write-Host "Idle timeout: $(if ($IdleTimeoutMinutes -eq 0) { 'ALL active sessions' } else { "$IdleTimeoutMinutes minutes" })" -ForegroundColor Yellow | |
| Write-Host "Disconnect timeout: $(if ($DisconnectTimeoutMinutes -eq 0) { 'ALL disconnected sessions' } else { "$DisconnectTimeoutMinutes minutes" })" -ForegroundColor Yellow | |
| Write-Host "" | |
| $totalLoggedOff = 0 | |
| foreach ($server in $ServerNames) { | |
| Write-Host "Checking server: $server" -ForegroundColor Green | |
| try { | |
| # Get user sessions from remote server | |
| $qUserOutput = Invoke-Command -ComputerName $server -ScriptBlock { | |
| quser 2>&1 | |
| } -ErrorAction Stop | |
| if ($qUserOutput -match "No User exists") { | |
| Write-Host " No active sessions found." -ForegroundColor Gray | |
| continue | |
| } | |
| # Parse the output | |
| $sessions = Parse-QUserOutput -QUserOutput $qUserOutput | |
| if ($sessions.Count -eq 0) { | |
| Write-Host " No sessions parsed." -ForegroundColor Gray | |
| continue | |
| } | |
| foreach ($session in $sessions) { | |
| $shouldLogoff = $false | |
| $reason = "" | |
| $idleMinutes = Convert-IdleTimeToMinutes -IdleTime $session.IdleTime | |
| if ($session.State -eq 'Disc') { | |
| # Disconnected session | |
| if ($DisconnectTimeoutMinutes -eq 0) { | |
| $shouldLogoff = $true | |
| $reason = "Disconnected (logging off all disconnected sessions)" | |
| } elseif ($idleMinutes -ge $DisconnectTimeoutMinutes) { | |
| $shouldLogoff = $true | |
| $reason = "Disconnected for $idleMinutes minutes (threshold: $DisconnectTimeoutMinutes)" | |
| } | |
| } elseif ($session.State -eq 'Active') { | |
| # Active session | |
| if ($IdleTimeoutMinutes -eq 0) { | |
| $shouldLogoff = $true | |
| $reason = "Active session (logging off all active sessions)" | |
| } elseif ($idleMinutes -ge $IdleTimeoutMinutes) { | |
| $shouldLogoff = $true | |
| $reason = "Idle for $idleMinutes minutes (threshold: $IdleTimeoutMinutes)" | |
| } | |
| } | |
| if ($shouldLogoff) { | |
| $message = "$server - $($session.Username) (ID: $($session.Id), State: $($session.State), Idle: $idleMinutes min)" | |
| if ($PSCmdlet.ShouldProcess($message, "Logoff")) { | |
| try { | |
| Invoke-Command -ComputerName $server -ScriptBlock { | |
| param($SessionId) | |
| logoff $SessionId | |
| } -ArgumentList $session.Id -ErrorAction Stop | |
| Write-Host " ✓ Logged off: $($session.Username) (ID: $($session.Id)) - $reason" -ForegroundColor Green | |
| $totalLoggedOff++ | |
| } catch { | |
| Write-Host " ✗ Failed to logoff $($session.Username): $($_.Exception.Message)" -ForegroundColor Red | |
| } | |
| } | |
| } else { | |
| Write-Host " Skipping: $($session.Username) (ID: $($session.Id), State: $($session.State), Idle: $idleMinutes min)" -ForegroundColor Gray | |
| } | |
| } | |
| } catch { | |
| Write-Host " Error querying server: $($_.Exception.Message)" -ForegroundColor Red | |
| } | |
| Write-Host "" | |
| } | |
| Write-Host "Cleanup complete. Total sessions logged off: $totalLoggedOff" -ForegroundColor Cyan |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment