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 對 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 }