"""svc.sh 超级管理员凭据文件路径登记回归(superadmin_envfile=)。 deploy/start/restart 时传一次 AGENT_SYSTEM_ENV_FILE:svc 校验(普通文件、属当前用户、 无组/其他权限位)后只登记路径;裸 restart 与 recover(清空环境的生命周期子进程) 复用该路径转发给 serve;权限过宽在停旧服务前拒绝;空值清除登记;svc.env/svc.log 绝不出现文件内容。桩二进制与夹具复用 tests/test_svc_recover.py。 """ import importlib.util import os import shutil import unittest from pathlib import Path SOURCE = Path(__file__).resolve().parents[1] _spec = importlib.util.spec_from_file_location("tsr", SOURCE / "tests" / "test_svc_recover.py") tsr = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(tsr) # 桩 serve 额外记录实际收到的 AGENT_SYSTEM_ENV_FILE(未设置记 )。 _ANCHOR = '> "$trace_dir/serve.txt"\n' assert _ANCHOR in tsr.STUB STUB = tsr.STUB.replace( _ANCHOR, _ANCHOR + r""" printf 'AGENT_SYSTEM_ENV_FILE=%s\n' "${AGENT_SYSTEM_ENV_FILE-}" >> "$trace_dir/serve.txt" """, 1) PASSWORD = "Dummy-SA-Pw-9f4c2e71-do-not-leak" UNSET = "" class SuperAdminEnvfileTests(tsr.SvcAutoRecoverTests): def setUp(self): super().setUp() shutil.copy2(SOURCE / "scripts" / "svc_manifest.py", self.root / "scripts" / "svc_manifest.py") self.stub.write_text(STUB) for key in ("AGENT_SYSTEM_ENV_FILE", "SUPER_ADMIN_OWNER", "SUPER_ADMIN_PASSWORD"): self.env.pop(key, None) self.secrets = self.root / "secrets" self.secrets.mkdir(mode=0o700) self.cred = self.make_cred("sa.env", 0o600) # ---- 夹具 ---- def make_cred(self, name, mode): p = self.secrets / name p.write_text("SUPER_ADMIN_OWNER=op\nSUPER_ADMIN_PASSWORD=%s\n" % PASSWORD) p.chmod(mode) return p def env_with(self, **extra): env = dict(self.env) env.update(extra) return env def served_envfile(self): for line in (self.trace / "serve.txt").read_text().splitlines(): if line.startswith("AGENT_SYSTEM_ENV_FILE="): return line.split("=", 1)[1] return None def fresh_trace(self): f = self.trace / "serve.txt" if f.exists(): f.unlink() def assert_same_path(self, recorded, expected): self.assertIsNotNone(recorded) self.assertTrue(os.path.isabs(recorded), recorded) self.assertEqual(os.path.realpath(recorded), os.path.realpath(expected)) def assert_no_secret(self, name, *outputs): for fname in ("svc.env", "svc.log", "manifest.json", "serve.env", "history.jsonl"): p = self.runtime(name) / fname if p.exists(): self.assertNotIn(PASSWORD, p.read_text(errors="replace"), fname) for out in outputs: self.assertNotIn(PASSWORD, out) def start_with_cred(self, name, cred=None): result = self.svc(name, "start", env=self.env_with(AGENT_SYSTEM_ENV_FILE=str(cred or self.cred))) self.assertIn("已启动", result.stdout) return result # ---- 用例 ---- def test_sa_start_records_path_and_bare_restart_reuses_it(self): self.seed_agent("dev", tsr.free_fixture_port()) r1 = self.start_with_cred("dev") self.assert_same_path(self.kv("dev", "superadmin_envfile"), self.cred) self.assert_same_path(self.served_envfile(), self.cred) status = self.svc("dev", "status") self.assertIn("超管凭据文件", status.stdout) self.fresh_trace() r2 = self.svc("dev", "restart") # 环境里不再有 AGENT_SYSTEM_ENV_FILE self.assertIn("已启动", r2.stdout) self.assert_same_path(self.served_envfile(), self.cred) self.assert_same_path(self.kv("dev", "superadmin_envfile"), self.cred) self.assert_no_secret("dev", r1.stdout, r2.stdout, status.stdout) def test_sa_relative_path_recorded_absolute(self): self.seed_agent("dev", tsr.free_fixture_port()) result = self.svc("dev", "start", env=self.env_with(AGENT_SYSTEM_ENV_FILE="secrets/sa.env")) self.assertIn("已启动", result.stdout) self.assert_same_path(self.kv("dev", "superadmin_envfile"), self.cred) def test_sa_recover_forwards_recorded_path(self): self.seed_agent("dev", tsr.free_fixture_port()) self.start_with_cred("dev") old_pid = self.kill9("dev") self.fresh_trace() result = self.svc("recover", "--quiet") self.assertIn("recover 汇总:拉回 1", result.stdout) new_pid = self.pid_of("dev") self.assertTrue(self.pid_alive(new_pid)) self.assertNotEqual(new_pid, old_pid) self.assert_same_path(self.served_envfile(), self.cred) self.assert_same_path(self.kv("dev", "superadmin_envfile"), self.cred) self.assert_no_secret("dev", result.stdout) def test_sa_loose_permissions_refused_before_stop(self): self.seed_agent("dev", tsr.free_fixture_port()) self.start_ok("dev") pid = self.pid_of("dev") before = (self.runtime("dev") / "svc.env").read_bytes() loose = self.make_cred("loose.env", 0o644) result = self.svc("dev", "restart", check=False, env=self.env_with(AGENT_SYSTEM_ENV_FILE=str(loose))) self.assertNotEqual(result.returncode, 0, result.stdout) self.assertIn("权限过宽", result.stdout) self.assertIn(str(loose), result.stdout) self.assertEqual(self.pid_of("dev"), pid) self.assertTrue(self.pid_alive(pid), "拒绝时不得停掉运行中的服务") self.assertEqual((self.runtime("dev") / "svc.env").read_bytes(), before) self.assert_no_secret("dev", result.stdout) def test_sa_missing_file_refused_before_stop(self): self.seed_agent("dev", tsr.free_fixture_port()) self.start_ok("dev") pid = self.pid_of("dev") result = self.svc("dev", "restart", check=False, env=self.env_with(AGENT_SYSTEM_ENV_FILE=str(self.secrets / "nope.env"))) self.assertNotEqual(result.returncode, 0, result.stdout) self.assertIn("nope.env", result.stdout) self.assertTrue(self.pid_alive(pid)) self.assertEqual(self.pid_of("dev"), pid) def test_sa_empty_value_clears_key(self): self.seed_agent("dev", tsr.free_fixture_port()) self.start_with_cred("dev") self.assertIsNotNone(self.kv("dev", "superadmin_envfile")) self.fresh_trace() self.svc("dev", "restart", env=self.env_with(AGENT_SYSTEM_ENV_FILE="")) self.assertIsNone(self.kv("dev", "superadmin_envfile")) self.assertEqual(self.served_envfile(), "") self.fresh_trace() self.svc("dev", "restart") self.assertEqual(self.served_envfile(), UNSET) def test_sa_explicit_environment_overrides_record(self): self.seed_agent("dev", tsr.free_fixture_port()) self.start_with_cred("dev") self.fresh_trace() r = self.svc("dev", "restart", env=self.env_with(SUPER_ADMIN_OWNER="op")) self.assertEqual(self.served_envfile(), UNSET, "显式凭据环境优先,不回落登记路径") self.assert_same_path(self.kv("dev", "superadmin_envfile"), self.cred) other = self.make_cred("other.env", 0o600) self.fresh_trace() self.svc("dev", "restart", env=self.env_with(AGENT_SYSTEM_ENV_FILE=str(other))) self.assert_same_path(self.served_envfile(), other) self.assert_same_path(self.kv("dev", "superadmin_envfile"), other) self.assert_no_secret("dev", r.stdout) def test_sa_recorded_file_gone_warns_and_skips(self): self.seed_agent("dev", tsr.free_fixture_port()) self.start_with_cred("dev") self.cred.unlink() self.fresh_trace() result = self.svc("dev", "restart") self.assertIn("已不存在", result.stdout) self.assertEqual(self.served_envfile(), UNSET) # 只跑本文件用例;继承来的自恢复用例由 tests/test_svc_recover.py 负责。 for _name in dir(tsr.SvcAutoRecoverTests): if _name.startswith("test_") and _name not in SuperAdminEnvfileTests.__dict__: setattr(SuperAdminEnvfileTests, _name, None) if __name__ == "__main__": unittest.main()