diff --git a/chv-guest/build.sh b/chv-guest/build.sh index 365b0e8..f9d2d4e 100755 --- a/chv-guest/build.sh +++ b/chv-guest/build.sh @@ -216,8 +216,8 @@ if [ ! -f "$SCRIPT_DIR/chv-memory.py" ]; then echo "缺少 chv-memory.py(客内记忆文件脚本):$SCRIPT_DIR" >&2 exit 1 fi -# 对齐 Muse P6:privsep worker / 薄客户端 / 单元三件缺一即失败。 -for p6_file in chv-gitea-worker.py chv-sync-worker-client.py chv-gitea-worker.service; do +# 对齐 Muse P6:privsep worker / 薄客户端 / 单元 / 路径触发器四件缺一即失败。 +for p6_file in chv-gitea-worker.py chv-sync-worker-client.py chv-gitea-worker.service chv-gitea-worker.path; do if [ ! -f "$SCRIPT_DIR/$p6_file" ]; then echo "缺少 $p6_file(Muse P6 客机文件):$SCRIPT_DIR" >&2 exit 1 @@ -347,7 +347,12 @@ VC_ARGS=(-a "$OUT/disk.raw" --copy-in "$SCRIPT_DIR/chv-authd.service:/etc/systemd/system/" --run-command 'mv /usr/local/bin/authd.py /usr/local/bin/chv-authd && chmod +x /usr/local/bin/chv-authd /usr/local/bin/git-credential-chv-authd' --run-command 'systemctl enable chv-authd || true' - --run-command 'printf "runcmd:\n - systemctl restart ssh || service ssh restart || true\n - systemctl start chv-authd || true\n - test -f /etc/chv/guest-env && chown chv:chv /etc/chv/guest-env || true\n - systemctl start chv-gitea-worker || true\n - /usr/local/bin/chv-sync boot >>/var/log/chv-sync.log 2>&1 || true\n" > /etc/cloud/cloud.cfg.d/99-chv-sync.cfg' + # round-12 修复轮第三轮(缺口 ①):runcmd 只启动 .path 单元 + #(开机时它已由 enable 激活,这里是兜底),绝不直接 start 服务 + #——cloud-final 此时工作区尚未 clone,直接 start 服务会在命名 + # 空间搭建时 226;.path 单元在 boot 的 mkdir -p 建出工作区后 + # 才触发服务启动。 + --run-command 'printf "runcmd:\n - systemctl restart ssh || service ssh restart || true\n - systemctl start chv-authd || true\n - test -f /etc/chv/guest-env && chown chv:chv /etc/chv/guest-env || true\n - systemctl start chv-gitea-worker.path || true\n - /usr/local/bin/chv-sync boot >>/var/log/chv-sync.log 2>&1 || true\n" > /etc/cloud/cloud.cfg.d/99-chv-sync.cfg' --copy-in "$SCRIPT_DIR/chv-cdp-broker.py:/usr/local/bin/" --copy-in "$SCRIPT_DIR/chv_browser_guard.py:/usr/local/bin/" # round-12 修复轮(项 0):c3bf9fd 认领——browser-blocklist.txt @@ -365,8 +370,9 @@ VC_ARGS=(-a "$OUT/disk.raw" --copy-in "$SCRIPT_DIR/chv-gitea-worker.py:/usr/local/bin/" --copy-in "$SCRIPT_DIR/chv-sync-worker-client.py:/usr/local/bin/" --copy-in "$SCRIPT_DIR/chv-gitea-worker.service:/etc/systemd/system/" + --copy-in "$SCRIPT_DIR/chv-gitea-worker.path:/etc/systemd/system/" --run-command 'mv /usr/local/bin/chv-gitea-worker.py /usr/local/bin/chv-gitea-worker && mv /usr/local/bin/chv-sync-worker-client.py /usr/local/bin/chv-sync-worker-client && chmod +x /usr/local/bin/chv-gitea-worker /usr/local/bin/chv-sync-worker-client' - --run-command 'systemctl enable chv-gitea-worker || true' + --run-command 'systemctl enable chv-gitea-worker chv-gitea-worker.path || true' # round-11 真机修复(层 5):镜像不再设 credential.helper store #(authd helper 是唯一凭证 helper);P4 的 copy-in 行保留。 diff --git a/chv-guest/buildinfo.py b/chv-guest/buildinfo.py index f6ed314..839ab4f 100644 --- a/chv-guest/buildinfo.py +++ b/chv-guest/buildinfo.py @@ -48,9 +48,11 @@ BASE_CONTENTS = [ # 对齐 Muse P4:单元外 CDP broker + 浏览器安全共享模块 + # 静态拦截名单。 "chv-cdp-broker", "chv-browser-guard", "browser-blocklist", - # 对齐 Muse P6:privsep worker + 薄客户端 + 单元(无条件拷入)。 + # 对齐 Muse P6:privsep worker + 薄客户端 + 单元 + 路径触发器 + #(无条件拷入)。路径触发器是 round-12 修复轮第三轮缺口 ① + # 的拉起机制(工作区出现才启动服务),与单元同款登记。 "chv-gitea-worker", "chv-sync-worker-client", - "chv-gitea-worker.service", + "chv-gitea-worker.service", "chv-gitea-worker.path", ] diff --git a/chv-guest/check_lockfile.py b/chv-guest/check_lockfile.py index 72e7a0a..3f7cabf 100644 --- a/chv-guest/check_lockfile.py +++ b/chv-guest/check_lockfile.py @@ -49,10 +49,11 @@ DECLARED_SOURCES = { "chv-cdp-broker": "repo_commit", "chv-browser-guard": "repo_commit", "browser-blocklist": "repo_commit", - # 对齐 Muse P6:worker 三件 + 叶子模块(privsep/safety)。 + # 对齐 Muse P6:worker 四件(+ 路径触发器)+ 叶子模块(privsep/safety)。 "chv-gitea-worker": "repo_commit", "chv-sync-worker-client": "repo_commit", "chv-gitea-worker.service": "repo_commit", + "chv-gitea-worker.path": "repo_commit", "cloud/chv/privsep.py": "repo_commit", "cloud/chv/checkout_detect.py": "repo_commit", "cloud/chv/safety.py": "repo_commit", diff --git a/chv-guest/chv-gitea-worker.path b/chv-guest/chv-gitea-worker.path new file mode 100644 index 0000000..052a34f --- /dev/null +++ b/chv-guest/chv-gitea-worker.path @@ -0,0 +1,20 @@ +[Unit] +Description=Start chv-gitea-worker once the sync workspace exists (feat-chv-muse-p6) +# round-12 修复轮第三轮(缺口 ①):worker 的 ReadWritePaths 含 +# /home/chv/workspace,systemd 建 mount namespace 时该路径必须已 +# 存在;工作区由 cloud-final 的 chv-sync boot 克隆出来,晚于 +# multi-user.target 约 9 秒(R12 复测 2:worker 07:23:31 首启 +# 226,工作区 07:23:40.40 才出现,5 次启动预算同一秒内耗尽)。 +# 本单元只在工作区出现时触发服务启动——服务绝不在工作区出现前 +# 启动,命名空间搭建前即有保证。须与服务的 ConditionPathExists +# 配对:Condition 挡住开机那次过早启动(干净跳过、不 226),本 +# 单元提供 Condition 本身没有的“出现后再试一次”。 +# 不用 StartLimit 放宽(仍是时序赌博,wake 时间在负载下已翻倍, +# 预算仍可能耗尽)与 tmpfiles 补行(/home/chv 本身晚于 +# tmpfiles-setup,补了也没用),见服务单元 [Unit] 注释。 + +[Path] +PathExists=/home/chv/workspace + +[Install] +WantedBy=multi-user.target diff --git a/chv-guest/chv-gitea-worker.service b/chv-guest/chv-gitea-worker.service index dc954fa..0d195d4 100644 --- a/chv-guest/chv-gitea-worker.service +++ b/chv-guest/chv-gitea-worker.service @@ -4,6 +4,20 @@ After=network-online.target chv-authd.service Wants=network-online.target # authd 是可选依赖:worker 的 git 经 P3 helper 向 authd 取凭证; # authd 没起来时 worker 的 git 调用直接拒绝,不静默用旧凭证。 +# 开机时序(round-12 修复轮第三轮,缺口 ①):工作区 +# /home/chv/workspace 由 cloud-final 的 chv-sync boot 克隆出来, +# 晚于 multi-user.target 约 9 秒;而 ReadWritePaths 的路径必须在 +# systemd 建 mount namespace 时已存在,否则 226/NAMESPACE。 +# ConditionPathExists 让开机那次启动在工作区出现前干净跳过 +# (不耗重启预算、不 226);真正拉起靠 chv-gitea-worker.path +# (工作区一出现即触发启动)。Condition 本身绝不重试——没有 +# .path 单元它就是一次跳过、服务永远不起;反过来只有 .path 而 +# 无 Condition,开机那次启动仍会在命名空间搭建时 226 撞上限速。 +# 两者缺一不可。StartLimit 不放宽:放宽只是把竞态窗口拉大, +# wake 时间在负载下已翻倍(R12 复测 2),预算仍可能耗尽; +# tmpfiles.d 也不补工作区行:/home/chv 本身 07:23:40 才出现, +# 晚于 tmpfiles-setup(07:23:24),补了也没用。 +ConditionPathExists=/home/chv/workspace [Service] Type=simple @@ -28,8 +42,10 @@ SystemCallFilter=@system-service # 下 systemd 建 mount namespace 的硬要求,缺一个即 226/NAMESPACE): # /run/chv-gitea 与 /var/log/chv-gitea-worker.log 由 # /etc/tmpfiles.d/chv.conf 预建(build.sh 内联写,round-12 修复轮 -# 第二轮项 1);/home/chv/workspace 开机时已在(R12 复测的 226 -# 只报过前两处)。ExecStartPre 不能替代 tmpfiles.d——命名空间 +# 第二轮项 1);/home/chv/workspace 由 ConditionPathExists + +# chv-gitea-worker.path 守门(round-12 修复轮第三轮缺口 ①,见 +# [Unit];R12 复测 2 钉死它才是剩下的 226——开机时序竞态,不要 +# 再写“开机时已在”)。ExecStartPre 不能替代 tmpfiles.d——命名空间 # 搭建在它之前,226 发生在它运行之前。 ReadWritePaths=/home/chv/workspace /run/chv-gitea /var/log/chv-gitea-worker.log # worker 崩溃不拖垮 agent:broker/薄 CLI 收到类型化失败即返回, diff --git a/chv-guest/generate_lockfile.py b/chv-guest/generate_lockfile.py index 41be25d..339c6ec 100644 --- a/chv-guest/generate_lockfile.py +++ b/chv-guest/generate_lockfile.py @@ -71,10 +71,11 @@ DECLARED_SOURCES = { "chv-cdp-broker": "repo_commit", "chv-browser-guard": "repo_commit", "browser-blocklist": "repo_commit", - # 对齐 Muse P6:worker 三件 + 叶子模块(privsep/safety)。 + # 对齐 Muse P6:worker 四件(+ 路径触发器)+ 叶子模块(privsep/safety)。 "chv-gitea-worker": "repo_commit", "chv-sync-worker-client": "repo_commit", "chv-gitea-worker.service": "repo_commit", + "chv-gitea-worker.path": "repo_commit", "cloud/chv/privsep.py": "repo_commit", "cloud/chv/checkout_detect.py": "repo_commit", "cloud/chv/safety.py": "repo_commit", diff --git a/cloud/tests/test_chv_muse_p2.py b/cloud/tests/test_chv_muse_p2.py index 8117842..a4dbedf 100644 --- a/cloud/tests/test_chv_muse_p2.py +++ b/cloud/tests/test_chv_muse_p2.py @@ -633,7 +633,10 @@ class BuildDryRunTest(unittest.TestCase): # round-12 修复轮(项 2,P6):worker 三件亦 # 是 build.sh 硬校验,夹具须一并拷入。 "chv-gitea-worker.py", "chv-sync-worker-client.py", - "chv-gitea-worker.service"): + "chv-gitea-worker.service", + # round-12 修复轮第三轮(缺口 ①):.path 单元 + # 亦是 P6 预检四件之一,夹具须一并拷入。 + "chv-gitea-worker.path"): shutil.copy2(ROOT / "chv-guest" / name, os.path.join(guest, name)) shutil.copy2(ROOT / "host.py", os.path.join(repo, "host.py")) @@ -715,6 +718,7 @@ class BuildDryRunTest(unittest.TestCase): # 三件进镜像,干跑输出须含拷入与 enable。 "chv-gitea-worker", "chv-gitea-worker.service", + "chv-gitea-worker.path", "systemctl enable chv-gitea-worker", # round-12 修复轮(项 4,P10):结账检测 # 模块进镜像,干跑输出须含条件拷入行。 diff --git a/cloud/tests/test_chv_muse_p4.py b/cloud/tests/test_chv_muse_p4.py index 780e117..b75d653 100644 --- a/cloud/tests/test_chv_muse_p4.py +++ b/cloud/tests/test_chv_muse_p4.py @@ -859,7 +859,10 @@ class BuildDryRunTest(unittest.TestCase): # 是 build.sh 硬校验,夹具须一并拷入。 "chv-gitea-worker.py", "chv-sync-worker-client.py", - "chv-gitea-worker.service"): + "chv-gitea-worker.service", + # round-12 修复轮第三轮(缺口 ①):.path + # 单元亦是 P6 预检四件之一,夹具须一并拷入。 + "chv-gitea-worker.path"): shutil.copy2(ROOT / "chv-guest" / name, os.path.join(guest, name)) shutil.copy2(ROOT / "host.py", os.path.join(repo, "host.py")) @@ -920,6 +923,7 @@ class BuildDryRunTest(unittest.TestCase): # 三件进镜像,干跑输出须含拷入与 enable。 "chv-gitea-worker", "chv-gitea-worker.service", + "chv-gitea-worker.path", "systemctl enable chv-gitea-worker", # round-12 修复轮(项 4,P10):结账检测 # 模块进镜像,干跑输出须含条件拷入行。 diff --git a/cloud/tests/test_chv_muse_p6.py b/cloud/tests/test_chv_muse_p6.py index ee010bc..450f5b1 100644 --- a/cloud/tests/test_chv_muse_p6.py +++ b/cloud/tests/test_chv_muse_p6.py @@ -870,6 +870,93 @@ class WorkerUnitTmpfilesTest(unittest.TestCase): self.assertNotIn("ExecStartPre=", text) +# -- (7c) worker 开机时序竞态:Condition + path 触发 ---------------------- +class WorkerUnitBootRaceTest(unittest.TestCase): + """round-12 修复轮第三轮(缺口 ①,P6):R12 复测 2 钉死剩下的 + 226 只点名 /home/chv/workspace——工作区由 cloud-final 的 + chv-sync boot 克隆出来,晚于 multi-user.target 约 9 秒,worker + 的 5 次启动预算在工作区出现前耗尽。 + + 修法选 (a)(Condition + systemd.path),不用 (b)/(c): + - (a) 让服务根本不在工作区出现前启动:ConditionPathExists 把 + 开机那次启动挡在命名空间搭建之前(干净跳过、不 226、不耗 + 重启预算),.path 单元在工作区出现时再触发一次——Condition + 本身绝不重试,没有 .path 它就是一次跳过、服务永远不起。 + - (b) 放宽 StartLimit 只是把竞态窗口拉大:wake 时间在负载下 + 已翻倍,预算仍可能耗尽,仍是时序赌博。 + - (c) 把工作区创建提前到 tmpfiles-setup 之前不可行: + /home/chv 本身 07:23:40 才出现(cloud-init 建 chv 用户), + 晚于 tmpfiles-setup(07:23:24);硬约束:绝不盲目补 + tmpfiles 行。 + ExecStartPre 也救不了 226——命名空间搭建在它之前。 + """ + + def _service_text(self): + return (REPO_ROOT / "chv-guest" + / "chv-gitea-worker.service").read_text() + + def _path_text(self): + return (REPO_ROOT / "chv-guest" + / "chv-gitea-worker.path").read_text() + + def _build_text(self): + return (REPO_ROOT / "chv-guest" / "build.sh").read_text() + + def test_service_gated_by_condition(self): + self.assertIn( + "ConditionPathExists=/home/chv/workspace", + self._service_text()) + + def test_no_start_limit_widening(self): + # 修法是 (a) 不是 (b):重启预算保持 systemd 默认,绝不 + # 靠放宽 StartLimit 赌工作区出现。 + text = self._service_text() + self.assertNotIn("StartLimitBurst=", text) + self.assertNotIn("StartLimitIntervalSec=", text) + + def test_path_unit_triggers_on_workspace(self): + text = self._path_text() + self.assertIn("PathExists=/home/chv/workspace", text) + self.assertIn("WantedBy=multi-user.target", text) + # 不写 Unit=:默认触发同名 .service,与服务单元配对。 + self.assertNotIn("Unit=", text) + + def test_service_still_wanted_by_multi_user(self): + # WantedBy 保留:is-enabled 仍为 enabled;开机那次启动 + # 只会被 Condition 干净跳过,不会 226。 + self.assertIn("WantedBy=multi-user.target", + self._service_text()) + + def test_build_enables_path_unit(self): + text = self._build_text() + self.assertIn("chv-gitea-worker.path:/etc/systemd/system/", + text) + self.assertIn( + "systemctl enable chv-gitea-worker chv-gitea-worker.path", + text) + + def test_runcmd_starts_path_unit_not_service(self): + text = self._build_text() + self.assertIn("systemctl start chv-gitea-worker.path", text) + # cloud-final 时工作区尚未 clone,直接 start 服务会 226。 + self.assertNotIn("systemctl start chv-gitea-worker || true", + text) + + def test_no_tmpfiles_line_for_workspace(self): + # 硬约束:绝不盲目补 tmpfiles 行——/home/chv 本身晚于 + # tmpfiles-setup,补了也没用。 + self.assertNotIn("d /home/chv/workspace", self._build_text()) + + def test_buildinfo_and_lockfile_include_path_unit(self): + self.assertIn("chv-gitea-worker.path", + (REPO_ROOT / "chv-guest" + / "buildinfo.py").read_text()) + for name in ("check_lockfile.py", "generate_lockfile.py"): + self.assertIn( + '"chv-gitea-worker.path": "repo_commit"', + (REPO_ROOT / "chv-guest" / name).read_text()) + + class BuildInfoContentsTest(unittest.TestCase): """round-12 修复轮(项 2,P6):worker 三件须进 BUILDINFO contents(无条件拷入,与 P2/P4 同款钉法)。""" @@ -900,7 +987,8 @@ class BuildInfoContentsTest(unittest.TestCase): out, "chv-guest-24.04-0.164.0-alpha.9-abc1234", "24.04", "0.164.0-alpha.9", "abc1234", "0" * 64, True, "1248") for item in ("chv-gitea-worker", "chv-sync-worker-client", - "chv-gitea-worker.service"): + "chv-gitea-worker.service", + "chv-gitea-worker.path"): self.assertIn(item, info["contents"]) diff --git a/docs/chv-muse-p6-gap.md b/docs/chv-muse-p6-gap.md index 791066b..1af1be1 100644 --- a/docs/chv-muse-p6-gap.md +++ b/docs/chv-muse-p6-gap.md @@ -58,7 +58,13 @@ P3(authd/sentinel 本体——只读、不重写、不改接口)、不碰 P4 单元外;`ProtectSystem=strict`、`PrivateTmp`、`NoNewPrivileges`、 `CapabilityBoundingSet=` 空、`RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6`、`SystemCallFilter=@system-service`、读写路径只限 - `/home/chv/workspace` 与 `/run/chv-gitea`)。 + `/home/chv/workspace` 与 `/run/chv-gitea`)。round-12 修复轮第三轮 + (缺口 ①):加 `ConditionPathExists=/home/chv/workspace`(开机那次 + 启动在工作区出现前干净跳过、不 226)+ 新单元 + `chv-guest/chv-gitea-worker.path`(`PathExists=/home/chv/workspace`, + 工作区一出现即触发启动;两者缺一不可——Condition 本身绝不重试)。 + runcmd 只启动 .path 单元,不直接 start 服务;StartLimit 不放宽, + tmpfiles.d 不补工作区行(见服务单元注释)。 - `chv-guest/chv-sync-worker-client.py`(新,标准库薄客户端): `chv-sync pull/push/status` 在 worker socket 存在时改走 worker (发类型化请求、原样回显 worker 的 stdout/stderr、按 worker 的 @@ -156,7 +162,9 @@ Base64、零宽字符)、藏在图片/附件/二进制里的指令、需要理 ## 4. 真机待验证清单(等测试机空闲,排队在 round-11 之后) - worker 在 lite12 镜像里的启动时序(须在 broker/`chv-sync` 调用 - 之前就绪;与 P3 authd 的启动顺序关系)。 + 之前就绪;与 P3 authd 的启动顺序关系)。round-12 修复轮第三轮 + (缺口 ①)已改 Condition + .path 触发,待真机复测 3 验证 5 台 + VM 0 failed。 - worker 的 systemd 沙箱在真机上生效(`systemctl status`、cgroup 路径、受限调用被挡的表现)。 - broker → worker → helper → authd → git 全链路在真机上跑通