Skip to content

Commit b673cb3

Browse files
authored
fix: guard desktop-managed core restart (#9098)
1 parent 4cf210e commit b673cb3

6 files changed

Lines changed: 116 additions & 1 deletion

File tree

astrbot/core/desktop_runtime.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import os
2+
3+
DESKTOP_MANAGED_RESTART_MESSAGE = (
4+
"AstrBot Desktop manages this backend process. Please restart or update from "
5+
"the desktop app instead of the core WebUI."
6+
)
7+
8+
9+
def is_desktop_managed_backend() -> bool:
10+
return os.environ.get("ASTRBOT_DESKTOP_MANAGED") == "1"

astrbot/core/updator.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88

99
from astrbot.core import logger
1010
from astrbot.core.config.default import VERSION
11+
from astrbot.core.desktop_runtime import (
12+
DESKTOP_MANAGED_RESTART_MESSAGE,
13+
is_desktop_managed_backend,
14+
)
1115
from astrbot.core.utils.astrbot_path import get_astrbot_path
1216
from astrbot.core.utils.io import ensure_dir
1317

@@ -142,6 +146,10 @@ def _reboot(self, delay: int = 3) -> None:
142146
在指定的延迟后,终止所有子进程并重新启动程序
143147
这里只能使用 os.exec* 来重启程序
144148
"""
149+
if is_desktop_managed_backend():
150+
logger.error(DESKTOP_MANAGED_RESTART_MESSAGE)
151+
raise RuntimeError(DESKTOP_MANAGED_RESTART_MESSAGE)
152+
145153
time.sleep(delay)
146154
self.terminate_child_processes()
147155
executable = sys.executable

astrbot/dashboard/api/updates.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from fastapi.responses import JSONResponse
55

66
from astrbot.core import logger
7+
from astrbot.core.desktop_runtime import DESKTOP_MANAGED_RESTART_MESSAGE
78
from astrbot.dashboard.async_utils import run_maybe_async
89
from astrbot.dashboard.schemas import PipInstallRequest, UpdateRequest
910
from astrbot.dashboard.services.update_service import (
@@ -58,6 +59,15 @@ def _service_response(result: UpdateServiceResult) -> JSONResponse:
5859

5960
def _service_error(exc: UpdateServiceError) -> JSONResponse:
6061
logger.error(f"Dashboard update operation failed: {exc}", exc_info=True)
62+
if exc.code == "desktop_managed":
63+
return JSONResponse(
64+
{
65+
"status": "error",
66+
"message": DESKTOP_MANAGED_RESTART_MESSAGE,
67+
"data": None,
68+
},
69+
status_code=200,
70+
)
6171
return JSONResponse(
6272
{"status": "error", "message": "An internal error has occurred.", "data": None},
6373
status_code=200,

astrbot/dashboard/services/stat_service.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
2222
from astrbot.core.db import BaseDatabase
2323
from astrbot.core.db.po import ProviderStat
24+
from astrbot.core.desktop_runtime import (
25+
DESKTOP_MANAGED_RESTART_MESSAGE,
26+
is_desktop_managed_backend,
27+
)
2428
from astrbot.core.utils.astrbot_path import get_astrbot_path
2529
from astrbot.core.utils.auth_password import (
2630
is_default_dashboard_password,
@@ -57,6 +61,9 @@ async def restart_core(self) -> None:
5761
raise StatServiceError(
5862
"You are not permitted to do this operation in demo mode"
5963
)
64+
if is_desktop_managed_backend():
65+
raise StatServiceError(DESKTOP_MANAGED_RESTART_MESSAGE)
66+
6067
await self.core_lifecycle.restart()
6168

6269
@staticmethod

astrbot/dashboard/services/update_service.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
from astrbot.core import pip_installer as _pip_installer
1616
from astrbot.core.config.default import VERSION
1717
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
18+
from astrbot.core.desktop_runtime import (
19+
DESKTOP_MANAGED_RESTART_MESSAGE,
20+
is_desktop_managed_backend,
21+
)
1822
from astrbot.core.updator import AstrBotUpdator
1923
from astrbot.core.utils.astrbot_path import (
2024
get_astrbot_data_path,
@@ -67,7 +71,9 @@ class UpdateServiceResult:
6771

6872

6973
class UpdateServiceError(Exception):
70-
pass
74+
def __init__(self, message: str, *, code: str | None = None) -> None:
75+
super().__init__(message)
76+
self.code = code
7177

7278

7379
class UpdateService:
@@ -143,6 +149,12 @@ async def get_releases(self) -> UpdateServiceResult:
143149
raise UpdateServiceError(exc.__str__()) from exc
144150

145151
async def update_project(self, data: object) -> UpdateServiceResult:
152+
if is_desktop_managed_backend():
153+
raise UpdateServiceError(
154+
DESKTOP_MANAGED_RESTART_MESSAGE,
155+
code="desktop_managed",
156+
)
157+
146158
payload = data if isinstance(data, dict) else {}
147159
version = payload.get("version", "")
148160
reboot = payload.get("reboot", True)

tests/test_dashboard.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from astrbot.core import LogBroker
2121
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
2222
from astrbot.core.db.sqlite import SQLiteDatabase
23+
from astrbot.core.desktop_runtime import DESKTOP_MANAGED_RESTART_MESSAGE
2324
from astrbot.core.star.star import StarMetadata, star_registry
2425
from astrbot.core.star.star_handler import star_handlers_registry
2526
from astrbot.core.utils.auth_password import (
@@ -2662,6 +2663,35 @@ async def mock_get_dashboard_version(*args, **kwargs):
26622663
assert data["data"]["has_new_version"] is False
26632664

26642665

2666+
@pytest.mark.asyncio
2667+
async def test_restart_core_rejects_desktop_managed_backend(
2668+
app: FastAPIAppAdapter,
2669+
authenticated_header: dict,
2670+
core_lifecycle_td: AstrBotCoreLifecycle,
2671+
monkeypatch,
2672+
):
2673+
test_client = app.test_client()
2674+
restart_called = False
2675+
2676+
async def mock_restart():
2677+
nonlocal restart_called
2678+
restart_called = True
2679+
2680+
monkeypatch.setenv("ASTRBOT_DESKTOP_MANAGED", "1")
2681+
monkeypatch.setattr(core_lifecycle_td, "restart", mock_restart)
2682+
2683+
response = await test_client.post(
2684+
"/api/stat/restart-core",
2685+
headers=authenticated_header,
2686+
)
2687+
2688+
assert response.status_code == 400
2689+
data = await response.get_json()
2690+
assert data["status"] == "error"
2691+
assert data["message"] == DESKTOP_MANAGED_RESTART_MESSAGE
2692+
assert restart_called is False
2693+
2694+
26652695
@pytest.mark.asyncio
26662696
async def test_do_update(
26672697
app: FastAPIAppAdapter,
@@ -2826,6 +2856,44 @@ def mock_extract_dashboard(*args, **kwargs):
28262856
assert calls == ["download-dashboard", "download-core"]
28272857

28282858

2859+
@pytest.mark.asyncio
2860+
async def test_do_update_rejects_desktop_managed_backend(
2861+
app: FastAPIAppAdapter,
2862+
authenticated_header: dict,
2863+
core_lifecycle_td: AstrBotCoreLifecycle,
2864+
monkeypatch,
2865+
):
2866+
test_client = app.test_client()
2867+
calls = []
2868+
2869+
async def mock_download_core(*args, **kwargs):
2870+
del args, kwargs
2871+
calls.append("download-core")
2872+
2873+
async def mock_restart():
2874+
calls.append("restart")
2875+
2876+
monkeypatch.setenv("ASTRBOT_DESKTOP_MANAGED", "1")
2877+
monkeypatch.setattr(
2878+
core_lifecycle_td.astrbot_updator,
2879+
"download_update_package",
2880+
mock_download_core,
2881+
)
2882+
monkeypatch.setattr(core_lifecycle_td, "restart", mock_restart)
2883+
2884+
response = await test_client.post(
2885+
"/api/update/do",
2886+
headers=authenticated_header,
2887+
json={"version": "v3.4.0", "progress_id": "desktop-progress"},
2888+
)
2889+
2890+
assert response.status_code == 200
2891+
data = await response.get_json()
2892+
assert data["status"] == "error"
2893+
assert data["message"] == DESKTOP_MANAGED_RESTART_MESSAGE
2894+
assert calls == []
2895+
2896+
28292897
@pytest.mark.asyncio
28302898
async def test_do_update_does_not_apply_files_when_package_verification_fails(
28312899
app: FastAPIAppAdapter,

0 commit comments

Comments
 (0)