t194 二修:托盤 icon/右鍵選單/重複點擊/正常結束(leo 實測四點)
leo 真機測出四個問題,每個都查到真兇後才動手: ## ① 托盤 icon 變方塊 真兇:我塞 1024x1024 的**彩色 app icon** 當 template icon。 macOS 選單列規格是 **16-22pt、純黑+alpha**(由系統依深淺色自動上色) ⇒ 彩色大圖被縮成一坨方塊是必然。 修:用 CIS 的 chevron-double.svg 渲成 44x44 純黑 template icon。 ## ②③ 右鍵沒選單/第一次能開之後就不再跳 真兇(energye/systray 原始碼註解白紙黑字寫著): 「該方法主動調用後 如果托盤菜單已創建則添加進去, **之後鼠標事件失效**」 ⇒ 只要用 AddMenuItem 建了選單,SetOnClick/SetOnRClick **全部失效**。 修:SetMenuNil() 移除選單讓滑鼠事件生效,右鍵時自己 ShowMenu()。 另外 ShowWindow 只呼叫 WindowShow 對「已開但被蓋住」無效 ⇒ 補 Unminimise + AlwaysOnTop 閃一下搶焦點。 ## ④ 只能用強制結束 真兇:Wails 的 loop 不理 SIGTERM,且 collector 是我們自己 Start 的子行程 ⇒ 主程式被殺後**子行程變孤兒繼續跑**。 實測(機械驗證,非目視): 修前 kill -TERM → App 沒退、collector 孤兒還在 修後 kill -TERM → App 正常退出 ✅、collector 一起收掉 ✅、殘留 0 修:installSignalHandler 收 TERM/INT → stopSupervisor() → waitCollectorGone(3s) 確認子行程真的不見(逾時補一刀)→ 才 os.Exit。 「關了卻還在同步」比沒關更糟。 實跑驗證:App 活著 pid 88466、自己拉起 collector、crash 0、兩支機械閘全過。
This commit is contained in:
+14
-2
@@ -365,9 +365,21 @@ func (a *App) OpenURL(u string) { runtime.BrowserOpenURL(a.ctx, u) }
|
||||
// 🔴 leo 2026-08-05:「**點擊托盤的 icon 就立刻展開界面**」
|
||||
// ⇒ 左鍵不彈選單、直接開窗(Google Drive 的行為)。
|
||||
func (a *App) ShowWindow() {
|
||||
runtime.WindowShow(a.ctx)
|
||||
// 🔴 leo 實測③:「第一次點擊可以開啟,然後再點擊托盤就**不再跳出**」。
|
||||
// 真兇有二:(a) 選單建了 ⇒ 滑鼠事件失效(見 setupTray 的 SetMenuNil)
|
||||
// (b) 視窗其實還在、只是被蓋住或最小化 ⇒ 只呼叫 WindowShow 沒有效果。
|
||||
// ⇒ 三個都做:取消最小化、顯示、**強制帶到最前面**。
|
||||
runtime.WindowUnminimise(a.ctx)
|
||||
runtime.WindowShow(a.ctx)
|
||||
runtime.WindowSetAlwaysOnTop(a.ctx, true)
|
||||
runtime.WindowSetAlwaysOnTop(a.ctx, false) // 只用來搶焦點,不真的釘在最上層
|
||||
}
|
||||
|
||||
// Quit 真的結束程式(=停止看守)。只有托盤右鍵那一項會呼叫。
|
||||
func (a *App) Quit() { runtime.Quit(a.ctx) }
|
||||
//
|
||||
// 🔴 leo 實測④:「**用強制結束把它關掉才能測試**」——代表沒有一條正常的結束路徑。
|
||||
// 這裡先停掉 collector 子行程再關 App,否則子行程會變孤兒繼續跑。
|
||||
func (a *App) Quit() {
|
||||
stopSupervisor()
|
||||
runtime.Quit(a.ctx)
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 358 B |
+19
-5
@@ -35,6 +35,7 @@ func main() {
|
||||
// 它不阻塞,接著再把主執行緒交給 wails.Run()。
|
||||
setupTray(app)
|
||||
trayStart()
|
||||
installSignalHandler() // 收到 TERM/INT 要能正常退出(不必強制結束)
|
||||
defer trayEnd()
|
||||
|
||||
err := wails.Run(&options.App{
|
||||
@@ -87,15 +88,28 @@ var trayStart, trayEnd = func() {}, func() {}
|
||||
|
||||
func setupTray(app *App) {
|
||||
trayStart, trayEnd = systray.RunWithExternalLoop(func() {
|
||||
// ① icon:**必須是 template icon**——選單列規格是 16-22pt、純黑+alpha,
|
||||
// 由系統依深淺色自動上色。先前塞 1024x1024 的彩色 app icon
|
||||
// ⇒ 縮成一坨方塊(leo 實測)。改用 CIS 的 double chevron 渲成 44x44 純黑。
|
||||
systray.SetTemplateIcon(trayIcon, trayIcon)
|
||||
systray.SetTooltip("Arcrun — 你的知識庫同步小幫手")
|
||||
|
||||
// 左鍵:立刻展開主視窗
|
||||
systray.SetOnClick(func(menu systray.IMenu) { app.ShowWindow() })
|
||||
|
||||
// 右鍵:只有「結束」一項
|
||||
// 🔴 ②③ 的真兇(energye/systray 原始碼註解寫得很清楚):
|
||||
// 「該方法主動調用後 如果托盤菜單已創建則添加進去, **之後鼠標事件失效**」
|
||||
// ⇒ 只要用 AddMenuItem 建了選單,左右鍵 handler **全部失效**
|
||||
// ⇒ leo 實測「右鍵沒跳出 quit」「第一次能開之後就不再跳」。
|
||||
// 正解:SetMenuNil() 移除選單讓滑鼠事件生效,右鍵時**自己叫出選單**。
|
||||
quit := systray.AddMenuItem("結束 Arcrun", "停止同步並關閉")
|
||||
quit.Click(func() { app.Quit() })
|
||||
systray.SetOnRClick(func(menu systray.IMenu) { _ = menu.ShowMenu() })
|
||||
systray.SetMenuNil() // ← 沒有這行,下面兩個 handler 都不會被呼叫
|
||||
|
||||
// 左鍵:立刻展開主視窗(leo:「點擊托盤的 icon 就立刻展開界面」)
|
||||
systray.SetOnClick(func(menu systray.IMenu) { app.ShowWindow() })
|
||||
// 右鍵:把選單叫出來(只有「結束 Arcrun」一項)
|
||||
systray.SetOnRClick(func(menu systray.IMenu) {
|
||||
if menu != nil {
|
||||
_ = menu.ShowMenu()
|
||||
}
|
||||
})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
@@ -10,8 +10,13 @@ package main
|
||||
// 含 t191 的 phase:start/done ⇒ 「同步中…」)。這裡只做「找到執行檔、拉起來」。
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"arcrun-rag/collector/supervisor"
|
||||
)
|
||||
@@ -54,3 +59,53 @@ func restartWatch() {
|
||||
sup.Start()
|
||||
}
|
||||
}
|
||||
|
||||
// stopSupervisor 結束前停掉 collector 子行程。
|
||||
// 沒有這一步,按「結束 Arcrun」後 collector 會變孤兒繼續跑
|
||||
// ⇒ 使用者以為關了、其實還在同步,而且下次啟動會有兩個。
|
||||
func stopSupervisor() {
|
||||
if sup != nil {
|
||||
sup.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// installSignalHandler 讓 App 對 SIGTERM/SIGINT 有反應。
|
||||
//
|
||||
// 🔴 leo 實測④:「**用強制結束把它關掉才能測試**,一般托盤程式不會顯示在強制結束列表中」。
|
||||
//
|
||||
// 真兇:Wails 的 loop 不理會 TERM,而 collector 是我們自己 Start 的子行程
|
||||
// ⇒ 主程式被殺時**子行程變孤兒繼續跑**(實測:kill -TERM 後 collector 還在)。
|
||||
// ⇒ 收到訊號就先停子行程再退出,避免「關了卻還在同步」與「下次啟動有兩個」。
|
||||
func installSignalHandler() {
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT)
|
||||
go func() {
|
||||
<-ch
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user