dev.ps1 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. [CmdletBinding()]
  2. param(
  3. # 仅用于自动化验证;正常启动时保持 0,由 Ctrl+C 结束。
  4. [ValidateRange(0, 3600)]
  5. [int]$RunForSeconds = 0
  6. )
  7. $ErrorActionPreference = "Stop"
  8. $ProjectRoot = Split-Path -Parent $PSScriptRoot
  9. $FrontendPath = Join-Path $ProjectRoot "frontend"
  10. $BackendPath = Join-Path $ProjectRoot "backend"
  11. $ManagedProcessIds = [System.Collections.Generic.HashSet[int]]::new()
  12. $StartedProcesses = @()
  13. function Get-DevPortListeners {
  14. param([int]$Port)
  15. return @(
  16. Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue
  17. )
  18. }
  19. function Test-DevPortListening {
  20. param([int]$Port)
  21. $Endpoints = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().GetActiveTcpListeners()
  22. return $null -ne ($Endpoints | Where-Object { $_.Port -eq $Port } | Select-Object -First 1)
  23. }
  24. function Assert-DevPortAvailable {
  25. param(
  26. [int]$Port,
  27. [string]$ApplicationName
  28. )
  29. if (-not (Test-DevPortListening -Port $Port)) {
  30. return
  31. }
  32. # 只有端口确实被占用时才调用较慢的 Windows 连接查询,以便展示 PID。
  33. $Listeners = Get-DevPortListeners -Port $Port
  34. $Owners = foreach ($Listener in $Listeners) {
  35. $Process = Get-CimInstance Win32_Process `
  36. -Filter "ProcessId=$($Listener.OwningProcess)" `
  37. -ErrorAction SilentlyContinue
  38. if ($null -ne $Process) {
  39. "PID $($Process.ProcessId):$($Process.CommandLine)"
  40. }
  41. else {
  42. "PID $($Listener.OwningProcess)"
  43. }
  44. }
  45. throw "$ApplicationName 端口 $Port 已被占用。请先结束以下进程后重试:`n$($Owners -join "`n")"
  46. }
  47. function Update-ManagedProcessTree {
  48. # Uvicorn reload、pnpm 和 Vite 都会派生子进程。持续记录完整进程树,
  49. # 即使某个中间父进程先退出,最终清理时仍能找到它已经创建的子进程。
  50. # Win32_Process 单次查询在部分 Windows 机器上约需半秒,逐个父 PID
  51. # 查询会明显拖慢启动。因此每轮只取一次快照,在内存中展开进程树。
  52. $AllProcesses = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue)
  53. $AddedProcess = $true
  54. while ($AddedProcess) {
  55. $AddedProcess = $false
  56. foreach ($Child in $AllProcesses) {
  57. if (-not $ManagedProcessIds.Contains([int]$Child.ParentProcessId)) {
  58. continue
  59. }
  60. $ChildId = [int]$Child.ProcessId
  61. if ($ManagedProcessIds.Add($ChildId)) {
  62. $AddedProcess = $true
  63. }
  64. }
  65. }
  66. }
  67. function Test-ProcessRunning {
  68. param([System.Diagnostics.Process]$Process)
  69. try {
  70. $Process.Refresh()
  71. return -not $Process.HasExited
  72. }
  73. catch {
  74. return $false
  75. }
  76. }
  77. function Test-HttpEndpoint {
  78. param([string]$Uri)
  79. try {
  80. $Response = Invoke-WebRequest `
  81. -Uri $Uri `
  82. -UseBasicParsing `
  83. -TimeoutSec 2 `
  84. -ErrorAction Stop
  85. return $Response.StatusCode -ge 200 -and $Response.StatusCode -lt 500
  86. }
  87. catch {
  88. return $false
  89. }
  90. }
  91. function Wait-ForDevelopmentServices {
  92. param(
  93. [System.Diagnostics.Process]$FrontendProcess,
  94. [System.Diagnostics.Process]$BackendProcess,
  95. [int]$TimeoutSeconds = 45
  96. )
  97. $Deadline = (Get-Date).AddSeconds($TimeoutSeconds)
  98. while ((Get-Date) -lt $Deadline) {
  99. Update-ManagedProcessTree
  100. if (-not (Test-ProcessRunning -Process $FrontendProcess)) {
  101. throw "前端开发服务器启动失败,请检查上方 pnpm 输出。"
  102. }
  103. if (-not (Test-ProcessRunning -Process $BackendProcess)) {
  104. throw "后端开发服务器启动失败,请检查上方 API 与退款 Worker 输出。"
  105. }
  106. $H5Ready = Test-HttpEndpoint -Uri "http://127.0.0.1:5173/"
  107. $AdminReady = Test-HttpEndpoint -Uri "http://127.0.0.1:5174/"
  108. $BackendReady = Test-HttpEndpoint -Uri "http://127.0.0.1:8000/docs"
  109. if ($H5Ready -and $AdminReady -and $BackendReady) {
  110. return
  111. }
  112. Start-Sleep -Milliseconds 300
  113. }
  114. throw "开发服务在 $TimeoutSeconds 秒内未全部就绪,请检查上方启动日志。"
  115. }
  116. function Stop-ManagedProcesses {
  117. Write-Host "`n正在停止智保通开发服务..." -ForegroundColor Yellow
  118. # 多刷新几次,覆盖清理开始前恰好生成的 reload/Vite 子进程。
  119. 1..3 | ForEach-Object {
  120. Update-ManagedProcessTree
  121. Start-Sleep -Milliseconds 100
  122. }
  123. $Processes = @(
  124. Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
  125. Where-Object {
  126. $_.ProcessId -ne $PID -and
  127. $ManagedProcessIds.Contains([int]$_.ProcessId)
  128. }
  129. )
  130. # 子进程通常晚于父进程创建;倒序停止可避免父进程退出后留下孤儿。
  131. $Processes |
  132. Sort-Object CreationDate -Descending |
  133. ForEach-Object {
  134. Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
  135. }
  136. # 确认本次脚本启动的三个监听端口已经释放。
  137. $Deadline = (Get-Date).AddSeconds(5)
  138. while ((Get-Date) -lt $Deadline) {
  139. $AnyPortListening = $false
  140. foreach ($Port in 5173, 5174, 8000) {
  141. if (Test-DevPortListening -Port $Port) {
  142. $AnyPortListening = $true
  143. break
  144. }
  145. }
  146. if (-not $AnyPortListening) {
  147. break
  148. }
  149. Start-Sleep -Milliseconds 200
  150. }
  151. Write-Host "智保通开发服务已停止。" -ForegroundColor Green
  152. }
  153. foreach ($Service in @(
  154. @{ Port = 5173; Name = "H5" },
  155. @{ Port = 5174; Name = "管理后台" },
  156. @{ Port = 8000; Name = "后端 API" }
  157. )) {
  158. Assert-DevPortAvailable -Port $Service.Port -ApplicationName $Service.Name
  159. }
  160. $PnpmPath = (Get-Command pnpm.cmd -ErrorAction Stop).Source
  161. $UvPath = (Get-Command uv.exe -ErrorAction Stop).Source
  162. try {
  163. Write-Host "正在启动 H5、管理后台和后端 API..." -ForegroundColor Cyan
  164. $FrontendProcess = Start-Process `
  165. -FilePath $PnpmPath `
  166. -ArgumentList "dev" `
  167. -WorkingDirectory $FrontendPath `
  168. -NoNewWindow `
  169. -PassThru
  170. $StartedProcesses += $FrontendProcess
  171. [void]$ManagedProcessIds.Add($FrontendProcess.Id)
  172. $BackendProcess = Start-Process `
  173. -FilePath $UvPath `
  174. -ArgumentList @(
  175. "run", "zbt-serve"
  176. ) `
  177. -WorkingDirectory $BackendPath `
  178. -NoNewWindow `
  179. -PassThru
  180. $StartedProcesses += $BackendProcess
  181. [void]$ManagedProcessIds.Add($BackendProcess.Id)
  182. Wait-ForDevelopmentServices `
  183. -FrontendProcess $FrontendProcess `
  184. -BackendProcess $BackendProcess
  185. Write-Host ""
  186. Write-Host "智保通开发服务已全部启动:" -ForegroundColor Green
  187. Write-Host "H5:http://127.0.0.1:5173"
  188. Write-Host "管理后台:http://127.0.0.1:5174"
  189. Write-Host "后端 API:http://127.0.0.1:8000/docs"
  190. Write-Host "按 Ctrl+C 可一次性停止全部服务。" -ForegroundColor DarkGray
  191. $ReadyAt = Get-Date
  192. while ($true) {
  193. Update-ManagedProcessTree
  194. foreach ($StartedProcess in $StartedProcesses) {
  195. if (-not (Test-ProcessRunning -Process $StartedProcess)) {
  196. throw "开发服务进程 PID $($StartedProcess.Id) 意外退出,请检查上方日志。"
  197. }
  198. }
  199. if ($RunForSeconds -gt 0 -and
  200. ((Get-Date) - $ReadyAt).TotalSeconds -ge $RunForSeconds) {
  201. break
  202. }
  203. Start-Sleep -Milliseconds 500
  204. }
  205. }
  206. finally {
  207. Stop-ManagedProcesses
  208. }