From 028eeca3ef2e3e58a11b684a21da073ca0b24055 Mon Sep 17 00:00:00 2001 From: wess09 <> Date: Tue, 28 Jul 2026 20:38:13 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/git.py | 31 +++++++++++++++++ module/webui/app_lifecycle.py | 2 +- module/webui/updater.py | 52 +++++++++++++++++++++++++++++ tests/test_webui_updater.py | 63 +++++++++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+), 1 deletion(-) diff --git a/deploy/git.py b/deploy/git.py index 36478409e..b4e9ee8a0 100644 --- a/deploy/git.py +++ b/deploy/git.py @@ -8,6 +8,7 @@ CLOUD_UPDATE_CONTROL_URL = 'https://alas-apiv2.nanoda.work/api/updata' +CLOUD_FORCE_UPDATE_CONTROL_URL = 'https://alas-apiv2.nanoda.work/api/force_update' class GitManager(DeployConfig): @@ -113,6 +114,36 @@ def cloud_auto_update_enabled(): logger.info(f'Cloud update control is inaccessible: {text}') return None + @staticmethod + def cloud_force_update_enabled(): + logger.info(f'Check cloud force update control: {CLOUD_FORCE_UPDATE_CONTROL_URL}') + try: + resp = requests.get( + CLOUD_FORCE_UPDATE_CONTROL_URL, + timeout=5, + headers={'User-Agent': 'alas AzurPilot'}, + ) + resp.raise_for_status() + except Exception as e: + logger.warning(f'Failed to check cloud force update control: {e}') + return None + + text = resp.text.strip() + try: + data = resp.json() + except ValueError: + data = text + + if data is True or (isinstance(data, str) and data.lower() in ('true', 'ture')): + logger.info('Cloud force update control is enabled') + return True + if data is False or (isinstance(data, str) and data.lower() in ('false', 'fales')): + logger.info('Cloud force update control is disabled') + return False + + logger.info(f'Cloud force update control is inaccessible: {text}') + return None + def cloud_update_access_failed(self, fatal=True): logger.hr('Cloud Update Control Failed', 0) if fatal: diff --git a/module/webui/app_lifecycle.py b/module/webui/app_lifecycle.py index cc63987ee..f5802ff42 100644 --- a/module/webui/app_lifecycle.py +++ b/module/webui/app_lifecycle.py @@ -41,7 +41,7 @@ def startup() -> None: lang.reload() updater.event = State.manager.Event() if updater.delay > 0: - task_handler.add(updater.check_update, updater.delay) + task_handler.add(updater.check_update_loop(), 1) task_handler.add(updater.schedule_update(), 86400) task_handler.start() if State.deploy_config.DiscordRichPresence: diff --git a/module/webui/updater.py b/module/webui/updater.py index 35024b326..7e6827cda 100644 --- a/module/webui/updater.py +++ b/module/webui/updater.py @@ -30,6 +30,8 @@ def __init__(self, file=DEPLOY_CONFIG): self.state = 0 self.event: threading.Event = None self._update_lock = threading.Lock() + self.force_update = False + self._force_update_checking = False def alas_kill(self): import os @@ -82,6 +84,10 @@ def _check_cloud_update(self) -> bool: """检查云端更新开关""" return self.cloud_auto_update_enabled() + def _check_cloud_force_update(self) -> bool: + """检查云端强制更新开关。""" + return self.cloud_force_update_enabled() + def _check_update(self) -> bool: self.state = "checking" @@ -90,9 +96,15 @@ def _check_update(self) -> bool: self.cloud_update_access_failed(fatal=False) return False if not cloud_update: + self.force_update = False logger.info("云更新标志为false,跳过更新检查") return False + force_update = self._check_cloud_force_update() + self.force_update = force_update is True + if force_update is None: + logger.warning("强制更新开关不可访问,按关闭处理") + if State.deploy_config.GitOverCdn: status = self.goc_client.get_status() if status == "uptodate": @@ -211,10 +223,31 @@ def _check_update_thread(self): try: result = self._check_update() self.state = result + if result and self.force_update: + logger.info("强制更新开关已开启,立即执行更新") + self.run_update() except Exception as e: logger.exception(e) self.state = 0 + def _check_force_update_thread(self): + """已有更新时,仅检查强制更新开关以保留前端状态。""" + try: + cloud_update = self._check_cloud_update() + if cloud_update is not True: + self.force_update = False + return + + force_update = self._check_cloud_force_update() + self.force_update = force_update is True + if self.force_update: + logger.info("强制更新开关已开启,立即执行已检测到的更新") + self.run_update() + except Exception as e: + logger.exception(e) + finally: + self._force_update_checking = False + def check_update(self): if self.state in (0, "failed", "finish"): self.state = "checking" @@ -222,6 +255,25 @@ def check_update(self): target=self._check_update_thread, daemon=True ).start() + elif self.state == 1 and not self._force_update_checking: + self._force_update_checking = True + threading.Thread( + target=self._check_force_update_thread, + daemon=True, + ).start() + + def check_update_loop(self) -> Generator: + """按普通或强制模式调度更新检查。""" + th: TaskHandler + th = yield + next_check = 0.0 + while True: + now = time.monotonic() + if self.force_update or now >= next_check: + self.check_update() + next_check = now + (1 if self.force_update else self.delay) + th._task.delay = 1 + yield @retry(ExecutionError, tries=3, delay=5, logger=None) def git_install(self): diff --git a/tests/test_webui_updater.py b/tests/test_webui_updater.py index 42ed5c3e5..f4d3a3b40 100644 --- a/tests/test_webui_updater.py +++ b/tests/test_webui_updater.py @@ -352,3 +352,66 @@ def test_wait_update_cancels_when_forced_worker_stop_fails(self): updater._run_update.assert_not_called() updater.event.clear.assert_called_once_with() restart.assert_called_once_with([worker], updater.event) + + +class TestUpdaterForceUpdate(unittest.TestCase): + @staticmethod + def _updater(): + updater = object.__new__(Updater) + updater.state = 0 + updater.force_update = False + updater._force_update_checking = False + return updater + + def test_detected_update_starts_immediately_when_force_update_is_enabled(self): + updater = self._updater() + updater.force_update = True + updater._check_update = Mock(return_value=True) + updater.run_update = Mock() + + updater._check_update_thread() + + self.assertEqual(1, updater.state) + updater.run_update.assert_called_once_with() + + def test_detected_update_waits_for_manual_or_scheduled_update_when_force_is_disabled(self): + updater = self._updater() + updater._check_update = Mock(return_value=True) + updater.run_update = Mock() + + updater._check_update_thread() + + self.assertEqual(1, updater.state) + updater.run_update.assert_not_called() + + def test_existing_update_starts_when_force_update_is_later_enabled(self): + updater = self._updater() + updater._check_cloud_update = Mock(return_value=True) + updater._check_cloud_force_update = Mock(return_value=True) + updater.run_update = Mock() + + updater._check_force_update_thread() + + self.assertTrue(updater.force_update) + self.assertFalse(updater._force_update_checking) + updater.run_update.assert_called_once_with() + + def test_force_update_uses_one_second_check_schedule(self): + updater = self._updater() + object.__setattr__(updater, "CheckUpdateInterval", 5) + updater.read = Mock() + updater.check_update = Mock() + handler = types.SimpleNamespace(_task=types.SimpleNamespace(delay=None)) + loop = updater.check_update_loop() + next(loop) + + with patch( + "module.webui.updater.time.monotonic", side_effect=[0.0, 0.5, 1.5] + ): + loop.send(handler) + next(loop) + updater.force_update = True + next(loop) + + self.assertEqual(2, updater.check_update.call_count) + self.assertEqual(1, handler._task.delay)