Public/Stop-sqmSqlProcess.ps1
|
<#
.SYNOPSIS Beendet eine oder mehrere SQL Server Sessions (SPIDs) und benachrichtigt optional den Besitzer per Netzwerknachricht. .DESCRIPTION Liest vor dem Kill die Sitzungsdaten (Login, Hostname, Programm, Datenbank) aus sys.dm_exec_sessions, beendet die angegebene(n) Session(en) per Stop-DbaProcess (KILL) und sendet danach optional (-NotifyOwner) eine Nachricht an alle aktiven interaktiven Sitzungen auf dem Hostnamen der Session, damit der Anwender merkt, dass seine Verbindung/Abfrage beendet wurde. "net send" existiert seit Windows Vista nicht mehr (Messenger-Dienst entfernt). Der Nachfolger msg.exe wurde geprueft, ist aber auf Windows-Client-SKUs (verifiziert: Windows 11 Home) haeufig NICHT installiert - deshalb ruft diese Funktion stattdessen die zugrundeliegende WTSSendMessage-API aus wtsapi32.dll direkt per P/Invoke auf (siehe Send-sqmWtsMessage), die auf jeder Windows-Installation vorhanden ist. Das braucht weiterhin eine aktive interaktive/RDP-Sitzung auf der Zielmaschine sowie Zugriffsrechte dort. host_name in sys.dm_exec_sessions ist ein vom Client gemeldeter Wert (nicht verifiziert) - bei Verbindungen ueber Anwendungsserver/Service-Accounts sitzt dort haeufig niemand Interaktives, die Benachrichtigung geht dann ins Leere. Ein Fehlschlag der Benachrichtigung bricht den Kill NICHT ab, sondern wird nur protokolliert. .PARAMETER SqlInstance SQL Server instance (default: current computer name). .PARAMETER SqlCredential PSCredential fuer die Verbindung. .PARAMETER Spid Eine oder mehrere Session-IDs (SPIDs), die beendet werden sollen. .PARAMETER Reason Freitext-Begruendung, die in der Netzwerknachricht an den Besitzer mitgeschickt wird. .PARAMETER NotifyOwner Sendet nach dem Kill eine Nachricht an alle aktiven interaktiven Sitzungen auf dem Hostnamen der beendeten Session (per WTSSendMessage, siehe Send-sqmWtsMessage). Standardmaessig AUS, damit automatisierte/Job-basierte Kills keine ungewollten Popups ausloesen. .PARAMETER NotifyTimeoutSeconds Anzeigedauer der msg.exe-Nachricht beim Empfaenger in Sekunden. Standard: 30. .PARAMETER EnableException Wirft Exceptions sofort statt sie als Fehler zurueckzugeben. .EXAMPLE Stop-sqmSqlProcess -SqlInstance "SQL01" -Spid 62 .EXAMPLE Stop-sqmSqlProcess -SqlInstance "SQL01" -Spid 62,71 -Reason "Blockiert Tagesabschluss" -NotifyOwner .EXAMPLE # Alle Sessions einer Blockierungskette beenden und Besitzer benachrichtigen $chain = Get-sqmBlockingReport -SqlInstance "SQL01" Stop-sqmSqlProcess -SqlInstance "SQL01" -Spid $chain.BlockedSessions.BlockedSpid -Reason "Blockierung aufgeloest" -NotifyOwner .NOTES Requires: dbatools, Invoke-sqmLogging, Send-sqmWtsMessage Benoetigt VIEW SERVER STATE (lesen) und ALTER ANY CONNECTION (killen) auf der Instanz. Die Benachrichtigung nutzt WTSSendMessage (wtsapi32.dll) statt msg.exe, da msg.exe auf Windows-Client-SKUs oft fehlt (siehe Send-sqmWtsMessage). Live verifiziert: DEV03 lokal und DEV03 -> DEV01 cross-machine in einer Workgroup-Umgebung ohne AD-Domaene. #> function Stop-sqmSqlProcess { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $false, Position = 0)] [string]$SqlInstance, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential]$SqlCredential, [Parameter(Mandatory = $true)] [int[]]$Spid, [Parameter(Mandatory = $false)] [string]$Reason, [Parameter(Mandatory = $false)] [switch]$NotifyOwner, [Parameter(Mandatory = $false)] [int]$NotifyTimeoutSeconds = 30, [Parameter(Mandatory = $false)] [switch]$EnableException ) begin { $functionName = $MyInvocation.MyCommand.Name if (-not $PSBoundParameters.ContainsKey('SqlInstance') -or [string]::IsNullOrWhiteSpace($SqlInstance)) { $SqlInstance = $env:COMPUTERNAME } if (-not $script:dbatoolsAvailable) { $msg = "dbatools-Modul nicht gefunden. Bitte installieren: Install-Module dbatools" Invoke-sqmLogging -Message $msg -FunctionName $functionName -Level "ERROR" throw $msg } Invoke-sqmLogging -Message "Starte $functionName auf $SqlInstance (Spid=$($Spid -join ','), NotifyOwner=$($NotifyOwner.IsPresent))" -FunctionName $functionName -Level "INFO" } process { try { # ----------------------------------------------------------------------- # Sitzungsdaten VOR dem Kill lesen - nach KILL ist die Zeile in # sys.dm_exec_sessions weg, dann waeren Hostname/Login nicht mehr ermittelbar. # ----------------------------------------------------------------------- $spidList = ($Spid | Select-Object -Unique) -join ',' $sessionQuery = @" SELECT session_id AS Spid, login_name AS LoginName, host_name AS HostName, program_name AS ProgramName, DB_NAME(database_id) AS DatabaseName, status AS Status FROM sys.dm_exec_sessions WHERE session_id IN ($spidList) AND is_user_process = 1 "@ $connParams = @{ SqlInstance = $SqlInstance SqlCredential = $SqlCredential Database = 'master' ErrorAction = 'Stop' } $sessionInfo = Invoke-DbaQuery @connParams -Query $sessionQuery $foundSpids = @($sessionInfo | Select-Object -ExpandProperty Spid) $results = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($targetSpid in ($Spid | Select-Object -Unique)) { $row = $sessionInfo | Where-Object { $_.Spid -eq $targetSpid } | Select-Object -First 1 if (-not $row) { $notFoundMsg = "SPID $targetSpid nicht (mehr) als aktiver User-Prozess auf $SqlInstance gefunden - vermutlich bereits beendet." Invoke-sqmLogging -Message $notFoundMsg -FunctionName $functionName -Level "WARNING" $results.Add([PSCustomObject]@{ Spid = $targetSpid LoginName = $null HostName = $null DatabaseName = $null KillStatus = 'NotFound' NotifyStatus = 'Skipped' Message = $notFoundMsg }) continue } $target = "$SqlInstance (SPID $targetSpid, Login '$($row.LoginName)', Host '$($row.HostName)')" if (-not $PSCmdlet.ShouldProcess($target, 'Stop-DbaProcess (KILL)')) { $results.Add([PSCustomObject]@{ Spid = $targetSpid LoginName = $row.LoginName HostName = $row.HostName DatabaseName = $row.DatabaseName KillStatus = 'WhatIf' NotifyStatus = 'Skipped' Message = 'ShouldProcess abgelehnt (WhatIf).' }) continue } $killStatus = 'Success' $killMessage = "SPID $targetSpid beendet." try { Stop-DbaProcess -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Spid $targetSpid -Confirm:$false -EnableException -ErrorAction Stop Invoke-sqmLogging -Message "SPID $targetSpid (Login '$($row.LoginName)', Host '$($row.HostName)', DB '$($row.DatabaseName)') auf $SqlInstance beendet. Grund: $Reason" -FunctionName $functionName -Level "INFO" } catch { $killStatus = 'Failed' $killMessage = "Konnte SPID $targetSpid nicht beenden: $($_.Exception.Message)" Invoke-sqmLogging -Message $killMessage -FunctionName $functionName -Level "ERROR" } # ----------------------------------------------------------------------- # Optionale Besitzerbenachrichtigung per msg.exe. Ein Fehlschlag hier # (Host offline/nicht erreichbar, keine Berechtigung, kein Hostname bekannt) # darf den bereits erfolgten Kill nicht als Fehlschlag erscheinen lassen - # daher eigenes NotifyStatus-Feld statt Exception nach oben durchzureichen. # ----------------------------------------------------------------------- $notifyStatus = 'Skipped' $notifyMessage = 'Nicht angefordert (-NotifyOwner nicht gesetzt).' if ($NotifyOwner -and $killStatus -eq 'Success') { if ([string]::IsNullOrWhiteSpace($row.HostName)) { $notifyStatus = 'Skipped' $notifyMessage = 'Kein Hostname fuer diese Session bekannt - Benachrichtigung uebersprungen.' Invoke-sqmLogging -Message "SPID $targetSpid`: $notifyMessage" -FunctionName $functionName -Level "WARNING" } else { $messageText = "Ihre Verbindung zu '$SqlInstance' (SPID $targetSpid, Datenbank '$($row.DatabaseName)') wurde soeben durch den DBA beendet. Die Verbindung ist geschlossen, eine laufende Transaktion wurde automatisch zurueckgerollt. Bitte melden Sie sich bei Bedarf erneut an." if ($Reason) { $messageText += " Grund: $Reason" } $wtsResult = Send-sqmWtsMessage -ComputerName $row.HostName -Title 'sqmSQLTool' -Message $messageText -TimeoutSeconds $NotifyTimeoutSeconds if ($wtsResult.WasDelivered) { $notifyStatus = 'Success' $notifyMessage = "Nachricht an '$($row.HostName)' gesendet (Sitzung(en): $($wtsResult.SessionIds -join ','))." } else { $notifyStatus = 'Failed' $notifyMessage = "Zustellung an '$($row.HostName)' fehlgeschlagen: $($wtsResult.Error)" } $logLevel = if ($notifyStatus -eq 'Success') { 'INFO' } else { 'WARNING' } Invoke-sqmLogging -Message "SPID $targetSpid`: $notifyMessage" -FunctionName $functionName -Level $logLevel } } $results.Add([PSCustomObject]@{ Spid = $targetSpid LoginName = $row.LoginName HostName = $row.HostName DatabaseName = $row.DatabaseName KillStatus = $killStatus NotifyStatus = $notifyStatus Message = "$killMessage $notifyMessage".Trim() }) } return $results } catch { $errMsg = "Fehler in $functionName auf $SqlInstance`: $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } Write-Error $errMsg return $null } } end { Invoke-sqmLogging -Message "$functionName abgeschlossen." -FunctionName $functionName -Level "INFO" } } |