Logoff RDP sessions on multiple servers

Standard

Last year I got a request from a colleague to make a script which can logoff all users (remote desktop sessions) from all windows servers in our environment. I wanted to help and came up with the script in this post. Over time I updated the script a little so it now runs the logoff tasks in different threads to save time (is important if you have hundreds of servers). It also checks port availability so it doesn’t stop or timeout on unavailable servers.

How does the script work?

Basically the script works like this:
1. The script loops through the list of servers in “servers.txt” in the script directory. You must create and populate this file with the names of the servers on which to logoff users before running the script.
2. The script checks if the remote server is available by testing that the ports 135 and 445 are open. If not the server is skipped.
3. A pre-logoff message is sent to the server warning the users that they will be logged off soon. The amount of time to wait before logging off the user sessions can be defined in the $timeleft object in the #variables section of the script.
4. After waiting for the amount of time set in $timeleft the rdp sessions are logged off on the remote server(s).

In case of any problems the script has a lot of debug info that can be viewed/not viewed according to how you set the $debugpreference in the #variables section.

How I wrote the script

I have structured the script in three different parts functions, variables and script main. I will go over and explain these parts now.

Functions

The script uses 4 functions which provide the main functionality.
1. Test-PortAlive

function Test-PortAlive {
	#############################################################################################
	##Function:			Test-PortAlive
	##
	##Description:		Tests connection on a given server on a given port.
	##
	##Created by:		Noam Wajnman
	##Creation Date:	April 02, 2014	
	##############################################################################################
	[CmdletBinding()]
	[OutputType([System.boolean])]
	param(
		[Parameter(ValueFromPipeline=$true)][System.String[]]$server,
		[int]$port
	)
	$socket = new-object Net.Sockets.TcpClient
	$connect = $socket.BeginConnect($server, $port, $null, $null)
	$NoTimeOut = $connect.AsyncWaitHandle.WaitOne(500, $false)
	if ($NoTimeOut) {
		$socket.EndConnect($connect) | Out-Null
		return $true				
	}
	else {
		return $false
	}
}

I use this function to check if ports 135 and 445 are open on a server before anything else. If they aren’t open the server will be skipped. I won’t go into details about this function as I have covered it in another separate post on this blog.
2. Send-PreLogoffMessage

function Send-PreLogoffMessage {
	#############################################################################################
	##Function Send-PreLogoffMessage
	##
	##Description:		Pipeline function. Sends a pre-logoff warning to an array of servers given 
	#+					as input. 
	##Created by:		Noam Wajnman
	##Creation Date:	February 2, 2013
	##Updated:			April 06, 2014
	#############################################################################################
	[CmdletBinding()]
	[OutputType([System.String])]
	param(
		[Parameter(ValueFromPipeline=$true)][ValidateNotNullOrEmpty()][System.String[]]$servers
	)
	begin {		
		[int]$timeleftSeconds = 60 * $timeleft
		Write-Debug "Beginning send of warning messages to servers."
	}
	process {
		foreach ($server in $servers) {
			Write-Debug "testing connectivity to $servers on port 135 and 445"
			$alive_rpc = Test-PortAlive -server $servers -port 135
			$alive_smb = Test-PortAlive -server $servers -port 445
			if ($alive_rpc -and $alive_smb) {
				Write-Debug "Connection to $server on ports 135 and 445 was successful"
				Write-Debug "Sending warning message to $server"
				msg * /SERVER:$server "Your remote session will be logged off automatically in $timeleft minutes, you should save your work."
			}
			else {
				Write-Debug "Error - Cannot connect to $server on ports 135 and 445"
				Write-Debug "Skipping $server"
			}
		}
	}
	end {
		Write-Debug "Warning messages sent to all servers. Sleeping for $timeleftSeconds..."
		sleep $timeleftSeconds		
	}
}

I use this function to send a little pop-up box to any server where users will be logged off. This is useful because it gives users a chance to save their work and stop before they are suddenly kicked from the server.
the function first uses the previously mentioned function “test-portalive” and tests if ports 135 and 445 are available. If they are open then a pop-up message is sent to the server using the standard msg.exe windows executable.
3. Get-RDPSessions

function Get-RDPSessions {
	#############################################################################################
	##Name: 			Get-RDPSession
	##Description:		Pipeline function. Retrieves all user sessions from local or remote 
	#+					server/s. Requires query.exe in order to run properly. Takes an array of 
	#+					servers as a parameter.
	##Created by:		Noam Wajnman
	##Link:				Based on code found here - http://poshcode.org/2342
	##Creation Date:	February 2, 2013
	##Updated:			April 06, 2014
	#############################################################################################
	[CmdletBinding()]
	[OutputType([System.String])]
	param(
		[Parameter(ValueFromPipeline=$true)][ValidateNotNullOrEmpty()][System.String[]]$servers
	)
	process {
		Write-Debug "testing connectivity to $servers on port 135 and 445"
		$alive_rpc = Test-PortAlive -server $servers -port 135
		$alive_smb = Test-PortAlive -server $servers -port 445		
		if ($alive_rpc -and $alive_smb) {
			Write-Debug "Connection to $server on ports 135 and 445 was successful"
			# Parse 'query session' and store in $sessions:
			Write-Debug "Getting RDP session info from $servers"
		    $sessions = query session /server:$servers
		    1..($sessions.count -1) | % {
		        $session = "" | Select Computer,SessionName, Username, Id, State, Type, Device
		        $session.Computer = $servers
		        $session.SessionName = $sessions[$_].Substring(1,18).Trim()
		        $session.Username = $sessions[$_].Substring(19,20).Trim()
		        $session.Id = $sessions[$_].Substring(39,9).Trim()
		        $session.State = $sessions[$_].Substring(48,8).Trim()
		        $session.Type = $sessions[$_].Substring(56,12).Trim()
		        $session.Device = $sessions[$_].Substring(68).Trim()
				return $session
		    }
		}		
		else {
			Write-Debug "Error - Cannot connect to $server on ports 135 and 445"
			Write-Debug "Skipping $server"
		}
	}	
}

This functions uses query.exe (comes with windows) to get all the active rdp (remote desktop) sessions on a server. First the function checks that ports 135 and 445 are open (using the previously mentioned test-portalive function) and then gets the session info which is returned. If the ports are closed the server is skipped.
Some of the code used in this function was taken from here http://poshcode.org/2342 and the author deserves credit for doing a great job of parsing the session info.
4. Logoff-RDPSessions

function Logoff-RDPSessions {
	#############################################################################################
	##Function Logoff-RDPSessions
	##
	##Description:		Pipeline function. Logs off the rdp sessions given as the parameter. Use 
	#+					the "asjob" parameter to specify whether to run the logoffs sequentially 
	#+					or in parallel. asjob = $true will start the logoffs in parallel. If the 
	#+					asjob parameter is not given the script will default to an asjob value of 
	#+					$false.
	##Created by:		Noam Wajnman
	##Creation Date:	February 2, 2013
	##Updated:			March 23, 2014
	#############################################################################################
	[CmdletBinding()]
	[OutputType([System.String])]
	param(
		[Parameter(ValueFromPipeline=$true)][ValidateNotNullOrEmpty()][System.String[]]$Sessions,		
		$asjob = $false
	)
	begin {		
		Write-Debug "Beginning logoff of RDP sessions"
		Get-Job | Stop-Job | Remove-Job		
		$script:SB = {
			param (
				$sessionID,
				$server
			)				
			logoff $sessionID /server:$server				
		}
	}
	process {
		$server = $_.computer		
		$sessionID = $_.Id			
		$SessionUserName = $_.UserName		
		if ($SessionUserName -and $sessionID -ne 65536 -and $sessionID -ne $null) {	
			if ($asjob) {
				Write-Debug "Logging off session ID $sessionID for user $SessionUserName on $server..."
				Write-Debug "Starting new job"				
				Start-Job -scriptBlock $SB -ArgumentList $sessionID,$server -Name "LogoffRDPSession"
			}
			else {
				Write-Debug "Logging off session ID $sessionID for user $SessionUserName on $server..."
				Invoke-Command -scriptBlock $SB -ArgumentList $sessionID,$server
			}			
		}		
	}	
}

This function is where the magic happens 🙂 The session info returned by the last function is given as an input parameter and users are logged off using “logoff.exe” (comes with windows). If you provide the “asjob” parameter when running the function it will start each logoff task as a separate process which will save a lot of time because all servers will log of their users in parallel. If “asjob” isn’t given then the logoff tasks will run sequentially which may also be useful on occasion.

Variables

#VARIABLES
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
$servers = gc "$dir\servers.txt"
#parameters
[int]$timeleft = 5 #Countdown time until Logoff (Mins)
#$DebugPreference = "continue" #uncomment to get debug-info

$dir is an object which always points to the directory in which the script is run. It allows me to easily create paths to files used in/by scripts without having to modify them whenever I move/copy the script to a new location.
$servers is an array of server names which the script loops through to logoff remote desktop sessions. You must create the file “servers.txt” and populate it with the servers on which you want to logoff RDP sessions (one server name per line). Place it in the same directory as the script before running.
$timeleft is the amount of time to wait from starting the script to when the servers will be logged off. Before the RDP sessions are logged off the users will be warned/notified that they will be kicked in this amount of time. Be sure to enter a value fitting your needs before running the script.
$debugpreference can be uncommented (remove the “#” in the beginning of the line) to get the debug info. This is useful if there is trouble with some servers or if you simply want to know what the script is doing at each stage.

Script Main

#SCRIPT MAIN
clear
$servers | Send-PreLogoffMessage
$servers | Get-RDPSessions | Logoff-RDPSessions -asjob $true

I created the functions used in the script to be used from the pipeline. This makes the script main a no-brainer which consists of a few lines.
First I pipe the $servers array into the Send-PreLogoffMessage which sends the warning to the users logged to to the servers. Then I again pipe the $servers array into the Get-RDPSessions and again pipe the retrieved sessions into Logoff-RDPSessions which logs off the remote desktop sessions. I use the “-asjob” parameter so that the logoff tasks will run in parallel and save time but you can omit it if you want to logoff users on servers sequentially.

How do I run the script?

1. Copy the full script and save it as Logoff-RDPSessions.ps1.
2. Create the file servers.txt and place it in the same dir as the script. Populate this file with the names of the servers on which you want to logoff all rdp sessions.
3. Open Logoff-RDPSessions.ps1 in an editor (powerGUI or similar) or simply using notepad. Go to the #parameters section and edit the variables to the values you want and save the file.
4. Run the script from the editor or open a powershell prompt and run it.

That’s it! I hope you find the script useful.

Full script here.

################################################################################################
##Script:			Logoff-RDPSessions.ps1
##
##Description:		Logs off all RDP sessions on servers specified in the input list servers.txt
#+					(Use with caution)
##Created by:		Noam Wajnman
##Creation Date:	February 2, 2013
##Updated:			April 06, 2014
################################################################################################
#FUNCTIONS
function Test-PortAlive {
	#############################################################################################
	##Function:			Test-PortAlive
	##
	##Description:		Tests connection on a given server on a given port.
	##
	##Created by:		Noam Wajnman
	##Creation Date:	April 02, 2014	
	##############################################################################################
	[CmdletBinding()]
	[OutputType([System.boolean])]
	param(
		[Parameter(ValueFromPipeline=$true)][System.String[]]$server,
		[int]$port
	)
	$socket = new-object Net.Sockets.TcpClient
	$connect = $socket.BeginConnect($server, $port, $null, $null)
	$NoTimeOut = $connect.AsyncWaitHandle.WaitOne(500, $false)
	if ($NoTimeOut) {
		$socket.EndConnect($connect) | Out-Null
		return $true				
	}
	else {
		return $false
	}
}
function Send-PreLogoffMessage {
	#############################################################################################
	##Function Send-PreLogoffMessage
	##
	##Description:		Pipeline function. Sends a pre-logoff warning to an array of servers given 
	#+					as input. 
	##Created by:		Noam Wajnman
	##Creation Date:	February 2, 2013
	##Updated:			April 06, 2014
	#############################################################################################
	[CmdletBinding()]
	[OutputType([System.String])]
	param(
		[Parameter(ValueFromPipeline=$true)][ValidateNotNullOrEmpty()][System.String[]]$servers
	)
	begin {		
		[int]$timeleftSeconds = 60 * $timeleft
		Write-Debug "Beginning send of warning messages to servers."
	}
	process {
		foreach ($server in $servers) {
			Write-Debug "testing connectivity to $servers on port 135 and 445"
			$alive_rpc = Test-PortAlive -server $servers -port 135
			$alive_smb = Test-PortAlive -server $servers -port 445
			if ($alive_rpc -and $alive_smb) {
				Write-Debug "Connection to $server on ports 135 and 445 was successful"
				Write-Debug "Sending warning message to $server"
				msg * /SERVER:$server "Your remote session will be logged off automatically in $timeleft minutes, you should save your work."
			}
			else {
				Write-Debug "Error - Cannot connect to $server on ports 135 and 445"
				Write-Debug "Skipping $server"
			}
		}
	}
	end {
		Write-Debug "Warning messages sent to all servers. Sleeping for $timeleftSeconds..."
		sleep $timeleftSeconds		
	}
}
function Get-RDPSessions {
	#############################################################################################
	##Name: 			Get-RDPSession
	##Description:		Pipeline function. Retrieves all user sessions from local or remote 
	#+					server/s. Requires query.exe in order to run properly. Takes an array of 
	#+					servers as a parameter.
	##Created by:		Noam Wajnman
	##Link:				Based on code found here - http://poshcode.org/2342
	##Creation Date:	February 2, 2013
	##Updated:			April 06, 2014
	#############################################################################################
	[CmdletBinding()]
	[OutputType([System.String])]
	param(
		[Parameter(ValueFromPipeline=$true)][ValidateNotNullOrEmpty()][System.String[]]$servers
	)
	process {
		Write-Debug "testing connectivity to $servers on port 135 and 445"
		$alive_rpc = Test-PortAlive -server $servers -port 135
		$alive_smb = Test-PortAlive -server $servers -port 445		
		if ($alive_rpc -and $alive_smb) {
			Write-Debug "Connection to $server on ports 135 and 445 was successful"
			# Parse 'query session' and store in $sessions:
			Write-Debug "Getting RDP session info from $servers"
		    $sessions = query session /server:$servers
		    1..($sessions.count -1) | % {
		        $session = "" | Select Computer,SessionName, Username, Id, State, Type, Device
		        $session.Computer = $servers
		        $session.SessionName = $sessions[$_].Substring(1,18).Trim()
		        $session.Username = $sessions[$_].Substring(19,20).Trim()
		        $session.Id = $sessions[$_].Substring(39,9).Trim()
		        $session.State = $sessions[$_].Substring(48,8).Trim()
		        $session.Type = $sessions[$_].Substring(56,12).Trim()
		        $session.Device = $sessions[$_].Substring(68).Trim()
				return $session
		    }
		}		
		else {
			Write-Debug "Error - Cannot connect to $server on ports 135 and 445"
			Write-Debug "Skipping $server"
		}
	}	
}
function Logoff-RDPSessions {
	#############################################################################################
	##Function Logoff-RDPSessions
	##
	##Description:		Pipeline function. Logs off the rdp sessions given as the parameter. Use 
	#+					the "asjob" parameter to specify whether to run the logoffs sequentially 
	#+					or in parallel. asjob = $true will start the logoffs in parallel. If the 
	#+					asjob parameter is not given the script will default to an asjob value of 
	#+					$false.
	##Created by:		Noam Wajnman
	##Creation Date:	February 2, 2013
	##Updated:			March 23, 2014
	#############################################################################################
	[CmdletBinding()]
	[OutputType([System.String])]
	param(
		[Parameter(ValueFromPipeline=$true)][ValidateNotNullOrEmpty()][System.String[]]$Sessions,		
		$asjob = $false
	)
	begin {		
		Write-Debug "Beginning logoff of RDP sessions"
		Get-Job | Stop-Job | Remove-Job		
		$script:SB = {
			param (
				$sessionID,
				$server
			)				
			logoff $sessionID /server:$server				
		}
	}
	process {
		$server = $_.computer		
		$sessionID = $_.Id			
		$SessionUserName = $_.UserName		
		if ($SessionUserName -and $sessionID -ne 65536 -and $sessionID -ne $null) {	
			if ($asjob) {
				Write-Debug "Logging off session ID $sessionID for user $SessionUserName on $server..."
				Write-Debug "Starting new job"				
				Start-Job -scriptBlock $SB -ArgumentList $sessionID,$server -Name "LogoffRDPSession"
			}
			else {
				Write-Debug "Logging off session ID $sessionID for user $SessionUserName on $server..."
				Invoke-Command -scriptBlock $SB -ArgumentList $sessionID,$server
			}			
		}		
	}	
}
#VARIABLES
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
$servers = gc "$dir\servers.txt"
#parameters
[int]$timeleft = 5 #Countdown time until Logoff (Mins)
$DebugPreference = "continue" #uncomment to get debug-info
#SCRIPT MAIN
clear
$servers | Send-PreLogoffMessage
$servers | Get-RDPSessions | Logoff-RDPSessions -asjob $true
Advertisement