fix: 🐛 electron IPC error handling (#3208)

* 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
This commit is contained in:
Priya Patel authored and GitHub committed 2026-03-27 14:26:31 -07:00
1 parent 7454ecf57a
commit 605b7940e3
2 files changed
+70 -79

No files matched your search

+5 -10
View File
@@ -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;
});
};
+65 -69
View File
@@ -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(),
});
},
});