fix(desktop-client): prevent duplicate window creation and serialize mode switches
Frontend CI / Frontend (push) Successful in 2m24s

- Add creation promise guards (mainWindowCreatePromise etc.) so concurrent
  ensureXxxWindow() calls share a single in-flight BrowserWindow, avoiding
  duplicate windows on rapid IPC
- Add windowModeSwitchQueue to serialize desktop:set-window-mode calls and
  the initial startup switch, preventing races when login/logout fire quickly
- Fix retireInactiveAppWindows: also retire existing login windows when
  switching to login mode, so stale login windows don't accumulate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Xu Liang
2026-05-01 18:05:12 +08:00
parent 5cd5d6685b
commit ba6ebec892
+52 -12
View File
@@ -97,6 +97,10 @@ let loginWindow: ElectronBrowserWindow | null = null;
let settingsWindow: ElectronBrowserWindow | null = null; let settingsWindow: ElectronBrowserWindow | null = null;
let mainRendererContents: ElectronWebContents | null = null; let mainRendererContents: ElectronWebContents | null = null;
let quitReleaseInFlight = false; let quitReleaseInFlight = false;
let mainWindowCreatePromise: Promise<ElectronBrowserWindow> | null = null;
let loginWindowCreatePromise: Promise<ElectronBrowserWindow> | null = null;
let settingsWindowCreatePromise: Promise<ElectronBrowserWindow> | null = null;
let windowModeSwitchQueue: Promise<void> = Promise.resolve();
const forceCloseWindows = new WeakSet<ElectronBrowserWindow>(); const forceCloseWindows = new WeakSet<ElectronBrowserWindow>();
const appWindowModes = new WeakMap<ElectronBrowserWindow, RendererWindowMode>(); const appWindowModes = new WeakMap<ElectronBrowserWindow, RendererWindowMode>();
@@ -281,7 +285,7 @@ function retireInactiveAppWindows(mode: WindowMode, target: ElectronBrowserWindo
const candidateMode = rendererModeFromWindow(candidate); const candidateMode = rendererModeFromWindow(candidate);
if ( if (
(mode === "main" && (candidateMode === "login" || candidateMode === "main")) (mode === "main" && (candidateMode === "login" || candidateMode === "main"))
|| (mode === "login" && (candidateMode === "main" || candidateMode === "settings")) || (mode === "login" && (candidateMode === "login" || candidateMode === "main" || candidateMode === "settings"))
) { ) {
retireWindowAfterIpcReturn(candidate); retireWindowAfterIpcReturn(candidate);
} }
@@ -333,9 +337,17 @@ async function ensureMainWindow(): Promise<ElectronBrowserWindow> {
if (existing) { if (existing) {
return existing; return existing;
} }
const window = await createAppWindow("main"); if (!mainWindowCreatePromise) {
mainWindow = window; mainWindowCreatePromise = createAppWindow("main")
return window; .then((window) => {
mainWindow = window;
return window;
})
.finally(() => {
mainWindowCreatePromise = null;
});
}
return mainWindowCreatePromise;
} }
async function ensureLoginWindow(): Promise<ElectronBrowserWindow> { async function ensureLoginWindow(): Promise<ElectronBrowserWindow> {
@@ -343,9 +355,17 @@ async function ensureLoginWindow(): Promise<ElectronBrowserWindow> {
if (existing) { if (existing) {
return existing; return existing;
} }
const window = await createAppWindow("login"); if (!loginWindowCreatePromise) {
loginWindow = window; loginWindowCreatePromise = createAppWindow("login")
return window; .then((window) => {
loginWindow = window;
return window;
})
.finally(() => {
loginWindowCreatePromise = null;
});
}
return loginWindowCreatePromise;
} }
async function ensureSettingsWindow(): Promise<ElectronBrowserWindow> { async function ensureSettingsWindow(): Promise<ElectronBrowserWindow> {
@@ -353,9 +373,17 @@ async function ensureSettingsWindow(): Promise<ElectronBrowserWindow> {
if (existing) { if (existing) {
return existing; return existing;
} }
const window = await createSettingsWindow(); if (!settingsWindowCreatePromise) {
settingsWindow = window; settingsWindowCreatePromise = createSettingsWindow()
return window; .then((window) => {
settingsWindow = window;
return window;
})
.finally(() => {
settingsWindowCreatePromise = null;
});
}
return settingsWindowCreatePromise;
} }
async function ensureWindow(mode: WindowMode): Promise<ElectronBrowserWindow> { async function ensureWindow(mode: WindowMode): Promise<ElectronBrowserWindow> {
@@ -424,6 +452,18 @@ async function switchWindowMode(
retireInactiveAppWindows(mode, target); retireInactiveAppWindows(mode, target);
} }
function queueWindowModeSwitch(
mode: WindowMode,
request: WindowModeRequest = {},
sourceWindow: ElectronBrowserWindow | null = null,
): Promise<void> {
const nextSwitch = windowModeSwitchQueue
.catch(() => undefined)
.then(() => switchWindowMode(mode, request, sourceWindow));
windowModeSwitchQueue = nextSwitch.catch(() => undefined);
return nextSwitch;
}
async function openSettingsWindow(): Promise<void> { async function openSettingsWindow(): Promise<void> {
const window = await ensureSettingsWindow(); const window = await ensureSettingsWindow();
await revealWindow(window); await revealWindow(window);
@@ -722,7 +762,7 @@ function registerBridgeHandlers(): void {
}); });
ipcMain.handle("desktop:set-window-mode", async (event, mode: WindowMode, request?: unknown) => { ipcMain.handle("desktop:set-window-mode", async (event, mode: WindowMode, request?: unknown) => {
if (mode === "login" || mode === "main") { if (mode === "login" || mode === "main") {
await switchWindowMode(mode, normalizeWindowModeRequest(request), windowFromIpcEvent(event)); await queueWindowModeSwitch(mode, normalizeWindowModeRequest(request), windowFromIpcEvent(event));
} }
return null; return null;
}); });
@@ -755,7 +795,7 @@ if (!hasSingleInstanceLock) {
} }
mainRendererContents.send("desktop:runtime-invalidated", event); mainRendererContents.send("desktop:runtime-invalidated", event);
}); });
await switchWindowMode(currentWindowMode); await queueWindowModeSwitch(currentWindowMode);
initTray(() => { initTray(() => {
revealActiveWindowSafely("tray"); revealActiveWindowSafely("tray");
}); });