From 4a7ca25334031a16709224bf42f31a27311f7653 Mon Sep 17 00:00:00 2001 From: host Date: Sat, 7 Nov 2026 04:01:24 +0000 Subject: [PATCH] =?UTF-8?q?fix(chv-muse-r11fix):=20browser.sock=20?= =?UTF-8?q?=E7=A7=BB=E8=BF=9B=20chv-browser=20=E8=87=AA=E6=9C=89=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=EF=BC=88=E7=BC=BA=E9=99=B7=204=EF=BC=89+=20vsock=20?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=90=AF=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 缺陷 4:/run/chv 是 0755 root root,chv-browser 在 bind 时 EACCES 而无限 crash loop(restart 21)。browser.sock 从 /run/chv/browser.sock 移到 /run/chv-browser/browser.sock (broker 与 browser 两端常量同改);build.sh 建目录 + tmpfiles 登记(0755 chv-browser 所有)。不用组可写 /run/chv——那会给 chv-browser 对 broker.sock/cell.pid 的 unlink 能力。 vsock 自动启动:此前 build.sh 只拷 vsock_notify.py、不建服务, vsock 通道从未自动启动。按 vsock_notify.py 的 docstring 补 vsock-notify.service(ExecStart=/usr/local/bin/vsock-notify %H, Restart=no,After/WantedBy=multi-user.target)并 enable; printf 格式串里 %H 写 %%H。测试:干跑针 /run/chv-browser; vsock 在场时干跑含服务与 enable 语句。 --- chv-guest/build.sh | 14 ++-- chv-guest/chv-broker.py | 7 +- chv-guest/chv-browser.py | 5 +- cloud/tests/test_chv_muse_p2.py | 138 +++++++++++++++++++------------- 4 files changed, 103 insertions(+), 61 deletions(-) diff --git a/chv-guest/build.sh b/chv-guest/build.sh index e33c10e..819ae92 100755 --- a/chv-guest/build.sh +++ b/chv-guest/build.sh @@ -316,10 +316,10 @@ VC_ARGS=(-a "$OUT/disk.raw" --run-command 'printf "ssh_deletekeys: false\nbootcmd:\n - \"[ -f /etc/ssh/ssh_host_ed25519_key ] || ssh-keygen -A\"\n" > /etc/cloud/cloud.cfg.d/99-chv-hostkey.cfg; systemctl enable ssh || true' --run-command 'printf "PasswordAuthentication no\nPermitRootLogin prohibit-password\nChallengeResponseAuthentication no\nKbdInteractiveAuthentication no\nPubkeyAuthentication yes\n" > /etc/ssh/sshd_config.d/99-chv.conf && mkdir -p /run/sshd && ssh-keygen -A && sshd -t && grep -q "^PasswordAuthentication no" /etc/ssh/sshd_config.d/99-chv.conf && grep -q "^PermitRootLogin prohibit-password" /etc/ssh/sshd_config.d/99-chv.conf && rm -f /etc/ssh/ssh_host_* && test -z "$(ls /etc/ssh/ssh_host_* 2>/dev/null)"' --run-command 'dpkg-query -W -f "\${Package} \${Version}\n" > /app/guest-versions-dpkg.txt; pip3 freeze > /app/guest-versions-pip.txt || true' - --run-command 'useradd --system --home-dir /var/lib/chv-browser --shell /usr/sbin/nologin chv-browser && mkdir -p /var/lib/chv-browser /run/chv/shots && chown chv-browser:chv-browser /var/lib/chv-browser /run/chv/shots && chmod 0755 /run/chv /run/chv/shots' + --run-command 'useradd --system --home-dir /var/lib/chv-browser --shell /usr/sbin/nologin chv-browser && mkdir -p /var/lib/chv-browser /run/chv/shots /run/chv-browser && chown chv-browser:chv-browser /var/lib/chv-browser /run/chv/shots /run/chv-browser && chmod 0755 /run/chv /run/chv/shots /run/chv-browser' --run-command 'printf "[Unit]\nDescription=CHV cell broker (Muse P2)\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=/usr/bin/python3 /usr/local/bin/chv-broker\nRestart=on-failure\nRestartSec=2\n\n[Install]\nWantedBy=multi-user.target\n" > /etc/systemd/system/chv-broker.service' --run-command 'printf "[Unit]\nDescription=CHV browser sub-agent (Muse P2)\nAfter=network.target\n\n[Service]\nType=simple\nUser=chv-browser\nGroup=chv-browser\nExecStart=/usr/bin/python3 /usr/local/bin/chv-browser\nRestart=on-failure\nRestartSec=2\n\n[Install]\nWantedBy=multi-user.target\n" > /etc/systemd/system/chv-browser.service' - --run-command 'printf "d /run/chv 0755 root root -\nd /run/chv/shots 0755 chv-browser chv-browser -\n" > /etc/tmpfiles.d/chv.conf && systemctl enable chv-broker chv-browser || true') + --run-command 'printf "d /run/chv 0755 root root -\nd /run/chv/shots 0755 chv-browser chv-browser -\nd /run/chv-browser 0755 chv-browser chv-browser -\n" > /etc/tmpfiles.d/chv.conf && systemctl enable chv-broker chv-browser || true') # P3 交接(Muse P3 的 authd/sentinel 叶子模块):文件在仓库里存在时 # 拷入镜像(本分支已有,合入后自动生效);不存在时只警告、不失败—— # M4 既定 build.sh 不硬要求 cloud/,见上。缺叶子模块时 chv-authd @@ -338,12 +338,16 @@ if [ "$P3_LIBS_PRESENT" = "1" ]; then --run-command 'mv /usr/local/lib/chv-authd/cloud/chv/authd_init_stub.py /usr/local/lib/chv-authd/cloud/chv/__init__.py') fi # P1 交接(Muse P1 的 vsock_notify.py):文件在仓库里存在时拷入 -# 镜像(P1 分支才有,本分支尚无);不存在时只警告、不失败—— -# 合并后自动生效,P2 不改该脚本内容。 +# 镜像并建 vsock-notify.service(自动启动,round-11 真机修复: +# 此前只拷文件、不建服务,vsock 通道从未自动启动;服务形状按 +# vsock_notify.py 的 docstring:ExecStart=/usr/local/bin/ +# vsock-notify %H,Restart=no,After/WantedBy=multi-user.target; +# printf 格式串里 %H 须写 %%H);不存在时只警告、不失败。 VSOCK_NOTIFY_PRESENT=0 if [ -f "$REPO_DIR/cloud/chv/guest/vsock_notify.py" ]; then VC_ARGS+=(--copy-in "$REPO_DIR/cloud/chv/guest/vsock_notify.py:/usr/local/bin/" - --run-command 'mv /usr/local/bin/vsock_notify.py /usr/local/bin/vsock-notify && chmod +x /usr/local/bin/vsock-notify') + --run-command 'mv /usr/local/bin/vsock_notify.py /usr/local/bin/vsock-notify && chmod +x /usr/local/bin/vsock-notify' + --run-command 'printf "[Unit]\nDescription=CHV vsock notify (Muse P1)\nAfter=multi-user.target\n\n[Service]\nType=simple\nExecStart=/usr/local/bin/vsock-notify %%H\nRestart=no\n\n[Install]\nWantedBy=multi-user.target\n" > /etc/systemd/system/vsock-notify.service && systemctl enable vsock-notify || true') VSOCK_NOTIFY_PRESENT=1 else echo "警告:cloud/chv/guest/vsock_notify.py 不在仓库里(P1 未合并),镜像暂不含 vsock notify" >&2 diff --git a/chv-guest/chv-broker.py b/chv-guest/chv-broker.py index 9a82df2..13b7271 100644 --- a/chv-guest/chv-broker.py +++ b/chv-guest/chv-broker.py @@ -31,7 +31,12 @@ import subprocess import sys BROKER_SOCK = "/run/chv/broker.sock" -BROWSER_SOCK = "/run/chv/browser.sock" +# round-11 真机修复(缺陷 4):browser.sock 从 /run/chv 移到 +# chv-browser 自有目录——/run/chv 是 0755 root root, +# chv-browser 在 bind 时 EACCES 而无限 crash loop(restart 21); +# 放进组可写的 /run/chv 会给 chv-browser 对 broker.sock/cell.pid +# 的 unlink 能力,所以用独立目录(0755 chv-browser 所有)。 +BROWSER_SOCK = "/run/chv-browser/browser.sock" CELL_PID_FILE = "/run/chv/cell.pid" UID_BASE = 1000 UID_COUNT = 65536 diff --git a/chv-guest/chv-browser.py b/chv-guest/chv-browser.py index 743feb9..a1f3804 100644 --- a/chv-guest/chv-browser.py +++ b/chv-guest/chv-browser.py @@ -31,7 +31,10 @@ import socket import sys import uuid -BROWSER_SOCK = "/run/chv/browser.sock" +# round-11 真机修复(缺陷 4):自有目录 /run/chv-browser, +# 0755 chv-browser 所有——/run/chv 是 0755 root root, +# 在此 bind 会 EACCES 而无限 crash loop(见 chv-broker.py)。 +BROWSER_SOCK = "/run/chv-browser/browser.sock" SHOTS_DIR = "/run/chv/shots" PLAYWRIGHT_BROWSERS_PATH = "/usr/local/share/ms-playwright" MAX_REQUEST_BYTES = 65536 diff --git a/cloud/tests/test_chv_muse_p2.py b/cloud/tests/test_chv_muse_p2.py index 8d8d9d9..033274e 100644 --- a/cloud/tests/test_chv_muse_p2.py +++ b/cloud/tests/test_chv_muse_p2.py @@ -562,62 +562,76 @@ class BuildInfoTest(unittest.TestCase): class BuildDryRunTest(unittest.TestCase): + def _fixture(self, tmp, with_vsock=False): + # 隔离的临时 git 仓:工作树干净、HEAD 与 REPO_COMMIT + # 一致、假 codex 二进制报对版本。with_vsock 时一并拷入 + # vsock_notify.py(round-11 真机修复:服务须随文件建)。 + repo = os.path.join(tmp, "repo") + guest = os.path.join(repo, "chv-guest") + bindir = os.path.join(tmp, "bin") + os.makedirs(guest) + os.makedirs(os.path.join(bindir, "codex-resources")) + for name in ("build.sh", "buildinfo.py", "check_lockfile.py", + "generate_lockfile.py", "chv-sync.sh", + "chv-cell.py", "chv-broker.py", "chv-browser.py", + "authd.py", "git-credential-chv-authd", + "chv-authd.service", "authd_init_stub.py"): + shutil.copy2(ROOT / "chv-guest" / name, + os.path.join(guest, name)) + shutil.copy2(ROOT / "host.py", os.path.join(repo, "host.py")) + # round-11:build.sh 要求 P3 云端叶子模块在仓里(fail-fast), + # 夹具须一并拷入,否则干跑在 P3 预检就失败。 + for rel in ("cloud/chv/authd.py", "cloud/chv/sentinel.py", + "cloud/chv/errors.py", + "cloud/__init__.py", "cloud/chv/__init__.py"): + dest = os.path.join(repo, rel) + os.makedirs(os.path.dirname(dest), exist_ok=True) + shutil.copy2(ROOT / rel, dest) + if with_vsock: + rel = "cloud/chv/guest/vsock_notify.py" + dest = os.path.join(repo, rel) + os.makedirs(os.path.dirname(dest), exist_ok=True) + shutil.copy2(ROOT / rel, dest) + codex = os.path.join(bindir, "codex") + with open(codex, "w") as fh: + fh.write("#!/bin/sh\necho 'codex-cli 0.164.0-alpha.9'\n") + os.chmod(codex, 0o755) + host_bin = os.path.join(bindir, "codex-code-mode-host") + with open(host_bin, "w") as fh: + fh.write("#!/bin/sh\nexit 0\n") + os.chmod(host_bin, 0o755) + subprocess.run(["git", "init", "-q"], cwd=repo, check=True) + subprocess.run(["git", "add", "-A"], cwd=repo, check=True) + subprocess.run( + ["git", "-c", "user.email=t@t", "-c", "user.name=t", + "commit", "-qm", "p2 dry-run fixture"], + cwd=repo, check=True) + head = subprocess.run( + ["git", "rev-parse", "--short", "HEAD"], cwd=repo, + capture_output=True, text=True, check=True).stdout.strip() + env = dict(os.environ) + env.update({ + "CODEX_BIN_DIR": bindir, + "UBUNTU_VERSION": "24.04", + "CODEX_VERSION": "0.164.0-alpha.9", + "REPO_COMMIT": head, + "UBUNTU_IMAGE_SHA256": "a" * 64, + }) + return repo, guest, env + + def _dry_run(self, repo, guest, env): + proc = subprocess.run( + ["bash", os.path.join(guest, "build.sh"), + "--dry-run", "out"], + cwd=repo, capture_output=True, text=True, env=env) + self.assertEqual(proc.returncode, 0, proc.stderr) + return proc.stdout + proc.stderr + def test_dry_run_contains_p2_steps(self): - # 在隔离的临时 git 仓里跑 build.sh --dry-run:工作树干净、 - # HEAD 与 REPO_COMMIT 一致、假 codex 二进制报对版本。 + # 在隔离的临时 git 仓里跑 build.sh --dry-run。 with tempfile.TemporaryDirectory() as tmp: - repo = os.path.join(tmp, "repo") - guest = os.path.join(repo, "chv-guest") - bindir = os.path.join(tmp, "bin") - os.makedirs(guest) - os.makedirs(os.path.join(bindir, "codex-resources")) - for name in ("build.sh", "buildinfo.py", "check_lockfile.py", - "generate_lockfile.py", "chv-sync.sh", - "chv-cell.py", "chv-broker.py", "chv-browser.py", - "authd.py", "git-credential-chv-authd", - "chv-authd.service", "authd_init_stub.py"): - shutil.copy2(ROOT / "chv-guest" / name, - os.path.join(guest, name)) - shutil.copy2(ROOT / "host.py", os.path.join(repo, "host.py")) - # round-11:build.sh 要求 P3 云端叶子模块在仓里(fail-fast), - # 夹具须一并拷入,否则干跑在 P3 预检就失败。 - for rel in ("cloud/chv/authd.py", "cloud/chv/sentinel.py", - "cloud/chv/errors.py", - "cloud/__init__.py", "cloud/chv/__init__.py"): - dest = os.path.join(repo, rel) - os.makedirs(os.path.dirname(dest), exist_ok=True) - shutil.copy2(ROOT / rel, dest) - codex = os.path.join(bindir, "codex") - with open(codex, "w") as fh: - fh.write("#!/bin/sh\necho 'codex-cli 0.164.0-alpha.9'\n") - os.chmod(codex, 0o755) - host_bin = os.path.join(bindir, "codex-code-mode-host") - with open(host_bin, "w") as fh: - fh.write("#!/bin/sh\nexit 0\n") - os.chmod(host_bin, 0o755) - subprocess.run(["git", "init", "-q"], cwd=repo, check=True) - subprocess.run(["git", "add", "-A"], cwd=repo, check=True) - subprocess.run( - ["git", "-c", "user.email=t@t", "-c", "user.name=t", - "commit", "-qm", "p2 dry-run fixture"], - cwd=repo, check=True) - head = subprocess.run( - ["git", "rev-parse", "--short", "HEAD"], cwd=repo, - capture_output=True, text=True, check=True).stdout.strip() - env = dict(os.environ) - env.update({ - "CODEX_BIN_DIR": bindir, - "UBUNTU_VERSION": "24.04", - "CODEX_VERSION": "0.164.0-alpha.9", - "REPO_COMMIT": head, - "UBUNTU_IMAGE_SHA256": "a" * 64, - }) - proc = subprocess.run( - ["bash", os.path.join(guest, "build.sh"), - "--dry-run", "out"], - cwd=repo, capture_output=True, text=True, env=env) - self.assertEqual(proc.returncode, 0, proc.stderr) - out = proc.stdout + proc.stderr + repo, guest, env = self._fixture(tmp) + out = self._dry_run(repo, guest, env) for needle in ("systemd-container", "chv-cell", "chv-broker", "chv-browser", "chv-browser.service", "chv-broker.service", "useradd --system", @@ -625,6 +639,10 @@ class BuildDryRunTest(unittest.TestCase): "chv-authd", "git-credential-chv-authd", "systemctl enable chv-authd", "systemctl start chv-authd", + # round-11 真机修复(缺陷 4):browser.sock + # 移进 chv-browser 自有目录,build.sh 须 + # 建目录 + tmpfiles 登记。 + "/run/chv-browser", # round-11 真机修复(层 1):叶子树含 # errors.py + stub __init__,不拷真实的 # cloud/chv/__init__.py。 @@ -637,6 +655,18 @@ class BuildDryRunTest(unittest.TestCase): self.assertNotIn("credential.helper store", out) self.assertIn("git config --system user.name chv", out) + def test_dry_run_with_vsock_creates_service(self): + # round-11 真机修复(vsock 自动启动):vsock_notify.py + # 在场时建 vsock-notify.service 并 enable,不再只警告。 + with tempfile.TemporaryDirectory() as tmp: + repo, guest, env = self._fixture(tmp, with_vsock=True) + out = self._dry_run(repo, guest, env) + self.assertNotIn("vsock_notify.py 不在仓库里", out) + for needle in ("vsock-notify.service", + "systemctl enable vsock-notify", + "ExecStart=/usr/local/bin/vsock-notify"): + self.assertIn(needle, out, needle) + if __name__ == "__main__": unittest.main()