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)