From ee2fc2a8031746350845c29b5add1423c621403e Mon Sep 17 00:00:00 2001 From: richblack Date: Thu, 6 Aug 2026 12:20:25 +0800 Subject: [PATCH] =?UTF-8?q?Windows=20=E4=B8=89=E4=BF=AE=EF=BC=9A=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E7=84=A1=E9=99=90=E5=A4=9A=E9=96=8B=EF=BC=8F=E9=A6=96?= =?UTF-8?q?=E9=A0=81=E4=B8=8D=E5=86=8D=E8=AC=8A=E5=A0=B1=E3=80=8C=E5=BC=95?= =?UTF-8?q?=E6=93=8E=E6=B2=92=E5=9C=A8=E8=B7=91=E3=80=8D=EF=BC=8F=E8=A6=96?= =?UTF-8?q?=E7=AA=97=E6=94=B9=E8=9E=A2=E5=B9=95=2050%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit leo 08-06 封測回報①④+圖三標註。三個都是**只在 Windows 現形**的病。 ## ④ 托盤一次開好幾個 icon leo:「點一次開一個新的,測試者原本給我看顯示到 4 個」。 真兇:main.go 的 options.App 沒設 SingleInstanceLock(Wails v2.13 有這選項)。 每個實例還各自拉一份 collector 子行程。 macOS 由 LaunchServices 天然單實例 ⇒ Mac 上驗不出來。 修:加 SingleInstanceLock,第二次啟動叫醒既有視窗後自己退出。 ## ① 根本不運行(首頁恆顯示「同步引擎沒有在跑」) 真兇:collectorAlive() 的憑據是 `pgrep -f `,**Windows 沒有 pgrep** ⇒ 恆回 false ⇒ app.go:356 一定走「同步引擎沒有在跑」,與實際狀態無關。 修:憑據換成 supervisor 自己的狀態機(親手拉起的子行程、runOnce 以 cmd.Wait 收尾) ——比掃行程表更有憑據且跨平台,順帶修掉 pgrep 會匹配到別的實例的偽陽性。 ⚠️ 不是發明第三種判斷法:db17f28(08-05)已為「同步中」立下同一條路 「改讀 sup.Status().State ⇒ 接回既有機制,不發明第三種判斷法」, 當時只改了 collectorSyncing 那半邊,這裡把漏掉的 collectorAlive 補上。 順帶移除 waitCollectorGone/pkill:翻 supervisor 原始碼確認 Stop() 回來時 子行程已被 cmd.Wait 收屍完畢,補刀多餘;且那兩支指令 Windows 根本沒有。 ## 視窗高度擠出畫面 leo 圖三:「視窗尺寸應該是 default 50% 螢幕長寬,高度太高快擠到外面了」。 修:OnStartup 依 ScreenGetAll 算 50% 並置中;保底值 1000x700 → 960x600 (拿不到螢幕資訊時不瞎猜,沿用保底值)。 ## 驗(實測輸出) · go vet 全綠(darwin + GOOS=windows 各一次) · mac build OK;GOOS=windows CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc 交叉編譯 OK · 位元組比對交叉編譯出的 .exe: 新 CIS trayicon.ico 在 exe 裡 = True 舊 Wails「W」圖在 exe 裡 = False · 未驗:Windows 真機行為(需重打包後由封測者確認)⇒ 狀態 ◐,不是 ✅ --- cmd/arcrun-app/main.go | 60 ++++++++++++++++++++++++++++++++++-- cmd/arcrun-app/supervise.go | 61 ++++++++++++++++++++----------------- 2 files changed, 91 insertions(+), 30 deletions(-) diff --git a/cmd/arcrun-app/main.go b/cmd/arcrun-app/main.go index a1509aa..e8a40c5 100644 --- a/cmd/arcrun-app/main.go +++ b/cmd/arcrun-app/main.go @@ -40,12 +40,28 @@ func main() { err := wails.Run(&options.App{ Title: "Arcrun", - Width: 1000, Height: 700, + // 這裡的 Width/Height 只是**開得起來的保底值**;真正的尺寸在 OnStartup + // 依螢幕算成 50%(見下方 resizeToHalfScreen)。保底值不能大於常見筆電 + // 的可視高度,否則 ScreenGetAll 失敗時會沿用它而擠出畫面外 + // (leo 08-06 實撞:1000x700 在他的 Windows 上「高度太高快擠到外面了」)。 + Width: 960, Height: 600, MinWidth: 820, MinHeight: 560, AssetServer: &assetserver.Options{Assets: assets}, + // 🔴 2026-08-06:沒有這段,Arcrun.exe **點幾次就開幾個實例**, + // 每個實例各掛一個系統匣 icon(leo 的封測者開到 4 個,分不清該點哪個), + // 而且每個實例還各自拉一份 collector 子行程。 + // macOS 由 LaunchServices 天然單實例 ⇒ 這個病**只在 Windows 現形**。 + // 第二次啟動=叫醒既有視窗後自己退出(Wails 內部 os.Exit(0))。 + SingleInstanceLock: &options.SingleInstanceLock{ + UniqueId: "dev.arcrun.rag.daemon", + OnSecondInstanceLaunch: func(options.SecondInstanceData) { + app.ShowWindow() + }, + }, OnStartup: func(ctx wailsCtx) { app.startup(ctx) - startSupervisor() // 🔴 daemon 本體:不啟動就不會同步 + resizeToHalfScreen(ctx) // 視窗=螢幕的一半,置中(leo 08-06) + startSupervisor() // 🔴 daemon 本體:不啟動就不會同步 // 🔴 托盤必須在**主執行緒**建立(NSStatusItem 會 new NSWindow)。 // OnStartup 不是主執行緒 ⇒ 直接呼叫會炸 // 「NSWindow should only be instantiated on the main thread!」(實測)。 @@ -68,6 +84,46 @@ func main() { } } +// resizeToHalfScreen 把視窗調成螢幕長寬的一半並置中。 +// +// 🔴 leo 2026-08-06(Windows 封測,圖三標註): +// +// 「視窗尺寸應該是 default 50% 螢幕長寬,**高度太高快擠到外面了**」 +// +// 寫死 1000x700 在 1366x768 這種常見筆電上,扣掉工作列就幾乎頂到底。 +// 依螢幕算才不會在別人機器上爆版——我們沒有全世界的螢幕可以試。 +// +// 拿不到螢幕資訊時**什麼都不做**(沿用 options 的保底值),不要瞎猜一個數字: +// 猜錯的後果就是現在這個病。 +func resizeToHalfScreen(ctx wailsCtx) { + screens, err := wruntime.ScreenGetAll(ctx) + if err != nil || len(screens) == 0 { + return + } + // 優先用「視窗現在所在的那面」;沒有就退回主螢幕,再退回第一面。 + s := screens[0] + for _, c := range screens { + if c.IsCurrent { + s = c + break + } + if c.IsPrimary { + s = c + } + } + // Size 是邏輯像素(Wails 設定尺寸用的單位);舊欄位 Width/Height 已 deprecated, + // 但某些平台只填舊欄位 ⇒ 兩個都試,取得到值的那個。 + w, h := s.Size.Width, s.Size.Height + if w == 0 || h == 0 { + w, h = s.Width, s.Height + } + if w == 0 || h == 0 { + return + } + wruntime.WindowSetSize(ctx, w/2, h/2) + wruntime.WindowCenter(ctx) +} + // setupTray 建立選單列 icon。 // // 🔴 leo 2026-08-05:「依照 google drive,托盤裡**只剩下按右鍵會結束**, diff --git a/cmd/arcrun-app/supervise.go b/cmd/arcrun-app/supervise.go index 3528757..35ae866 100644 --- a/cmd/arcrun-app/supervise.go +++ b/cmd/arcrun-app/supervise.go @@ -11,11 +11,9 @@ package main import ( "fmt" "os" - "os/exec" "os/signal" "path/filepath" "runtime" - "strings" "syscall" "time" @@ -106,43 +104,50 @@ func installSignalHandler() { signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT) go func() { <-ch + // 🔴 2026-08-06:這裡以前 stopSupervisor() 之後還跑一輪 waitCollectorGone() + // (pgrep 輪詢 + pkill 補刀),因為當時擔心「Stop() 回來時子行程還在收屍」。 + // 翻 supervisor 原始碼確認**那個擔心不成立**: + // Stop() → cancel ctx → 阻塞等 loop goroutine 關閉 + // → loop 等 runOnce 回來 → runOnce 最後一行是 `return cmd.Wait()` + // ⇒ Stop() 回來時子行程**已經被 Wait 收屍完畢**,不需要補刀。 + // 而那兩支指令 Windows 根本沒有 ⇒ 在 Windows 上本來就只是空轉 3 秒。 + // (leo 撞到的孤兒是「強制結束」=SIGKILL 的情境,訊號處理器根本不會執行, + // 補刀也救不了;那條要靠 Windows job object/macOS 的 App 生命週期解,另案。) stopSupervisor() - // CommandContext 的 kill 是非同步的:Stop() 回來時子行程可能還在收屍。 - // 實測若直接 os.Exit(0),collector 會變孤兒繼續跑(leo 撞到的④)。 - // ⇒ 等它真的不見,最多 3 秒;逾時就自己補一刀。 - waitCollectorGone(3 * time.Second) os.Exit(0) }() } -// waitCollectorGone 等同綑的 collector 子行程真的結束。 -// 逾時就直接殺——寧可強制,也不要留一個「使用者以為關了卻還在同步」的孤兒。 -func waitCollectorGone(max time.Duration) { - bin := collectorBinPath() - deadline := time.Now().Add(max) - for time.Now().Before(deadline) { - if !processRunning(bin) { - return - } - time.Sleep(120 * time.Millisecond) - } - _ = exec.Command("pkill", "-TERM", "-f", bin).Run() -} - -// processRunning 用 pgrep 查有沒有這支執行檔在跑(純查詢,不動別人的行程)。 -func processRunning(bin string) bool { - out, _ := exec.Command("pgrep", "-f", bin).Output() - return len(strings.TrimSpace(string(out))) > 0 -} - // collectorAlive 回報「同步引擎是不是真的在跑」。 -// 🔴 t195:燈號不可以只憑「訊號檔存在」就說在同步——leo 實撞過 +// +// 🔴 t195 立的原則不變:**燈號要有憑有據,不准說謊**——leo 實撞過 // collector 已死、畫面卻一直顯示「正在整理知識卡」13 分鐘。 +// +// 🔴 2026-08-06 換憑據來源(leo Windows 封測回報「根本不運行」,首頁恆顯示 +// 「同步引擎沒有在跑」): +// +// 舊憑據是 `pgrep -f `。**Windows 根本沒有 pgrep** ⇒ 這裡恆回 false +// ⇒ app.go 的狀態一定走「同步引擎沒有在跑」,與 collector 實際狀態無關。 +// Mac 有 pgrep 所以驗不出來——又一個只在 Windows 現形的病。 +// +// 新憑據=**supervisor 自己的狀態機**(我們親手 exec.CommandContext 拉起子行程、 +// runOnce 以 cmd.Wait() 收尾,行程一死立刻進 StateError/StateStopped) +// ⇒ 比掃行程表**更有憑據**,而且跨平台。 +// 順帶修掉 pgrep 的偽陽性:`pgrep -f` 會匹配到**別的實例**拉起的 collector。 +// +// 這不是第三種判斷法——db17f28(08-05)已為「同步中」立下同一條路: +// 「改讀 sup.Status().State ⇒ **接回既有機制,不發明第三種判斷法**」。 +// 當時只改了 collectorSyncing 那半邊,這裡是把漏掉的另一半補上。 func collectorAlive() bool { if sup == nil { return false } - return processRunning(collectorBinPath()) + switch sup.Status().State { + case supervisor.StateStopped, supervisor.StateError: + return false + default: // Starting/Watching/Syncing + return true + } } // collectorSyncing 回報 collector 是不是**正在跑一輪**。