Files
codex-app-server/chv-guest/buildinfo.py
T
host e31354dc58 fix(chv-muse-r12fix3-1): worker 开机时序竞态:Condition + path 触发,不赌重启预算(P6 缺口 ①)
R12 复测 2 钉死:剩下的 226 只点名 /home/chv/workspace——工作区由
cloud-final 的 chv-sync boot 克隆出来,晚于 multi-user.target 约
9 秒,worker 的 5 次启动预算在工作区出现前耗尽(5 台 VM 2 台
failed,手工 reset-failed + start 后立即 active,二进制无辜)。

修法三选一,选 (a)——让服务根本不在工作区出现前启动:
新单元 chv-gitea-worker.path(PathExists=/home/chv/workspace,
工作区一出现即触发启动)+ 服务加
ConditionPathExists=/home/chv/workspace(开机那次启动在命名空间
搭建之前干净跳过,不 226、不耗重启预算)。两者缺一不可:
Condition 本身绝不重试,没有 .path 它就是一次跳过、服务永远不
起;只有 .path 而无 Condition,开机那次启动仍会在命名空间搭建
时 226 撞上限速。WantedBy=multi-user.target 保留(is-enabled 仍
为 enabled),runcmd 只启动 .path 单元(cloud-final 时工作区尚
未 clone,直接 start 服务会 226);.path 进 buildinfo contents
与两处 lockfile 登记,P6 预检由三件扩为四件。

不用 (b) 放宽 StartLimit:放宽只是把竞态窗口拉大,wake 时间在
负载下已翻倍(R12 复测 2),预算仍可能耗尽,仍是时序赌博。
不用 (c) 把工作区创建提前到 tmpfiles-setup 之前:/home/chv 本身
07:23:40 才出现(cloud-init 建 chv 用户),晚于 tmpfiles-setup
(07:23:24);硬约束:绝不盲目补 tmpfiles 行。ExecStartPre 也不
行——命名空间搭建在它之前,救不了 226。

测试:WorkerUnitBootRaceTest 新增 8 项——Condition 在位、
StartLimit 未放宽(修法语义钉)、.path 触发配对(PathExists +
WantedBy、无 Unit= 覆盖)、服务 WantedBy 保留、build.sh 启用
.path + runcmd 只起 .path、绝无工作区 tmpfiles 行、buildinfo 与
lockfile 登记;BuildInfoContentsTest 的 P6 件加 .path;P2/P4 干
跑夹具一并拷入 .path。P6/P2/P4 三模块 174 项全过。
2026-11-08 09:45:48 +00:00

200 lines
8.4 KiB
Python
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
"""生成 CHV 客机镜像的 BUILDINFO.json(build.sh 调用)。
从 build.sh 抽出(B-02),使其可独立单元测试。输入全部是
已落盘的构建产物与读回文件,不编造版本号:versions 来自
客机读回的 dpkg-query / pip freeze(Playwright=1 时另有
ms-playwright 缓存目录名作 chromium 标识);artifacts 的
sha256/size 是对产物文件实测的;image_sha256 即 disk.raw
的 sha256,供 generate_lockfile.py 沿用。
用法:
python3 buildinfo.py OUT_DIR TAG UBUNTU CODEX_VERSION \\
REPO_COMMIT UBUNTU_IMAGE_SHA256 PLAYWRIGHT(0|1)
[CHROMIUM_HEADLESS_SHELL_REV]
CHROMIUM_HEADLESS_SHELL_REV(可选,纯数字):钉死的
ms-playwright 缓存修订号(如 1248)。PLAYWRIGHT=1 且
给了该值时,versions.chromium 里须恰好出现
chromium_headless_shell-<rev>,否则退出 1、构建失败;
未给时只记录、不强制,BUILDINFO.chromium_pinned_rev
记 null,构建输出由 build.sh 明说“未钉死”。
"""
import hashlib
import json
import os
import re
import sys
import time
# 轻量口径(2026-11-06 用户定):Ubuntu Minimal 底座,只装
# codex + Python + SSH + cloud-init + 薄 agent + git + gt +
# 最小中文字体;已裁剪 pandoc/texlive/python-docx 等文档工具
# 与 qemu-guest-agent。chv-sync 是仓库内脚本,版本随 repo_commit。
BASE_CONTENTS = [
"git", "openssh-server", "openssh-client", "cloud-init",
"python3", "python3-pip", "curl", "ca-certificates",
"xz-utils", "fonts-wqy-microhei", "fontconfig", "codex",
"codex-code-mode-host", "codex-resources/bwrap",
"/app/host.py", "gt", "chv-sync", "chv-memory",
# 对齐 Muse P2:nspawn 单元启动器 + broker + 浏览器子代理。
"systemd-container", "chv-cell", "chv-broker", "chv-browser",
# 对齐 Muse P3(round-11 合入镜像):authd 守护进程 + git
# credential helper + systemd 单元(三件套是无条件拷入的)。
# 云端叶子模块(cloud/chv/authd.py、sentinel.py)不进 BASE:
# 它们是条件拷入(缺失只警告、不失败),存在时才经
# extra_contents 登记,绝不在缺失的构建里谎称已进镜像。
"chv-authd", "git-credential-chv-authd", "chv-authd.service",
# 对齐 Muse P4:单元外 CDP broker + 浏览器安全共享模块 +
# 静态拦截名单。
"chv-cdp-broker", "chv-browser-guard", "browser-blocklist",
# 对齐 Muse P6:privsep worker + 薄客户端 + 单元 + 路径触发器
#(无条件拷入)。路径触发器是 round-12 修复轮第三轮缺口 ①
# 的拉起机制(工作区出现才启动服务),与单元同款登记。
"chv-gitea-worker", "chv-sync-worker-client",
"chv-gitea-worker.service", "chv-gitea-worker.path",
]
# 执行器默认内核 cmdline(config.py),构建产物须与它一起记录(B-12),
# 否则换宿主/换执行器配置时无法复现。
KERNEL_CMDLINE = "console=hvc0 root=/dev/vda1 rw"
# 浏览器装在系统路径(B-22),不是某用户的 ~/.cache:构建时
# PLAYWRIGHT_BROWSERS_PATH 指这里,agent 运行时也须设同一变量。
PLAYWRIGHT_BROWSERS_PATH = "/usr/local/share/ms-playwright"
def _read_lines(path):
try:
with open(path) as fh:
return [line.strip() for line in fh if line.strip()]
except OSError:
return []
def _sha256_size(path):
digest = hashlib.sha256()
size = 0
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(1 << 20), b""):
digest.update(chunk)
size += len(chunk)
return digest.hexdigest(), size
def build(outdir, tag, ubuntu, codex_version, repo_commit,
ubuntu_image_sha256, playwright, chromium_pinned_rev=None,
extra_contents=None):
disk_path = os.path.join(outdir, "disk.raw")
vmlinux_path = os.path.join(outdir, "vmlinux")
disk_sha, disk_size = _sha256_size(disk_path)
vmlinux_sha, vmlinux_size = _sha256_size(vmlinux_path)
contents = list(BASE_CONTENTS)
# extra_contents:构建时才确定的附加内容(如 P1 的 vsock_notify,
# 文件存在才拷入)。调用方传什么记什么,不编默认值。
for item in extra_contents or []:
if item and item not in contents:
contents.append(item)
versions = {
"dpkg": _read_lines(
os.path.join(outdir, "guest-versions-dpkg.txt")),
"pip": _read_lines(
os.path.join(outdir, "guest-versions-pip.txt")),
"gt": _read_lines(
os.path.join(outdir, "guest-versions-gt.txt")),
}
gt_version = ""
for line in versions["gt"]:
m = re.search(r"(\d+\.\d+\.\d+)", line)
if m:
gt_version = m.group(1)
break
if playwright:
contents += ["playwright", "chromium-headless-shell"]
versions["chromium"] = _read_lines(
os.path.join(outdir, "guest-versions-chromium.txt"))
info = {
"tag": tag,
"ubuntu": ubuntu,
"codex_version": codex_version,
"repo_commit": repo_commit,
"gt_version": gt_version,
"built_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"contents": contents,
"versions": versions,
"ubuntu_image_sha256": ubuntu_image_sha256,
"ubuntu_image_sha256_set": True,
"kernel_cmdline": KERNEL_CMDLINE,
"playwright": bool(playwright),
"playwright_browsers_path": PLAYWRIGHT_BROWSERS_PATH
if playwright else "",
# feat-chv-closeup(M4):钉死的 Chromium 修订号(未设
# 记 null,绝不编一个默认值出来假装已钉死)。
"chromium_pinned_rev": chromium_pinned_rev or None,
"image_sha256": disk_sha,
"artifacts": {
"disk.raw": {"sha256": disk_sha, "size": disk_size},
"vmlinux": {"sha256": vmlinux_sha, "size": vmlinux_size},
},
}
path = os.path.join(outdir, "BUILDINFO.json")
with open(path, "w") as fh:
json.dump(info, fh, indent=2)
fh.write("\n")
return info
def main(argv):
if len(argv) not in (8, 9, 10):
print("用法:buildinfo.py OUT TAG UBUNTU CODEX_VERSION "
"REPO_COMMIT UBUNTU_IMAGE_SHA256 PLAYWRIGHT "
"[CHROMIUM_HEADLESS_SHELL_REV] [EXTRA_CONTENTS]", file=sys.stderr)
return 2
_, outdir, tag, ubuntu, codex_version, repo_commit, \
ubuntu_image_sha256, playwright = argv[:8]
chromium_pinned_rev = argv[8] if len(argv) >= 9 else None
# 第 10 参数:逗号分隔的附加内容(如 vsock_notify),可空。
extra_contents = None
if len(argv) == 10 and argv[9]:
extra_contents = [s for s in argv[9].split(",") if s]
if chromium_pinned_rev and not re.fullmatch(r"[0-9]+",
chromium_pinned_rev):
print("CHROMIUM_HEADLESS_SHELL_REV 须为纯数字:%s"
% chromium_pinned_rev, file=sys.stderr)
return 2
info = build(outdir, tag, ubuntu, codex_version, repo_commit,
ubuntu_image_sha256, playwright == "1",
chromium_pinned_rev, extra_contents)
if not info["versions"]["dpkg"]:
print("BUILDINFO versions.dpkg 为空", file=sys.stderr)
return 1
if not info["versions"]["pip"]:
print("BUILDINFO versions.pip 为空", file=sys.stderr)
return 1
if not info["gt_version"]:
print("BUILDINFO gt_version 为空(gt 未装好)", file=sys.stderr)
return 1
if playwright == "1" and not info["versions"].get("chromium"):
print("BUILDINFO versions.chromium 为空(浏览器未装好或"
"读回路径不对)", file=sys.stderr)
return 1
if playwright == "1" and chromium_pinned_rev:
# feat-chv-closeup(M4):钉死校验——读回的缓存目录名
# 须恰好是该修订号,playwright 升级导致目录名变了就
# 在这里失败,不静默换浏览器版本。
expected = "chromium_headless_shell-%s" % chromium_pinned_rev
if expected not in (info["versions"].get("chromium") or []):
print("Chromium 修订号与钉死值不符:读回 %s,钉死 %s"
% (info["versions"].get("chromium"), expected),
file=sys.stderr)
return 1
if info["tag"] != tag:
print("BUILDINFO tag 不一致", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))