fix(chv): 数据盘挂载升格为 oneshot 服务,浏览器启动时重解析 profile

This commit is contained in:
host committed 2026-11-08 09:45:00 +00:00
1 parent c7ffb1fe6f
commit 5dc75128f8
5 files changed
+145 -12

No files matched your search

+1 -1
View File
@@ -795,7 +795,7 @@ class BuildDryRunTest(unittest.TestCase):
"chv-browser", "chv-browser.service",
"chv-broker.service", "useradd --system",
"systemctl enable chv-broker chv-browser "
"chv-daemon chv-execd chv-syncd",
"chv-daemon chv-data-mount chv-execd chv-syncd",
"chv-daemon.service", "chv-execd.service",
"chv-syncd.service"):
self.assertIn(needle, out, needle)
+1 -1
View File
@@ -421,7 +421,7 @@ class BuildWiringTest(unittest.TestCase):
self.assertIn("%s.service" % name, text)
self.assertIn(
"systemctl enable chv-broker chv-browser "
"chv-daemon chv-execd chv-syncd", text)
"chv-daemon chv-data-mount chv-execd chv-syncd", text)
# 99-chv-sync.cfg 的 runcmd 顺序一条不改。
cfg_line = next(
line for line in text.splitlines()
+105 -5
View File
@@ -151,6 +151,81 @@ class ProfileResolveTest(unittest.TestCase):
self.assertFalse(on_data)
class ProfileReresolveTest(unittest.TestCase):
def _agent_no_explicit(self, tmp):
ctx = FakeContext([FakePage()])
entered_holder = {}
class PW:
def __enter__(self):
entered = FakeEntered(ctx)
entered_holder["entered"] = entered
return entered
def __exit__(self, *exc):
return False
agent = BROWSER.BrowserAgent(
playwright_factory=lambda: PW(), shots_dir=tmp,
full_browsers_dir=os.path.join(tmp, "browsers"))
return agent, ctx, entered_holder
def test_ensure_browser_reresolves_profile_when_disk_appears(self):
# 进程启动时数据盘未挂(回落),首次 open 时盘已挂:应拿到
# 数据盘 profile,而不是把回落态锁死到服务重启。构建 #10
# 驱动实证的启动竞态(浏览器比挂盘早 5 秒)即靠此自愈。
with tempfile.TemporaryDirectory() as tmp:
data_profile = os.path.join(tmp, "workspace-profile")
calls = []
orig = BROWSER.resolve_profile_dir
def fake_resolve(env=None, ismount=None):
calls.append(1)
if len(calls) == 1:
return (BROWSER.PROFILE_FALLBACK_DIR, False)
return (data_profile, True)
BROWSER.resolve_profile_dir = fake_resolve
try:
agent, ctx, holder = self._agent_no_explicit(tmp)
self.assertEqual(agent._profile_dir,
BROWSER.PROFILE_FALLBACK_DIR)
ok, err = agent._ensure_browser()
self.assertTrue(ok, err)
self.assertEqual(holder["entered"].calls[0][0],
data_profile)
self.assertTrue(agent._profile_on_data_disk)
finally:
BROWSER.resolve_profile_dir = orig
def test_explicit_profile_never_reresolved(self):
with tempfile.TemporaryDirectory() as tmp:
orig = BROWSER.resolve_profile_dir
BROWSER.resolve_profile_dir = lambda **kw: (
os.path.join(tmp, "other-profile"), True)
try:
agent, ctx, holder = _agent([FakePage()], tmp)
ok, err = agent._ensure_browser()
self.assertTrue(ok, err)
self.assertEqual(
holder["entered"].calls[0][0],
os.path.join(tmp, "profile"))
finally:
BROWSER.resolve_profile_dir = orig
def test_ensure_browser_creates_profile_dir_0700(self):
# 不依赖 chv-data-mount 的时序(它在 cloud-final 才跑,
# 可能晚于浏览器首次启动),登录态目录绝不留 0755。
with tempfile.TemporaryDirectory() as tmp:
agent, ctx, holder = _agent([FakePage()], tmp)
profile = os.path.join(tmp, "profile")
self.assertFalse(os.path.exists(profile))
ok, err = agent._ensure_browser()
self.assertTrue(ok, err)
self.assertEqual(oct(os.stat(profile).st_mode & 0o777),
"0o700")
class FullPresentTest(unittest.TestCase):
def test_headless_shell_dir_alone_is_not_full(self):
with tempfile.TemporaryDirectory() as tmp:
@@ -394,8 +469,11 @@ class ScreenshotTest(unittest.TestCase):
self.assertFalse(second["changed"])
self.assertEqual(second["size"], 0)
self.assertIsNone(second["path"])
# 文件只落了一张。
self.assertEqual(len(os.listdir(tmp)), 1)
# 文件只落了一张(profile 目录是 _ensure_browser 建的,
# 不计入截图数)。
self.assertEqual(
len([f for f in os.listdir(tmp)
if f.endswith(".jpg")]), 1)
class CdpAndStatusTest(unittest.TestCase):
@@ -551,15 +629,37 @@ class BuildWiringTest(unittest.TestCase):
# 绝不能 After=cloud-final.service:cloud-final 自己
# After=multi-user.target,与浏览器的 Before=multi-user.target
# 成环,systemd 启动时直接删掉浏览器任务(2026-11-08 构建 #9
# 驱动实证)。排在数据盘挂载单元之后才是真依赖。
# 驱动实证)。也绝不能 After=home-chv-workspace.mount:那是
# mount(8) 运行时生成的临时单元,启动事务加载时不存在、
# 排序被忽略,构建 #10 驱动实证浏览器比挂盘早 5 秒启动、
# profile 回落本机盘。真依赖是 chv-data-mount.service。
text = (ROOT / "chv-guest" / "build.sh").read_text()
idx = text.find("Description=CHV browser sub-agent")
self.assertGreater(idx, 0)
block = text[idx:idx + 1200]
self.assertIn("Restart=always", block)
self.assertIn("After=network.target home-chv-workspace.mount",
self.assertIn("After=network.target chv-data-mount.service",
block)
self.assertNotIn("cloud-final.service", block)
self.assertNotIn("home-chv-workspace.mount", block)
def test_build_sh_data_mount_service_orders_after_cloud_config(self):
# 挂盘服务是 oneshot、RemainAfterExit,排在 cloud-config
# (建 chv 用户与 /home/chv)之后、multi-user 之前;且已
# enable,启动时真跑。构建 #10 驱动实证了无此服务时浏览器
# 比挂盘早启动、profile 回落的根因。
text = (ROOT / "chv-guest" / "build.sh").read_text()
idx = text.find("Description=CHV data disk mount")
self.assertGreater(idx, 0)
block = text[idx:idx + 800]
self.assertIn("After=cloud-config.service", block)
self.assertIn("Type=oneshot", block)
self.assertIn("RemainAfterExit=yes", block)
self.assertIn("ExecStart=/usr/local/bin/chv-data-mount", block)
self.assertIn("WantedBy=multi-user.target", block)
self.assertIn("systemctl enable chv-broker chv-browser "
"chv-daemon chv-data-mount chv-execd chv-syncd",
text)
def test_tmpfiles_lets_browser_bind_its_socket(self):
# 阶段 3 live 根因:/run/chv 0755 root:root 时 chv-browser
@@ -622,7 +722,7 @@ class BuildWiringTest(unittest.TestCase):
self.assertEqual(proc.returncode, 0, proc.stderr)
out = proc.stdout + proc.stderr
self.assertIn("Restart=always", out)
self.assertIn("home-chv-workspace.mount", out)
self.assertIn("chv-data-mount.service", out)
if __name__ == "__main__":