diff --git a/src/main.ts b/src/main.ts index 191e3d43d2..871fcd7e3f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,12 @@ -import { app } from 'electron'; -import electronDl from 'electron-dl'; +import { app, shell } from 'electron'; -import { performElectronStartup, setupApp } from './app/main/app'; +import { + performElectronStartup, + setupApp, + initializeScreenCaptureFallbackState, + setupGpuCrashHandler, + markMainWindowStable, +} from './app/main/app'; import { mergePersistableValues, watchAndPersistChanges, @@ -10,12 +15,26 @@ import { setUserDataDirectory } from './app/main/dev'; import { setupDeepLinks, processDeepLinksInArgs } from './deepLinks/main'; import { startDocumentViewerHandler } from './documentViewer/ipc'; import { setupDownloads } from './downloads/main'; +import { setupElectronDlWithTracking } from './downloads/main/setup'; import { setupMainErrorHandling } from './errors'; import i18n from './i18n/main'; +import { handle } from './ipc/main'; import { handleJitsiDesktopCapturerGetSources } from './jitsi/ipc'; +import { startLogViewerWindowHandler } from './logViewerWindow/ipc'; +import { + logger, + setupWebContentsLogging, + cleanupOldLogs, + setupDebugLoggingWatch, +} from './logging'; import { setupNavigation } from './navigation/main'; +import attentionDrawing from './notifications/attentionDrawing'; import { setupNotifications } from './notifications/main'; -import { startOutlookCalendarUrlHandler } from './outlookCalendar/ipc'; +import { + startOutlookCalendarUrlHandler, + stopOutlookCalendarSync, +} from './outlookCalendar/ipc'; +import { setupOutlookLogger } from './outlookCalendar/logger'; import { setupScreenSharing } from './screenSharing/main'; import { handleClearCacheDialog } from './servers/cache'; import { setupServers } from './servers/main'; @@ -36,22 +55,40 @@ import touchBar from './ui/main/touchBar'; import trayIcon from './ui/main/trayIcon'; import { setupUpdates } from './updates/main'; import { setupPowerMonitor } from './userPresence/main'; +import { openExternal } from './utils/browserLauncher'; import { handleDesktopCapturerGetSources, startVideoCallWindowHandler, + cleanupVideoCallResources, } from './videoCallWindow/ipc'; -electronDl({ saveAs: true }); - const start = async (): Promise => { setUserDataDirectory(); + logger.info('Starting Rocket.Chat Desktop application'); + + setupWebContentsLogging(); + performElectronStartup(); + // Set up GPU crash handler BEFORE whenReady to catch early GPU failures + setupGpuCrashHandler(); + await app.whenReady(); + cleanupOldLogs(); + createMainReduxStore(); + setupOutlookLogger(); + setupDebugLoggingWatch(); + + // Initialize screen capture fallback state after store is available + initializeScreenCaptureFallbackState(); + + // Set up electron-dl with our download tracking callbacks + setupElectronDlWithTracking(); + const localStorage = await exportLocalStorage(); await mergePersistableValues(localStorage); await setupServers(localStorage); @@ -68,14 +105,14 @@ const start = async (): Promise => { attachGuestWebContentsEvents(); await showRootWindow(); - // React DevTools is currently incompatible with Electron 10 - // if (process.env.NODE_ENV === 'development') { - // installDevTools(); - // } + // Mark main window as stable - GPU crashes after this won't trigger fallback + markMainWindowStable(); watchMachineTheme(); setupNotifications(); + attentionDrawing.setUp(); setupScreenSharing(); startVideoCallWindowHandler(); + startLogViewerWindowHandler(); await setupSpellChecking(); @@ -96,6 +133,9 @@ const start = async (): Promise => { menuBar.tearDown(); touchBar.tearDown(); trayIcon.tearDown(); + attentionDrawing.tearDown(); + stopOutlookCalendarSync(); + cleanupVideoCallResources(); }); watchAndPersistChanges(); @@ -105,7 +145,37 @@ const start = async (): Promise => { startDocumentViewerHandler(); checkSupportedVersionServers(); + handle('open-external', async (_webContents, rawUrl) => { + let url: URL; + + try { + url = new URL(rawUrl); + } catch { + console.warn('Blocked malformed external URL'); + return; + } + + const { isProtocolAllowed } = await import('./navigation/main'); + + if (!(await isProtocolAllowed(url.toString()))) { + console.warn('Blocked external URL with disallowed protocol'); + return; + } + + if (url.protocol === 'http:' || url.protocol === 'https:') { + await openExternal(url.toString()); + return; + } + + await shell.openExternal(url.toString()); + }); + await processDeepLinksInArgs(); + + console.info('Application initialization completed successfully'); }; -start(); +start().catch((error) => { + logger.error('Failed to start application', error); + app.exit(1); +}); diff --git a/src/ui/main/rootWindow.ts b/src/ui/main/rootWindow.ts index cc8f7c14b4..e9d495afc1 100644 --- a/src/ui/main/rootWindow.ts +++ b/src/ui/main/rootWindow.ts @@ -10,6 +10,7 @@ import { APP_MAIN_WINDOW_TITLE_SET, } from '../../app/actions'; import { setupRootWindowReload } from '../../app/main/dev'; +import { getPersistedValues } from '../../app/main/persistence'; import { select, watch, listen, dispatchLocal, dispatch } from '../../store'; import type { RootState } from '../../store/rootReducer'; import { ROOT_WINDOW_STATE_CHANGED, WEBVIEW_FOCUS_REQUESTED } from '../actions'; @@ -43,30 +44,74 @@ const selectRootWindowState = ({ rootWindowState }: RootState): WindowState => let _rootWindow: BrowserWindow; let tempWindow: BrowserWindow; +let crashHandlerRegistered = false; export const getRootWindow = (): Promise => new Promise((resolve, reject) => { setTimeout(() => { - _rootWindow ? resolve(_rootWindow) : reject(new Error()); + if (!_rootWindow) { + reject(new Error('Root window not initialized')); + return; + } + if (_rootWindow.isDestroyed()) { + reject(new Error('Root window has been destroyed')); + return; + } + resolve(_rootWindow); }, 300); }); const platformTitleBarStyle = process.platform === 'darwin' ? 'hidden' : 'default'; +const isMac = process.platform === 'darwin'; +const getEnableVibrancy = (): boolean => { + if (!isMac) { + return false; + } + try { + const persistedValues: { isTransparentWindowEnabled?: boolean } = + getPersistedValues(); + return persistedValues?.isTransparentWindowEnabled === true; + } catch (error) { + return false; + } +}; + export const createRootWindow = (): void => { + const enableVibrancy = getEnableVibrancy(); _rootWindow = new BrowserWindow({ width: 1000, height: 600, minWidth: 400, minHeight: 400, titleBarStyle: platformTitleBarStyle, - backgroundColor: '#2f343d', + backgroundColor: enableVibrancy ? '#00000000' : '#2f343d', show: false, webPreferences, + ...(enableVibrancy + ? { + transparent: true, + vibrancy: 'sidebar', + visualEffectState: 'active', + } + : {}), }); - _rootWindow.addListener('close', (event) => { + // Block navigation to smb:// protocol + _rootWindow.webContents.on('will-navigate', (event, url) => { + if (typeof url === 'string' && url.toLowerCase().startsWith('smb://')) { + event.preventDefault(); + } + }); + _rootWindow.webContents.setWindowOpenHandler(({ url }: { url: string }) => { + if (url.toLowerCase().startsWith('smb://')) { + return { action: 'deny' }; + } + return { action: 'allow' }; + }); + + _rootWindow.addListener('close', (event: any) => { event.preventDefault(); }); @@ -175,41 +220,58 @@ const fetchRootWindowState = async (): Promise< }; export const setupRootWindow = (): void => { + const safeWindowOperation = async ( + operation: (window: BrowserWindow) => Promise | T, + operationName: string + ): Promise => { + try { + const window = await getRootWindow(); + if (window.isDestroyed()) { + return; + } + return await operation(window); + } catch (error) { + if (process.env.NODE_ENV === 'development') { + console.warn(`${operationName} skipped:`, error); + } + } + }; + const unsubscribers = [ watch(selectGlobalBadgeCount, async (globalBadgeCount) => { - const browserWindow = await getRootWindow(); + await safeWindowOperation(async (browserWindow) => { + if (browserWindow.isFocused() || globalBadgeCount === 0) { + return; + } - if (browserWindow.isFocused() || globalBadgeCount === 0) { - return; - } + const { isShowWindowOnUnreadChangedEnabled, isFlashFrameEnabled } = + select( + ({ isShowWindowOnUnreadChangedEnabled, isFlashFrameEnabled }) => ({ + isShowWindowOnUnreadChangedEnabled, + isFlashFrameEnabled, + }) + ); - const { isShowWindowOnUnreadChangedEnabled, isFlashFrameEnabled } = - select( - ({ isShowWindowOnUnreadChangedEnabled, isFlashFrameEnabled }) => ({ - isShowWindowOnUnreadChangedEnabled, - isFlashFrameEnabled, - }) - ); + if (isShowWindowOnUnreadChangedEnabled && !browserWindow.isVisible()) { + const isMinimized = browserWindow.isMinimized(); + const isMaximized = browserWindow.isMaximized(); - if (isShowWindowOnUnreadChangedEnabled && !browserWindow.isVisible()) { - const isMinimized = browserWindow.isMinimized(); - const isMaximized = browserWindow.isMaximized(); + browserWindow.showInactive(); - browserWindow.showInactive(); + if (isMinimized) { + browserWindow.minimize(); + } - if (isMinimized) { - browserWindow.minimize(); + if (isMaximized) { + browserWindow.maximize(); + } + return; } - if (isMaximized) { - browserWindow.maximize(); + if (isFlashFrameEnabled && process.platform !== 'darwin') { + browserWindow.flashFrame(true); } - return; - } - - if (isFlashFrameEnabled && process.platform !== 'darwin') { - browserWindow.flashFrame(true); - } + }, 'Badge count update'); }), watch( ({ currentView, servers }) => { @@ -220,26 +282,35 @@ export const setupRootWindow = (): void => { return currentServer?.pageTitle || currentServer?.title || app.name; }, async (windowTitle) => { - const browserWindow = await getRootWindow(); - browserWindow.setTitle(windowTitle); - dispatch({ - type: APP_MAIN_WINDOW_TITLE_SET, - payload: windowTitle, - }); + await safeWindowOperation((browserWindow) => { + browserWindow.setTitle(windowTitle); + dispatch({ + type: APP_MAIN_WINDOW_TITLE_SET, + payload: windowTitle, + }); + }, 'Window title update'); } ), listen(WEBVIEW_FOCUS_REQUESTED, async () => { - const rootWindow = await getRootWindow(); - rootWindow.focus(); - rootWindow.show(); + await safeWindowOperation((rootWindow) => { + rootWindow.focus(); + rootWindow.show(); + }, 'Webview focus request'); }), ]; const fetchAndDispatchWindowState = debounce(async (): Promise => { - dispatchLocal({ - type: ROOT_WINDOW_STATE_CHANGED, - payload: await fetchRootWindowState(), - }); + try { + const state = await fetchRootWindowState(); + dispatchLocal({ + type: ROOT_WINDOW_STATE_CHANGED, + payload: state, + }); + } catch (error) { + if (process.env.NODE_ENV === 'development') { + console.warn('Failed to fetch window state:', error); + } + } }, 1000); getRootWindow().then((rootWindow) => { @@ -260,40 +331,79 @@ export const setupRootWindow = (): void => { rootWindow.flashFrame(false); }); - rootWindow.addListener('close', async () => { - if (rootWindow?.isFullScreen()) { - await new Promise((resolve) => - rootWindow.once('leave-full-screen', () => resolve()) - ); - rootWindow.setFullScreen(false); - } + rootWindow.addListener('close', async (event) => { + try { + if (rootWindow.isDestroyed()) { + return; + } - if (process.platform !== 'linux') rootWindow.blur(); + if (rootWindow.isFullScreen()) { + await new Promise((resolve) => + rootWindow.once('leave-full-screen', () => resolve()) + ); + rootWindow.setFullScreen(false); + } - const isTrayIconEnabled = select( - ({ isTrayIconEnabled }) => isTrayIconEnabled ?? true - ); + if (process.platform !== 'linux' && !rootWindow.isDestroyed()) { + rootWindow.blur(); + } - if (process.platform === 'darwin' || isTrayIconEnabled) { - rootWindow.hide(); - return; - } + let isTrayIconEnabled: boolean; + let isMinimizeOnCloseEnabled: boolean; - const isMinimizeOnCloseEnabled = select( - ({ isMinimizeOnCloseEnabled }) => isMinimizeOnCloseEnabled ?? true - ); + try { + isTrayIconEnabled = select( + ({ isTrayIconEnabled }) => isTrayIconEnabled ?? true + ); + isMinimizeOnCloseEnabled = select( + ({ isMinimizeOnCloseEnabled }) => isMinimizeOnCloseEnabled ?? true + ); + } catch (error) { + console.warn( + 'Failed to access application state during close:', + error + ); + isTrayIconEnabled = true; + isMinimizeOnCloseEnabled = true; + } - if (process.platform === 'win32' && isMinimizeOnCloseEnabled) { - rootWindow.minimize(); - return; - } + if (process.platform === 'darwin' || isTrayIconEnabled) { + if (!rootWindow.isDestroyed()) { + rootWindow.hide(); + } + return; + } - app.quit(); + if (process.platform === 'win32' && isMinimizeOnCloseEnabled) { + if (!rootWindow.isDestroyed()) { + rootWindow.minimize(); + } + return; + } + + // Prevent race condition: window destruction during app.quit() + event.preventDefault(); + app.quit(); + } catch (error) { + console.error('Error in close event handler:', error); + event.preventDefault(); + app.quit(); + } }); unsubscribers.push(() => { - rootWindow.removeAllListeners(); - rootWindow.close(); + try { + if (rootWindow && !rootWindow.isDestroyed()) { + rootWindow.removeAllListeners(); + setImmediate(() => { + if (rootWindow && !rootWindow.isDestroyed()) { + rootWindow.close(); + } + }); + } + } catch (error) { + console.error('Error during root window cleanup:', error); + } }); }); @@ -305,101 +415,139 @@ export const setupRootWindow = (): void => { unsubscribers.push( watch(selectRootWindowIcon, async ({ globalBadge, rootWindowIcon }) => { - const browserWindow = await getRootWindow(); - - if (!rootWindowIcon) { - browserWindow.setIcon( - nativeImage.createFromPath( - getTrayIconPath({ - platform: process.platform, - badge: globalBadge, - }) - ) - ); - return; - } - - const icon = nativeImage.createEmpty(); - const { scaleFactor } = screen.getPrimaryDisplay(); + await safeWindowOperation(async (browserWindow) => { + if (!rootWindowIcon) { + browserWindow.setIcon( + nativeImage.createFromPath( + getTrayIconPath({ + platform: process.platform, + badge: globalBadge, + }) + ) + ); + return; + } - if (process.platform === 'linux') { - rootWindowIcon.icon.forEach((representation) => { - icon.addRepresentation({ - ...representation, - scaleFactor, - }); - }); - } + const icon = nativeImage.createEmpty(); + const { scaleFactor } = screen.getPrimaryDisplay(); - if (process.platform === 'win32') { - for (const representation of rootWindowIcon.icon) { - icon.addRepresentation({ - ...representation, - scaleFactor: representation.width ?? 0 / 32, + if (process.platform === 'linux') { + rootWindowIcon.icon.forEach((representation) => { + icon.addRepresentation({ + ...representation, + scaleFactor, + }); }); } - } - browserWindow.setIcon(icon); - - if (process.platform === 'win32') { - let overlayIcon: NativeImage | null = null; - const overlayDescription: string = - (typeof globalBadge === 'number' && - i18next.t('unreadMention', { - appName: app.name, - count: globalBadge, - })) || - (globalBadge === '•' && - i18next.t('unreadMessage', { appName: app.name })) || - i18next.t('noUnreadMessage', { appName: app.name }); - if (rootWindowIcon.overlay) { - overlayIcon = nativeImage.createEmpty(); - - for (const representation of rootWindowIcon.overlay) { - overlayIcon.addRepresentation({ + if (process.platform === 'win32') { + for (const representation of rootWindowIcon.icon) { + icon.addRepresentation({ ...representation, - scaleFactor: 1, + scaleFactor: Math.max((representation.width ?? 0) / 32, 1), }); } } - const isTrayIconEnabled = select( - ({ isTrayIconEnabled }) => isTrayIconEnabled ?? true - ); + browserWindow.setIcon(icon); + + if (process.platform === 'win32') { + let overlayIcon: NativeImage | null = null; + const overlayDescription: string = + (typeof globalBadge === 'number' && + i18next.t('unreadMention', { + appName: app.name, + count: globalBadge, + })) || + (globalBadge === '•' && + i18next.t('unreadMessage', { appName: app.name })) || + i18next.t('noUnreadMessage', { appName: app.name }); + if (rootWindowIcon.overlay) { + overlayIcon = nativeImage.createEmpty(); + + for (const representation of rootWindowIcon.overlay) { + overlayIcon.addRepresentation({ + ...representation, + scaleFactor: 1, + }); + } + } + + const isTrayIconEnabled = select( + ({ isTrayIconEnabled }) => isTrayIconEnabled ?? true + ); - if (!isTrayIconEnabled) { - const t = i18next.t.bind(i18next); - const translate = `taskbar.${overlayDescription}`; - const taskbarTitle = - globalBadge !== undefined - ? `(${globalBadge}) ${t(translate)}` - : t(translate); + if (!isTrayIconEnabled) { + const t = i18next.t.bind(i18next); + const translate = `taskbar.${overlayDescription}`; + const taskbarTitle = + globalBadge !== undefined + ? `(${globalBadge}) ${t(translate)}` + : t(translate); - browserWindow.setTitle(taskbarTitle); + browserWindow.setTitle(taskbarTitle); + } + browserWindow.setOverlayIcon(overlayIcon, overlayDescription); } - browserWindow.setOverlayIcon(overlayIcon, overlayDescription); - } + }, 'Window icon update'); }), watch( ({ isMenuBarEnabled }) => isMenuBarEnabled, async (isMenuBarEnabled) => { - const browserWindow = await getRootWindow(); - browserWindow.autoHideMenuBar = !isMenuBarEnabled; - browserWindow.setMenuBarVisibility(isMenuBarEnabled); + await safeWindowOperation((browserWindow) => { + browserWindow.autoHideMenuBar = !isMenuBarEnabled; + browserWindow.setMenuBarVisibility(isMenuBarEnabled); + }, 'Menu bar visibility update'); } ) ); } app.addListener('before-quit', () => { - unsubscribers.forEach((unsubscriber) => unsubscriber()); + unsubscribers.forEach((unsubscriber) => { + try { + unsubscriber(); + } catch (error) { + console.warn('Unsubscriber error during quit:', error); + } + }); }); }; export const showRootWindow = async (): Promise => { const browserWindow = await getRootWindow(); + // Handle renderer process crashes + if (!crashHandlerRegistered) { + crashHandlerRegistered = true; + + browserWindow.webContents.on( + 'render-process-gone', + async (_event, details) => { + console.error('Renderer process crashed:', details.reason); + try { + const { session } = browserWindow.webContents; + await session.clearCache(); + await session.clearStorageData({ + storages: [ + 'cookies', + 'indexdb', + 'filesystem', + 'shadercache', + 'websql', + 'serviceworkers', + 'cachestorage', + ], + }); + console.log('Cache cleared. Reloading window...'); + browserWindow.reload(); + } catch (error) { + console.error('Failed to recover from crash:', error); + } + } + ); + } + browserWindow.loadFile(path.join(app.getAppPath(), 'app/index.html')); if (process.env.NODE_ENV === 'development') { @@ -420,6 +568,7 @@ export const showRootWindow = async (): Promise => { } setupRootWindow(); + resolve(); }); });