fix(local-tool): Windows 首次啟動再修 — waitHealthy pause + shutdown modal hide
續 a209470 修 Windows 乾淨環境啟動問題。使用者回報:
- 紅 banner「伺服器無法啟動 / 啟動時間超過 60 秒」— 即 pipeline total-timeout
- 但上方狀態列顯示「執行中 :3721 PID 8568 uptime 00:00:44」— server 實際活著
- Settings popup 上疊 shutdown-modal「正在停止伺服器…」永遠卡住
三個獨立問題:
1. Stage 3 waitHealthy 在 Windows 首次啟動時,Defender real-time scan
會延遲 30-60 秒才讓 visiona-local-server.exe 真正 bind port。原本
30 秒 timeout 可能 stage-failure,且這段等候時間計入 pipeline 60 秒
total budget。修法:
(a) healthCheckTimeout 30 秒 → 60 秒
(b) startServerV2 的 waitHealthy call 在冷啟動時(IsInColdStart)
包進 Pause/Resume hard timeout — 和 Stage 2 Python bootstrap 同理,
首次 bootstrap 的 Windows Defender 掃描不該算進日常啟動預算。
Restart(pipeline 已 ready)維持嚴格計時,不 pause。
2. stopGraceful 只 emit "shutdown:modal-show" 沒有對稱的 hide event,
前端 popup 顯示後無法關閉(只能等應用重開)。修法:
(a) stopGraceful 用 defer emit "shutdown:modal-hide"(若曾 show)
(b) 前端 app.js 加對應 EventsOn listener 把 hidden attribute 設回
3. 配套:cwd bash working dir 會在 session 內持久(system prompt 明說
"working directory persists between commands"),但 env vars 不持久
— 非本次 commit 相關,僅自己的 mental note。
驗證:
- visiona-local 套件 go build / vet / test -race 全綠
- macOS dmg 重 build 163MB OK
給 Windows 驗證用的 log 位置:
%APPDATA%\visiona-local\logs\server.stdout.log — server 端 log
%APPDATA%\visiona-local\logs\server.stderr.log — server 端 panic / 崩潰
%APPDATA%\visiona-local\logs\wails.log — Wails app (appLog) 訊息
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a2094708ec
commit
c649a81d9f
@ -42,7 +42,13 @@ import (
|
||||
const (
|
||||
defaultPreferredPort = 3721
|
||||
portSearchRange = 20
|
||||
healthCheckTimeout = 30 * time.Second
|
||||
// healthCheckTimeout:waitHealthy 等 server /api/system/health 200 的上限。
|
||||
//
|
||||
// 從 30s → 60s 的理由:Windows 首次啟動時 Defender real-time scan 會對
|
||||
// visiona-local-server.exe 做完整掃描(未簽章 + 首次執行)可延遲 30-60 秒才
|
||||
// 允許 process 真正執行。30 秒不夠,60 秒涵蓋絕大多數企業環境(含 EDR)。
|
||||
// 日常啟動 server 幾百毫秒就能回應,放寬上限不會影響正常情境。
|
||||
healthCheckTimeout = 60 * time.Second
|
||||
shutdownGracePeriod = 5 * time.Second
|
||||
appName = "visiona-local"
|
||||
)
|
||||
|
||||
@ -338,6 +338,11 @@ function subscribeEvents() {
|
||||
const m = document.getElementById('shutdown-modal');
|
||||
if (m) m.removeAttribute('hidden');
|
||||
});
|
||||
// stopGraceful 結束時對稱 hide,避免 popup 卡住。
|
||||
EventsOn('shutdown:modal-hide', () => {
|
||||
const m = document.getElementById('shutdown-modal');
|
||||
if (m) m.setAttribute('hidden', '');
|
||||
});
|
||||
|
||||
// app level fatal(保留相容)
|
||||
EventsOn('app:error', (msg) => {
|
||||
|
||||
@ -406,6 +406,15 @@ func (p *ServerProcess) stopGraceful() {
|
||||
defer modalTimer.Stop()
|
||||
defer graceTimer.Stop()
|
||||
|
||||
// 追蹤 modal 是否曾 show,return 時需要對稱地 hide(避免前端卡在
|
||||
// 「正在停止伺服器…」popup)。
|
||||
modalShown := false
|
||||
defer func() {
|
||||
if modalShown && p.app != nil && p.app.ctx != nil {
|
||||
wailsRuntime.EventsEmit(p.app.ctx, "shutdown:modal-hide", nil)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
@ -414,6 +423,7 @@ func (p *ServerProcess) stopGraceful() {
|
||||
case <-modalTimer.C:
|
||||
if p.app != nil && p.app.ctx != nil {
|
||||
wailsRuntime.EventsEmit(p.app.ctx, "shutdown:modal-show", nil)
|
||||
modalShown = true
|
||||
}
|
||||
case <-graceTimer.C:
|
||||
_ = p.cmd.Process.Kill()
|
||||
@ -590,7 +600,23 @@ func (a *App) startServerV2(preferredPort int) (*ServerProcess, error) {
|
||||
go a.logPump(stderrPipe, "stderr", stderrLog)
|
||||
|
||||
// 11. 等 health check — 對應啟動階段 3「啟動本機伺服器」
|
||||
//
|
||||
// 冷啟動時 pause hard timeout:Windows 首次執行 visiona-local-server.exe 會被
|
||||
// Defender / EDR real-time scan 卡 30-60 秒,60 秒 healthCheckTimeout 本身足夠,
|
||||
// 但不該把這段 scan 時間算進 startup pipeline 的 60 秒 total budget(那是日常
|
||||
// 啟動預算,首次 bootstrap 應豁免,和 Stage 2 Python bootstrap 同理)。
|
||||
//
|
||||
// 判斷冷啟動:pipeline 處於 stage 1-6 範圍內(IsInColdStart)。
|
||||
// RestartServer(pipeline 已 ready,current==7)不 pause,維持嚴格計時。
|
||||
pausedForWait := false
|
||||
if a.startupPipeline != nil && a.startupPipeline.IsInColdStart() {
|
||||
a.startupPipeline.PauseHardTimeout()
|
||||
pausedForWait = true
|
||||
}
|
||||
if err := waitHealthy(port, healthCheckTimeout); err != nil {
|
||||
if pausedForWait {
|
||||
a.startupPipeline.ResumeHardTimeout()
|
||||
}
|
||||
proc.forceKill()
|
||||
removeIPCPort(a.dataDir)
|
||||
// 階段 3 失敗
|
||||
@ -599,6 +625,9 @@ func (a *App) startServerV2(preferredPort int) (*ServerProcess, error) {
|
||||
}
|
||||
return nil, fmt.Errorf("server did not become healthy: %w", err)
|
||||
}
|
||||
if pausedForWait {
|
||||
a.startupPipeline.ResumeHardTimeout()
|
||||
}
|
||||
// 階段 3 完成 → 自動進入階段 4 running
|
||||
if a.startupPipeline != nil {
|
||||
a.startupPipeline.CompleteStage(3)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user