[CmdletBinding()] param() $ErrorActionPreference = "Stop" $ProjectRoot = Split-Path -Parent $MyInvocation.MyCommand.Path $StatePath = Join-Path $ProjectRoot ".runtime\processes.json" function Get-ListeningProcessId { param([int]$Port) $connection = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 if ($null -eq $connection) { return $null } return [int]$connection.OwningProcess } function Get-ProcessCommandLine { param([int]$ProcessId) $process = Get-CimInstance Win32_Process -Filter "ProcessId = $ProcessId" -ErrorAction SilentlyContinue return [string]$process.CommandLine } function Stop-ProcessTree { param([int]$RootProcessId) if ($RootProcessId -le 0 -or -not (Get-Process -Id $RootProcessId -ErrorAction SilentlyContinue)) { return } # 递归收集并从最深层开始停止,确保 node/python 的子进程不会残留。 $pending = [System.Collections.Generic.List[int]]::new() $pending.Add($RootProcessId) $allProcessIds = [System.Collections.Generic.List[int]]::new() while ($pending.Count -gt 0) { $currentProcessId = $pending[0] $pending.RemoveAt(0) $allProcessIds.Add($currentProcessId) Get-CimInstance Win32_Process -Filter "ParentProcessId = $currentProcessId" -ErrorAction SilentlyContinue | ForEach-Object { $pending.Add([int]$_.ProcessId) } } $orderedProcessIds = @($allProcessIds) [array]::Reverse($orderedProcessIds) foreach ($processId in $orderedProcessIds) { Stop-Process -Id $processId -Force -ErrorAction SilentlyContinue } } $processIds = [System.Collections.Generic.HashSet[int]]::new() if (Test-Path -LiteralPath $StatePath -PathType Leaf) { try { $state = Get-Content -LiteralPath $StatePath -Raw | ConvertFrom-Json foreach ($entry in @($state.backend, $state.frontend)) { if ($null -eq $entry) { continue } foreach ($value in @($entry.listener_pid, $entry.launcher_pid)) { if ($null -ne $value -and [int]$value -gt 0) { [void]$processIds.Add([int]$value) } } } } catch { Write-Warning "运行状态文件无法读取,将使用端口和命令行进行兜底检查。" } } # 状态文件丢失时,只接管命令行明确属于本项目的默认端口进程。 foreach ($port in @(8000, 5173)) { $listenerPid = Get-ListeningProcessId -Port $port if ($null -eq $listenerPid) { continue } $commandLine = Get-ProcessCommandLine -ProcessId $listenerPid $isBackend = $port -eq 8000 -and $commandLine -like "*uvicorn*app.main:app*" $isFrontend = $port -eq 5173 -and $commandLine -like "*vite*" if ($isBackend -or $isFrontend) { [void]$processIds.Add($listenerPid) } } if ($processIds.Count -eq 0) { Remove-Item -LiteralPath $StatePath -Force -ErrorAction SilentlyContinue Write-Host "项目当前未运行。" -ForegroundColor Yellow exit 0 } foreach ($processId in $processIds) { Stop-ProcessTree -RootProcessId $processId } Remove-Item -LiteralPath $StatePath -Force -ErrorAction SilentlyContinue Write-Host "项目已关闭。" -ForegroundColor Green