a55488040d
## ② 兩個檔很迷惑 → 一個檔
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「宣告版本 == 產物版本」驗證閘、前端顯示版本與下載入口
174 lines
7.0 KiB
Go
174 lines
7.0 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"
|
||
"runtime"
|
||
"syscall"
|
||
"time"
|
||
|
||
"arcrun-rag/collector/supervisor"
|
||
)
|
||
|
||
var sup *supervisor.Supervisor
|
||
|
||
// collectorBinPath 找同綑的 collector 執行檔。
|
||
//
|
||
// 兩種同綑方式,依平台:
|
||
// - Windows:collector **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 對 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 那半邊,這裡是把漏掉的另一半補上。
|
||
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
|
||
}
|