Files
arcrun-collector/cmd/arcrun-app/daemon-version.py
T
Leo 6698f9fbda 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 並大聲警告。
2026-08-06 18:27:08 +08:00

172 lines
7.8 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""daemon-version.py — daemon 版本號的唯一產生器(2026-08-06 二版)
🔴 leo 08-06:「給版本號、本版更新內容、打包產品、顯示在前端,
這一整串都應該是機械化,**不應該每次手工做**」
── 病根(實測)────────────────────────────────────────────────────
manifest 宣告 daemon v0.18.5,線上產物拆開卻是 v0.18.4。
原因:本 repo **一個 git tag 都沒有**build 腳本寫的是 `git describe --tags`
⇒ 拿不到 v0.18.x ⇒ 版本只能靠打包時人工 `VERSION=v0.18.4 ./build-win.sh` 敲。
人工敲 ⇒ 兩條打包線(win/mac)各敲各的 ⇒ manifest 說謊。
── 為什麼不是「數 commit 數」(一版的做法,已否決)─────────────────
第一版用「動過 collector/ 的 commit 數」當 patch。確實不用手打,
但**每個 commit 都變成一版**(一天 5 個 commit 就跳 5 版),
而且每跳一版就要補一段 changelog——把機械化變成新的手工活。
── 現在的做法:版本由 changelog 決定,且不用手打數字 ──────────────
單一真相源= docs-site/src/content/docs/help/changelog.md(用戶語言那份)。
· 要出新版 ⇒ 在檔案最上面加一段標題 `## 下一版(未發佈)`,底下寫白話更新內容。
· 打包時本腳本把它**戳成正式版號**(上一版 patch + 1)並補上今天日期。
· 沒有「未發佈」段 ⇒ 版本=最上面那一版(重打同一版,不會虛增)。
⇒ **版本號從頭到尾沒有人打過**,而且「更新內容」與「版本」天生綁在一起——
不可能出現「版本升了但沒人知道改了什麼」,也不可能「寫了內容卻忘了升版」。
⇒ 不需要 git tagActions/輪詢(守 D20 紅線)。
換線(0.18 → 0.19):改 `DAEMON_LINE`,下一版就從 0.19.0 開始。
用法:
daemon-version.py # 印出版本(唯讀,不改檔)
daemon-version.py --stamp # 把「下一版(未發佈)」戳成正式版號+日期後印出
"""
import datetime
import hashlib
import json
import re
import subprocess
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
ROOT = Path(subprocess.check_output(
["git", "rev-parse", "--show-toplevel"], cwd=HERE, text=True).strip())
CHANGELOG = ROOT / "docs-site/src/content/docs/help/changelog.md"
LINE_FILE = ROOT / "DAEMON_LINE"
UNRELEASED = "## 下一版(未發佈)"
# 版本 → 當時原始碼指紋。用來擋「同一個版號、不同的執行檔」。
SOURCE_LOCK = HERE / ".version-source.json"
RELEASED_RE = re.compile(r"^## v(\d+)\.(\d+)\.(\d+)", re.M)
def die(msg):
print(msg, file=sys.stderr)
sys.exit(1)
def source_fingerprint():
"""會被編進執行檔的原始碼指紋(已提交的樹 + 未提交的差異)。
🔴 2026-08-06 補這道閘的由來:
戳完版號後我又改了 code,再打一次包,腳本判定「沒有未發佈段=重打同一版」
⇒ **兩個內容不同的執行檔都叫 v0.18.9**(實測 sha256 前 20 碼
63ec1077a29383b31379 vs 1ca73a5aef5bf4f90bd3)。
這正是 leo 08-05 點名的「版本號手寫=說謊溫床」換個形式復發——
機械產生版本號還不夠,還要保證**同一個版號只對應一份原始碼**。
"""
def sh(*args):
return subprocess.check_output(args, cwd=ROOT, text=True).strip()
try:
tree = sh("git", "rev-parse", "HEAD:collector")
except subprocess.CalledProcessError:
return "" # 不在 git 裡就不擋(例如從 tarball 解出來 build
diff = subprocess.run(["git", "diff", "HEAD", "--", "collector"],
cwd=ROOT, text=True, capture_output=True).stdout
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()
if not fp:
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)。
代表你在戳完版號之後又改了 code。再打一次會產生
**兩個內容不同、卻都叫 %s 的執行檔** —— 那就是「版本號說謊」。
要出新版:在 changelog 最上面加一段
## 下一版(未發佈)
- (用戶語言的更新內容)
然後重跑打包,版號會自動變成下一個。""" % (version, known, fp, version))
if stamp and lock.get(version) != fp:
lock[version] = fp
SOURCE_LOCK.write_text(json.dumps(lock, indent=1, ensure_ascii=False) + "\n")
def main():
stamp = "--stamp" in sys.argv
if not CHANGELOG.exists():
die("❌ 找不到 changelog%s" % CHANGELOG)
line = LINE_FILE.read_text().strip() if LINE_FILE.exists() else "0.18"
if not re.fullmatch(r"\d+\.\d+", line):
die("❌ DAEMON_LINE 必須是 MAJOR.MINOR(例 0.18),現在是 %r" % line)
text = CHANGELOG.read_text()
top = RELEASED_RE.search(text)
if not top:
die("❌ changelog 裡找不到任何 `## vX.Y.Z(…)` 的版本段落")
prev = (int(top.group(1)), int(top.group(2)), int(top.group(3)))
has_unreleased = UNRELEASED in text
if has_unreleased and text.index(UNRELEASED) > top.start():
die("❌ `%s` 必須放在所有已發佈版本的**上面**(它代表還沒出的那一版)" % UNRELEASED)
if not has_unreleased:
# 沒有待發佈內容=重打同一版。冪等,不虛增。
version = "v%d.%d.%d" % prev
check_or_record(version, stamp)
if stamp:
print(" ️ changelog 沒有「下一版(未發佈)」段,版本維持 %s(重打同一版)"
% version, file=sys.stderr)
print(version)
return
# 有待發佈內容 ⇒ 升版。換線時從該線的 .0 開始。
major, minor = (int(x) for x in line.split("."))
if (major, minor) != (prev[0], prev[1]):
nxt = (major, minor, 0)
else:
nxt = (prev[0], prev[1], prev[2] + 1)
version = "v%d.%d.%d" % nxt
if stamp:
check_or_record(version, True)
today = datetime.date.today().isoformat()
heading = "## %s%s" % (version, today)
CHANGELOG.write_text(text.replace(UNRELEASED, heading, 1))
print(" ✅ changelog:「下一版(未發佈)」→ %s" % heading, file=sys.stderr)
print(version)
if __name__ == "__main__":
main()