fcdb1df71d
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、兩支機械閘全過。
112 lines
3.6 KiB
Go
112 lines
3.6 KiB
Go
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
|
||
}
|