-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
fix: make project update flow atomic #8805
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
471ccd2
0d261f1
a05eb6f
0687e7d
57081bd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,20 +1,18 @@ | ||||||||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import asyncio | ||||||||||||||||||||||||||||
| import inspect | ||||||||||||||||||||||||||||
| import traceback | ||||||||||||||||||||||||||||
| import uuid | ||||||||||||||||||||||||||||
| import zipfile | ||||||||||||||||||||||||||||
| from collections.abc import Awaitable, Callable | ||||||||||||||||||||||||||||
| from dataclasses import dataclass | ||||||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||||
| from typing import Any | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| from astrbot.core import ( | ||||||||||||||||||||||||||||
| DEMO_MODE as _DEMO_MODE, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| from astrbot.core import ( | ||||||||||||||||||||||||||||
| logger, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| from astrbot.core import ( | ||||||||||||||||||||||||||||
| pip_installer as _pip_installer, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| from astrbot.core import DEMO_MODE as _DEMO_MODE | ||||||||||||||||||||||||||||
| from astrbot.core import logger | ||||||||||||||||||||||||||||
| from astrbot.core import pip_installer as _pip_installer | ||||||||||||||||||||||||||||
| from astrbot.core.config.default import VERSION | ||||||||||||||||||||||||||||
| from astrbot.core.core_lifecycle import AstrBotCoreLifecycle | ||||||||||||||||||||||||||||
| from astrbot.core.db.migration.helper import ( | ||||||||||||||||||||||||||||
|
|
@@ -24,16 +22,24 @@ | |||||||||||||||||||||||||||
| do_migration_v4 as _do_migration_v4, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| from astrbot.core.updator import AstrBotUpdator | ||||||||||||||||||||||||||||
| from astrbot.core.utils.astrbot_path import ( | ||||||||||||||||||||||||||||
| get_astrbot_data_path, | ||||||||||||||||||||||||||||
| get_astrbot_system_tmp_path, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| from astrbot.core.utils.io import ( | ||||||||||||||||||||||||||||
| download_dashboard as _download_dashboard, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| from astrbot.core.utils.io import ( | ||||||||||||||||||||||||||||
| extract_dashboard as _extract_dashboard, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| from astrbot.core.utils.io import ( | ||||||||||||||||||||||||||||
| get_dashboard_version as _get_dashboard_version, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| DEMO_MODE = _DEMO_MODE | ||||||||||||||||||||||||||||
| pip_installer = _pip_installer | ||||||||||||||||||||||||||||
| download_dashboard = _download_dashboard | ||||||||||||||||||||||||||||
| extract_dashboard = _extract_dashboard | ||||||||||||||||||||||||||||
| get_dashboard_version = _get_dashboard_version | ||||||||||||||||||||||||||||
| default_check_migration_needed_v4 = _check_migration_needed_v4 | ||||||||||||||||||||||||||||
| default_do_migration_v4 = _do_migration_v4 | ||||||||||||||||||||||||||||
|
|
@@ -43,6 +49,15 @@ async def call_download_dashboard(*args, **kwargs): | |||||||||||||||||||||||||||
| return await download_dashboard(*args, **kwargs) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| async def call_extract_dashboard(*args, **kwargs): | ||||||||||||||||||||||||||||
| if inspect.iscoroutinefunction(extract_dashboard): | ||||||||||||||||||||||||||||
| return await extract_dashboard(*args, **kwargs) | ||||||||||||||||||||||||||||
| result = await asyncio.to_thread(extract_dashboard, *args, **kwargs) | ||||||||||||||||||||||||||||
| if inspect.isawaitable(result): | ||||||||||||||||||||||||||||
| return await result | ||||||||||||||||||||||||||||
| return result | ||||||||||||||||||||||||||||
|
Comment on lines
+52
to
+58
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The We should run it in a separate thread using
Suggested change
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| async def call_get_dashboard_version(*args, **kwargs): | ||||||||||||||||||||||||||||
| return await get_dashboard_version(*args, **kwargs) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
@@ -78,6 +93,7 @@ def __init__( | |||||||||||||||||||||||||||
| core_lifecycle: AstrBotCoreLifecycle, | ||||||||||||||||||||||||||||
| *, | ||||||||||||||||||||||||||||
| download_dashboard_func: Callable[..., Awaitable[Any]], | ||||||||||||||||||||||||||||
| extract_dashboard_func: Callable[..., Any], | ||||||||||||||||||||||||||||
| get_dashboard_version_func: Callable[..., Awaitable[str | None]], | ||||||||||||||||||||||||||||
| pip_install_func: Callable[..., Awaitable[Any]], | ||||||||||||||||||||||||||||
| check_migration_needed_func: Callable[..., Awaitable[bool]], | ||||||||||||||||||||||||||||
|
|
@@ -88,6 +104,7 @@ def __init__( | |||||||||||||||||||||||||||
| self.astrbot_updator = astrbot_updator | ||||||||||||||||||||||||||||
| self.core_lifecycle = core_lifecycle | ||||||||||||||||||||||||||||
| self.download_dashboard = download_dashboard_func | ||||||||||||||||||||||||||||
| self.extract_dashboard = extract_dashboard_func | ||||||||||||||||||||||||||||
| self.get_dashboard_version = get_dashboard_version_func | ||||||||||||||||||||||||||||
| self.pip_install = pip_install_func | ||||||||||||||||||||||||||||
| self.check_migration_needed = check_migration_needed_func | ||||||||||||||||||||||||||||
|
|
@@ -177,6 +194,11 @@ async def update_project(self, data: object) -> UpdateServiceResult: | |||||||||||||||||||||||||||
| proxy = proxy.removesuffix("/") | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| self._init_update_progress(progress_id, version) | ||||||||||||||||||||||||||||
| update_temp_dir = Path(get_astrbot_system_tmp_path()) / "updates" | ||||||||||||||||||||||||||||
| update_temp_dir.mkdir(parents=True, exist_ok=True) | ||||||||||||||||||||||||||||
| update_token = uuid.uuid4().hex | ||||||||||||||||||||||||||||
| dashboard_zip_path = update_temp_dir / f"{update_token}-dashboard.zip" | ||||||||||||||||||||||||||||
| core_zip_path = update_temp_dir / f"{update_token}-core.zip" | ||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||
| self._set_update_stage( | ||||||||||||||||||||||||||||
| progress_id, | ||||||||||||||||||||||||||||
|
|
@@ -186,6 +208,7 @@ async def update_project(self, data: object) -> UpdateServiceResult: | |||||||||||||||||||||||||||
| 0, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| await self.download_dashboard( | ||||||||||||||||||||||||||||
| path=str(dashboard_zip_path), | ||||||||||||||||||||||||||||
| latest=latest, | ||||||||||||||||||||||||||||
| version=version, | ||||||||||||||||||||||||||||
| proxy=proxy or "", | ||||||||||||||||||||||||||||
|
|
@@ -195,6 +218,7 @@ async def update_project(self, data: object) -> UpdateServiceResult: | |||||||||||||||||||||||||||
| 0, | ||||||||||||||||||||||||||||
| 45, | ||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||
| extract=False, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| self._set_update_stage( | ||||||||||||||||||||||||||||
| progress_id, | ||||||||||||||||||||||||||||
|
|
@@ -211,16 +235,19 @@ async def update_project(self, data: object) -> UpdateServiceResult: | |||||||||||||||||||||||||||
| "正在下载 AstrBot 项目代码...", | ||||||||||||||||||||||||||||
| 45, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| await self.astrbot_updator.update( | ||||||||||||||||||||||||||||
| latest=latest, | ||||||||||||||||||||||||||||
| version=version, | ||||||||||||||||||||||||||||
| proxy=proxy or "", | ||||||||||||||||||||||||||||
| progress_callback=self._make_progress_callback( | ||||||||||||||||||||||||||||
| progress_id, | ||||||||||||||||||||||||||||
| "core", | ||||||||||||||||||||||||||||
| 45, | ||||||||||||||||||||||||||||
| 45, | ||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||
| core_zip_path = Path( | ||||||||||||||||||||||||||||
| await self.astrbot_updator.download_update_package( | ||||||||||||||||||||||||||||
| latest=latest, | ||||||||||||||||||||||||||||
| version=version, | ||||||||||||||||||||||||||||
| proxy=proxy or "", | ||||||||||||||||||||||||||||
| path=core_zip_path, | ||||||||||||||||||||||||||||
| progress_callback=self._make_progress_callback( | ||||||||||||||||||||||||||||
| progress_id, | ||||||||||||||||||||||||||||
| "core", | ||||||||||||||||||||||||||||
| 45, | ||||||||||||||||||||||||||||
| 45, | ||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| self._set_update_stage( | ||||||||||||||||||||||||||||
| progress_id, | ||||||||||||||||||||||||||||
|
|
@@ -230,6 +257,53 @@ async def update_project(self, data: object) -> UpdateServiceResult: | |||||||||||||||||||||||||||
| 90, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| self._set_update_stage( | ||||||||||||||||||||||||||||
| progress_id, | ||||||||||||||||||||||||||||
| "verify", | ||||||||||||||||||||||||||||
| "running", | ||||||||||||||||||||||||||||
| "下载完成,正在校验更新包...", | ||||||||||||||||||||||||||||
| 90, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def _verify_update_packages() -> None: | ||||||||||||||||||||||||||||
| for zip_path in (dashboard_zip_path, core_zip_path): | ||||||||||||||||||||||||||||
| with zipfile.ZipFile(zip_path, "r") as archive: | ||||||||||||||||||||||||||||
| corrupt_member = archive.testzip() | ||||||||||||||||||||||||||||
| if corrupt_member: | ||||||||||||||||||||||||||||
| raise UpdateServiceError(f"更新包校验失败: {corrupt_member}") | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| await asyncio.to_thread(_verify_update_packages) | ||||||||||||||||||||||||||||
| self._set_update_stage( | ||||||||||||||||||||||||||||
| progress_id, | ||||||||||||||||||||||||||||
| "verify", | ||||||||||||||||||||||||||||
| "done", | ||||||||||||||||||||||||||||
| "更新包校验完成。", | ||||||||||||||||||||||||||||
| 91, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| self._set_update_stage( | ||||||||||||||||||||||||||||
| progress_id, | ||||||||||||||||||||||||||||
| "apply", | ||||||||||||||||||||||||||||
| "running", | ||||||||||||||||||||||||||||
| "下载完成,正在应用更新...", | ||||||||||||||||||||||||||||
| 91, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| await asyncio.to_thread( | ||||||||||||||||||||||||||||
| self.astrbot_updator.apply_update_package, | ||||||||||||||||||||||||||||
| core_zip_path, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| await self.extract_dashboard( | ||||||||||||||||||||||||||||
| dashboard_zip_path, | ||||||||||||||||||||||||||||
| Path(get_astrbot_data_path()), | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| self._set_update_stage( | ||||||||||||||||||||||||||||
| progress_id, | ||||||||||||||||||||||||||||
| "apply", | ||||||||||||||||||||||||||||
| "done", | ||||||||||||||||||||||||||||
| "更新文件应用完成。", | ||||||||||||||||||||||||||||
| 92, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| self._set_update_stage( | ||||||||||||||||||||||||||||
| progress_id, | ||||||||||||||||||||||||||||
| "dependencies", | ||||||||||||||||||||||||||||
|
|
@@ -284,6 +358,13 @@ async def update_project(self, data: object) -> UpdateServiceResult: | |||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| logger.error(f"/api/update_project: {traceback.format_exc()}") | ||||||||||||||||||||||||||||
| raise UpdateServiceError(exc.__str__()) from exc | ||||||||||||||||||||||||||||
| finally: | ||||||||||||||||||||||||||||
| for zip_path in (dashboard_zip_path, core_zip_path): | ||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||
| if zip_path.exists(): | ||||||||||||||||||||||||||||
| zip_path.unlink() | ||||||||||||||||||||||||||||
| except Exception as cleanup_exc: | ||||||||||||||||||||||||||||
| logger.warning(f"清理更新临时文件失败: {zip_path}, {cleanup_exc}") | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| async def update_dashboard(self) -> UpdateServiceResult: | ||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.