Files
arcrun-collector/cmd/arcrun-app/supervise.go
T
Leo a55488040d Windows 交付改單一 exe+版本號不再手打(leo 08-06 回報②與「整串該機械化」)
## ② 兩個檔很迷惑 → 一個檔
leo 圖一標註:「下載還是 ZIP ⇒ 應該是 MSIX 或 EXE;解開還是 2 個檔 ⇒ 應該只有 1 個檔案」。
舊做法 zip 裡放 Arcrun.exe+arcrun-collector.exe 同層,少解一個檔就永遠不會同步,
而畫面不會說是這個原因。
修:collector 用 go:embed 收進 Arcrun.exe,第一次執行攤到 ~/.arcrun-rag/bin/
    (冪等:sha256 相同不重寫;先寫暫存再 rename,避免留半截執行檔)。
    build-win.sh 直接產單一 Arcrun-win-<ver>.exe,不再打 zip。
    加機械閘:產出的 exe 若比 collector 還小 ⇒ 沒 embed 成功 ⇒ 中止(過去只會
    默默得到一顆「裝了也不會同步」的空殼)。
不走 MSIX:未上架的 MSIX 要使用者先開「開發人員模式」,比 zip 更麻煩。

## 版本號機械化(leo:「這一整串都應該是機械化,不應該每次手工做」)
病根實測:本 repo **一個 git tag 都沒有**,而四支 build 腳本寫的是
`git describe --tags` ⇒ 永遠拿不到 v0.18.x ⇒ 只能靠打包時人工敲 VERSION=
⇒ 兩條打包線各敲各的 ⇒ manifest 宣告 0.18.5、產物其實是 0.18.4。

修:新增 daemon-version.sh 當唯一產生器,沿用 release.mjs 已在跑的原則
   (「版本號由內容算出來,不是由人宣告」),不另立新制:
     MINOR ← DAEMON_LINE(人決定,大改版才動)
     PATCH ← 動過 collector/ 的 commit 數 − DAEMON_PATCH_BASE(機器決定)
   ⇒ 沒改 daemon 重跑打包不會虛增;改了才 +1;四支腳本在同一 commit 必得同號。
   ⇒ 不需要 git tag/Actions/輪詢(守 D20 紅線)。
   校準:DAEMON_LINE=0.18、BASE=87 ⇒ 現在的樹算出 v0.18.7(上次出貨過 0.18.6)。
   build-win/mac/dmg/msix 四支全部改用它。

## icon 漂移根治接上打包
build-win.sh 步驟⓪ 每次都跑 gen-icons.py 重產 icon.ico/trayicon.ico。

## 驗(實測輸出,非推測)
· ./build-win.sh 全程跑通,產出 dist/Arcrun-win-v0.18.7.exe(27,445,248 bytes)
· strings 抽內嵌版本 = v0.18.7、buildTime = 20260806-1233(機器算的,沒人敲)
· 位元組比對該 exe:
    CIS trayicon 在裡面        = True
    Wails「W」圖還在           = False
    collector 有 embed 進去    = True(16,104,960 bytes)
· go vet 全綠(darwin + windows);mac build OK
· 未驗:Windows 真機行為(需封測者實跑)⇒ 狀態 ◐,不是 
· 未做:CHANGELOG 閘、manifest「宣告版本 == 產物版本」驗證閘、前端顯示版本與下載入口
2026-08-06 12:35:54 +08:00

174 lines
7.0 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 (
"fmt"
"os"
"os/signal"
"path/filepath"
"runtime"
"syscall"
"time"
"arcrun-rag/collector/supervisor"
)
var sup *supervisor.Supervisor
// collectorBinPath 找同綑的 collector 執行檔。
//
// 兩種同綑方式,依平台:
// - Windowscollector **embed 在 Arcrun.exe 裡**,第一次執行攤到 ~/.arcrun-rag/bin/
// leo 08-06:「解開還是 2 個檔 ⇒ 應該只有 1 個檔案」)。見 bundled_collector_windows.go。
// - macOS:放在 .app 的 Contents/MacOS/ 同層(使用者只看到一個可拖曳的 Arcrun.app)。
//
// 內嵌攤開失敗時**退回找同層**——舊版打包出來的資料夾仍然能跑,不會因為換機制而全壞。
func collectorBinPath() string {
if p := bundledCollectorPath(); p != "" {
return p
}
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() {
bin := collectorBinPath()
cfg := configPath()
// 🔴 t195:這裡以前只 Stat(config) 就靜默 return,出問題完全查不到。
// leo 實撞:App 活著、collector 從沒啟動、畫面卻顯示「正在整理知識卡」。
// ⇒ 每個 return 點都要留下痕跡(寫進 collector.log 旁邊的 app.log)。
if _, err := os.Stat(bin); err != nil {
appLog("啟動同步引擎失敗:找不到 %s(%v)", bin, err)
return
}
if _, err := os.Stat(cfg); err != nil {
appLog("尚未連上知識庫(沒有 %s),同步引擎先不啟動", cfg)
return
}
sup = supervisor.New(bin, cfg)
sup.Start()
appLog("同步引擎已啟動:%s", bin)
}
// appLog 把 App 層的關鍵事件寫進 ~/.arcrun-rag/app.log。
// 沒有這個,「為什麼沒同步」就只能用猜的(leo 這次就卡在這)。
func appLog(format string, args ...any) {
f, err := os.OpenFile(filepath.Join(appDir(), "app.log"),
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return
}
defer f.Close()
fmt.Fprintf(f, "%s %s\n", time.Now().Format(time.RFC3339), fmt.Sprintf(format, args...))
}
// 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
// 🔴 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 objectmacOS 的 App 生命週期解,另案。)
stopSupervisor()
os.Exit(0)
}()
}
// collectorAlive 回報「同步引擎是不是真的在跑」。
//
// 🔴 t195 立的原則不變:**燈號要有憑有據,不准說謊**——leo 實撞過
// collector 已死、畫面卻一直顯示「正在整理知識卡」13 分鐘。
//
// 🔴 2026-08-06 換憑據來源(leo Windows 封測回報「根本不運行」,首頁恆顯示
// 「同步引擎沒有在跑」):
//
// 舊憑據是 `pgrep -f <bin>`。**Windows 根本沒有 pgrep** ⇒ 這裡恆回 false
// ⇒ app.go 的狀態一定走「同步引擎沒有在跑」,與 collector 實際狀態無關。
// Mac 有 pgrep 所以驗不出來——又一個只在 Windows 現形的病。
//
// 新憑據=**supervisor 自己的狀態機**(我們親手 exec.CommandContext 拉起子行程、
// runOnce 以 cmd.Wait() 收尾,行程一死立刻進 StateErrorStateStopped
// ⇒ 比掃行程表**更有憑據**,而且跨平台。
// 順帶修掉 pgrep 的偽陽性:`pgrep -f` 會匹配到**別的實例**拉起的 collector。
//
// 這不是第三種判斷法——db17f28(08-05)已為「同步中」立下同一條路:
// 「改讀 sup.Status().State ⇒ **接回既有機制,不發明第三種判斷法**」。
// 當時只改了 collectorSyncing 那半邊,這裡是把漏掉的另一半補上。
func collectorAlive() bool {
if sup == nil {
return false
}
switch sup.Status().State {
case supervisor.StateStopped, supervisor.StateError:
return false
default: // StartingWatchingSyncing
return true
}
}
// collectorSyncing 回報 collector 是不是**正在跑一輪**。
//
// 🔴 2026-08-05:資料來源是 t191 就做好、上面這支 supervisor 一直在維護的狀態機——
// collector 開工印 phase:"start"、跑完印 "done"supervisor 據此進出 StateSyncing。
// 換 Wails 時 App 沒接這條線、改看 sync-now 訊號檔,於是「拖檔進資料夾」這種
// 自動觸發的同步整輪都不會亮燈(訊號檔只有手動按鈕才產生,而且 collector 是先刪再跑)。
func collectorSyncing() bool {
if sup == nil {
return false
}
return sup.Status().State == supervisor.StateSyncing
}