Occasionally I forget to log off from a server or am disconnected through an error and I don't remember the name of the server. And my domain account starts getting periodically locked out, so I have to access logs on DC to find out which server(s) keep locking my account and log off from it/them. So I wanted to write to script in powershell that would log me off from all servers in a domain (with the exception of the server where I run the script on of course) without me needing to search which to log off from. This is what I have:
$ErrorActionPreference = "Silentlycontinue"
$Servers = (Get-ADComputer -Filter *).Name
$ScriptBlock = {
$Sessions = quser | ?{$_ -match $env:USERNAME}
if (($Sessions).Count -ge 1)
{
$SessionIDs = ($Sessions -split ' ')[2]
Write-Host "Found $(($SessionIDs).Count) user login(s) on $Server."
$SessionIDs | ForEach-Object
{
Write-Host "Logging off session [$($_)]..."
logoff $_
}
}
}
foreach ($Server in $Servers)
{
if ($Server -isnot $env:COMPUTERNAME)
{
Invoke-Command -ComputerName $Server -ScriptBlock {$ScriptBlock}
}
}
But when I launch the script, nothing happens. The script doesn't return any errors but doesn't log me off from any server, nor does it write any of the messages from Write-Host cmdlet, obviously. I noticed the $SessionIDs
variable definition only returns ID of the first session. Usually this shouldn't be a problem, since it's unlikely I will have more than one session on a server, but I'd like to have this insurance. Can anyone tell me what's wrong in the script?
CodePudding user response:
I notice a few things...
First, I don't think quser | Where-Object {$_ -match $env:USERNAME}
will ever return anything. The output of quser
will not contain the hostname.
Try this for getting logon sessions:
$Sessions = (query user /server:$Env:ComputerName) -split "\n" -replace '\s\s ', ';' |
ConvertFrom-Csv -Delimiter ';'
Next, when you reference the $Server
variable on the remote machine in your script block, it is out of scope. You would need to use $Using:Server
in the script block.
Lastly, the -isnot
operator doesn't compare value, it compares type. So in your last foreach, the if statement evaluates to "if type string is not type string" and will not run. Try -ne
or -notlike
instead.
CodePudding user response:
So I modified the script this way and it works, sort of. It logs off account from servers, which is the main goal. There are still some glitches, like the message it sends from the first Write-Host doesn't give server's name, the message from second one gives a different value than it should (it gives [1] value after -split instead of [2] for some reason; but those are not really that important things, even though I will try to make at least the first message right) and $SessionIDs
still gives only the first value, but usually you shouldn't have more than one RDP session per server. I've seen more sessions of one user, but that is very rare. But I'd also like to fix this if possible. Nevertheless, the script basically does the most important thing. But if someone has a suggestion how to fix the glitches I mentioned I would be grateful.
$ErrorActionPreference = "Silentlycontinue"
$Servers = (Get-ADComputer -Filter *).Name
$ScriptBlock = {
$Sessions = quser | ?{$_ -match $env:USERNAME}
if (($Sessions).Count -ge 1)
{
$SessionIDs = , ($Sessions -split ' ')[2]
Write-Host "Found $(($SessionIDs).Count) user login(s) on $Server."
Foreach ($SessionID in $SessionIDs)
{
Write-Host "Logging off session $SessionID..."
logoff $SessionID
}
}
}
foreach ($Server in $Servers)
{
if ($Server -ne $env:COMPUTERNAME)
{
Invoke-Command -ComputerName $Server -ScriptBlock $ScriptBlock
}
}
CodePudding user response:
Working with objects is much easier if you can just parse the output of QUser.exe
. Given your scenario, here's my take on it:
$servers = (Get-ADComputer -Filter '*').Name.Where{$_ -ne $env:COMPUTERNAME}
foreach ($server in $servers)
{
if (-not ($quser = ((QUser.exe /server:$server) -replace '\s{20,39}',',,' -replace '\s{2,}',',' 2>&1) | Where-Object -FilterScript { $_ -match $env:USERNAME })) {
Continue
}
Write-Verbose -Message "$($quser.Count) session(s) found on $server." -Verbose
($quser.Trim() | ConvertFrom-Csv -Header 'USERNAME','SESSIONNAME','ID','STATE','IDLE TIME','LOGON TIME').foreach{
Write-Verbose -Message "Logging user [$($_.UserName)] off." -Verbose
LogOff.exe $_.ID /server:$server
}
}
Filtering should always happen before hand meaning, filter out your computer name on your first call to Get-ADComputer
. Since you're using QUser.exe
and LogOff.exe
to begin with, I'd recommend the use of it all the way through since LogOff
accepts an ID
value that QUser
outputs.
Next, placing the call to quser
inside your if
statement does two things in this case.
- Filters for all users matching
$ENV:UserName
- Returns
$true
if anything is found, and$false
if not found.- So, switching the results using
-not
will turn$false
into$true
allowing the execution of the code block which will justcontinue
to the next server. - This in turn doesn't bother with the rest of the code and continues onto the next computer if no matching names were found.
- So, switching the results using
The use of $quser
inside the if
statement is so you can save the results to it if more than one name is found; (..)
allows this as it turns the variable assignment into an expression having the output pass through onto the pipeline where it is either empty, or not.
Finally, referencing the $quser
variable we can convert the strings into objects piping to ConvertFrom-Csv
. Only step left to do is iterate through each row and passing it over to LogOff
to perform the actual logoff.
If you've noticed, the headers are manually-specified because it is filtered out by the Where-Object
cmdlet. This is a better approach seeing as there could be "more than one" RDP Session, now you're just left with those sessions matching the name which can be saved to $quser
, so no extra filtering is needed down the line.