Skip to content

Instantly share code, notes, and snippets.

@WimObiwan
Created January 15, 2026 15:19
Show Gist options
  • Select an option

  • Save WimObiwan/84ebf33e9f5ad90b89e31cf9ca3e9eea to your computer and use it in GitHub Desktop.

Select an option

Save WimObiwan/84ebf33e9f5ad90b89e31cf9ca3e9eea to your computer and use it in GitHub Desktop.
#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