Files
arcrun-collector/cmd/arcrun-app/supervise.go
T
Leo fcdb1df71d 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、兩支機械閘全過。
2026-08-05 01:41:31 +08:00

112 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
// supervise.go — 看守 collector 子行程(t194
//
// 🔴 沒有這一段,這個 App 就只是個「會顯示設定的空殼」——
// daemon 的本體是 `collector direct`,它才在監看資料夾、萃卡、上傳。
// (寫 Wails 版時我一度漏了它,等於做出一個裝了也不會同步的東西。)
//
// 邊界:**複用既有的 supervisor 套件**(重起退避、狀態機、round 解析都在裡面,
// 含 t191 的 phase:start/done ⇒ 「同步中…」)。這裡只做「找到執行檔、拉起來」。
import (
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"arcrun-rag/collector/supervisor"
)
var sup *supervisor.Supervisor
// collectorBinPath 找同綑的 collector 執行檔。
// 規則與 arcrun-tray 一致:跟 App 執行檔同層(build 時會複製進 .app)。
func collectorBinPath() string {
name := "arcrun-collector"
if runtime.GOOS == "windows" {
name += ".exe"
}
exe, err := os.Executable()
if err != nil {
return name
}
if p, err := filepath.EvalSymlinks(exe); err == nil {
exe = p
}
return filepath.Join(filepath.Dir(exe), name)
}
// startSupervisor 在 App 啟動時拉起 collector;沒有 config 就先不啟動
// (使用者還沒連知識庫,啟動只會一直失敗刷 log)。
func startSupervisor() {
sup = supervisor.New(collectorBinPath(), configPath())
if _, err := os.Stat(configPath()); err == nil {
sup.Start()
}
}
// restartWatch 在設定變更後重起看守,讓新設定立刻生效。
func restartWatch() {
if sup == nil {
return
}
sup.Stop()
if _, err := os.Stat(configPath()); err == nil {
sup.Start()
}
}
// stopSupervisor 結束前停掉 collector 子行程。
// 沒有這一步,按「結束 Arcrun」後 collector 會變孤兒繼續跑
// ⇒ 使用者以為關了、其實還在同步,而且下次啟動會有兩個。
func stopSupervisor() {
if sup != nil {
sup.Stop()
}
}
// installSignalHandler 讓 App 對 SIGTERMSIGINT 有反應。
//
// 🔴 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
}