fix: close and recreate macOS windows safely

This commit is contained in:
鲨鱼辣椒 committed 2026-09-29 19:50:25 +08:00
1 parent f443f3bc66
commit b5e6c663ef
19 files changed
+1079 -172

No files matched your search

+67 -34
View File
@@ -22,6 +22,11 @@ import type { BackgroundLifecycleController, DesktopActivity } from './backgroun
import { collectPerformanceSnapshot } from './performance-diagnostics';
import type { HostApiContext } from '../api/context';
import { takeCloudAgentRoute } from './app-deep-link';
import { resolveMainWindow, type MainWindowProvider } from './window-provider';
export interface WindowIpcBindings {
bindWindow(window: BrowserWindow): void;
}
type UnifiedRequest = {
id?: string;
@@ -236,24 +241,41 @@ async function handleUnifiedRequest(
}
export function registerIpcHandlers(
mainWindow: BrowserWindow,
mainWindow: MainWindowProvider,
lifecycle?: BackgroundLifecycleController,
hostApiContext?: HostApiContext,
): void {
registerHostApiProxyHandlers(hostApiContext);
registerTranscriptExportHandler(mainWindow);
registerConversationLinkContextMenuHandler(mainWindow);
const rendererLeases = new Set<string>();
const releaseRendererLeases = (): void => {
if (rendererLeases.size === 0) return;
for (const id of rendererLeases) lifecycle?.releaseLease(id);
rendererLeases.clear();
): WindowIpcBindings {
const getMainWindow = (): BrowserWindow | null => resolveMainWindow(mainWindow);
const getRequestWindow = (event: Electron.IpcMainInvokeEvent): BrowserWindow | null => (
(event?.sender && BrowserWindow.fromWebContents(event.sender)) || getMainWindow()
);
const requireRequestWindow = (event: Electron.IpcMainInvokeEvent): BrowserWindow => {
const window = getRequestWindow(event);
if (!window || window.isDestroyed()) throw new Error('Main window is unavailable');
return window;
};
const rendererWebContents = mainWindow.webContents;
if (rendererWebContents && typeof rendererWebContents.on === 'function') {
rendererWebContents.on('render-process-gone', releaseRendererLeases);
rendererWebContents.on('destroyed', releaseRendererLeases);
}
registerHostApiProxyHandlers(hostApiContext);
registerTranscriptExportHandler(getMainWindow);
registerConversationLinkContextMenuHandler(getMainWindow);
const leasesByRenderer = new WeakMap<Electron.WebContents, Set<string>>();
const bindRenderer = (contents: Electron.WebContents): Set<string> => {
const existing = leasesByRenderer.get(contents);
if (existing) return existing;
const leases = new Set<string>();
leasesByRenderer.set(contents, leases);
const releaseLeases = (): void => {
for (const id of leases) lifecycle?.releaseLease(id);
leases.clear();
};
contents.on('render-process-gone', releaseLeases);
contents.on('destroyed', releaseLeases);
return leases;
};
const bindWindow = (window: BrowserWindow): void => {
if (!window.isDestroyed() && !window.webContents.isDestroyed()) bindRenderer(window.webContents);
};
const initialWindow = getMainWindow();
if (initialWindow) bindWindow(initialWindow);
ipcMain.handle('app:request', async (_event, request: UnifiedRequest) => (
handleUnifiedRequest(request)
));
@@ -270,31 +292,40 @@ export function registerIpcHandlers(
});
ipcMain.handle('app:performance', () => collectPerformanceSnapshot(lifecycle));
ipcMain.handle('lifecycle:activity', (_event, activity: DesktopActivity) => {
ipcMain.handle('lifecycle:activity', (event, activity: DesktopActivity) => {
const window = getMainWindow();
if (event?.sender && event.sender !== window?.webContents) {
return lifecycle?.getActivity();
}
const nextActivity = activity && typeof activity === 'object'
? activity
: { visible: true, module: null };
lifecycle?.setActivity(nextActivity);
if ((!nextActivity.visible || nextActivity.module !== 'painting')
&& !mainWindow.isDestroyed()
&& !mainWindow.webContents.isDestroyed()) {
mainWindow.webContents.send('lifecycle:pause');
&& window
&& !window.webContents.isDestroyed()) {
window.webContents.send('lifecycle:pause');
}
return lifecycle?.getActivity() ?? nextActivity;
});
ipcMain.handle('lifecycle:lease', (_event, input: { id: string; kind: string; active: boolean }) => {
ipcMain.handle('lifecycle:lease', (event, input: { id: string; kind: string; active: boolean }) => {
if (!lifecycle) return { count: 0 };
const id = typeof input?.id === 'string' ? input.id.trim() : '';
if (!id) return { count: lifecycle.getLeaseCount() };
const contents = event?.sender ?? getMainWindow()?.webContents;
if (!contents || contents.isDestroyed()) return { count: lifecycle.getLeaseCount() };
const rendererLeases = bindRenderer(contents);
// A late destroyed event from an old renderer must not release a new one's lease.
const leaseId = `renderer:${contents.id}:${id}`;
if (input?.active) {
lifecycle.acquireLease({
id,
id: leaseId,
kind: typeof input.kind === 'string' && input.kind.trim() ? input.kind.trim() : 'unknown',
});
rendererLeases.add(id);
rendererLeases.add(leaseId);
} else {
lifecycle.releaseLease(id);
rendererLeases.delete(id);
lifecycle.releaseLease(leaseId);
rendererLeases.delete(leaseId);
}
return { count: lifecycle.getLeaseCount() };
});
@@ -330,19 +361,21 @@ export function registerIpcHandlers(
ipcMain.handle('shell:showItemInFolder', (_event, path: string) => shell.showItemInFolder(path));
ipcMain.handle('shell:openPath', (_event, path: string) => shell.openPath(path));
ipcMain.handle('dialog:open', (_event, options: Electron.OpenDialogOptions) => dialog.showOpenDialog(mainWindow, options));
ipcMain.handle('dialog:save', (_event, options: Electron.SaveDialogOptions) => dialog.showSaveDialog(mainWindow, options));
ipcMain.handle('dialog:message', (_event, options: Electron.MessageBoxOptions) => dialog.showMessageBox(mainWindow, options));
ipcMain.handle('dialog:open', (event, options: Electron.OpenDialogOptions) => dialog.showOpenDialog(requireRequestWindow(event), options));
ipcMain.handle('dialog:save', (event, options: Electron.SaveDialogOptions) => dialog.showSaveDialog(requireRequestWindow(event), options));
ipcMain.handle('dialog:message', (event, options: Electron.MessageBoxOptions) => dialog.showMessageBox(requireRequestWindow(event), options));
ipcMain.handle('window:minimize', () => mainWindow.minimize());
ipcMain.handle('window:maximize', () => {
if (mainWindow.isMaximized()) {
mainWindow.unmaximize();
ipcMain.handle('window:minimize', (event) => requireRequestWindow(event).minimize());
ipcMain.handle('window:maximize', (event) => {
const window = requireRequestWindow(event);
if (window.isMaximized()) {
window.unmaximize();
return false;
}
mainWindow.maximize();
window.maximize();
return true;
});
ipcMain.handle('window:close', () => mainWindow.close());
ipcMain.handle('window:isMaximized', () => mainWindow.isMaximized());
ipcMain.handle('window:close', (event) => requireRequestWindow(event).close());
ipcMain.handle('window:isMaximized', (event) => requireRequestWindow(event).isMaximized());
return { bindWindow };
}