# Stop only recorded project processes whose creation time and identity still match. $ErrorActionPreference = 'Stop' $root = $PSScriptRoot $stateFile = Join-Path $root '.runtime\dev-logs\processes.json' if (-not (Test-Path -LiteralPath $stateFile)) { Write-Host 'No recorded local services.'; exit 0 } $state = Get-Content -Raw -LiteralPath $stateFile | ConvertFrom-Json if ($state.Root -ne $root) { throw 'Process state belongs to another directory. Nothing stopped.' } function Get-Identity([int]$ProcessId) { $item = Get-CimInstance Win32_Process -Filter "ProcessId = $ProcessId" -ErrorAction SilentlyContinue if (-not $item) { return $null } return [pscustomobject]@{ Pid = [int]$item.ProcessId; CreatedUtc = $item.CreationDate.ToUniversalTime().ToString('o'); ExecutablePath = [string]$item.ExecutablePath; CommandLine = [string]$item.CommandLine } } function Stop-MatchedProcess($Expected, [switch]$VerifiedPythonChild) { $actual = Get-Identity $Expected.Pid if (-not $actual) { return } if ($actual.CreatedUtc -ne $Expected.CreatedUtc -or $actual.ExecutablePath -ne $Expected.ExecutablePath -or $actual.CommandLine -ne $Expected.CommandLine -or (-not $VerifiedPythonChild -and $actual.CommandLine.IndexOf($root, [StringComparison]::OrdinalIgnoreCase) -lt 0)) { Write-Warning "PID $($Expected.Pid) no longer matches this project; skipped." return } # Only matched project descendants; never kill by process name. $moduleArguments = if ($actual.CommandLine -match '\s-m\s+(.+)$') { $matches[1] } else { '' } $children = @(Get-CimInstance Win32_Process -Filter "ParentProcessId = $($actual.Pid)" | Where-Object { $_.CommandLine -and $_.CreationDate.ToUniversalTime() -ge [datetime]::Parse($actual.CreatedUtc).ToUniversalTime() }) foreach ($child in $children) { $identity = Get-Identity $child.ProcessId if (-not $identity) { continue } if ($identity.CreatedUtc -ne $child.CreationDate.ToUniversalTime().ToString('o') -or $identity.CommandLine -ne $child.CommandLine) { continue } if ($identity.CommandLine.IndexOf($root, [StringComparison]::OrdinalIgnoreCase) -ge 0) { Stop-MatchedProcess $identity } elseif ($moduleArguments -and $actual.ExecutablePath -match '\\python.exe$' -and $identity.ExecutablePath -match '\\python.exe$' -and $identity.CommandLine -match '\s-m\s+(.+)$' -and $matches[1] -eq $moduleArguments) { # Same module and arguments, live parent link, and later creation time establish venv ownership. Stop-MatchedProcess $identity -VerifiedPythonChild } } $recheck = Get-Identity $actual.Pid if ($recheck -and $recheck.CreatedUtc -eq $actual.CreatedUtc -and $recheck.CommandLine -eq $actual.CommandLine -and $recheck.ExecutablePath -eq $actual.ExecutablePath) { Stop-Process -Id $actual.Pid -Force -ErrorAction Stop Write-Host "Stopped PID $($actual.Pid)" } } foreach ($entry in @($state.Services | Sort-Object { if ($_.Name -match 'worker') { 0 } else { 1 } })) { Stop-MatchedProcess $entry.Process } Write-Host 'Recorded project services stopped; logs retained in .runtime\dev-logs.'