Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions deploy/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion module/webui/app_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
52 changes: 52 additions & 0 deletions module/webui/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"

Expand All @@ -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":
Expand Down Expand Up @@ -211,17 +223,57 @@ 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"
threading.Thread(
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):
Expand Down
63 changes: 63 additions & 0 deletions tests/test_webui_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading