diff --git a/dev.cmd b/dev.cmd index 1f520b5..4e93388 100644 --- a/dev.cmd +++ b/dev.cmd @@ -1,5 +1,34 @@ @echo off -rem dev.cmd — 双击即可启动本地开发环境(等同于 .\dev\dev.ps1) -rem 参数原样透传,例如:dev.cmd -Vision -Open +rem dev.cmd ¡ª ˫»÷¼´¿ÉÆô¶¯±¾µؿª·¢»·¾³£¨µÈͬÓÚ .\dev\dev.ps1£© +rem ²ÎÊýԭÑù͸´«£¬ÀýÈ磺dev.cmd -Vision -Open dev.cmd -Foreground +rem +rem ´°¿ڲßÂԣº +rem ʧ°Ü ¡ú pause£¬±£Áô±¨´í£¬²»ÔÙһÉÁ¶ø¹ý +rem ǰ̨ģʽ ¡ú ½ű¾±¾Éí»á×èÈûµ½ Ctrl+C£¬½áÊøºó pause +rem ³ɹ¦£¨ºǫ́£© ¡ú pause£¬±£ÁôµØַÐÅϢ£¨¹ش°¿ڲ»ӰÏì·þÎñ£© +setlocal powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0dev\dev.ps1" %* -if errorlevel 1 pause +set RC=%ERRORLEVEL% + +echo. +if not "%RC%"=="0" ( + echo [Æô¶¯ʧ°ܣ¬Í˳öÂë %RC%] ÉÏÃæ¾ÍÊÇԭÒ򣬴°¿ÚÏÈÁôך£ + echo ÏêϸÈÕ־£ºapp-cli\.logs\ ²鿴£º.\dev\logs.ps1 -Follow + pause + exit /b %RC% +) + +echo %* | findstr /i /c:"-Foreground" >nul 2>&1 +if not errorlevel 1 ( + echo [ǰ̨ģʽÒѽáÊø] + pause + exit /b 0 +) + +echo [·þÎñÒÑÔںǫ́ÔËÐУ¬¹صô±¾´°¿ڲ»»áֹͣ·þÎñ] +echo ¿´ʵʱÊä³ö£ºdev.cmd -Foreground ^(ǰºó¶ËÊä³ö¶¼ÔÚÕâ¸ö´°¿Ú^) +echo ²鿴ÈÕ־Îļþ£º.\dev\logs.ps1 -Follow +echo ֹͣ·þÎñ£º.\dev\stop.ps1 +echo. +pause +exit /b 0 diff --git a/dev/dev.ps1 b/dev/dev.ps1 index 78582fb..e20d3c3 100644 --- a/dev/dev.ps1 +++ b/dev/dev.ps1 @@ -24,6 +24,18 @@ .EXAMPLE .\dev\dev.ps1 -Force 端口被占时先杀掉占用进程再启动 + +.EXAMPLE + .\dev\dev.ps1 backend -Foreground + 前台运行后端:日志(含 gin 路由、8092 管理后台请求)直接打在终端,Ctrl+C 结束 + +.EXAMPLE + .\dev\dev.ps1 frontend -Foreground + 前台运行前端:astro 的请求日志、编译告警直接打在终端,Ctrl+C 结束 + +.EXAMPLE + .\dev\dev.ps1 -Foreground + 推荐日常调试:后端 + 前端都前台跑,两边输出一起打在当前终端 #> param( [Parameter(Position = 0)] @@ -33,7 +45,11 @@ param( [switch]$Vision, [switch]$SkipBuild, [switch]$Open, - [switch]$Force + [switch]$Force, + + # 前台模式:不重定向、不隐藏窗口,服务输出直接打在当前终端(Ctrl+C 结束)。 + # 不带服务名(all)时,前后端输出会一起打在当前终端,适合日常调试。 + [switch]$Foreground ) $ErrorActionPreference = 'Stop' @@ -43,6 +59,14 @@ $CliRoot = Split-Path -Parent $PSScriptRoot $envMap = Import-LocalEnv (Join-Path $CliRoot 'config\local.env') $svc = Resolve-Services -EnvMap $envMap -Root $CliRoot +if ($Foreground -and $Target -eq 'vision') { + Write-Fail '视觉服务不支持 -Foreground(用 .\vision\vision.ps1 logs 查看输出)' + exit 1 +} + +# 前台模式下正在运行的进程(all 模式会有多个,输出共用当前控制台) +$script:fgProcs = @() + $backendPort = [int](Get-Setting $envMap 'BACKEND_PORT' '8090') $frontendPort = [int](Get-Setting $envMap 'FRONTEND_PORT' '4321') $visionPort = [int](Get-Setting $envMap 'VISION_PORT' '8900') @@ -62,6 +86,18 @@ function Assert-CanStart { $owner = Get-PortOwnerPid $Port $saved = Get-ServicePid $Name if ($saved -gt 0 -and $saved -eq $owner) { + # 前台模式的语义就是「把输出占在我的终端」,已在后台跑的那份必须让位。 + # 否则会出现「全部跳过 → 前台循环没进程 → 脚本直接退出」的假象。 + if ($Foreground) { + Write-Note "$Name 已在后台运行(PID $owner),前台接管:先停止再前台启动" + Stop-NamedService $Name | Out-Null + Start-Sleep -Milliseconds 600 + if (Test-PortInUse $Port) { + Write-Fail "端口 $Port 仍未释放,无法前台接管" + return 'abort' + } + return 'start' + } Write-Note "$Name 已在运行(PID $owner,端口 $Port),跳过" return 'skip' } @@ -127,6 +163,30 @@ function Start-Backend { Remove-Item Env:\VISION_ENABLED -ErrorAction SilentlyContinue } + if ($Foreground) { + Write-Step "前台运行后端(公开 $backendPort / SSG 8091 / 后台 8092)" + + # 只起后端:直接阻塞在这个进程上,Ctrl+C 就是结束进程,最干净。 + if ($Target -eq 'backend') { + Write-Note '输出直接打在下面;Ctrl+C 结束' + Push-Location $svc.Backend.Path + try { + & $exe + } finally { + Pop-Location + Remove-Item Env:\VISION_ENABLED -ErrorAction SilentlyContinue + } + exit 0 + } + + # all 模式:-NoNewWindow 让子进程继承当前控制台,输出实时可见(不重定向) + $p = Start-Process -FilePath $exe -WorkingDirectory $svc.Backend.Path -NoNewWindow -PassThru + $script:fgProcs += [PSCustomObject]@{ Name = 'backend'; Proc = $p; Reported = $false } + Remove-Item Env:\VISION_ENABLED -ErrorAction SilentlyContinue + Write-Ok "后端已在前台启动(PID $($p.Id))" + return $true + } + Write-Step "启动后端(公开 $backendPort / SSG 8091 / 后台 8092)" Start-LoggedProcess -Name 'backend' -FilePath $exe -ArgumentList @() -WorkingDirectory $svc.Backend.Path | Out-Null Remove-Item Env:\VISION_ENABLED -ErrorAction SilentlyContinue @@ -177,6 +237,9 @@ function Start-Frontend { if ($action -eq 'skip') { return $true } if ($action -eq 'abort') { return $false } + # 前台接管时旧实例刚被停掉,锁文件可能残留(PID 被复用会误判「已在运行」) + if ($Foreground) { Remove-AstroDevLock $svc.Frontend.Path } + # astro 单实例锁:端口没冲突也可能起不来 $lock = Get-AstroDevLock $svc.Frontend.Path if ($lock -and $lock.Alive) { @@ -218,6 +281,28 @@ function Start-Frontend { return $false } + if ($Foreground) { + Write-Step "前台运行前端(astro dev,端口 $frontendPort)" + + if ($Target -eq 'frontend') { + Write-Note '输出直接打在下面;Ctrl+C 结束' + Push-Location $svc.Frontend.Path + try { + & node.exe $astro dev --port "$frontendPort" + } finally { + Pop-Location + } + exit 0 + } + + $p = Start-Process -FilePath 'node.exe' ` + -ArgumentList @($astro, 'dev', '--port', "$frontendPort") ` + -WorkingDirectory $svc.Frontend.Path -NoNewWindow -PassThru + $script:fgProcs += [PSCustomObject]@{ Name = 'frontend'; Proc = $p; Reported = $false } + Write-Ok "前端已在前台启动(PID $($p.Id))" + return $true + } + Write-Step "启动前端(astro dev,端口 $frontendPort)" Start-LoggedProcess -Name 'frontend' -FilePath 'node.exe' ` -ArgumentList @($astro, 'dev', '--port', "$frontendPort") ` @@ -310,6 +395,54 @@ if ($Vision -or $Target -eq 'vision') { if (-not (Start-Vision)) { $failed += 'vision' } } +# ---------------------------------------------------------------- 前台聚合输出 +# 前台模式下半套启动没有意义(端口冲突等):直接报错退出,不留孤儿进程。 +if ($Foreground -and $failed.Count -gt 0) { + Write-Fail ("前台启动失败:" + ($failed -join ', ')) + Write-Note '常见原因:8090/4323 已被之前的进程占用,先 .\dev\stop.ps1 再重试' + exit 1 +} + +# 兜底:前台模式必须有进程在跑,否则静默退出会让人误以为脚本自己崩了。 +if ($Foreground -and $script:fgProcs.Count -eq 0) { + Write-Fail '前台模式没有启动任何进程,请查看上面的输出' + exit 1 +} + +# all + -Foreground:子进程共用当前控制台,输出由它们直接写终端(无重定向、无延迟)。 +# 这里只负责等待与收尾:任一进程退出就报告,Ctrl+C 时一起停掉。 +if ($Foreground -and $script:fgProcs.Count -gt 0) { + $names = ($script:fgProcs | ForEach-Object { $_.Name }) -join ' + ' + Write-Host '' + Write-Host ' ----------------------------------------' -ForegroundColor DarkGray + Write-Host " $names 的输出直接打在下面,Ctrl+C 一起停止" -ForegroundColor Yellow + Write-Host ' ----------------------------------------' -ForegroundColor DarkGray + Write-Host '' + try { + while ($true) { + Start-Sleep -Milliseconds 400 + $alive = @($script:fgProcs | Where-Object { -not $_.Proc.HasExited }) + if ($alive.Count -eq 0) { break } + foreach ($e in $script:fgProcs) { + if (-not $e.Reported -and $e.Proc.HasExited) { + Write-Fail "$($e.Name) 已退出(退出码 $($e.Proc.ExitCode)),另一个仍在运行" + $e.Reported = $true + } + } + } + } finally { + foreach ($e in $script:fgProcs) { + if (-not $e.Proc.HasExited) { + Write-Note "停止 $($e.Name)(PID $($e.Proc.Id))" + Stop-PidTree $e.Proc.Id | Out-Null + } + } + } + Write-Host '' + Write-Note '前台模式已退出(这些进程不在 .run\ 登记,stop.ps1 不管它们)' + exit 0 +} + # ---------------------------------------------------------------- 结果汇总 Write-Host '' @@ -347,7 +480,9 @@ if (Test-PortInUse $backendPort) { } Write-Host '' -Write-Note '日志:.logs\backend.log / .logs\frontend.log / .logs\vision.log' +Write-Note '本窗口只显示启动过程;服务输出写在 .logs\ 下,不会打到终端(这是后台模式的正常表现)' +Write-Note '要实时看输出:.\dev\logs.ps1 backend -Follow 或 .\dev\logs.ps1 frontend -Follow' +Write-Note '要实时看全部输出:.\dev\dev.ps1 -Foreground(前后端一起前台跑,Ctrl+C 停)' Write-Note '停止:.\dev\stop.ps1 状态:.\dev\status.ps1' if ($Open) { diff --git a/dev/logs.ps1 b/dev/logs.ps1 new file mode 100644 index 0000000..666bbf6 --- /dev/null +++ b/dev/logs.ps1 @@ -0,0 +1,81 @@ +<# +.SYNOPSIS + 查看本地服务的运行日志(dev.ps1 用后台进程启动,输出不落在终端)。 + +.DESCRIPTION + dev.ps1 把后端/前端的 stdout/stderr 重定向到 .logs\ 下的文件, + 所以「服务在跑但终端没有任何输出」是正常的。本脚本就是看那些文件的入口。 + + 每个服务有两个文件: + .logs\.log 标准输出(业务日志、gin 路由、astro 启动信息) + .logs\.err.log 标准错误(编译报错、panic、连接失败) + +.EXAMPLE + .\dev\logs.ps1 + 看后端最近 50 行(默认) + +.EXAMPLE + .\dev\logs.ps1 frontend -Lines 100 + 看前端最近 100 行 + +.EXAMPLE + .\dev\logs.ps1 backend -Follow + 持续跟随后端日志(Ctrl+C 退出),等价于 Get-Content -Wait + +.EXAMPLE + .\dev\logs.ps1 all + 三个服务的日志都看一遍(只看不跟随) +#> +param( + [Parameter(Position = 0)] + [ValidateSet('backend', 'frontend', 'vision', 'all')] + [string]$Name = 'backend', + + [int]$Lines = 50, + + [switch]$Follow +) + +$ErrorActionPreference = 'Stop' +$CliRoot = Split-Path -Parent $PSScriptRoot +$LogDir = Join-Path $CliRoot '.logs' + +if (-not (Test-Path $LogDir)) { + Write-Host " 还没有日志:$LogDir" -ForegroundColor Yellow + Write-Host " 先跑 .\dev\dev.ps1 启动服务" -ForegroundColor Gray + exit 1 +} + +$targets = if ($Name -eq 'all') { @('backend', 'frontend', 'vision') } else { @($Name) } + +if ($Follow -and $targets.Count -gt 1) { + Write-Host " -Follow 一次只能跟一个服务,改成:.\dev\logs.ps1 backend -Follow" -ForegroundColor Yellow + exit 1 +} + +foreach ($t in $targets) { + foreach ($suffix in @('.log', '.err.log')) { + $f = Join-Path $LogDir ($t + $suffix) + if (-not (Test-Path $f)) { continue } + + $size = (Get-Item $f).Length + Write-Host '' + Write-Host (" --- {0}{1} ({2} 字节) ---" -f $t, $suffix, $size) -ForegroundColor DarkGray + + if ($size -eq 0) { + Write-Host ' (空文件:进程可能刚启动,或输出还在缓冲区)' -ForegroundColor DarkGray + continue + } + + if ($Follow) { + # -Wait 会阻塞直到 Ctrl+C;先打最近 N 行再跟随新增内容 + Write-Host " 跟随中,Ctrl+C 退出" -ForegroundColor DarkGray + Get-Content -Path $f -Tail $Lines -Wait + } else { + Get-Content -Path $f -Tail $Lines | ForEach-Object { Write-Host " $_" } + } + } +} + +Write-Host '' +Write-Host ' PID 登记:.run\.pid 状态:.\dev\status.ps1' -ForegroundColor DarkGray