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
+73
-53
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* Electron Main Process Entry
|
||||
* Manages window creation, system tray, and IPC handlers
|
||||
*/
|
||||
@@ -18,6 +18,7 @@ import { logger } from '../utils/logger';
|
||||
import { initTelemetry } from '../utils/telemetry';
|
||||
|
||||
import { isQuitting, setQuitting } from './app-state';
|
||||
import { registerMainWindowClose } from './main-window-close';
|
||||
import { applyProxySettings } from './proxy';
|
||||
import { syncLaunchAtStartupSettingFromStore } from './launch-at-startup';
|
||||
import {
|
||||
@@ -204,6 +205,8 @@ let imageWorkspaceModule: DesignWorkspaceModule | null = null;
|
||||
let backgroundLifecycle!: BackgroundLifecycleController;
|
||||
let releaseJobs: ReleaseJobManager | null = null;
|
||||
let codingProducts: CodingProductComposition | null = null;
|
||||
let windowIpcBindings: ReturnType<typeof registerIpcHandlers> | null = null;
|
||||
let unsubscribeAuthSession: (() => void) | null = null;
|
||||
const mainWindowFocusState = createMainWindowFocusState();
|
||||
const quitLifecycleState = createQuitLifecycleState();
|
||||
const launchDeepLinkUrl = findNianCodeDeepLinkUrl(process.argv);
|
||||
@@ -337,10 +340,21 @@ function focusMainWindow(): void {
|
||||
focusWindow(mainWindow);
|
||||
}
|
||||
|
||||
function ensureMainWindow(): BrowserWindow | null {
|
||||
if (isQuitting()) return null;
|
||||
if (mainWindow && !mainWindow.isDestroyed()) return mainWindow;
|
||||
// Startup creates the first window after storage is ready. Later activations
|
||||
// recreate only the window, keeping the Host API and running tasks alive.
|
||||
if (!codingProducts) return null;
|
||||
return createMainWindow();
|
||||
}
|
||||
|
||||
function requestMainWindowFocus(reason: string): void {
|
||||
if (isQuitting()) return;
|
||||
const window = ensureMainWindow();
|
||||
const focusRequest = requestSecondInstanceFocus(
|
||||
mainWindowFocusState,
|
||||
Boolean(mainWindow && !mainWindow.isDestroyed()),
|
||||
Boolean(window && !window.webContents.isLoadingMainFrame()),
|
||||
);
|
||||
|
||||
if (focusRequest === 'focus-now') {
|
||||
@@ -378,30 +392,34 @@ function registerMakeloreProtocolClient(): void {
|
||||
if (!registered) logger.warn(`Failed to register ${NIANCODE_APP_PROTOCOL}:// protocol handler`);
|
||||
}
|
||||
|
||||
function sendAuthSession(
|
||||
session: ReturnType<typeof getWorksSquareSessionSnapshot>,
|
||||
reason: WorksSquareSessionChangeReason = 'changed',
|
||||
previousSession: ReturnType<typeof getWorksSquareSessionSnapshot> = null,
|
||||
): void {
|
||||
if (!session && getWorksSquareSessionRestoreStatus() === 'unavailable') return;
|
||||
if (mainWindow && !mainWindow.isDestroyed() && !mainWindow.webContents.isDestroyed()) {
|
||||
mainWindow.webContents.send('auth:session-changed', session);
|
||||
}
|
||||
if (!session && reason === 'terminal') {
|
||||
void clearManagedWorksSquareRuntimeBestEffort({
|
||||
codingProducts: codingProducts ?? undefined,
|
||||
imageWorkspace: imageWorkspaceModule ?? undefined,
|
||||
}, 'terminal session invalidation', previousSession?.accessToken);
|
||||
}
|
||||
}
|
||||
|
||||
function createMainWindow(): BrowserWindow {
|
||||
const win = createWindow();
|
||||
const sendAuthSession = (
|
||||
session: ReturnType<typeof getWorksSquareSessionSnapshot>,
|
||||
reason: WorksSquareSessionChangeReason = 'changed',
|
||||
previousSession: ReturnType<typeof getWorksSquareSessionSnapshot> = null,
|
||||
) => {
|
||||
if (!session && getWorksSquareSessionRestoreStatus() === 'unavailable') return;
|
||||
if (!win.isDestroyed() && !win.webContents.isDestroyed()) {
|
||||
win.webContents.send('auth:session-changed', session);
|
||||
}
|
||||
if (!session && reason === 'terminal') {
|
||||
void clearManagedWorksSquareRuntimeBestEffort({
|
||||
codingProducts: codingProducts ?? undefined,
|
||||
imageWorkspace: imageWorkspaceModule ?? undefined,
|
||||
}, 'terminal session invalidation', previousSession?.accessToken);
|
||||
}
|
||||
};
|
||||
const unsubscribeAuthSession = subscribeWorksSquareSession(sendAuthSession);
|
||||
mainWindow = win;
|
||||
windowIpcBindings?.bindWindow(win);
|
||||
appUpdater.setMainWindow(win);
|
||||
win.webContents.on('did-finish-load', () => {
|
||||
sendAuthSession(getWorksSquareSessionSnapshot());
|
||||
if (mainWindow === win) sendAuthSession(getWorksSquareSessionSnapshot());
|
||||
});
|
||||
|
||||
const closeAgentBrowserForHostRenderer = (reason: string): void => {
|
||||
if (mainWindow !== win) return;
|
||||
void agentBrowser?.close().catch((error) => {
|
||||
logger.warn(`Failed to close Agent Browser after ${reason}:`, error);
|
||||
});
|
||||
@@ -432,14 +450,14 @@ function createMainWindow(): BrowserWindow {
|
||||
win.show();
|
||||
});
|
||||
|
||||
win.on('close', (event) => {
|
||||
if (!isQuitting() && !isE2EMode) {
|
||||
event.preventDefault();
|
||||
win.hide();
|
||||
}
|
||||
registerMainWindowClose(win, {
|
||||
platform: process.platform,
|
||||
isQuitting,
|
||||
hideOnClose: process.platform !== 'darwin' && !isE2EMode,
|
||||
});
|
||||
|
||||
win.on('hide', () => {
|
||||
if (mainWindow !== win) return;
|
||||
backgroundLifecycle?.setActivity({
|
||||
...backgroundLifecycle.getActivity(),
|
||||
visible: false,
|
||||
@@ -452,6 +470,7 @@ function createMainWindow(): BrowserWindow {
|
||||
});
|
||||
|
||||
win.on('show', () => {
|
||||
if (mainWindow !== win) return;
|
||||
backgroundLifecycle?.setActivity({
|
||||
...backgroundLifecycle.getActivity(),
|
||||
visible: true,
|
||||
@@ -459,18 +478,20 @@ function createMainWindow(): BrowserWindow {
|
||||
});
|
||||
|
||||
win.on('closed', () => {
|
||||
unsubscribeAuthSession();
|
||||
const browser = agentBrowser;
|
||||
agentBrowser = null;
|
||||
void browser?.dispose().catch((error) => {
|
||||
logger.warn('Failed to dispose Agent Browser after the main window closed:', error);
|
||||
if (mainWindow !== win) return;
|
||||
mainWindow = null;
|
||||
appUpdater.setMainWindow(null);
|
||||
backgroundLifecycle?.setActivity({
|
||||
...backgroundLifecycle.getActivity(),
|
||||
visible: false,
|
||||
});
|
||||
// The module belongs to the process, not this window. Disposing it here
|
||||
// would leave the coding tools and Host API holding an unusable browser.
|
||||
void agentBrowser?.close().catch((error) => {
|
||||
logger.warn('Failed to close Agent Browser after the main window closed:', error);
|
||||
});
|
||||
if (mainWindow === win) {
|
||||
mainWindow = null;
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow = win;
|
||||
return win;
|
||||
}
|
||||
|
||||
@@ -509,23 +530,23 @@ async function initialize(): Promise<void> {
|
||||
logger.info('AI painting workspace is using the Works Square V2 cloud contract');
|
||||
|
||||
// Set application menu
|
||||
createMenu();
|
||||
createMenu(ensureMainWindow);
|
||||
|
||||
// Create the main window
|
||||
const window = createMainWindow();
|
||||
agentBrowser = new AgentBrowserModule(new ElectronAgentBrowserAdapter(window));
|
||||
agentBrowser = new AgentBrowserModule(new ElectronAgentBrowserAdapter(() => mainWindow));
|
||||
|
||||
// Create system tray
|
||||
if (!isE2EMode) {
|
||||
createTray(window);
|
||||
createTray(() => mainWindow, ensureMainWindow);
|
||||
}
|
||||
|
||||
// Register IPC handlers
|
||||
backgroundLifecycle = new BackgroundLifecycleController({
|
||||
...(isE2EMode ? { idleStopMs: 250 } : {}),
|
||||
onSleep: () => {
|
||||
if (!window.isDestroyed() && !window.webContents.isDestroyed()) {
|
||||
window.webContents.send('lifecycle:sleep');
|
||||
if (mainWindow && !mainWindow.isDestroyed() && !mainWindow.webContents.isDestroyed()) {
|
||||
mainWindow.webContents.send('lifecycle:sleep');
|
||||
}
|
||||
},
|
||||
onStopRuntime: async () => await codingProducts?.sleep('background_sleep'),
|
||||
@@ -544,8 +565,8 @@ async function initialize(): Promise<void> {
|
||||
});
|
||||
releaseJobs.on('status', (status) => {
|
||||
hostEventBus.emit('release-job:status', status);
|
||||
if (!window.isDestroyed() && !window.webContents.isDestroyed()) {
|
||||
window.webContents.send('release-job:status', status);
|
||||
if (mainWindow && !mainWindow.isDestroyed() && !mainWindow.webContents.isDestroyed()) {
|
||||
mainWindow.webContents.send('release-job:status', status);
|
||||
}
|
||||
});
|
||||
const codingAppPath = app.isPackaged
|
||||
@@ -557,14 +578,14 @@ async function initialize(): Promise<void> {
|
||||
browser: agentBrowser,
|
||||
requestAgentBrowserPresentation: (snapshot) => {
|
||||
hostEventBus.emit('agent-browser:show', snapshot);
|
||||
if (!window.isDestroyed() && !window.webContents.isDestroyed()) {
|
||||
window.webContents.send('agent-browser:show', snapshot);
|
||||
if (mainWindow && !mainWindow.isDestroyed() && !mainWindow.webContents.isDestroyed()) {
|
||||
mainWindow.webContents.send('agent-browser:show', snapshot);
|
||||
}
|
||||
},
|
||||
publishAgentBrowserState: (snapshot) => {
|
||||
hostEventBus.emit('agent-browser:state', snapshot);
|
||||
if (!window.isDestroyed() && !window.webContents.isDestroyed()) {
|
||||
window.webContents.send('agent-browser:state', snapshot);
|
||||
if (mainWindow && !mainWindow.isDestroyed() && !mainWindow.webContents.isDestroyed()) {
|
||||
mainWindow.webContents.send('agent-browser:state', snapshot);
|
||||
}
|
||||
},
|
||||
getLocalProxyCredential: () => getHostApiToken() || undefined,
|
||||
@@ -587,7 +608,7 @@ async function initialize(): Promise<void> {
|
||||
const hostApiContext: HostApiContext = {
|
||||
codingProjectStore,
|
||||
eventBus: hostEventBus,
|
||||
mainWindow: window,
|
||||
get mainWindow() { return mainWindow; },
|
||||
agentBrowser,
|
||||
worksSubmissionBinding: worksSubmissionBinding ?? undefined,
|
||||
imageWorkspace,
|
||||
@@ -596,7 +617,8 @@ async function initialize(): Promise<void> {
|
||||
codingProducts,
|
||||
previewDataSession: codingProducts.previewDataSession,
|
||||
};
|
||||
registerIpcHandlers(window, backgroundLifecycle, hostApiContext);
|
||||
windowIpcBindings = registerIpcHandlers(() => mainWindow, backgroundLifecycle, hostApiContext);
|
||||
unsubscribeAuthSession = subscribeWorksSquareSession(sendAuthSession);
|
||||
|
||||
void initializeRememberedPassword({
|
||||
secureStorage: useSecureWorksSquareSessionPersistence ? safeStorage : null,
|
||||
@@ -754,16 +776,12 @@ if (gotTheLock) {
|
||||
// Register activate handler AFTER app is ready to prevent
|
||||
// "Cannot create BrowserWindow before app is ready" on macOS.
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createMainWindow();
|
||||
} else {
|
||||
focusMainWindow();
|
||||
}
|
||||
requestMainWindowFocus('app activation');
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin' || isE2EMode) {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
@@ -783,6 +801,8 @@ if (gotTheLock) {
|
||||
return;
|
||||
}
|
||||
|
||||
unsubscribeAuthSession?.();
|
||||
unsubscribeAuthSession = null;
|
||||
hostEventBus.closeAll();
|
||||
codingProducts?.previewDataSession?.invalidate('main_shutdown');
|
||||
hostApiServer?.close();
|
||||
|
||||
Reference in new issue
Block a user