458ff3fd88
leo 08-06 用共用資料夾實測,兩張圖把最後一個病逼出來:
「第一次直接跑可以,**清空再來一次,就不跑了**」。
## 真兇:restartWatch 第一行就早退
全新使用者的順序是:
① 打開程式(**還沒有任何設定**)⇒ startSupervisor 早退,`sup` 是 nil
② 連上知識庫、加資料夾 ⇒ restartWatch
③ 舊版:`if sup == nil { return }` ⇒ **什麼都沒發生**
⇒ 引擎永遠不啟動,畫面叫人「請結束 Arcrun 再重新開啟」
「不算錯」(重開真的會好),但等於要求剛裝好的人自己想到去重開程式。
**每一個第一次用的人都會撞到**;開發機永遠有設定,所以永遠測不到。
修:`sup == nil` 時**現場建立**(設定剛出現,正是該啟動的時機)。
新測試 `TestFirstRunStartsEngineAfterConnect` 從「沒有設定」開始走完整順序;
反向驗證拿掉即紅。
## 順帶把那句沒用的話拿掉
「請結束 Arcrun 再重新開啟」=把系統的無能推給使用者。
改成「正在啟動同步引擎,請稍候…」;還沒連知識庫時說
「按『新增知識庫帳號』就會開始」。
## log 不再說謊(今天第三次同一個主題)
啟動訊息印 `cfg.triggerURL(...)`=用**根層**的 cypher_url/namespace 組的,
而多帳號設定根層是空的 ⇒ 印出 `→ /webhooks/named//rag_ingest/trigger`
(沒網域、雙斜線)**看起來像設定壞了,其實功能正常**。
改印每個帳號真正會用到的網址。
## 實測證據(共用資料夾自動收回來的 log,非截圖轉述)
19:13:54 設定檔缺必填欄位,已自動補上 manifest=C:\Users\leo21\.arcrun-rag\manifest.json
19:13:55 collector direct daemon 啟動:監看 …\win_on_mac_test_lib {"phase":"start"}
⇒ v0.18.13 的自我修復在真機生效,config 已補上 manifest,collector 真的跑起來。
222 lines
9.9 KiB
Go
222 lines
9.9 KiB
Go
package main
|
||
|
||
// supervise.go — 看守 collector 子行程(t194)
|
||
//
|
||
// 🔴 沒有這一段,這個 App 就只是個「會顯示設定的空殼」——
|
||
// daemon 的本體是 `collector direct`,它才在監看資料夾、萃卡、上傳。
|
||
// (寫 Wails 版時我一度漏了它,等於做出一個裝了也不會同步的東西。)
|
||
//
|
||
// 邊界:**複用既有的 supervisor 套件**(重起退避、狀態機、round 解析都在裡面,
|
||
// 含 t191 的 phase:start/done ⇒ 「同步中…」)。這裡只做「找到執行檔、拉起來」。
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"os/signal"
|
||
"path/filepath"
|
||
"syscall"
|
||
"time"
|
||
|
||
"arcrun-rag/collector/supervisor"
|
||
)
|
||
|
||
var sup *supervisor.Supervisor
|
||
|
||
// collectorBinPath 回傳「要跑哪一支執行檔當同步引擎」——答案是**我們自己**。
|
||
//
|
||
// 🔴 2026-08-06 leo 裁決:「不要兩支,寫成一支檔案」。
|
||
//
|
||
// 先前兩代做法都有病:
|
||
// ① zip 裡放兩支 exe 同層 ⇒ 使用者少拿一支就永遠不同步(leo 封測回報②)
|
||
// ② 內嵌 collector.exe、執行時攤到磁碟再跑 ⇒ **Windows Defender 眼中的 dropper**
|
||
// (封測者實撞:檔案「包含病毒或潛在的垃圾軟體」當場被刪)
|
||
// 現在:collector 已改成可被引用的套件(arcrun-rag/collector.Run),
|
||
// App 直接把它編進同一個執行檔,靠 `--collector` 參數換身分。
|
||
// ⇒ **磁碟上永遠只有一個 exe,沒有任何東西被攤出來。**
|
||
func collectorBinPath() string {
|
||
exe, err := os.Executable()
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
if p, err := filepath.EvalSymlinks(exe); err == nil {
|
||
exe = p
|
||
}
|
||
return exe
|
||
}
|
||
|
||
// collectorArgPrefix 是「把自己當 collector 跑」的參數(見 main.go 開頭的分派)。
|
||
func collectorArgPrefix() []string { return []string{collectorModeFlag} }
|
||
|
||
// startSupervisor 在 App 啟動時拉起 collector;沒有 config 就先不啟動
|
||
// (使用者還沒連知識庫,啟動只會一直失敗刷 log)。
|
||
func startSupervisor() {
|
||
bin := collectorBinPath()
|
||
cfg := configPath()
|
||
// 🔴 t195:這裡以前只 Stat(config) 就靜默 return,出問題完全查不到。
|
||
// leo 實撞:App 活著、collector 從沒啟動、畫面卻顯示「正在整理知識卡」。
|
||
// ⇒ 每個 return 點都要留下痕跡(寫進 collector.log 旁邊的 app.log)。
|
||
if bin == "" {
|
||
appLog("啟動同步引擎失敗:取不到自己的執行檔路徑")
|
||
return
|
||
}
|
||
if _, err := os.Stat(cfg); err != nil {
|
||
appLog("尚未連上知識庫(沒有 %s),同步引擎先不啟動", cfg)
|
||
return
|
||
}
|
||
sup = supervisor.New(bin, cfg)
|
||
sup.ArgPrefix = collectorArgPrefix() // 同一支執行檔,換身分跑
|
||
// 🔴 2026-08-06:每次狀態變成 Error 都寫進 app.log。
|
||
// 沒有這一段,「子行程一啟動就死」在檔案裡**一個字都沒有**——
|
||
// leo 在 Windows ARM 撞到時,畫面只有閃爍、log 只有「同步引擎已啟動」,
|
||
// 死因(Status().LastError)只活在記憶體裡,跟著程式一起消失。
|
||
// 只在錯誤訊息**變了**或每 10 次才記一次,避免重試迴圈把 log 洗爆。
|
||
var lastLogged string
|
||
var lastN int
|
||
sup.SetOnChange(func(st supervisor.Status) {
|
||
if st.State != supervisor.StateError {
|
||
return
|
||
}
|
||
if st.LastError == lastLogged && st.Restarts-lastN < 10 {
|
||
return
|
||
}
|
||
lastLogged, lastN = st.LastError, st.Restarts
|
||
appLog("同步引擎異常結束(第 %d 次重試):%s", st.Restarts, st.LastError)
|
||
})
|
||
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() {
|
||
// 🔴 2026-08-06 leo 實測(「第一次直接跑可以,清空再來一次就不跑了」):
|
||
// 這裡以前第一行是 `if sup == nil { return }` ⇒ **每一個全新使用者都會撞到**——
|
||
// ① 開程式時還沒有設定 ⇒ startSupervisor 早退,`sup` 是 nil
|
||
// ② 使用者接著連知識庫、加資料夾 ⇒ 呼叫這支 ⇒ **nil 就直接 return,什麼都沒發生**
|
||
// ③ 引擎永遠不啟動,畫面說「請結束 Arcrun 再重新開啟」
|
||
// ——它「不算錯」(重開真的會好),但等於要求剛裝好的人自己想到去重開程式。
|
||
// ⇒ 還沒建立就**現在建立**:設定剛剛才出現,正是該啟動的時機。
|
||
if sup == nil {
|
||
startSupervisor()
|
||
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
|
||
// 🔴 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 object/macOS 的 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() 收尾,行程一死立刻進 StateError/StateStopped)
|
||
// ⇒ 比掃行程表**更有憑據**,而且跨平台。
|
||
// 順帶修掉 pgrep 的偽陽性:`pgrep -f` 會匹配到**別的實例**拉起的 collector。
|
||
//
|
||
// 這不是第三種判斷法——db17f28(08-05)已為「同步中」立下同一條路:
|
||
// 「改讀 sup.Status().State ⇒ **接回既有機制,不發明第三種判斷法**」。
|
||
// 當時只改了 collectorSyncing 那半邊,這裡是把漏掉的另一半補上。
|
||
//
|
||
// collectorFailure 回報「同步引擎是不是一直啟動失敗」,以及死因。
|
||
//
|
||
// 🔴 2026-08-06 leo 在 Windows 11 ARM 實測:畫面「一直在『看守中』和『沒有在跑』
|
||
//
|
||
// 中間閃…我覺得它在跑個迴圈不停重複」。
|
||
// 真相就是那樣——子行程一啟動就死,supervisor 退避後再拉,狀態在
|
||
// Starting(collectorAlive=true)與 Error(false)之間彈跳 ⇒ 畫面跟著閃。
|
||
// **而死因一直被記在 Status().LastError 裡,從來沒有上過畫面**
|
||
// ⇒ 使用者只看到閃爍與「請重新開啟」,重開當然無效(死因沒變)。
|
||
// 這是「安靜地略過」的同一種病,換一個地方發作。
|
||
//
|
||
// restarts 達到門檻才算「一直失敗」——偶爾重起一次是正常的(例如換設定)。
|
||
func collectorFailure() (msg string, restarts int, looping bool) {
|
||
if sup == nil {
|
||
return "", 0, false
|
||
}
|
||
st := sup.Status()
|
||
return st.LastError, st.Restarts, st.Restarts >= crashLoopThreshold
|
||
}
|
||
|
||
// crashLoopThreshold=重起幾次算「一直失敗」。3 次以內可能只是換設定或暫時性錯誤。
|
||
const crashLoopThreshold = 3
|
||
|
||
func collectorAlive() bool {
|
||
if sup == nil {
|
||
return false
|
||
}
|
||
switch sup.Status().State {
|
||
case supervisor.StateStopped, supervisor.StateError:
|
||
return false
|
||
default: // Starting/Watching/Syncing
|
||
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
|
||
}
|