v0.18.10:讓「同步引擎沒在跑」說出死因(leo Windows 實測①②的診斷前置)

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 並大聲警告。
This commit is contained in:
2026-08-06 18:27:08 +08:00
parent 0b8bc83bd1
commit 6698f9fbda
6 changed files with 128 additions and 26 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
{
"v0.18.9": "8be9260eb542f527"
"v0.18.9": "8be9260eb542f527",
"v0.18.10": "a440f044a849b7f2"
}
+19 -1
View File
@@ -369,8 +369,26 @@ func loadSyncStatus() syncStatus {
// supervisor 據此維護 StateSyncingsupervisor.go)。**那條線本來就在,
// Wails 版換代時沒接上而已**——不要再自己發明第三種判斷法。
func describeStatus(s syncStatus) (syncing bool, big, sub string) {
// 🔴 一直啟動失敗(crash loop)要**優先**判,而且不受 collectorAlive 影響——
// 重起過程中狀態會短暫變成 Starting(alive=true),若照順序判就會與
// 「沒有在跑」交替出現 ⇒ 就是 leo 08-06 在 Windows ARM 看到的閃爍。
// 這裡讓失敗訊息**黏住**,畫面才會穩定,也才說得出死因。
if msg, n, looping := collectorFailure(); looping {
sub := fmt.Sprintf("已自動重試 %d 次都失敗,所以重新開啟也沒有用。", n)
if msg != "" {
sub += "原因:" + msg
} else {
sub += "詳細訊息請看記錄檔 app.log。"
}
return false, "同步引擎一直啟動失敗", sub
}
if !collectorAlive() {
return false, "同步引擎沒有在跑", "資料夾不會被自動整理 ⇒ 請結束 Arcrun 再重新開啟"
msg, n, _ := collectorFailure()
sub := "資料夾不會被自動整理 ⇒ 請結束 Arcrun 再重新開啟"
if msg != "" {
sub = fmt.Sprintf("原因:%s(已重試 %d 次)", msg, n)
}
return false, "同步引擎沒有在跑", sub
}
if collectorSyncing() {
return true, "同步中… 正在讀檔並整理成知識卡", "請稍候,完成後會顯示整理了幾份"
+38 -1
View File
@@ -67,9 +67,28 @@ json.dump(c, open('wails.json', 'w'), indent=2, ensure_ascii=False)
PY
trap 'mv -f wails.json.bak wails.json 2>/dev/null || true' EXIT
# 🔴 makensis 壞掉時**不能**還帶 -nsiswails 會整個中止,連 exe 都不產
# 2026-08-06 實撞:Homebrew makensis 3.12 在這台 macOS 連兩行的最小腳本都丟
# std::bad_alloc ⇒ 三種壓縮器全崩、brew extract 舊版也被 homebrew/core 擋)。
# ⇒ 先用最小腳本煙霧測試,過了才開安裝程式。
NSIS_FLAG=""
if command -v makensis >/dev/null 2>&1; then
SMOKE="$(mktemp -d)"
printf 'OutFile "%s/s.exe"\nSection\nSectionEnd\n' "$SMOKE" > "$SMOKE/s.nsi"
if makensis "$SMOKE/s.nsi" >/dev/null 2>&1; then
NSIS_FLAG="-nsis"
echo " ✅ makensis 可用 → 會一併產生安裝程式"
else
echo " ⚠️ makensis 裝了但**跑不起來**(最小腳本就崩)⇒ 這次不產安裝程式,只出裸 exe"
fi
rm -rf "$SMOKE"
else
echo " ⚠️ 沒有 makensis ⇒ 這次不產安裝程式,只出裸 exebrew install makensis"
fi
echo "② wails buildwindows/amd64CGo 走 mingw"
CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc \
wails build -platform windows/amd64 -clean \
wails build -platform windows/amd64 -clean ${NSIS_FLAG} \
-ldflags "-X main.version=${VERSION} -X main.buildTime=${BUILD_TIME} \
-X arcrun-rag/collector.version=${VERSION} -X arcrun-rag/collector.buildTime=${BUILD_TIME}"
@@ -107,6 +126,24 @@ if [ -z "$PROBE_OUT" ]; then
fi
echo " ✅ 自帶同步引擎(--collector --version → ${PROBE_OUT}"
# 🔴 2026-08-06 leo 封測回報③:「Windows 用戶通常習慣 install 過程,它則只是一個執行檔,
# 表示安裝後不會在 Windows 開始資料夾出現,關掉下次要用就找不到了,也沒有 uninstall,
# 希望跟 Mac 一樣用建議的安裝方式」⇒ 出 NSIS 安裝程式(`-nsis`)。
# 裝完會有:開始功能表項目、桌面捷徑、控制台可解除安裝——與 Mac 的 DMG 拖曳同級的「正常」。
# 裸 exe 仍保留(給想免安裝試用的人),但**主打是安裝程式**。
SETUP_SRC="build/bin/arcrun-app-amd64-installer.exe"
if [ -f "$SETUP_SRC" ]; then
SETUP="$(cd "$OUT" && pwd)/Arcrun-setup-${VERSION}.exe"
cp "$SETUP_SRC" "$SETUP"
echo " ✅ 安裝程式:$(basename "$SETUP")$(ls -lh "$SETUP" | awk '{print $5}')"
else
echo "⚠️ 沒產出 NSIS 安裝程式 —— **leo 08-06 要的『正常安裝流程』尚未兌現**"
echo " 真兇:Homebrew 的 makensis 3.12 在這台 macOS 壞掉(連兩行的最小腳本都"
echo " 在寫檔階段丟 std::bad_alloc,三種壓縮器全崩 ⇒ 工具鏈問題,不是我們的 .nsi)。"
echo " 已試過且無效:zlib/bzip2/lzma 三壓縮器、brew extract 舊版(homebrew/core 不給 tap)。"
echo " ⇒ 本次仍只產裸 exe。安裝程式的替代路線見 docs/store-submission.mdMSIX/Store)。"
fi
SIZE=$(ls -lh "$EXE" | awk '{print $5}')
echo "✅ 完成:${EXE}${SIZE},單一檔案、自帶同步引擎)"
echo
@@ -5,19 +5,19 @@
!include "FileFunc.nsh"
!ifndef INFO_PROJECTNAME
!define INFO_PROJECTNAME "{{.Name}}"
!define INFO_PROJECTNAME "arcrun-app"
!endif
!ifndef INFO_COMPANYNAME
!define INFO_COMPANYNAME "{{.Info.CompanyName}}"
!define INFO_COMPANYNAME "arcrun-app"
!endif
!ifndef INFO_PRODUCTNAME
!define INFO_PRODUCTNAME "{{.Info.ProductName}}"
!define INFO_PRODUCTNAME "Arcrun"
!endif
!ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
!define INFO_PRODUCTVERSION "0.18.10"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "{{.Info.Copyright}}"
!define INFO_COPYRIGHT "Copyright........."
!endif
!ifndef PRODUCT_EXECUTABLE
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
@@ -238,20 +238,12 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
!macro wails.associateFiles
; Create file associations
{{range .Info.FileAssociations}}
!insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
File "..\{{.IconName}}.ico"
{{end}}
!macroend
!macro wails.unassociateFiles
; Delete app associations
{{range .Info.FileAssociations}}
!insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
Delete "$INSTDIR\{{.IconName}}.ico"
{{end}}
!macroend
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
@@ -270,15 +262,10 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
!macro wails.associateCustomProtocols
; Create custom protocols associations
{{range .Info.Protocols}}
!insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
{{end}}
!macroend
!macro wails.unassociateCustomProtocols
; Delete app custom protocol associations
{{range .Info.Protocols}}
!insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
{{end}}
!macroend
+19 -1
View File
@@ -78,6 +78,15 @@ def source_fingerprint():
return hashlib.sha256((tree + diff).encode()).hexdigest()[:16]
def artifacts_for(version):
"""磁碟上有沒有這個版號的產物(exe/dmg/msix)。有=真的出過東西,不准重戳。"""
dist = HERE / "dist"
if not dist.is_dir():
return []
return [f.name for f in dist.iterdir()
if version in f.name and f.suffix in (".exe", ".dmg", ".msix", ".zip")]
def check_or_record(version, stamp):
"""同一個版號只准對應一份原始碼;不同就擋下並說怎麼辦。"""
fp = source_fingerprint()
@@ -85,6 +94,15 @@ def check_or_record(version, stamp):
return
lock = json.loads(SOURCE_LOCK.read_text()) if SOURCE_LOCK.exists() else {}
known = lock.get(version)
# 🔴 2026-08-06:打包**失敗**時版號已被戳、產物卻沒生出來 ⇒ 那個版號被白白燒掉,
# 修完再打就被自己的閘擋下,只能去手改 JSON(爛示範,遲早有人順手改成「都放行」)。
# ⇒ 沒有任何產物的版號=**從未出貨**,本來就該可以重戳。
# 判準是「磁碟上有沒有這個版號的產物」,不是「我覺得它沒出貨」。
if known and known != fp and not artifacts_for(version):
print("%s 之前戳過但**沒有產出任何檔案**(打包失敗),視為未出貨 → 重新對應這次的原始碼"
% version, file=sys.stderr)
known = None
lock.pop(version, None)
if known and known != fp:
die("""%s 這個版號已經對應過另一份原始碼了(記錄 %s,現在 %s)。
@@ -98,7 +116,7 @@ def check_or_record(version, stamp):
- (用戶語言的更新內容)
然後重跑打包,版號會自動變成下一個。""" % (version, known, fp, version))
if stamp and not known:
if stamp and lock.get(version) != fp:
lock[version] = fp
SOURCE_LOCK.write_text(json.dumps(lock, indent=1, ensure_ascii=False) + "\n")
+41
View File
@@ -64,6 +64,23 @@ func startSupervisor() {
}
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)
}
@@ -146,6 +163,30 @@ func installSignalHandler() {
// 這不是第三種判斷法——db17f28(08-05)已為「同步中」立下同一條路:
// 「改讀 sup.Status().State ⇒ **接回既有機制,不發明第三種判斷法**」。
// 當時只改了 collectorSyncing 那半邊,這裡是把漏掉的另一半補上。
//
// collectorFailure 回報「同步引擎是不是一直啟動失敗」,以及死因。
//
// 🔴 2026-08-06 leo 在 Windows 11 ARM 實測:畫面「一直在『看守中』和『沒有在跑』
//
// 中間閃…我覺得它在跑個迴圈不停重複」。
// 真相就是那樣——子行程一啟動就死,supervisor 退避後再拉,狀態在
// StartingcollectorAlive=true)與 Errorfalse)之間彈跳 ⇒ 畫面跟著閃。
// **而死因一直被記在 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