dev.ps1 7.9 KB

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