6698f9fbda
leo 08-06 Windows 回報:「一直在『看守中』和『沒有在跑』中間閃,要我重啟但重啟無效, 我覺得它在跑個迴圈不停重複」+「加一個資料夾明顯沒產生看守資料夾」。 朋友的一般 PC(非 ARM)也一樣,停在等待中 ⇒ **不是 ARM 模擬的問題,是 Windows 通用**。 ## 已定位的機制(不是根因,是「為什麼查不出根因」) 閃爍=子行程一啟動就死 → supervisor 退避重拉 → 狀態在 Starting(alive=true)與 Error(false)之間彈跳 ⇒ 畫面跟著閃。而**死因一直被記在 Status().LastError 裡, 從來沒有上過畫面、也沒寫進 app.log** ⇒ 使用者只看到閃爍與「請重新開啟」, 重開當然無效(死因沒變)。這是「安靜地略過」的同一種病換地方發作。 ## 這版做的 - 死因上畫面:重試 >= 3 次改顯示「同步引擎一直啟動失敗,已自動重試 N 次|原因:…」, 且**黏住不閃**(不受重起過程中的 Starting 影響) - 每次異常結束寫進 app.log(同錯誤 10 次內只記一次,避免洗爆) ## 排除掉的嫌疑(查過,不是) - PDF 引擎(pdfium/wasm):**延遲初始化**,第一次讀 PDF 才啟動 ⇒ 不會在啟動時炸 - ARM 模擬:leo 朋友的一般 x64 PC 同樣症狀 - 待查嫌疑:Wails 的 Windows 版是 `-H windowsgui`(GUI subsystem,無主控台), 而 v0.18.8 的 collector.exe 是 `go build` 的 console subsystem—— v0.18.9 合併成單一 binary 後,子行程換成 GUI subsystem 的自己。**尚未證實。** ## 順帶修好自己的閘(它擋對了我) 指紋閘擋下「v0.18.10 已對應另一份原始碼」——因為第一次打包失敗、版號卻已被戳。 ⇒ 加判準:**磁碟上沒有該版號產物=從未出貨,允許重戳**(不用去手改 JSON, 那種爛示範遲早被改成「都放行」)。 ## 安裝程式(leo ③)尚未兌現,誠實記錄 `wails build -nsis` 需要 makensis;Homebrew 的 3.12 在這台 macOS **連兩行的最小腳本 都在寫檔階段丟 std::bad_alloc**,zlib/bzip2/lzma 三壓縮器全崩,brew extract 舊版 被 homebrew/core 擋。且帶 -nsis 會讓 wails 整個中止、連 exe 都不產 ⇒ build-win.sh 改成**先煙霧測試 makensis**,壞的就只出裸 exe 並大聲警告。
214 lines
9.1 KiB
Go
214 lines
9.1 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() {
|
||
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
|
||
// 🔴 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
|
||
}
|