| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252 |
- [CmdletBinding()]
- param(
- # 仅用于自动化验证;正常启动时保持 0,由 Ctrl+C 结束。
- [ValidateRange(0, 3600)]
- [int]$RunForSeconds = 0,
- # 首次启动可能需要加载本地嵌入模型,因此预留充足的冷启动时间。
- [ValidateRange(10, 600)]
- [int]$StartupTimeoutSeconds = 120
- )
- $ErrorActionPreference = "Stop"
- $ProjectRoot = Split-Path -Parent $PSScriptRoot
- $FrontendPath = Join-Path $ProjectRoot "frontend"
- $BackendPath = Join-Path $ProjectRoot "backend"
- $ManagedProcessIds = [System.Collections.Generic.HashSet[int]]::new()
- $StartedProcesses = @()
- function Get-DevPortListeners {
- param([int]$Port)
- return @(
- Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue
- )
- }
- function Test-DevPortListening {
- param([int]$Port)
- $Endpoints = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().GetActiveTcpListeners()
- return $null -ne ($Endpoints | Where-Object { $_.Port -eq $Port } | Select-Object -First 1)
- }
- function Assert-DevPortAvailable {
- param(
- [int]$Port,
- [string]$ApplicationName
- )
- if (-not (Test-DevPortListening -Port $Port)) {
- return
- }
- # 只有端口确实被占用时才调用较慢的 Windows 连接查询,以便展示 PID。
- $Listeners = Get-DevPortListeners -Port $Port
- $Owners = foreach ($Listener in $Listeners) {
- $Process = Get-CimInstance Win32_Process `
- -Filter "ProcessId=$($Listener.OwningProcess)" `
- -ErrorAction SilentlyContinue
- if ($null -ne $Process) {
- "PID $($Process.ProcessId):$($Process.CommandLine)"
- }
- else {
- "PID $($Listener.OwningProcess)"
- }
- }
- throw "$ApplicationName 端口 $Port 已被占用。请先结束以下进程后重试:`n$($Owners -join "`n")"
- }
- function Update-ManagedProcessTree {
- # Uvicorn reload、pnpm 和 Vite 都会派生子进程。持续记录完整进程树,
- # 即使某个中间父进程先退出,最终清理时仍能找到它已经创建的子进程。
- # Win32_Process 单次查询在部分 Windows 机器上约需半秒,逐个父 PID
- # 查询会明显拖慢启动。因此每轮只取一次快照,在内存中展开进程树。
- $AllProcesses = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue)
- $AddedProcess = $true
- while ($AddedProcess) {
- $AddedProcess = $false
- foreach ($Child in $AllProcesses) {
- if (-not $ManagedProcessIds.Contains([int]$Child.ParentProcessId)) {
- continue
- }
- $ChildId = [int]$Child.ProcessId
- if ($ManagedProcessIds.Add($ChildId)) {
- $AddedProcess = $true
- }
- }
- }
- }
- function Test-ProcessRunning {
- param([System.Diagnostics.Process]$Process)
- try {
- $Process.Refresh()
- return -not $Process.HasExited
- }
- catch {
- return $false
- }
- }
- function Test-HttpEndpoint {
- param([string]$Uri)
- try {
- $Response = Invoke-WebRequest `
- -Uri $Uri `
- -UseBasicParsing `
- -TimeoutSec 2 `
- -ErrorAction Stop
- return $Response.StatusCode -ge 200 -and $Response.StatusCode -lt 500
- }
- catch {
- return $false
- }
- }
- function Wait-ForDevelopmentServices {
- param(
- [System.Diagnostics.Process]$FrontendProcess,
- [System.Diagnostics.Process]$BackendProcess,
- [int]$TimeoutSeconds = 120
- )
- $Deadline = (Get-Date).AddSeconds($TimeoutSeconds)
- while ((Get-Date) -lt $Deadline) {
- Update-ManagedProcessTree
- if (-not (Test-ProcessRunning -Process $FrontendProcess)) {
- throw "前端开发服务器启动失败,请检查上方 pnpm 输出。"
- }
- if (-not (Test-ProcessRunning -Process $BackendProcess)) {
- throw "后端开发服务器启动失败,请检查上方 API 与退款 Worker 输出。"
- }
- $H5Ready = Test-HttpEndpoint -Uri "http://127.0.0.1:5173/"
- $AdminReady = Test-HttpEndpoint -Uri "http://127.0.0.1:5174/"
- $BackendReady = Test-HttpEndpoint -Uri "http://127.0.0.1:8000/docs"
- if ($H5Ready -and $AdminReady -and $BackendReady) {
- return
- }
- Start-Sleep -Milliseconds 300
- }
- throw "开发服务在 $TimeoutSeconds 秒内未全部就绪,请检查上方启动日志。"
- }
- function Stop-ManagedProcesses {
- Write-Host "`n正在停止智保通开发服务..." -ForegroundColor Yellow
- # 多刷新几次,覆盖清理开始前恰好生成的 reload/Vite 子进程。
- 1..3 | ForEach-Object {
- Update-ManagedProcessTree
- Start-Sleep -Milliseconds 100
- }
- $Processes = @(
- Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
- Where-Object {
- $_.ProcessId -ne $PID -and
- $ManagedProcessIds.Contains([int]$_.ProcessId)
- }
- )
- # 子进程通常晚于父进程创建;倒序停止可避免父进程退出后留下孤儿。
- $Processes |
- Sort-Object CreationDate -Descending |
- ForEach-Object {
- Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
- }
- # 确认本次脚本启动的三个监听端口已经释放。
- $Deadline = (Get-Date).AddSeconds(5)
- while ((Get-Date) -lt $Deadline) {
- $AnyPortListening = $false
- foreach ($Port in 5173, 5174, 8000) {
- if (Test-DevPortListening -Port $Port) {
- $AnyPortListening = $true
- break
- }
- }
- if (-not $AnyPortListening) {
- break
- }
- Start-Sleep -Milliseconds 200
- }
- Write-Host "智保通开发服务已停止。" -ForegroundColor Green
- }
- foreach ($Service in @(
- @{ Port = 5173; Name = "H5" },
- @{ Port = 5174; Name = "管理后台" },
- @{ Port = 8000; Name = "后端 API" }
- )) {
- Assert-DevPortAvailable -Port $Service.Port -ApplicationName $Service.Name
- }
- $PnpmPath = (Get-Command pnpm.cmd -ErrorAction Stop).Source
- $UvPath = (Get-Command uv.exe -ErrorAction Stop).Source
- try {
- Write-Host "正在启动 H5、管理后台和后端 API..." -ForegroundColor Cyan
- $FrontendProcess = Start-Process `
- -FilePath $PnpmPath `
- -ArgumentList "dev" `
- -WorkingDirectory $FrontendPath `
- -NoNewWindow `
- -PassThru
- $StartedProcesses += $FrontendProcess
- [void]$ManagedProcessIds.Add($FrontendProcess.Id)
- $BackendProcess = Start-Process `
- -FilePath $UvPath `
- -ArgumentList @(
- "run", "zbt-serve"
- ) `
- -WorkingDirectory $BackendPath `
- -NoNewWindow `
- -PassThru
- $StartedProcesses += $BackendProcess
- [void]$ManagedProcessIds.Add($BackendProcess.Id)
- Wait-ForDevelopmentServices `
- -FrontendProcess $FrontendProcess `
- -BackendProcess $BackendProcess `
- -TimeoutSeconds $StartupTimeoutSeconds
- Write-Host ""
- Write-Host "智保通开发服务已全部启动:" -ForegroundColor Green
- Write-Host "H5:http://127.0.0.1:5173"
- Write-Host "管理后台:http://127.0.0.1:5174"
- Write-Host "后端 API:http://127.0.0.1:8000/docs"
- Write-Host "按 Ctrl+C 可一次性停止全部服务。" -ForegroundColor DarkGray
- $ReadyAt = Get-Date
- while ($true) {
- Update-ManagedProcessTree
- foreach ($StartedProcess in $StartedProcesses) {
- if (-not (Test-ProcessRunning -Process $StartedProcess)) {
- throw "开发服务进程 PID $($StartedProcess.Id) 意外退出,请检查上方日志。"
- }
- }
- if ($RunForSeconds -gt 0 -and
- ((Get-Date) - $ReadyAt).TotalSeconds -ge $RunForSeconds) {
- break
- }
- Start-Sleep -Milliseconds 500
- }
- }
- finally {
- Stop-ManagedProcesses
- }
|