From 6698f9fbda24db040cac9c115ff5bba3411c1165 Mon Sep 17 00:00:00 2001 From: richblack Date: Thu, 6 Aug 2026 18:27:08 +0800 Subject: [PATCH] =?UTF-8?q?v0.18.10=EF=BC=9A=E8=AE=93=E3=80=8C=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E5=BC=95=E6=93=8E=E6=B2=92=E5=9C=A8=E8=B7=91=E3=80=8D?= =?UTF-8?q?=E8=AA=AA=E5=87=BA=E6=AD=BB=E5=9B=A0=EF=BC=88leo=20Windows=20?= =?UTF-8?q?=E5=AF=A6=E6=B8=AC=E2=91=A0=E2=91=A1=E7=9A=84=E8=A8=BA=E6=96=B7?= =?UTF-8?q?=E5=89=8D=E7=BD=AE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 並大聲警告。 --- cmd/arcrun-app/.version-source.json | 3 +- cmd/arcrun-app/app.go | 20 ++++++++- cmd/arcrun-app/build-win.sh | 39 +++++++++++++++++- .../build/windows/installer/wails_tools.nsh | 31 ++++---------- cmd/arcrun-app/daemon-version.py | 20 ++++++++- cmd/arcrun-app/supervise.go | 41 +++++++++++++++++++ 6 files changed, 128 insertions(+), 26 deletions(-) diff --git a/cmd/arcrun-app/.version-source.json b/cmd/arcrun-app/.version-source.json index 47f7948..39e3625 100644 --- a/cmd/arcrun-app/.version-source.json +++ b/cmd/arcrun-app/.version-source.json @@ -1,3 +1,4 @@ { - "v0.18.9": "8be9260eb542f527" + "v0.18.9": "8be9260eb542f527", + "v0.18.10": "a440f044a849b7f2" } diff --git a/cmd/arcrun-app/app.go b/cmd/arcrun-app/app.go index 8a70974..16d0eb7 100644 --- a/cmd/arcrun-app/app.go +++ b/cmd/arcrun-app/app.go @@ -369,8 +369,26 @@ func loadSyncStatus() syncStatus { // supervisor 據此維護 StateSyncing(supervisor.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, "同步中… 正在讀檔並整理成知識卡", "請稍候,完成後會顯示整理了幾份" diff --git a/cmd/arcrun-app/build-win.sh b/cmd/arcrun-app/build-win.sh index 2abcb8a..34aeb69 100755 --- a/cmd/arcrun-app/build-win.sh +++ b/cmd/arcrun-app/build-win.sh @@ -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 壞掉時**不能**還帶 -nsis:wails 會整個中止,連 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 ⇒ 這次不產安裝程式,只出裸 exe(brew install makensis)" +fi + echo "② wails build(windows/amd64,CGo 走 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.md(MSIX/Store)。" +fi + SIZE=$(ls -lh "$EXE" | awk '{print $5}') echo "✅ 完成:${EXE}(${SIZE},單一檔案、自帶同步引擎)" echo diff --git a/cmd/arcrun-app/build/windows/installer/wails_tools.nsh b/cmd/arcrun-app/build/windows/installer/wails_tools.nsh index 6dd9057..87250e5 100644 --- a/cmd/arcrun-app/build/windows/installer/wails_tools.nsh +++ b/cmd/arcrun-app/build/windows/installer/wails_tools.nsh @@ -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 diff --git a/cmd/arcrun-app/daemon-version.py b/cmd/arcrun-app/daemon-version.py index bbce66f..59d923f 100755 --- a/cmd/arcrun-app/daemon-version.py +++ b/cmd/arcrun-app/daemon-version.py @@ -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") diff --git a/cmd/arcrun-app/supervise.go b/cmd/arcrun-app/supervise.go index 414974b..e28fbd8 100644 --- a/cmd/arcrun-app/supervise.go +++ b/cmd/arcrun-app/supervise.go @@ -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 退避後再拉,狀態在 +// 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