Private/Get-BotProcess.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Finding and stopping the running bot process. .DESCRIPTION Shared by Stop-BATCRelayBot and the uninstaller so there is one way to answer "is the bot running" and "stop it". The detection deliberately does not use Get-Process: a Process object has no CommandLine property on Windows PowerShell 5.1, so the previous `Get-Process python | Where-Object { $_.CommandLine -match 'bot\.py' }` silently matched nothing. The bot was therefore never detected as running and never stopped - by the uninstaller either, which then deleted files out from under a live process. Win32_Process does expose CommandLine on 5.1. pythonw.exe matters as much as python.exe: Start-BATCRelayBot launches the windowless variant. #> function Find-BotProcess { <# .SYNOPSIS Finds running bot.py processes belonging to one installation. .DESCRIPTION Matches this installation's own bot.py, so an unrelated Python program is never targeted. .OUTPUTS Array of process ids. Empty when nothing matches. #> [OutputType([int[]])] param( [Parameter(Mandatory = $true)] [string]$BotPath ) $matchesFound = @() try { $processes = Get-CimInstance Win32_Process ` -Filter "Name='python.exe' OR Name='pythonw.exe'" -ErrorAction Stop } catch { return $matchesFound } $normalisedBotPath = $BotPath.TrimEnd('\') foreach ($process in $processes) { if (-not $process.CommandLine) { continue } if ($process.CommandLine -notmatch 'bot\.py') { continue } # An absolute path in the command line is conclusive. if ($process.CommandLine -like "*$normalisedBotPath*") { $matchesFound += [int]$process.ProcessId continue } # Otherwise the command line is just "bot.py" and the installation # directory is the working directory, which Win32_Process does not # expose. Fall back to the executable path recorded in config.json. try { $configPath = Join-Path $normalisedBotPath "config.json" if (Test-Path $configPath) { $config = Get-Content $configPath -Raw -ErrorAction Stop | ConvertFrom-Json $expected = $config.python_path if ($expected) { $expectedDir = Split-Path -Parent $expected if ($process.ExecutablePath -and (Split-Path -Parent $process.ExecutablePath) -eq $expectedDir) { $matchesFound += [int]$process.ProcessId continue } } } } catch {} # Last resort: a bare "bot.py" command line with no other installation # on this machine to confuse it. if ($process.CommandLine -match '(^|[\s"])bot\.py("|\s|$)') { $matchesFound += [int]$process.ProcessId } } return @($matchesFound | Select-Object -Unique) } function Stop-BotProcess { <# .SYNOPSIS Stops the bot, preferring the graceful path. .DESCRIPTION Writes stop.signal, which bot.py checks every second: it leaves the voice channel cleanly, closes the Discord connection and exits. Only if that does not happen within the timeout is the process terminated. Force-killing a connected voice client leaves the bot visibly stuck in the channel for a while, so the graceful path is always tried first. .OUTPUTS Hashtable with Stopped, Method, ProcessIds. #> [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] [string]$BotPath, [int]$TimeoutSeconds = 15 ) $running = @(Find-BotProcess -BotPath $BotPath) if ($running.Count -eq 0) { return @{ Stopped = $true; Method = 'not running'; ProcessIds = @() } } $stopSignal = Join-Path $BotPath "stop.signal" $signalWritten = $false if (Test-Path $BotPath) { try { New-Item -Path $stopSignal -ItemType File -Force -ErrorAction Stop | Out-Null $signalWritten = $true } catch { # Fall through to termination below. } } if ($signalWritten) { $deadline = (Get-Date).AddSeconds($TimeoutSeconds) while ((Get-Date) -lt $deadline) { if (@(Find-BotProcess -BotPath $BotPath).Count -eq 0) { Remove-Item $stopSignal -Force -ErrorAction SilentlyContinue return @{ Stopped = $true; Method = 'graceful'; ProcessIds = $running } } Start-Sleep -Milliseconds 500 } Remove-Item $stopSignal -Force -ErrorAction SilentlyContinue } foreach ($processId in $running) { try { Stop-Process -Id $processId -Force -ErrorAction Stop } catch {} } $stillRunning = @(Find-BotProcess -BotPath $BotPath) return @{ Stopped = ($stillRunning.Count -eq 0) Method = 'forced' ProcessIds = $running } } Export-ModuleMember -Function @('Find-BotProcess', 'Stop-BotProcess') |