Files
arcrun-collector/cmd/arcrun-app/supervise.go
T
Leo 6bf0daa786 修首頁狀態說謊+DMG 補上拖曳版面(leo 08-05 封測回報 ①②)
## ② 首頁狀態是壞的
leo:「拖新的檔案進資料夾,完成本地萃取、上傳,但自始至終 daemon 的首頁都顯示
『有變動就自動開始』『等待中』,都顯示綠燈沒動,實際上已經做完了。」

兩個獨立的真兇:

**A. 「同步中」判斷用錯依據(回歸)**
t191 早就做好機制:collector 開工印 phase:"start"、跑完印 "done",
supervisor 據此維護 StateSyncing。**換 Wails 時 App 沒接這條線**,改看 sync-now 訊號檔:
  ① 訊號檔只有手動按「立刻同步」才產生 ⇒ 拖檔進資料夾(自動觸發)整輪不亮燈
  ② 就算手動按,collector 是**先刪檔再跑**(consumeSyncNowSignal)⇒ 真正在跑時檔早沒了
⇒ 改讀 sup.Status().State == StateSyncing。**接回既有機制,不發明第三種判斷法。**

**B. 做完的證據被下一輪抹掉**
ExtractedOK/ExtractFailed 是**本輪**計數、每輪覆寫整份 status.json
⇒ 有產出那輪寫下 N,十幾秒後空轉的一輪把它蓋成 0
⇒ 「上一輪 N 份」永遠空白、四步時間軸的綠燈只靠「跑過任何一輪」判斷=與實際進度脫鉤。
⇒ 新增 last_activity_{at,ok,failed}:有產出記本輪、空轉沿用上一輪(CarryForwardActivity)。
   首頁改顯示「幾點整理了幾份」,綠燈只在真的整理過東西時才亮。

## ① Mac DMG 少了「看得出要拖」的版面
leo:「要跳出虛擬隨身碟,打開一個 finder 的視窗,**顯示 Arcrun 和 Application 的捷徑**,
用戶把 App 拖進 Application」。
原本 DMG 內容是對的(Arcrun.app+Applications 捷徑),但**沒設版面**
⇒ 預設清單視圖、位置隨機,看不出「要往右拖」。
⇒ build-dmg.sh 改走 UDRW → Finder AppleScript 設大圖示/視窗大小/左右並排 → 轉 UDZO。
   best-effort:無桌面工作階段時只警告不中止(版面是加分,不該讓打包掛掉)。

⚠️ 真正讓 leo 拿到 zip 的原因不在這裡——是**線上 portal 還沒出貨**(見下)。

## 驗(實測輸出)
· collector 全測綠;新增 TestCarryForwardActivity 四子測(含「空轉不該抹掉上次成果」)
· 掛載 dist/Arcrun-v0.18.5.dmg 實查:
    內容剛好兩項(Arcrun.app + Applications→/Applications 捷徑)
    .DS_Store 6148 bytes(版面已存進去)
    Contents/MacOS/ 有 arcrun-app **和 arcrun-collector**
    CFBundleShortVersionString = 0.18.5(不是 1.0.0)/LSUIElement = true/codesign -v 通過
· 三支機械閘 check-cis/check-render/check-tray 全 PASS

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 18:01:17 +08:00

160 lines
5.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 (
"fmt"
"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() {
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
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
}
// collectorAlive 回報「同步引擎是不是真的在跑」。
// 🔴 t195:燈號不可以只憑「訊號檔存在」就說在同步——leo 實撞過
// collector 已死、畫面卻一直顯示「正在整理知識卡」13 分鐘。
func collectorAlive() bool {
if sup == nil {
return false
}
return processRunning(collectorBinPath())
}
// 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
}