From 605b7940e3d71b00c476823c75bb0300386ff8f0 Mon Sep 17 00:00:00 2001 From: Priya Patel Date: Fri, 27 Mar 2026 14:26:31 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20=F0=9F=90=9B=20electron=20IPC=20error=20?= =?UTF-8?q?handling=20(#3208)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 🐛 fixed electron error handling * chore: 🤖 addressed feedback * chore: 🤖 removed error.name, return error as is from main * chore: 🤖 updated error name * chore: 🤖 adds new error on renderer side * chore: 🤖 send error message only * chore: 🤖 adds error fallback --- .../electron-app/src/ipc/ipc-handler.js | 15 +- ui/desktop/electron-app/src/preload.js | 134 +++++++++--------- 2 files changed, 70 insertions(+), 79 deletions(-) diff --git a/ui/desktop/electron-app/src/ipc/ipc-handler.js b/ui/desktop/electron-app/src/ipc/ipc-handler.js index 9727569e9..3ee0c821c 100644 --- a/ui/desktop/electron-app/src/ipc/ipc-handler.js +++ b/ui/desktop/electron-app/src/ipc/ipc-handler.js @@ -22,18 +22,13 @@ const log = (method, request = '', response = '', type = 'log') => { module.exports = function handle(command, handler) { ipcMain.handle(command, async function (event, request) { const requestPayload = request; - let result; try { - result = await handler(requestPayload); + const result = await handler(requestPayload); log(command, requestPayload, result); - } catch (e) { - // If e is an Error instance, we need to extract the message and wrap - // it in a POJO before stringifying it. If not an Error, e is assumed to - // be already a POJO. - const errorMessage = e instanceof Error ? { message: e.message } : e; - result = new Error(JSON.stringify(errorMessage)); - log(command, requestPayload, e, 'error'); + return { result }; + } catch (error) { + log(command, requestPayload, error, 'error'); + return { error }; } - return result; }); }; diff --git a/ui/desktop/electron-app/src/preload.js b/ui/desktop/electron-app/src/preload.js index 7820f7810..e7b8acfef 100644 --- a/ui/desktop/electron-app/src/preload.js +++ b/ui/desktop/electron-app/src/preload.js @@ -3,40 +3,18 @@ * SPDX-License-Identifier: BUSL-1.1 */ -const { ipcRenderer, contextBridge } = require('electron'); +const { ipcRenderer, contextBridge, webFrame } = require('electron'); /** - * Exposing terminal creation to an isolated context (Ember) - * More information about contextBridge https://www.electronjs.org/docs/latest/api/context-bridge - * usage example: window.terminal.send(data); + * Helper function to invoke IPC calls to properly handle errors */ -contextBridge.exposeInMainWorld('terminal', { - // We could've sent data through our established postMessage pattern - // but we don't need a response back so we can make it include it here - // to make it simpler. This keeps sending and receiving handlers symmetrical. - send: (data, id) => { - ipcRenderer.send(`terminalKeystroke-${id}`, data); - }, - receive: (callback, id) => { - const incomingDataChannel = `terminalIncomingData-${id}`; - const listenerCallback = (_event, value) => callback(value); - ipcRenderer.on(incomingDataChannel, listenerCallback); - - // Return a function for the caller to handle cleaning up the listener - return () => { - return ipcRenderer.removeListener(incomingDataChannel, listenerCallback); - }; - }, - create: (vars) => { - ipcRenderer.send('createTerminal', vars); - }, - remove: (id) => { - ipcRenderer.send(`removeTerminal-${id}`); - }, - resize: (size, id) => { - ipcRenderer.send(`resize-${id}`, size); - }, -}); +async function invoke(method, payload) { + const { error, result } = await ipcRenderer.invoke(method, payload); + if (error) { + throw new Error(error.message ?? 'Unknown error'); + } + return result; +} /** * Exposes `desktop` via Electron ContextBridge to the renderer process. It is grouped into nested objects based on the type of functionality. @@ -44,65 +22,62 @@ contextBridge.exposeInMainWorld('terminal', { */ contextBridge.exposeInMainWorld('desktop', { cluster: { - getClusterUrl: () => ipcRenderer.invoke('getClusterUrl'), - setClusterUrl: (url) => ipcRenderer.invoke('setClusterUrl', url), - resetClusterUrl: () => ipcRenderer.invoke('resetClusterUrl'), + getClusterUrl: () => invoke('getClusterUrl'), + setClusterUrl: (url) => invoke('setClusterUrl', url), + resetClusterUrl: () => invoke('resetClusterUrl'), }, system: { - openExternal: (href) => ipcRenderer.invoke('openExternal', href), - cliExists: () => ipcRenderer.invoke('cliExists'), - getCliVersion: () => ipcRenderer.invoke('getCliVersion'), - checkCommand: (command) => ipcRenderer.invoke('checkCommand', command), - checkOS: () => ipcRenderer.invoke('checkOS'), - getDesktopVersion: () => ipcRenderer.invoke('getDesktopVersion'), + openExternal: (href) => invoke('openExternal', href), + cliExists: () => invoke('cliExists'), + getCliVersion: () => invoke('getCliVersion'), + checkCommand: (command) => invoke('checkCommand', command), + checkOS: () => invoke('checkOS'), + getDesktopVersion: () => invoke('getDesktopVersion'), }, session: { - connectSession: (params) => ipcRenderer.invoke('connect', params), - stopSession: (params) => ipcRenderer.invoke('stop', params), - stopAllSessions: () => ipcRenderer.invoke('stopAll'), - hasRunningSessions: () => ipcRenderer.invoke('hasRunningSessions'), + connectSession: (params) => invoke('connect', params), + stopSession: (params) => invoke('stop', params), + stopAllSessions: () => invoke('stopAll'), + hasRunningSessions: () => invoke('hasRunningSessions'), }, windowAction: { - hasMacOSChrome: () => ipcRenderer.invoke('hasMacOSChrome'), - showWindowActions: () => ipcRenderer.invoke('showWindowActions'), - minimizeWindow: () => ipcRenderer.invoke('minimizeWindow'), - toggleFullscreenWindow: () => ipcRenderer.invoke('toggleFullscreenWindow'), - closeWindow: () => ipcRenderer.invoke('closeWindow'), - focusWindow: () => ipcRenderer.invoke('focusWindow'), + hasMacOSChrome: () => invoke('hasMacOSChrome'), + showWindowActions: () => invoke('showWindowActions'), + minimizeWindow: () => invoke('minimizeWindow'), + toggleFullscreenWindow: () => invoke('toggleFullscreenWindow'), + closeWindow: () => invoke('closeWindow'), + focusWindow: () => invoke('focusWindow'), }, daemon: { - addTokenToDaemons: (data) => ipcRenderer.invoke('addTokenToDaemons', data), - searchCacheDaemon: (request) => - ipcRenderer.invoke('searchCacheDaemon', request), - isCacheDaemonRunning: () => ipcRenderer.invoke('isCacheDaemonRunning'), - cacheDaemonStatus: () => ipcRenderer.invoke('cacheDaemonStatus'), + addTokenToDaemons: (data) => invoke('addTokenToDaemons', data), + searchCacheDaemon: (request) => invoke('searchCacheDaemon', request), + isCacheDaemonRunning: () => invoke('isCacheDaemonRunning'), + cacheDaemonStatus: () => invoke('cacheDaemonStatus'), }, clientAgent: { - getClientAgentSessions: () => ipcRenderer.invoke('getClientAgentSessions'), - isClientAgentRunning: () => ipcRenderer.invoke('isClientAgentRunning'), - clientAgentStatus: () => ipcRenderer.invoke('clientAgentStatus'), - pauseClientAgent: () => ipcRenderer.invoke('pauseClientAgent'), - resumeClientAgent: () => ipcRenderer.invoke('resumeClientAgent'), + getClientAgentSessions: () => invoke('getClientAgentSessions'), + isClientAgentRunning: () => invoke('isClientAgentRunning'), + clientAgentStatus: () => invoke('clientAgentStatus'), + pauseClientAgent: () => invoke('pauseClientAgent'), + resumeClientAgent: () => invoke('resumeClientAgent'), }, logging: { - getLogLevel: () => ipcRenderer.invoke('getLogLevel'), - setLogLevel: (logLevel) => ipcRenderer.invoke('setLogLevel', logLevel), - getLogPath: () => ipcRenderer.invoke('getLogPath'), + getLogLevel: () => invoke('getLogLevel'), + setLogLevel: (logLevel) => invoke('setLogLevel', logLevel), + getLogPath: () => invoke('getLogPath'), }, rdp: { - getRdpClients: () => ipcRenderer.invoke('getRdpClients'), - getPreferredRdpClient: () => ipcRenderer.invoke('getPreferredRdpClient'), - setPreferredRdpClient: (client) => - ipcRenderer.invoke('setPreferredRdpClient', client), - launchRdpClient: (sessionId) => - ipcRenderer.invoke('launchRdpClient', sessionId), + getRdpClients: () => invoke('getRdpClients'), + getPreferredRdpClient: () => invoke('getPreferredRdpClient'), + setPreferredRdpClient: (client) => invoke('setPreferredRdpClient', client), + launchRdpClient: (sessionId) => invoke('launchRdpClient', sessionId), }, app: { @@ -117,3 +92,24 @@ contextBridge.exposeInMainWorld('desktop', { }, }, }); + +contextBridge.exposeInMainWorld('webContentView', { + createTerminalView: (params) => { + ipcRenderer.send('createTerminalView', { + ...params, + zoomFactor: webFrame.getZoomFactor(), + }); + }, + destroyTerminalView: () => { + ipcRenderer.send('destroyTerminalView'); + }, + hideTerminalView: () => { + ipcRenderer.send('hideTerminalView'); + }, + positionTerminalView: (position) => { + ipcRenderer.send('positionTerminalView', { + position, + zoomFactor: webFrame.getZoomFactor(), + }); + }, +});