Logoff Disconnected Sessions

As a CU Admin I want to be able to set up a trigger for disconnected sessions, and run an action to clean up any / all disconnected sessions for that user on all managed machines.
The same script should work equally-well as a right-click action from a session view..
Version 3.1.12
Created on 2023-05-18
Modified on 2023-06-22
Created by Bill Powell
Downloads: 154

The Script Copy Script Copied to clipboard
<#
  .SYNOPSIS
  This script logs off all disconnected sessions for a user
  .DESCRIPTION
  The script logs off all disconnected sessions for a user. The script uses the CU Actions in version 8.8 to action all sessions, regardless of connection type
  .PARAMETER UserAccount
  The hostname to perform the action on, supplied via a trigger or right-click action against a session
  .NOTES
   Version:        0.1
   Context:        Computer, executes on Monitor
   Author:         Bill Powell
   Requires:       Realtime DX 8.8
   Creation Date:  2023-05-23

  .LINK
   https://support.controlup.com/docs/powershell-cmdlets-for-solve-actions
#>
[CmdletBinding()]
param (
    [Parameter(Mandatory = $true, HelpMessage = 'user account of machine to be actioned')]
    [ValidateNotNullOrEmpty()]
    [string]$UserAccount
)

#region ControlUpScriptingStandards
$VerbosePreference = $(if( $PSBoundParameters[ 'verbose' ] ) { $VerbosePreference } else { 'SilentlyContinue' })
$DebugPreference = $(if( $PSBoundParameters[ 'debug' ] ) { $DebugPreference } else { 'SilentlyContinue' })
$ErrorActionPreference = $(if( $PSBoundParameters[ 'erroraction' ] ) { $ErrorActionPreference } else { 'Stop' })
$ProgressPreference = 'SilentlyContinue'

[int]$outputWidth = 400
if( ( $PSWindow = (Get-Host).UI.RawUI ) -and ( $WideDimensions = $PSWindow.BufferSize ) )
{
    $WideDimensions.Width = $outputWidth
    $PSWindow.BufferSize = $WideDimensions
}
#endregion ControlUpScriptingStandards

#region Define the CUAction

$RequiredAction = "LogOff Session"
$RequiredActionCategory = 'Remote Desktop Services'

#endregion

#region Process Args

#$UserAccount = $args[0] # e.g. 'CUEMEA\billp'

#endregion

#region Load the version of the module to match the running monitor and check that it has the new features

function Get-MonitorDLLs {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)][string[]]$DLLList
    )
    [int]$DLLsFound = 0
    Get-CimInstance -Query "SELECT * from win32_Service WHERE Name = 'cuMonitor' AND State = 'Running'" | ForEach-Object {
        $MonitorService = $_
        if ($MonitorService.PathName -match '^(?<op>"{0,1})\b(?<text>[^"]*)\1$') {
            $Path = $Matches.text
            $MonitorFolder = Split-Path -Path $Path -Parent
            $DLLList | ForEach-Object {
                $DLLBase = $_
                $DllPath = Join-Path -Path $MonitorFolder -ChildPath $DLLBase
                if (Test-Path -LiteralPath $DllPath) {
                    $DllPath
                    $DLLsFound++
                }
                else {
                    throw "DLL $DllPath not found in running monitor folder"
                }
            }
        }
    }
    if ($DLLsFound -ne $DLLList.Count) {
        throw "cuMonitor is not installed or not running"
    }
}

$AcceptableModules = New-Object System.Collections.Generic.List[object]
try {
    $DllsToLoad = Get-MonitorDLLs -DLLList @('ControlUp.PowerShell.User.dll')
    $DllsToLoad | Import-Module 
    $DllsToLoad -replace "^.*\\",'' -replace "\.dll$",'' | ForEach-Object {$AcceptableModules.Add($_)}
}
catch {
    $exception = $_
    Write-Error "Required DLLs not loaded: $($exception.Exception.Message)"
}

if (-not ((Get-Command -Name 'Invoke-CUAction' -ErrorAction SilentlyContinue).Source) -in $AcceptableModules) {
   Write-Error "ControlUp version 8.8 commands are not available on this system"
   exit 0
}

#endregion

#region Perform CUAction on target

$ThisComputer = Get-CimInstance -ClassName Win32_ComputerSystem

Write-Output "Action $RequiredAction applied to $UserAccount from $($ThisComputer.Name)"

$Action = (Get-CUAvailableActions -DisplayName $RequiredAction | Where-Object {($_.Title -eq $RequiredAction) -and ($_.IsSBA -eq $false) -and ($_.Category -eq $RequiredActionCategory)})[0]

Write-Output "Action title '$($Action.Title)', ID $($Action.Id)" 

#
# in theory, a user could have 100s of disconnected session, so the code fetches chunks
# (this makes the script easy to adapt to just clear down *all* disconnected sessions, for example)

function Get-CUData {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)][string]$TableName,
        [Parameter(Mandatory=$false)][string]$Where,
        [Parameter(Mandatory=$false)][string[]]$FieldList,
        [Parameter(Mandatory=$false)][int]$ChunkSize = 100
    )
    if ($FieldList.Count -lt 1) {
        $FieldList = '*'
    }
    $ParamSplat = @{
        Table = $TableName;
        Fields = $FieldList;
        Take = $ChunkSize;
        Skip = 0;
    }
    if (-not [string]::IsNullOrWhiteSpace($Where)) {
        $ParamSplat["Where"] = $Where
    }
    do {
        $CUQueryResult = Invoke-CUQuery @ParamSplat
        $CUQueryResult.Data
        $ParamSplat["Skip"] += $chunkSize
    } while ($ParamSplat["Skip"] -lt $CUQueryResult.Total)
}

[System.Collections.Generic.List[object]]$SessionList = Get-CUData -TableName $Action.Table `
                                                        -Where "sUserAccount = '$UserAccount'"

$SessionList | 
  Where-Object {$_.eConnectState -eq 4} | # only disconnected sessions, omit for all sessions
  ForEach-Object {
    $Session = $_
    Invoke-CUAction -ActionId $Action.Id `
                    -Table $Action.Table `
                    -RecordsGuids @($Session.key) | Out-Null
    Write-Output "Logoff session for user $($Session.sUserAccount) on host $($Session.sServerName)"
}

#endregion

Write-Output "Done"