6bf0daa786
## ② 首頁狀態是壞的
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>
160 lines
5.6 KiB
Go
160 lines
5.6 KiB
Go
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 對 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
|
||
}
|
||
|
||
// 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
|
||
}
|