From c649a81d9f13b45c61c292099bc2b3278b1afd3e Mon Sep 17 00:00:00 2001 From: jim800121chen Date: Wed, 15 Apr 2026 21:59:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(local-tool):=20Windows=20=E9=A6=96=E6=AC=A1?= =?UTF-8?q?=E5=95=9F=E5=8B=95=E5=86=8D=E4=BF=AE=20=E2=80=94=20waitHealthy?= =?UTF-8?q?=20pause=20+=20shutdown=20modal=20hide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 續 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) --- local-tool/visiona-local/app.go | 8 +++++- local-tool/visiona-local/frontend/app.js | 5 ++++ local-tool/visiona-local/server_control.go | 29 ++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/local-tool/visiona-local/app.go b/local-tool/visiona-local/app.go index 7be0ce9..d1f5698 100644 --- a/local-tool/visiona-local/app.go +++ b/local-tool/visiona-local/app.go @@ -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" ) diff --git a/local-tool/visiona-local/frontend/app.js b/local-tool/visiona-local/frontend/app.js index c1b983c..bf560d1 100644 --- a/local-tool/visiona-local/frontend/app.js +++ b/local-tool/visiona-local/frontend/app.js @@ -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) => { diff --git a/local-tool/visiona-local/server_control.go b/local-tool/visiona-local/server_control.go index 709a29a..a799342 100644 --- a/local-tool/visiona-local/server_control.go +++ b/local-tool/visiona-local/server_control.go @@ -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)