feat(update): add startup health rollback fuse

This commit is contained in:
chengma
2026-07-13 12:23:46 +08:00
parent 04124060fc
commit f6db4f0d62
16 changed files with 481 additions and 14 deletions
+74 -2
View File
@@ -27,6 +27,7 @@ class UpdaterEntryTests(unittest.TestCase):
(staging / "_internal").mkdir(parents=True)
(staging / "cmshopee.exe").write_bytes(b"new-exe")
(staging / "_internal" / "new.dll").write_bytes(b"new")
(staging / "_internal" / "empty.marker").write_bytes(b"")
(staging / "version.txt").write_text("2.0.0", encoding="ascii")
(staging / "README.txt").write_text("新说明", encoding="utf-8")
(staging / "cmshopee-updater.exe").write_bytes(b"new-updater")
@@ -38,6 +39,7 @@ class UpdaterEntryTests(unittest.TestCase):
target_version="2.0.0",
transaction_id="transaction-1234",
log_path=(install / ".cmshopee-update/logs/update.log").resolve(),
package_sha256="b" * 64,
)
return install, staging, plan
@@ -51,6 +53,7 @@ class UpdaterEntryTests(unittest.TestCase):
plan,
wait_parent=lambda _pid: None,
launcher=lambda executable, args: launched.append((executable, args)),
health_waiter=lambda _plan, _process: "main_window_ready",
)
self.assertEqual(b"new-exe", (install / "cmshopee.exe").read_bytes())
@@ -61,7 +64,7 @@ class UpdaterEntryTests(unittest.TestCase):
before_hash,
hashlib.sha256((install / "data/cmshopee.db").read_bytes()).hexdigest(),
)
self.assertTrue((backup / "cmshopee.exe").is_file())
self.assertFalse(backup.exists())
self.assertEqual(1, len(launched))
def test_move_failure_rolls_back_old_program(self):
@@ -113,6 +116,41 @@ class UpdaterEntryTests(unittest.TestCase):
self.assertEqual(b"old-exe", (install / "cmshopee.exe").read_bytes())
self.assertTrue((install / "_internal/old.dll").is_file())
def test_health_failure_rolls_back_and_records_failed_release(self):
with tempfile.TemporaryDirectory() as temp_dir:
install, _staging, plan = self.make_trees(temp_dir)
def fail_health(_plan, _process):
raise updater_entry.UpdaterError("新版程序在主窗口就绪前退出")
with self.assertRaisesRegex(updater_entry.UpdaterError, "已恢复旧版"):
updater_entry.apply_update(
plan,
wait_parent=lambda _pid: None,
launcher=lambda *_args: object(),
health_waiter=fail_health,
)
self.assertEqual(b"old-exe", (install / "cmshopee.exe").read_bytes())
failed = json.loads(
(install / ".cmshopee-update/failed-versions.json").read_text(encoding="utf-8")
)
self.assertIn("2.0.0:%s" % ("b" * 64), failed["releases"])
def test_environment_block_keeps_new_program_and_backup(self):
with tempfile.TemporaryDirectory() as temp_dir:
install, _staging, plan = self.make_trees(temp_dir)
backup = updater_entry.apply_update(
plan,
wait_parent=lambda _pid: None,
launcher=lambda *_args: object(),
health_waiter=lambda _plan, _process: "environment_blocked",
)
self.assertEqual(b"new-exe", (install / "cmshopee.exe").read_bytes())
self.assertTrue((backup / "cmshopee.exe").is_file())
self.assertFalse((install / ".cmshopee-update/pending.json").exists())
def test_lock_blocks_concurrent_transaction(self):
with tempfile.TemporaryDirectory() as temp_dir:
install, _staging, plan = self.make_trees(temp_dir)
@@ -139,14 +177,48 @@ class UpdaterEntryTests(unittest.TestCase):
def test_create_and_load_plan_round_trip(self):
with tempfile.TemporaryDirectory() as temp_dir:
install, staging, _plan = self.make_trees(temp_dir)
plan_path = updater_entry.create_plan(install, staging, "2.0.0", 4321)
plan_path = updater_entry.create_plan(
install,
staging,
"2.0.0",
4321,
"c" * 64,
)
loaded = updater_entry.load_plan(plan_path)
payload = json.loads(plan_path.read_text(encoding="utf-8"))
self.assertEqual("2.0.0", loaded.target_version)
self.assertEqual(4321, payload["parent_pid"])
self.assertEqual("c" * 64, loaded.package_sha256)
self.assertNotIn("data", payload)
def test_health_waiter_accepts_ready_and_environment_markers(self):
with tempfile.TemporaryDirectory() as temp_dir:
install, _staging, plan = self.make_trees(temp_dir)
health_path = (
install
/ ".cmshopee-update"
/ "transactions"
/ plan.transaction_id
/ "health.json"
)
health_path.parent.mkdir(parents=True)
for status in ("main_window_ready", "environment_blocked"):
health_path.write_text(
json.dumps(
{
"transaction_id": plan.transaction_id,
"target_version": plan.target_version,
"status": status,
}
),
encoding="utf-8",
)
self.assertEqual(
status,
updater_entry.wait_for_health(plan, process=None, timeout=0.1),
)
if __name__ == "__main__":
unittest.main()