fix(chv): cell 不再把 /run/chv 的组改回 root

chv-cell 每次跑 cell 都 _ensure_bind_source(BROKER_DIR, uid=0,
gid=0),把 /run/chv 的组改回 root,抹掉 tmpfiles 的 chv-browser
组;之后 chv-browser 重启时 os.remove(browser.sock) 报
Permission denied、无限 crash-loop(构建 #8 客机 62 次重启才
被发现,03:42 驱动两次跑 cell 正好在补丁后、重启前)。改:组
改取 chv-browser 的 gid(不存在回落 0);serve() 删 socket
失败时报错说清修法,不再只剩裸 traceback。
This commit is contained in:
host committed 2026-11-08 09:45:00 +00:00
1 parent 0bfb00ab1b
commit 435664925e
3 files changed
+105 -3

No files matched your search

+71
View File
@@ -29,6 +29,7 @@ def _load_script(name):
BROWSER = _load_script("chv-browser.py")
CELL = _load_script("chv-cell.py")
EXECD = _load_script("chv-execd.py")
@@ -244,6 +245,76 @@ class SnapshotApiTest(unittest.TestCase):
self.assertEqual(snap["a11y"]["role"], "WebArea")
class CellBrokerDirTest(unittest.TestCase):
def test_broker_dir_gid_prefers_chv_browser_group(self):
fake = mock.Mock()
fake.gr_gid = 991
with mock.patch.object(CELL.grp, "getgrnam",
return_value=fake) as g:
self.assertEqual(CELL._broker_dir_gid(), 991)
g.assert_called_once_with("chv-browser")
def test_broker_dir_gid_falls_back_to_root_when_group_missing(self):
with mock.patch.object(CELL.grp, "getgrnam",
side_effect=KeyError("chv-browser")):
self.assertEqual(CELL._broker_dir_gid(), 0)
def test_ensure_bind_source_chowns_to_given_ids(self):
with tempfile.TemporaryDirectory() as tmp:
CELL._ensure_bind_source(tmp, uid=os.getuid(),
gid=os.getgid())
st = os.stat(tmp)
self.assertEqual((st.st_uid, st.st_gid),
(os.getuid(), os.getgid()))
def test_run_cell_keeps_browser_group_on_broker_dir(self):
# 接线钉死:run_cell 传给 broker 目录的 gid 必须是
# _broker_dir_gid(),绝不能再写死 gid=0(2026-11-08 live
# 根因:gid=0 每次跑 cell 都把 /run/chv 改回 root:root,
# chv-browser 下次重启删不掉旧 socket、无限 crash-loop)。
src = (ROOT / "chv-guest" / "chv-cell.py").read_text()
self.assertIn("_ensure_bind_source(BROKER_DIR, uid=0, "
"gid=_broker_dir_gid())", src)
self.assertNotIn("_ensure_bind_source(BROKER_DIR, uid=0, "
"gid=0)", src)
class StaleSocketTest(unittest.TestCase):
def test_remove_stale_socket_removes_existing_file(self):
with tempfile.TemporaryDirectory() as tmp:
sock = os.path.join(tmp, "browser.sock")
open(sock, "w").write("stale")
BROWSER._remove_stale_socket(sock)
self.assertFalse(os.path.exists(sock))
def test_remove_stale_socket_missing_is_noop(self):
with tempfile.TemporaryDirectory() as tmp:
BROWSER._remove_stale_socket(os.path.join(tmp, "nope.sock"))
def test_remove_stale_socket_permission_error_names_fix(self):
with tempfile.TemporaryDirectory() as tmp:
sub = os.path.join(tmp, "run-chv")
os.makedirs(sub)
sock = os.path.join(sub, "browser.sock")
open(sock, "w").write("stale")
os.chmod(sub, 0o555)
try:
# 以 root 跑单测时目录权限挡不住,用 mock 钉死分支。
if os.geteuid() == 0:
with mock.patch.object(
BROWSER.os, "remove",
side_effect=PermissionError(13, "deny")):
with self.assertRaises(PermissionError) as ctx:
BROWSER._remove_stale_socket(sock)
else:
with self.assertRaises(PermissionError) as ctx:
BROWSER._remove_stale_socket(sock)
self.assertIn("0775 root:chv-browser", str(ctx.exception))
self.assertIn("chv.conf", str(ctx.exception))
finally:
os.chmod(sub, 0o755)
class ScreenshotTest(unittest.TestCase):
def test_jpeg_q70_width_1280(self):
with tempfile.TemporaryDirectory() as tmp: