diff --git a/astrbot/core/updator.py b/astrbot/core/updator.py index 94cfebeb1..e7c2aa54b 100644 --- a/astrbot/core/updator.py +++ b/astrbot/core/updator.py @@ -102,6 +102,15 @@ class AstrBotUpdator(RepoZipUpdator): return [executable, *args] return [executable, *sys.argv] + @staticmethod + def _exec_reboot(executable: str, argv: list[str]) -> None: + if os.name == "nt" and getattr(sys, "frozen", False): + quoted_executable = f'"{executable}"' if " " in executable else executable + quoted_args = [f'"{arg}"' if " " in arg else arg for arg in argv[1:]] + os.execl(executable, quoted_executable, *quoted_args) + return + os.execv(executable, argv) + def _reboot(self, delay: int = 3) -> None: """重启当前程序 在指定的延迟后,终止所有子进程并重新启动程序 @@ -114,7 +123,7 @@ class AstrBotUpdator(RepoZipUpdator): try: self._reset_pyinstaller_environment() reboot_argv = self._build_reboot_argv(executable) - os.execv(executable, reboot_argv) + self._exec_reboot(executable, reboot_argv) except Exception as e: logger.error(f"重启失败({executable}, {e}),请尝试手动重启。") raise e diff --git a/dashboard/src/components/shared/WaitingForRestart.vue b/dashboard/src/components/shared/WaitingForRestart.vue index ce97eebbd..41ce9ece2 100644 --- a/dashboard/src/components/shared/WaitingForRestart.vue +++ b/dashboard/src/components/shared/WaitingForRestart.vue @@ -31,16 +31,20 @@ export default { } }, methods: { - async check() { + async check(initialStartTime = null) { this.newStartTime = -1 this.cnt = 0 this.visible = true this.status = "" - const commonStore = useCommonStore() - try { - this.startTime = await commonStore.fetchStartTime() - } catch (_error) { - this.startTime = commonStore.getStartTime() + if (typeof initialStartTime === 'number' && Number.isFinite(initialStartTime)) { + this.startTime = initialStartTime + } else { + const commonStore = useCommonStore() + try { + this.startTime = await commonStore.fetchStartTime() + } catch (_error) { + this.startTime = commonStore.getStartTime() + } } console.log('start wfr') setTimeout(() => { diff --git a/dashboard/src/utils/restartAstrBot.ts b/dashboard/src/utils/restartAstrBot.ts index 2b32c3b81..8fb0b188d 100644 --- a/dashboard/src/utils/restartAstrBot.ts +++ b/dashboard/src/utils/restartAstrBot.ts @@ -1,13 +1,27 @@ import axios from 'axios' type WaitingForRestartRef = { - check: () => void | Promise + check: (initialStartTime?: number | null) => void | Promise stop?: () => void } -async function triggerWaiting(waitingRef?: WaitingForRestartRef | null) { +async function triggerWaiting( + waitingRef?: WaitingForRestartRef | null, + initialStartTime?: number | null +) { if (!waitingRef) return - await waitingRef.check() + await waitingRef.check(initialStartTime) +} + +async function fetchCurrentStartTime(): Promise { + try { + const response = await axios.get('/api/stat/start-time', { timeout: 1500 }) + const rawStartTime = response?.data?.data?.start_time + const numericStartTime = Number(rawStartTime) + return Number.isFinite(numericStartTime) ? numericStartTime : null + } catch (_error) { + return null + } } export async function restartAstrBot( @@ -17,13 +31,15 @@ export async function restartAstrBot( if (desktopBridge?.isElectron) { const authToken = localStorage.getItem('token') + const initialStartTime = await fetchCurrentStartTime() try { - const result = await desktopBridge.restartBackend(authToken) + const restartPromise = desktopBridge.restartBackend(authToken) + await triggerWaiting(waitingRef, initialStartTime) + const result = await restartPromise if (!result.ok) { waitingRef?.stop?.() throw new Error(result.reason || 'Failed to restart backend.') } - await triggerWaiting(waitingRef) } catch (error) { waitingRef?.stop?.() throw error diff --git a/desktop/lib/backend-manager.js b/desktop/lib/backend-manager.js index cc94f50ac..477995027 100644 --- a/desktop/lib/backend-manager.js +++ b/desktop/lib/backend-manager.js @@ -317,13 +317,6 @@ class BackendManager { if (sawBackendDown && previousStartTime === null) { return { ok: true, reason: null }; } - if ( - sawBackendDown && - previousStartTime !== null && - currentStartTime === null - ) { - return { ok: true, reason: null }; - } } if (Date.now() - start >= gracefulWaitMs) { @@ -566,6 +559,40 @@ class BackendManager { return Array.from(pids); } + getWindowsProcessInfo(pid) { + const result = spawnSync( + 'tasklist', + ['/FI', `PID eq ${pid}`, '/FO', 'CSV', '/NH'], + { + stdio: ['ignore', 'pipe', 'ignore'], + encoding: 'utf8', + windowsHide: true, + }, + ); + if (result.status !== 0 || !result.stdout) { + return null; + } + + const firstLine = result.stdout + .split(/\r?\n/) + .map((line) => line.trim()) + .find((line) => line.length > 0); + if (!firstLine || firstLine.startsWith('INFO:')) { + return null; + } + + const fields = firstLine + .replace(/^"/, '') + .replace(/"$/, '') + .split('","'); + const imageName = fields[0] || ''; + const parsedPid = Number.parseInt(fields[1] || '', 10); + if (!imageName || !Number.isInteger(parsedPid) || parsedPid !== Number(pid)) { + return null; + } + return { imageName, pid: parsedPid }; + } + async stopUnmanagedBackendByPort() { if (!this.app.isPackaged || process.platform !== 'win32') { return false; @@ -585,7 +612,25 @@ class BackendManager { `Attempting unmanaged backend cleanup by port=${port} pids=${pids.join(',')}`, ); + const expectedImageName = ( + path.basename(this.getPackagedBackendPath() || '') || 'astrbot-backend.exe' + ).toLowerCase(); + for (const pid of pids) { + const processInfo = this.getWindowsProcessInfo(pid); + if (!processInfo) { + this.log(`Skip unmanaged cleanup for pid=${pid}: unable to resolve process info.`); + continue; + } + + const actualImageName = processInfo.imageName.toLowerCase(); + if (actualImageName !== expectedImageName) { + this.log( + `Skip unmanaged cleanup for pid=${pid}: unexpected process image ${processInfo.imageName}.`, + ); + continue; + } + try { // Synchronous taskkill is acceptable here because unmanaged cleanup // is performed only during shutdown/restart control flows.