stop.ps1 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. [CmdletBinding()]
  2. param()
  3. $ErrorActionPreference = "Stop"
  4. $ProjectRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
  5. $StatePath = Join-Path $ProjectRoot ".runtime\processes.json"
  6. function Get-ListeningProcessId {
  7. param([int]$Port)
  8. $connection = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue |
  9. Select-Object -First 1
  10. if ($null -eq $connection) {
  11. return $null
  12. }
  13. return [int]$connection.OwningProcess
  14. }
  15. function Get-ProcessCommandLine {
  16. param([int]$ProcessId)
  17. $process = Get-CimInstance Win32_Process -Filter "ProcessId = $ProcessId" -ErrorAction SilentlyContinue
  18. return [string]$process.CommandLine
  19. }
  20. function Stop-ProcessTree {
  21. param([int]$RootProcessId)
  22. if ($RootProcessId -le 0 -or -not (Get-Process -Id $RootProcessId -ErrorAction SilentlyContinue)) {
  23. return
  24. }
  25. # 递归收集并从最深层开始停止,确保 node/python 的子进程不会残留。
  26. $pending = [System.Collections.Generic.List[int]]::new()
  27. $pending.Add($RootProcessId)
  28. $allProcessIds = [System.Collections.Generic.List[int]]::new()
  29. while ($pending.Count -gt 0) {
  30. $currentProcessId = $pending[0]
  31. $pending.RemoveAt(0)
  32. $allProcessIds.Add($currentProcessId)
  33. Get-CimInstance Win32_Process -Filter "ParentProcessId = $currentProcessId" -ErrorAction SilentlyContinue |
  34. ForEach-Object { $pending.Add([int]$_.ProcessId) }
  35. }
  36. $orderedProcessIds = @($allProcessIds)
  37. [array]::Reverse($orderedProcessIds)
  38. foreach ($processId in $orderedProcessIds) {
  39. Stop-Process -Id $processId -Force -ErrorAction SilentlyContinue
  40. }
  41. }
  42. $processIds = [System.Collections.Generic.HashSet[int]]::new()
  43. if (Test-Path -LiteralPath $StatePath -PathType Leaf) {
  44. try {
  45. $state = Get-Content -LiteralPath $StatePath -Raw | ConvertFrom-Json
  46. foreach ($entry in @($state.backend, $state.frontend)) {
  47. if ($null -eq $entry) {
  48. continue
  49. }
  50. foreach ($value in @($entry.listener_pid, $entry.launcher_pid)) {
  51. if ($null -ne $value -and [int]$value -gt 0) {
  52. [void]$processIds.Add([int]$value)
  53. }
  54. }
  55. }
  56. } catch {
  57. Write-Warning "运行状态文件无法读取,将使用端口和命令行进行兜底检查。"
  58. }
  59. }
  60. # 状态文件丢失时,只接管命令行明确属于本项目的默认端口进程。
  61. foreach ($port in @(8000, 5173)) {
  62. $listenerPid = Get-ListeningProcessId -Port $port
  63. if ($null -eq $listenerPid) {
  64. continue
  65. }
  66. $commandLine = Get-ProcessCommandLine -ProcessId $listenerPid
  67. $isBackend = $port -eq 8000 -and $commandLine -like "*uvicorn*app.main:app*"
  68. $isFrontend = $port -eq 5173 -and $commandLine -like "*vite*"
  69. if ($isBackend -or $isFrontend) {
  70. [void]$processIds.Add($listenerPid)
  71. }
  72. }
  73. if ($processIds.Count -eq 0) {
  74. Remove-Item -LiteralPath $StatePath -Force -ErrorAction SilentlyContinue
  75. Write-Host "项目当前未运行。" -ForegroundColor Yellow
  76. exit 0
  77. }
  78. foreach ($processId in $processIds) {
  79. Stop-ProcessTree -RootProcessId $processId
  80. }
  81. Remove-Item -LiteralPath $StatePath -Force -ErrorAction SilentlyContinue
  82. Write-Host "项目已关闭。" -ForegroundColor Green