fix: close and recreate macOS windows safely
This commit is contained in:
1 parent
f443f3bc66
commit
b5e6c663ef
19 files changed
+1079
-172
No files matched your search
@@ -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 };
|
||||
}
|
||||
Reference in new issue
Block a user