fix: 修复 Windows 打包版后端重启失败问题 (#5009)
* fix: patch pip distlib finder for frozen electron runtime * fix: use certifi CA bundle for runtime SSL requests * fix: configure certifi CA before core imports * fix: improve mac font fallback for dashboard text * fix: harden frozen pip patch and unify TLS connector * refactor: centralize dashboard CJK font fallback stacks * perf: reuse TLS context and avoid repeated frozen pip patch * refactor: bootstrap TLS setup before core imports * fix: use async confirm dialog for provider deletions * fix: replace native confirm dialogs in dashboard - Add shared confirm helper in dashboard/src/utils/confirmDialog.ts for async dialog usage with safe fallback. - Migrate provider, chat, config, session, platform, persona, MCP, backup, and knowledge-base delete/close confirmations to use the shared helper. - Remove scattered inline confirm handling to keep behavior consistent and avoid native blocking dialog focus/caret issues in Electron. * fix: capture runtime bootstrap logs after logger init - Add bootstrap record buffer in runtime_bootstrap for early TLS patch logs before logger is ready. - Flush buffered bootstrap logs to astrbot logger at process startup in main.py. - Include concrete exception details for TLS bootstrap failures to improve diagnosis. * fix: harden runtime bootstrap and unify confirm handling - Simplify bootstrap log buffering and add a public initialize hook for non-main startup paths. - Guard aiohttp TLS patching with feature/type checks and keep graceful fallback when internals are unavailable. - Standardize dashboard confirmation flow via shared confirm helpers across composition and options API components. * refactor: simplify runtime tls bootstrap and tighten confirm typing * refactor: align ssl helper namespace and confirm usage * fix: avoid frozen restart crash from multiprocessing import * fix: include missing frozen dependencies for windows backend * fix: use execv for stable backend reboot args * Revert "fix: use execv for stable backend reboot args" This reverts commit9cc27becff. * Revert "fix: include missing frozen dependencies for windows backend" This reverts commit52554bea1f. * Revert "fix: avoid frozen restart crash from multiprocessing import" This reverts commit10548645b0. * fix: reset pyinstaller onefile env before reboot * fix: unify electron restart path and tray-exit backend cleanup * fix: stabilize desktop restart detection and frozen reboot args * fix: make dashboard restart wait detection robust * fix: revert dashboard restart waiting interaction tweaks * fix: pass auth token for desktop graceful restart * fix: avoid false failure during graceful restart wait * fix: start restart waiting before electron restart call * fix: harden restart waiting and reboot arg parsing * fix: parse start_time as numeric timestamp
This commit is contained in:
+63
-19
@@ -44,6 +44,64 @@ class AstrBotUpdator(RepoZipUpdator):
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _is_option_arg(arg: str) -> bool:
|
||||
return arg.startswith("-")
|
||||
|
||||
@classmethod
|
||||
def _collect_flag_values(cls, argv: list[str], flag: str) -> str | None:
|
||||
try:
|
||||
idx = argv.index(flag)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
if idx + 1 >= len(argv):
|
||||
return None
|
||||
|
||||
value_parts: list[str] = []
|
||||
for arg in argv[idx + 1 :]:
|
||||
if cls._is_option_arg(arg):
|
||||
break
|
||||
if arg:
|
||||
value_parts.append(arg)
|
||||
|
||||
if not value_parts:
|
||||
return None
|
||||
|
||||
return " ".join(value_parts).strip() or None
|
||||
|
||||
@classmethod
|
||||
def _resolve_webui_dir_arg(cls, argv: list[str]) -> str | None:
|
||||
return cls._collect_flag_values(argv, "--webui-dir")
|
||||
|
||||
def _build_frozen_reboot_args(self) -> list[str]:
|
||||
argv = list(sys.argv[1:])
|
||||
webui_dir = self._resolve_webui_dir_arg(argv)
|
||||
if not webui_dir:
|
||||
webui_dir = os.environ.get("ASTRBOT_WEBUI_DIR")
|
||||
|
||||
if webui_dir:
|
||||
return ["--webui-dir", webui_dir]
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def _reset_pyinstaller_environment() -> None:
|
||||
if not getattr(sys, "frozen", False):
|
||||
return
|
||||
os.environ["PYINSTALLER_RESET_ENVIRONMENT"] = "1"
|
||||
for key in list(os.environ.keys()):
|
||||
if key.startswith("_PYI_"):
|
||||
os.environ.pop(key, None)
|
||||
|
||||
def _build_reboot_argv(self, executable: str) -> list[str]:
|
||||
if os.environ.get("ASTRBOT_CLI") == "1":
|
||||
args = sys.argv[1:]
|
||||
return [executable, "-m", "astrbot.cli.__main__", *args]
|
||||
if getattr(sys, "frozen", False):
|
||||
args = self._build_frozen_reboot_args()
|
||||
return [executable, *args]
|
||||
return [executable, *sys.argv]
|
||||
|
||||
def _reboot(self, delay: int = 3) -> None:
|
||||
"""重启当前程序
|
||||
在指定的延迟后,终止所有子进程并重新启动程序
|
||||
@@ -51,28 +109,14 @@ class AstrBotUpdator(RepoZipUpdator):
|
||||
"""
|
||||
time.sleep(delay)
|
||||
self.terminate_child_processes()
|
||||
if os.name == "nt":
|
||||
py = f'"{sys.executable}"'
|
||||
else:
|
||||
py = sys.executable
|
||||
executable = sys.executable
|
||||
|
||||
try:
|
||||
# 仅 CLI 模式走 `python -m astrbot.cli.__main__`,
|
||||
# 打包后的后端可执行文件需要直接 exec 自身。
|
||||
if os.environ.get("ASTRBOT_CLI") == "1":
|
||||
if os.name == "nt":
|
||||
args = [f'"{arg}"' if " " in arg else arg for arg in sys.argv[1:]]
|
||||
else:
|
||||
args = sys.argv[1:]
|
||||
os.execl(sys.executable, py, "-m", "astrbot.cli.__main__", *args)
|
||||
else:
|
||||
if getattr(sys, "frozen", False):
|
||||
# Frozen executable should not receive argv[0] as a positional argument.
|
||||
os.execl(sys.executable, py, *sys.argv[1:])
|
||||
else:
|
||||
os.execl(sys.executable, py, *sys.argv)
|
||||
self._reset_pyinstaller_environment()
|
||||
reboot_argv = self._build_reboot_argv(executable)
|
||||
os.execv(executable, reboot_argv)
|
||||
except Exception as e:
|
||||
logger.error(f"重启失败({py}, {e}),请尝试手动重启。")
|
||||
logger.error(f"重启失败({executable}, {e}),请尝试手动重启。")
|
||||
raise e
|
||||
|
||||
async def check_update(
|
||||
|
||||
@@ -371,6 +371,7 @@ import { ref, computed, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useI18n } from '@/i18n/composables'
|
||||
import { askForConfirmation, useConfirmDialog } from '@/utils/confirmDialog'
|
||||
import { restartAstrBot as restartAstrBotRuntime } from '@/utils/restartAstrBot'
|
||||
import WaitingForRestart from './WaitingForRestart.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -948,12 +949,12 @@ const formatISODate = (isoString) => {
|
||||
}
|
||||
|
||||
// 重启 AstrBot
|
||||
const restartAstrBot = () => {
|
||||
axios.post('/api/stat/restart-core').then(() => {
|
||||
if (wfr.value) {
|
||||
wfr.value.check()
|
||||
}
|
||||
})
|
||||
const restartAstrBot = async () => {
|
||||
try {
|
||||
await restartAstrBotRuntime(wfr.value)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 重置所有状态
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useI18n } from '@/i18n/composables'
|
||||
import { restartAstrBot as restartAstrBotRuntime } from '@/utils/restartAstrBot'
|
||||
import ConsoleDisplayer from './ConsoleDisplayer.vue'
|
||||
import WaitingForRestart from './WaitingForRestart.vue'
|
||||
|
||||
@@ -258,12 +259,12 @@ const getPlatformLabel = (platform) => {
|
||||
}
|
||||
|
||||
// 重启 AstrBot
|
||||
const restartAstrBot = () => {
|
||||
axios.post('/api/stat/restart-core').then(() => {
|
||||
if (wfr.value) {
|
||||
wfr.value.check();
|
||||
}
|
||||
})
|
||||
const restartAstrBot = async () => {
|
||||
try {
|
||||
await restartAstrBotRuntime(wfr.value)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开对话框的方法
|
||||
|
||||
@@ -47,6 +47,11 @@ export default {
|
||||
this.timeoutInternal()
|
||||
}, 1000)
|
||||
},
|
||||
stop() {
|
||||
this.visible = false
|
||||
this.cnt = 0
|
||||
this.newStartTime = -1
|
||||
},
|
||||
timeoutInternal() {
|
||||
console.log('wfr: timeoutInternal', this.newStartTime, this.startTime)
|
||||
if (this.newStartTime === -1 && this.cnt < 60 && this.visible) {
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ declare global {
|
||||
restarting: boolean;
|
||||
canManage: boolean;
|
||||
}>;
|
||||
restartBackend: () => Promise<{
|
||||
restartBackend: (authToken?: string | null) => Promise<{
|
||||
ok: boolean;
|
||||
reason: string | null;
|
||||
}>;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import axios from 'axios'
|
||||
|
||||
type WaitingForRestartRef = {
|
||||
check: () => void | Promise<void>
|
||||
stop?: () => void
|
||||
}
|
||||
|
||||
async function triggerWaiting(waitingRef?: WaitingForRestartRef | null) {
|
||||
if (!waitingRef) return
|
||||
await waitingRef.check()
|
||||
}
|
||||
|
||||
export async function restartAstrBot(
|
||||
waitingRef?: WaitingForRestartRef | null
|
||||
): Promise<void> {
|
||||
const desktopBridge = window.astrbotDesktop
|
||||
|
||||
if (desktopBridge?.isElectron) {
|
||||
const authToken = localStorage.getItem('token')
|
||||
try {
|
||||
const result = await desktopBridge.restartBackend(authToken)
|
||||
if (!result.ok) {
|
||||
waitingRef?.stop?.()
|
||||
throw new Error(result.reason || 'Failed to restart backend.')
|
||||
}
|
||||
await triggerWaiting(waitingRef)
|
||||
} catch (error) {
|
||||
waitingRef?.stop?.()
|
||||
throw error
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
await axios.post('/api/stat/restart-core')
|
||||
await triggerWaiting(waitingRef)
|
||||
}
|
||||
@@ -190,6 +190,7 @@ import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||||
import StandaloneChat from '@/components/chat/StandaloneChat.vue';
|
||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import { restartAstrBot as restartAstrBotRuntime } from '@/utils/restartAstrBot';
|
||||
import {
|
||||
askForConfirmation as askForConfirmationDialog,
|
||||
useConfirmDialog
|
||||
@@ -375,9 +376,7 @@ export default {
|
||||
this.save_message_success = "success";
|
||||
|
||||
if (this.isSystemConfig) {
|
||||
axios.post('/api/stat/restart-core').then(() => {
|
||||
this.$refs.wfr.check();
|
||||
})
|
||||
restartAstrBotRuntime(this.$refs.wfr).catch(() => {})
|
||||
}
|
||||
} else {
|
||||
this.save_message = res.data.message || this.messages.saveError;
|
||||
|
||||
@@ -78,12 +78,12 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import axios from 'axios';
|
||||
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||||
import ProxySelector from '@/components/shared/ProxySelector.vue';
|
||||
import MigrationDialog from '@/components/shared/MigrationDialog.vue';
|
||||
import SidebarCustomizer from '@/components/shared/SidebarCustomizer.vue';
|
||||
import BackupDialog from '@/components/shared/BackupDialog.vue';
|
||||
import { restartAstrBot as restartAstrBotRuntime } from '@/utils/restartAstrBot';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import { useTheme } from 'vuetify';
|
||||
import { PurpleTheme } from '@/theme/LightTheme';
|
||||
@@ -136,10 +136,12 @@ const wfr = ref(null);
|
||||
const migrationDialog = ref(null);
|
||||
const backupDialog = ref(null);
|
||||
|
||||
const restartAstrBot = () => {
|
||||
axios.post('/api/stat/restart-core').then(() => {
|
||||
wfr.value.check();
|
||||
})
|
||||
const restartAstrBot = async () => {
|
||||
try {
|
||||
await restartAstrBotRuntime(wfr.value);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const startMigration = async () => {
|
||||
|
||||
+272
-32
@@ -7,6 +7,7 @@ const { spawn, spawnSync } = require('child_process');
|
||||
const { delay, ensureDir, normalizeUrl, waitForProcessExit } = require('./common');
|
||||
|
||||
const PACKAGED_BACKEND_TIMEOUT_FALLBACK_MS = 5 * 60 * 1000;
|
||||
const GRACEFUL_RESTART_WAIT_FALLBACK_MS = 20 * 1000;
|
||||
|
||||
function parseBackendTimeoutMs(app) {
|
||||
const defaultTimeoutMs = app.isPackaged ? 0 : 20000;
|
||||
@@ -177,6 +178,19 @@ class BackendManager {
|
||||
return this.backendConfig;
|
||||
}
|
||||
|
||||
getBackendPort() {
|
||||
try {
|
||||
const parsed = new URL(this.backendUrl);
|
||||
if (parsed.port) {
|
||||
const port = Number.parseInt(parsed.port, 10);
|
||||
return Number.isFinite(port) ? port : null;
|
||||
}
|
||||
return parsed.protocol === 'https:' ? 443 : 80;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
canManageBackend() {
|
||||
return Boolean(this.getBackendConfig().cmd);
|
||||
}
|
||||
@@ -207,13 +221,124 @@ class BackendManager {
|
||||
}
|
||||
}
|
||||
|
||||
getEffectiveWaitMs(maxWaitMs = 0) {
|
||||
if (maxWaitMs > 0) {
|
||||
return maxWaitMs;
|
||||
}
|
||||
if (this.app.isPackaged) {
|
||||
return PACKAGED_BACKEND_TIMEOUT_FALLBACK_MS;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async requestBackendJson(pathname, options = {}) {
|
||||
const timeoutMs = options.timeoutMs || 2000;
|
||||
const method = options.method || 'GET';
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
const requestUrl = new URL(pathname, this.backendUrl);
|
||||
requestUrl.searchParams.set('_ts', `${Date.now()}`);
|
||||
|
||||
const authToken =
|
||||
typeof options.authToken === 'string' && options.authToken
|
||||
? options.authToken
|
||||
: null;
|
||||
|
||||
try {
|
||||
const response = await fetch(requestUrl.toString(), {
|
||||
method,
|
||||
signal: controller.signal,
|
||||
redirect: 'manual',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
|
||||
...(options.headers || {}),
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
return { ok: false, data: null };
|
||||
}
|
||||
const data = await response.json();
|
||||
return { ok: true, data };
|
||||
} catch {
|
||||
return { ok: false, data: null };
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
async getBackendStartTime() {
|
||||
const result = await this.requestBackendJson('/api/stat/start-time', {
|
||||
timeoutMs: 1800,
|
||||
method: 'GET',
|
||||
});
|
||||
if (!result.ok || !result.data) {
|
||||
return null;
|
||||
}
|
||||
const rawStartTime = result.data?.data?.start_time;
|
||||
const numericStartTime = Number(rawStartTime);
|
||||
return Number.isFinite(numericStartTime) ? numericStartTime : null;
|
||||
}
|
||||
|
||||
async requestGracefulRestart(authToken = null) {
|
||||
const result = await this.requestBackendJson('/api/stat/restart-core', {
|
||||
timeoutMs: 2500,
|
||||
method: 'POST',
|
||||
authToken,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
return result.ok;
|
||||
}
|
||||
|
||||
async waitForGracefulRestart(previousStartTime, maxWaitMs = 0) {
|
||||
const effectiveMaxWaitMs = this.getEffectiveWaitMs(maxWaitMs);
|
||||
const gracefulWaitMs =
|
||||
effectiveMaxWaitMs > 0
|
||||
? effectiveMaxWaitMs
|
||||
: GRACEFUL_RESTART_WAIT_FALLBACK_MS;
|
||||
const start = Date.now();
|
||||
let sawBackendDown = false;
|
||||
|
||||
while (true) {
|
||||
const reachable = await this.pingBackend(700);
|
||||
if (!reachable) {
|
||||
sawBackendDown = true;
|
||||
} else {
|
||||
const currentStartTime = await this.getBackendStartTime();
|
||||
if (
|
||||
previousStartTime !== null &&
|
||||
currentStartTime !== null &&
|
||||
currentStartTime !== previousStartTime
|
||||
) {
|
||||
return { ok: true, reason: null };
|
||||
}
|
||||
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) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: `Timed out after ${gracefulWaitMs}ms waiting for graceful restart.`,
|
||||
};
|
||||
}
|
||||
|
||||
await delay(350);
|
||||
}
|
||||
}
|
||||
|
||||
async waitForBackend(maxWaitMs = 0, failOnProcessExit = false) {
|
||||
const effectiveMaxWaitMs =
|
||||
maxWaitMs > 0
|
||||
? maxWaitMs
|
||||
: this.app.isPackaged
|
||||
? PACKAGED_BACKEND_TIMEOUT_FALLBACK_MS
|
||||
: 0;
|
||||
const effectiveMaxWaitMs = this.getEffectiveWaitMs(maxWaitMs);
|
||||
const start = Date.now();
|
||||
while (true) {
|
||||
if (await this.pingBackend()) {
|
||||
@@ -258,6 +383,9 @@ class BackendManager {
|
||||
if (this.app.isPackaged) {
|
||||
env.ASTRBOT_ELECTRON_CLIENT = '1';
|
||||
}
|
||||
if (backendConfig.webuiDir) {
|
||||
env.ASTRBOT_WEBUI_DIR = backendConfig.webuiDir;
|
||||
}
|
||||
if (backendConfig.rootDir) {
|
||||
env.ASTRBOT_ROOT = backendConfig.rootDir;
|
||||
const logsDir = path.join(backendConfig.rootDir, 'logs');
|
||||
@@ -344,6 +472,8 @@ class BackendManager {
|
||||
|
||||
if (process.platform === 'win32' && pid) {
|
||||
try {
|
||||
// Synchronous taskkill is acceptable here because stop/restart is
|
||||
// already a control-path operation and not latency-sensitive.
|
||||
const result = spawnSync('taskkill', ['/pid', `${pid}`, '/t', '/f'], {
|
||||
stdio: 'ignore',
|
||||
windowsHide: true,
|
||||
@@ -386,6 +516,115 @@ class BackendManager {
|
||||
this.closeBackendLogFd();
|
||||
}
|
||||
|
||||
findListeningPidsOnWindows(port) {
|
||||
// Synchronous netstat parsing is acceptable here because this helper is
|
||||
// used only during shutdown/restart cleanup paths.
|
||||
const result = spawnSync('netstat', ['-ano', '-p', 'tcp'], {
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
encoding: 'utf8',
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
if (result.status !== 0 || !result.stdout) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const pids = new Set();
|
||||
const lines = result.stdout.split(/\r?\n/);
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || !trimmed.toUpperCase().startsWith('TCP')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parts = trimmed.split(/\s+/);
|
||||
if (parts.length < 5) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const localAddress = parts[1] || '';
|
||||
const state = (parts[3] || '').toUpperCase();
|
||||
const pid = parts[parts.length - 1];
|
||||
if (!/^\d+$/.test(pid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (state !== 'LISTENING') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cleanedLocalAddress = localAddress.replace(/\]$/, '');
|
||||
const segments = cleanedLocalAddress.split(':');
|
||||
const portStr = segments[segments.length - 1];
|
||||
const portNum = Number(portStr);
|
||||
if (Number.isInteger(portNum) && portNum === Number(port)) {
|
||||
pids.add(pid);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(pids);
|
||||
}
|
||||
|
||||
async stopUnmanagedBackendByPort() {
|
||||
if (!this.app.isPackaged || process.platform !== 'win32') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const port = this.getBackendPort();
|
||||
if (!port) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pids = this.findListeningPidsOnWindows(port);
|
||||
if (!pids.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.log(
|
||||
`Attempting unmanaged backend cleanup by port=${port} pids=${pids.join(',')}`,
|
||||
);
|
||||
|
||||
for (const pid of pids) {
|
||||
try {
|
||||
// Synchronous taskkill is acceptable here because unmanaged cleanup
|
||||
// is performed only during shutdown/restart control flows.
|
||||
spawnSync('taskkill', ['/pid', `${pid}`, '/t', '/f'], {
|
||||
stdio: 'ignore',
|
||||
windowsHide: true,
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
await delay(500);
|
||||
return !(await this.pingBackend(1200));
|
||||
}
|
||||
|
||||
async stopAnyBackend() {
|
||||
if (this.backendProcess) {
|
||||
await this.stopManagedBackend();
|
||||
const running = await this.pingBackend();
|
||||
if (!running) {
|
||||
return { ok: true, reason: null };
|
||||
}
|
||||
} else {
|
||||
const running = await this.pingBackend();
|
||||
if (!running) {
|
||||
return { ok: true, reason: null };
|
||||
}
|
||||
}
|
||||
|
||||
const cleaned = await this.stopUnmanagedBackendByPort();
|
||||
if (cleaned) {
|
||||
return { ok: true, reason: null };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'Backend is running but not managed by Electron.',
|
||||
};
|
||||
}
|
||||
|
||||
async ensureBackend() {
|
||||
this.backendStartupFailureReason = null;
|
||||
|
||||
@@ -415,7 +654,7 @@ class BackendManager {
|
||||
};
|
||||
}
|
||||
|
||||
async restartBackend() {
|
||||
async restartBackend(authToken = null) {
|
||||
if (!this.canManageBackend()) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -431,6 +670,31 @@ class BackendManager {
|
||||
|
||||
this.backendRestarting = true;
|
||||
try {
|
||||
const backendRunning = await this.pingBackend(900);
|
||||
if (backendRunning) {
|
||||
const previousStartTime = await this.getBackendStartTime();
|
||||
const gracefulRequested = await this.requestGracefulRestart(authToken);
|
||||
if (gracefulRequested) {
|
||||
const gracefulResult = await this.waitForGracefulRestart(
|
||||
previousStartTime,
|
||||
this.backendTimeoutMs,
|
||||
);
|
||||
if (gracefulResult.ok) {
|
||||
return {
|
||||
ok: true,
|
||||
reason: null,
|
||||
};
|
||||
}
|
||||
this.log(
|
||||
`Graceful restart did not complete: ${gracefulResult.reason || 'unknown reason'}`,
|
||||
);
|
||||
} else {
|
||||
this.log(
|
||||
'Graceful restart request failed; falling back to managed restart.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await this.stopManagedBackend();
|
||||
const startResult = await this.startBackendAndWait(this.backendTimeoutMs);
|
||||
if (!startResult.ok) {
|
||||
@@ -468,31 +732,7 @@ class BackendManager {
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this.backendProcess) {
|
||||
const running = await this.pingBackend();
|
||||
if (running) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'Backend is running but not managed by Electron.',
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
reason: null,
|
||||
};
|
||||
}
|
||||
await this.stopManagedBackend();
|
||||
const running = await this.pingBackend();
|
||||
if (running) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'Backend is still reachable after stop request.',
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
reason: null,
|
||||
};
|
||||
return await this.stopAnyBackend();
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
|
||||
+6
-8
@@ -245,8 +245,8 @@ function registerIpcHandlers() {
|
||||
return backendManager.getState();
|
||||
});
|
||||
|
||||
ipcMain.handle('astrbot-desktop:restart-backend', async () => {
|
||||
return backendManager.restartBackend();
|
||||
ipcMain.handle('astrbot-desktop:restart-backend', async (_event, authToken) => {
|
||||
return backendManager.restartBackend(authToken);
|
||||
});
|
||||
|
||||
ipcMain.handle('astrbot-desktop:stop-backend', async () => {
|
||||
@@ -358,12 +358,10 @@ app.on('before-quit', (event) => {
|
||||
.persistLocaleFromDashboard(mainWindow, backendManager.getBackendUrl())
|
||||
.catch(() => {})
|
||||
.then(() =>
|
||||
backendManager.stopManagedBackend().catch((error) => {
|
||||
logElectron(
|
||||
`stopBackend failed: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
backendManager.stopAnyBackend().then((result) => {
|
||||
if (!result.ok) {
|
||||
logElectron(`stopBackend failed: ${result.reason || 'unknown reason'}`);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.finally(() => {
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@ contextBridge.exposeInMainWorld('astrbotDesktop', {
|
||||
isElectron: true,
|
||||
isElectronRuntime: () => ipcRenderer.invoke('astrbot-desktop:is-electron-runtime'),
|
||||
getBackendState: () => ipcRenderer.invoke('astrbot-desktop:get-backend-state'),
|
||||
restartBackend: () => ipcRenderer.invoke('astrbot-desktop:restart-backend'),
|
||||
restartBackend: (authToken) =>
|
||||
ipcRenderer.invoke('astrbot-desktop:restart-backend', authToken),
|
||||
stopBackend: () => ipcRenderer.invoke('astrbot-desktop:stop-backend'),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user