Public/sessions/Disconnect-RdpSession.ps1

#Requires -Version 5.1

function Disconnect-RdpSession {
    <#
    .SYNOPSIS
        Disconnects one or more RDP sessions on a local or remote computer
    .DESCRIPTION
        Uses the built-in tsdiscon.exe utility to disconnect RDP sessions by session ID.
        Supports local and remote computers with optional credential pass-through via
        WinRM remoting. Accepts pipeline input from Get-RdpSession for bulk
        operations. Returns a result object per session indicating success or failure.
        For remote targets without credentials, Invoke-Command sends the disconnect
        command over WinRM. When credentials are provided, they are passed through
        Invoke-Command's -Credential parameter.
    .PARAMETER ComputerName
        The target computer name or IP address. Defaults to the local computer name.
        Accepts pipeline input by property name for integration with Get-RdpSession.
        Accepts a single computer name only — use the pipeline with Get-RdpSession
        to operate across multiple machines sequentially.
    .PARAMETER SessionID
        One or more RDP session IDs to disconnect. Valid range is 0 to 65536.
        Accepts pipeline input directly or by property name.
    .PARAMETER Credential
        Optional PSCredential for authenticating to remote computers. When provided,
        the disconnect command executes through Invoke-Command with WinRM remoting.
        Not used for local sessions.
    .EXAMPLE
        Disconnect-RdpSession -ComputerName 'SRV01' -SessionID 3
        Disconnects session ID 3 on server SRV01 after confirmation prompt.
    .EXAMPLE
        Get-RdpSession -ComputerName 'SRV01' | Disconnect-RdpSession -Confirm:$false
        Pipes all active sessions from SRV01 and disconnects them without prompting.
    .EXAMPLE
        Disconnect-RdpSession -ComputerName 'SRV01' -SessionID 3, 5 -Credential (Get-Credential) -Verbose
        Disconnects sessions 3 and 5 on SRV01 using alternate credentials with verbose output.
    .OUTPUTS
    PSWinOps.RdpSessionAction
        Disconnection action result with session details and status.
 
    .NOTES
        Author: Franck SALLET
        Version: 2.0.0
        Last Modified: 2026-03-12
        Requires: PowerShell 5.1+, tsdiscon.exe (built-in on all Windows editions)
        Permissions: Local admin or Remote Desktop Services disconnect rights on the target
                       WinRM access required when using the -Credential parameter
     
    .LINK
    https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/logoff
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    [OutputType('PSWinOps.RdpSessionAction')]
    param(
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('CN', 'Name', 'DNSHostName')]
        [string]$ComputerName = $env:COMPUTERNAME,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true)]
        [ValidateRange(0, 65536)]
        [int[]]$SessionID,

        [Parameter(Mandatory = $false)]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]$Credential
    )

    begin {
        Write-Verbose "[$($MyInvocation.MyCommand)] Starting - PowerShell $($PSVersionTable.PSVersion)"

        $tsdisconCmd = Get-Command -Name 'tsdiscon.exe' -CommandType Application -ErrorAction SilentlyContinue
        if ($null -eq $tsdisconCmd) {
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                [System.IO.FileNotFoundException]::new(
                    'tsdiscon.exe was not found on this system. Ensure Remote Desktop Services tools are available.'
                ),
                'TsdisconNotFound',
                [System.Management.Automation.ErrorCategory]::ObjectNotFound,
                'tsdiscon.exe'
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }

        Write-Verbose "[$($MyInvocation.MyCommand)] Found tsdiscon.exe at: $($tsdisconCmd.Source)"

        $LOCAL_IDENTIFIERS = @($env:COMPUTERNAME, 'localhost', '.', '127.0.0.1', '::1')

        $disconnectBlock = {
            param([int]$SessId)
            $null = & tsdiscon.exe $SessId 2>&1
            return $LASTEXITCODE
        }
    }

    process {
        foreach ($session in $SessionID) {
            $targetDescription = "$ComputerName (Session ID: $session)"
            Write-Verbose "[$($MyInvocation.MyCommand)] Processing $targetDescription"

            if ($PSCmdlet.ShouldProcess($targetDescription, 'Disconnect RDP session')) {
                $isLocalMachine = $ComputerName -in $LOCAL_IDENTIFIERS
                $success = $false

                try {
                    $invokeParams = @{
                        ScriptBlock  = $disconnectBlock
                        ArgumentList = @($session)
                        ErrorAction  = 'Stop'
                    }

                    if (-not $isLocalMachine) {
                        $invokeParams['ComputerName'] = $ComputerName
                        if ($PSBoundParameters.ContainsKey('Credential')) {
                            $invokeParams['Credential'] = $Credential
                        }
                    }

                    $exitCode = Invoke-Command @invokeParams
                    $success = ($null -ne $exitCode -and $exitCode -eq 0)

                    if ($success) {
                        Write-Verbose "[$($MyInvocation.MyCommand)] [OK] Disconnected $targetDescription"
                    } else {
                        Write-Warning "[$($MyInvocation.MyCommand)] tsdiscon.exe returned exit code $exitCode for $targetDescription"
                    }
                } catch [System.Management.Automation.Remoting.PSRemotingTransportException] {
                    Write-Error "[$($MyInvocation.MyCommand)] WinRM connection failed to $ComputerName - $_"
                } catch {
                    Write-Error "[$($MyInvocation.MyCommand)] Failed to disconnect $targetDescription - $_"
                }

                [PSCustomObject]@{
                    PSTypeName   = 'PSWinOps.RdpSessionAction'
                    ComputerName = $ComputerName
                    SessionID    = $session
                    Action       = 'Disconnect'
                    Success      = $success
                    Timestamp    = Get-Date -Format 'o'
                }
            }
        }
    }

    end {
        Write-Verbose "[$($MyInvocation.MyCommand)] Completed"
    }
}