From ea0307b4b6d2bfee1db64433db5bff10c60427c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20S=C3=A1nchez-Gallego?= Date: Fri, 12 Aug 2022 10:25:32 -0400 Subject: [PATCH] Clean up Electron API and remove exposed ipcRenderer invoke and on --- src/main/events.ts | 53 +++++++------- src/main/menu.ts | 2 +- src/main/preload.ts | 89 +++++++++++++++++------ src/main/tron/connection.ts | 2 +- src/renderer/components/commandButton.tsx | 4 +- src/renderer/hooks.ts | 19 ++--- src/renderer/views/connect.tsx | 13 ++-- src/renderer/views/log/message.tsx | 2 +- src/renderer/views/main.tsx | 16 ++-- src/renderer/views/preferences/index.tsx | 2 +- 10 files changed, 119 insertions(+), 83 deletions(-) diff --git a/src/main/events.ts b/src/main/events.ts index 5744093..fe0f373 100644 --- a/src/main/events.ts +++ b/src/main/events.ts @@ -27,21 +27,27 @@ export interface TronEventReplyIFace { export default function loadEvents() { // Add events to ipcMain - // Main - ipcMain.handle('window-open', async (event, name) => { + // Window handling + ipcMain.handle('window:open', async (event, name) => { createWindow(name); }); - ipcMain.handle('window-close', async (event, name) => { + ipcMain.handle('window:close', async (event, name) => { let win = windows.get(name)!; win.close(); }); - ipcMain.handle('window-get-size', async (event, name) => { + ipcMain.handle('window:get-size', async (event, name) => { let win = windows.get(name)!; return win.getSize(); }); + ipcMain.handle('window:set-size', async (event, name, width, height, animate = false) => { + let win = windows.get(name); + if (win !== undefined) win.setSize(width, height, animate); + }); + + // Show alerts ipcMain.handle( 'show-message-box', async (event, options: MessageBoxOptions) => await dialog.showMessageBox(options) @@ -52,49 +58,44 @@ export default function loadEvents() { async (event, title: string, content: string) => await dialog.showErrorBox(title, content) ); - ipcMain.handle('window-set-size', async (event, name, width, height, animate = false) => { - let win = windows.get(name); - if (win !== undefined) win.setSize(width, height, animate); - }); - // Store - ipcMain.handle('get-from-store', async (event, key) => { + ipcMain.handle('store:get', async (event, key) => { if (Array.isArray(key)) return key.map((k) => store.get(k)); return store.get(key); }); - ipcMain.handle('set-in-store', async (event, key, value) => { + ipcMain.handle('store:set', async (event, key, value) => { return store.set(key, value); }); - // keytar - ipcMain.handle('get-password', async (event, service, account) => { + // keytar passwords + ipcMain.handle('password:get', async (event, service, account) => { return await keytar.getPassword(service, account); }); - ipcMain.handle('set-password', async (event, service, account, value) => { + ipcMain.handle('password:set', async (event, service, account, value) => { return await keytar.setPassword(service, account, value); }); // Tron - ipcMain.handle('tron-connect', async (event, host: string, port: number) => { + ipcMain.handle('tron:connect', async (event, host: string, port: number) => { return await tron.connect(host, port); }); - ipcMain.handle('tron-authorise', async (event, credentials) => { + ipcMain.handle('tron:authorise', async (event, credentials) => { return await tron.authorise(credentials); }); - ipcMain.handle('tron-add-streamer-window', async (event, sendAll = false) => { + ipcMain.handle('tron:add-streamer-window', async (event, sendAll = false) => { tron.addStreamerWindow(event.sender.id, event.sender, sendAll); }); - ipcMain.handle('tron-remove-streamer-window', async (event) => { + ipcMain.handle('tron:remove-streamer-window', async (event) => { tron.removeStreamerWindow(event.sender.id); }); ipcMain.handle( - 'tron-send-command', + 'tron:send-command', async (event, commandString: string, raise: boolean = false) => { let command = await tron.sendCommand(commandString).then(); if (command.status === CommandStatus.Failed && raise) { @@ -109,22 +110,22 @@ export default function loadEvents() { } ); - ipcMain.handle('tron-simulate-data', async (event, sender: string, line: string) => + ipcMain.handle('tron:simulate-data', async (event, sender: string, line: string) => tron.parseData(`.${sender} 666 ${sender} d ${line}`) ); ipcMain.handle( - 'tron-register-model-listener', + 'tron:register-model-listener', async (event, keys: string[], listenOn, refresh = true) => { tron.model.registerListener(keys, event, listenOn, true, refresh); } ); - ipcMain.handle('tron-remove-model-listener', async (event, listenOn) => { - tron.model.removeListener(listenOn); + ipcMain.handle('tron:remove-model-listener', async (event, channel) => { + tron.model.removeListener(channel); }); - ipcMain.handle('tron-model-getall', async (event) => { + ipcMain.handle('tron:model-getall', async (event) => { return tron.model.keywords; }); @@ -145,7 +146,7 @@ export default function loadEvents() { } if (mainWindow) { - mainWindow.webContents.send('tron-status', tron.status); + mainWindow.webContents.send('tron:status', tron.status); } } @@ -160,7 +161,7 @@ export default function loadEvents() { nativeTheme.on('updated', () => reportTheme()); - ipcMain.handle('theme-use-dark', () => { + ipcMain.handle('theme:use-dark', () => { return nativeTheme.shouldUseDarkColors; }); } diff --git a/src/main/menu.ts b/src/main/menu.ts index 9a434ca..325faca 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -60,7 +60,7 @@ const template: any[] = [ .then((response) => { if (response.response === 0) { BrowserWindow.getAllWindows().forEach((win) => { - win.webContents.send('clear-logs'); + win.webContents.send('tron:clear-logs'); tron.clear(); }); } diff --git a/src/main/preload.ts b/src/main/preload.ts index 340a78b..e2a7a6a 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -12,24 +12,42 @@ import { contextBridge, dialog, ipcRenderer } from 'electron'; import log from 'electron-log'; import { TronEventReplyIFace } from './events'; import store from './store'; - -// TODO: According to https://bit.ly/38aeKXB, we should not expose the ipcRenderer -// directly. Instead, we should expose the channels from events.ts here. We should also -// not expose the ipcRendered invoke, send, on functions. +import { ConnectionStatus, KeywordMap } from './tron'; export interface IElectronAPI { log: log.LogFunctions; - invoke(arg0: string, ...arg1: any): Promise; - send(arg0: string, ...arg1: any): void; - on(arg0: string, listener: any): void; + window: { + open(name: string): Promise; + close(name: string): Promise; + getSize(name: string): Promise; + setSize(name: string, width: number, height: number, animate?: boolean): Promise; + }; store: { get(key: string | string[]): Promise; set(key: string, value: any): Promise; get_sync(key: string): any; }; + password: { + get(service: string, account: string): Promise; + set(service: string, account: string, value: any): Promise; + }; tron: { send(commandString: string, raise?: boolean): Promise; simulateTronData(sender: string, line: string): Promise; + registerModelListener(keywords: string[], channel: string, refresh?: boolean): Promise; + removeModelListener(channel: string): Promise; + subscribe(channel: string, callback: (keywords: KeywordMap) => void): Promise; + addStreamerWindow(sendAll?: boolean): Promise; + removeStreamerWindow(): Promise; + connect(host: string, port: number): Promise; + authorise(credentials: { + program: string; + user: string; + password: string; + }): Promise<[boolean, string | null]>; + onStatus(callback: (status: ConnectionStatus) => void): Promise; + onModelReceivedReply(callback: (replies: string[]) => void): Promise; + onClearLogs(callback: () => void): Promise; }; openInBrowser(path: string): void; openInApplication(command: string): Promise; @@ -39,28 +57,55 @@ export interface IElectronAPI { }; } -const API: IElectronAPI = { +const ElectronAPI: IElectronAPI = { log: log.functions, - invoke: (channel, ...params) => { - return ipcRenderer.invoke(channel, ...params); - }, - send: (channel, ...params) => { - return ipcRenderer.send(channel, ...params); - }, - on: (channel, listener) => { - ipcRenderer.removeAllListeners(channel); - ipcRenderer.on(channel, (event, ...args) => listener(...args)); + window: { + open: async (name) => ipcRenderer.invoke('window:open', name), + close: async (name) => ipcRenderer.invoke('window:close', name), + getSize: async (name) => ipcRenderer.invoke('window:get-size', name), + setSize: async (name, width, height, animate = false) => + ipcRenderer.invoke('window:set-size', name, width, height, animate) }, store: { - get: async (key) => ipcRenderer.invoke('get-from-store', key), - set: async (key, value) => ipcRenderer.invoke('set-in-store', key, value), + get: async (key) => ipcRenderer.invoke('store:get', key), + set: async (key, value) => ipcRenderer.invoke('store:set', key, value), get_sync: (key) => store.get(key) }, + password: { + get: async (service, account) => ipcRenderer.invoke('password:get', service, account), + set: async (service, account, value) => + ipcRenderer.invoke('password:set', service, account, value) + }, tron: { send: async (commandString, raise = false) => - ipcRenderer.invoke('tron-send-command', commandString, raise), + ipcRenderer.invoke('tron:send-command', commandString, raise), simulateTronData: async (sender, line) => { - ipcRenderer.invoke('tron-simulate-data', sender, line); + ipcRenderer.invoke('tron:simulate-data', sender, line); + }, + registerModelListener: async (...args) => + ipcRenderer.invoke('tron:register-model-listener', ...args), + removeModelListener: async (channel) => + ipcRenderer.invoke('tron:remove-model-listener', channel), + subscribe: async (channel, callback) => { + ipcRenderer.removeAllListeners(channel); + ipcRenderer.on(channel, (event, keywords) => callback(keywords)); + }, + addStreamerWindow: async (sendAll = false) => + ipcRenderer.invoke('tron:add-streamer-window', sendAll), + removeStreamerWindow: async () => ipcRenderer.invoke('tron:remove-streamer-window'), + connect: async (host, port) => ipcRenderer.invoke('tron:connect', host, port), + authorise: async (credentials) => ipcRenderer.invoke('tron:authorise', credentials), + onStatus: async (callback) => { + ipcRenderer.removeAllListeners('tron:status'); + ipcRenderer.on('tron:status', (event, status) => callback(status)); + }, + onModelReceivedReply: async (callback) => { + ipcRenderer.removeAllListeners('tron:model-received-reply'); + ipcRenderer.on('tron:model-received-reply', (event, replies) => callback(replies)); + }, + onClearLogs: async (callback) => { + ipcRenderer.removeAllListeners('tron:clear-logs'); + ipcRenderer.on('tron:clear-logs', (event) => callback()); } }, openInBrowser: (path) => { @@ -84,4 +129,4 @@ const API: IElectronAPI = { } }; -contextBridge.exposeInMainWorld('api', API); +contextBridge.exposeInMainWorld('api', ElectronAPI); diff --git a/src/main/tron/connection.ts b/src/main/tron/connection.ts index 3368fc8..610e1ad 100644 --- a/src/main/tron/connection.ts +++ b/src/main/tron/connection.ts @@ -260,7 +260,7 @@ export default class TronConnection { this._subscribedWindows.forEach((webContents, id) => { if (!windowId || windowId === id) { try { - webContents.send('tron-model-received-reply', reply); + webContents.send('tron:model-received-reply', reply); } catch { log.debug('Failed sending message to listener', id); log.debug('Purging listener', id); diff --git a/src/renderer/components/commandButton.tsx b/src/renderer/components/commandButton.tsx index 20c46f0..bfbebba 100644 --- a/src/renderer/components/commandButton.tsx +++ b/src/renderer/components/commandButton.tsx @@ -34,8 +34,8 @@ function createCommandObservable(command: string) { }); } return new Observable((subscriber) => { - window.api - .invoke('tron-send-command', command) + window.api.tron + .send(command) .then((reply: TronEventReplyIFace) => { if (reply.status === CommandStatus.Done) { subscriber.complete(); diff --git a/src/renderer/hooks.ts b/src/renderer/hooks.ts index bf3d471..7271e03 100644 --- a/src/renderer/hooks.ts +++ b/src/renderer/hooks.ts @@ -62,15 +62,10 @@ export function useKeywords(keys: string[], channel: any = null, refresh: boolea const channel = params.current.channel; // Subscribe to model and listen on channel. - window.api.on(channel, updateKeywords); - window.api.invoke( - 'tron-register-model-listener', - Array.from(lowerKeys.keys()), - channel, - refresh - ); - - const unload = () => window.api.invoke('tron-remove-model-listener', channel); + window.api.tron.subscribe(channel, updateKeywords); + window.api.tron.registerModelListener(Array.from(lowerKeys.keys()), channel, refresh); + + const unload = () => window.api.tron.removeModelListener(channel); window.addEventListener('unload', unload); // Unsubscribe when component unmounts. @@ -101,13 +96,13 @@ export function useListener(onReceived: (reply: Reply[]) => any, sendAll = true) ); useEffect(() => { - window.api.on('tron-model-received-reply', parseReplies); + window.api.tron.onModelReceivedReply(parseReplies); }, [parseReplies]); useEffect(() => { - window.api.invoke('tron-add-streamer-window', params.current.sendAll); + window.api.tron.addStreamerWindow(params.current.sendAll); - const unload = () => window.api.invoke('tron-remove-streamer-window'); + const unload = () => window.api.tron.removeStreamerWindow(); window.addEventListener('unload', unload); return () => { diff --git a/src/renderer/views/connect.tsx b/src/renderer/views/connect.tsx index 5d2b84e..6eebb7a 100644 --- a/src/renderer/views/connect.tsx +++ b/src/renderer/views/connect.tsx @@ -97,7 +97,7 @@ export default function ConnectView() { .get(['user.connection.program', 'user.connection.user', 'user.connection.host']) .then(async (res: any) => { let program = res[0]; - if (program) return [...res, await window.api.invoke('get-password', 'hub', program)]; + if (program) return [...res, await window.api.password.get('hub', program)]; return [...res, '']; }) .then((res: any) => { @@ -116,7 +116,7 @@ export default function ConnectView() { window.api.store.set('user.connection.program', connectForm.program.toLowerCase()); window.api.store.set('user.connection.user', connectForm.user); if (connectForm.program) - window.api.invoke('set-password', 'hub', connectForm.program, connectForm.password); + window.api.password.set('hub', connectForm.program, connectForm.password); }; let handleConnect = async (event: SyntheticEvent) => { @@ -126,17 +126,16 @@ export default function ConnectView() { let port = (await window.api.store.get('connection.port')) || 9877; - const connectionResult = await window.api.invoke('tron-connect', connectForm.host, port); + const connectionResult = await window.api.tron.connect(connectForm.host, port); switch (connectionResult) { case ConnectionStatus.Connected: - const [result, err]: [boolean, string | null] = await window.api.invoke( - 'tron-authorise', + const [result, err]: [boolean, string | null] = await window.api.tron.authorise( (({ program, user, password }) => ({ program, user, password }))(connectForm) ); if (result === true) { storeCredentials(); - window.api.invoke('window-close', NAME); + window.api.window.close(NAME); } else { setError(err!); } @@ -148,7 +147,7 @@ export default function ConnectView() { setError('Connection timed out'); break; default: - window.api.invoke('window-close', NAME); + window.api.window.close(NAME); break; } setButtonDisabled(false); diff --git a/src/renderer/views/log/message.tsx b/src/renderer/views/log/message.tsx index d1d5152..daaed57 100644 --- a/src/renderer/views/log/message.tsx +++ b/src/renderer/views/log/message.tsx @@ -257,7 +257,7 @@ const Messages: React.FC = ({ onConfigUpdate }) => { }, [actors, onConfigUpdate]); React.useEffect(() => { - window.api.on('clear-logs', () => dispatch({ type: 'clear' })); + window.api.tron.onClearLogs(() => dispatch({ type: 'clear' })); }, []); React.useEffect(() => { diff --git a/src/renderer/views/main.tsx b/src/renderer/views/main.tsx index d6e1798..74575d8 100644 --- a/src/renderer/views/main.tsx +++ b/src/renderer/views/main.tsx @@ -34,14 +34,10 @@ async function autoconnect() { if (!config.every((x: unknown) => x)) return [false, false]; const [program, user, host, port] = config; - const password: string | null = await window.api.invoke( - 'get-password', - 'hub', - program.toLowerCase() - ); + const password: string | null = await window.api.password.get('hub', program.toLowerCase()); if (!password) return [false, false]; - const connResult = await window.api.invoke('tron-connect', host, port); + const connResult = await window.api.tron.connect(host, port); switch (connResult) { case ConnectionStatus.Connected: break; @@ -51,7 +47,7 @@ async function autoconnect() { return [false, true]; } - const authResult = await window.api.invoke('tron-authorise', { + const authResult = await window.api.tron.authorise({ program, user, password @@ -86,7 +82,7 @@ async function getTabView(tab: ValidTabs) { ({ width, height } = await window.api.store.get('windows.main')); } - window.api.invoke('window-set-size', 'main', width + 100, height + 50, true); + window.api.window.setSize('main', width + 100, height + 50, true); let TabView: React.FunctionComponent; if (tab === 'tcc') { @@ -116,7 +112,7 @@ function ConnectSnackbar(): JSX.Element { }; React.useEffect(() => { - window.api.on('tron-status', handleTronStatus); + window.api.tron.onStatus(handleTronStatus); window.api.store.get('user.autoconnect').then((res: boolean) => { if (res) handleReconnect(false); // Initial connect }); @@ -126,7 +122,7 @@ function ConnectSnackbar(): JSX.Element { const result = await autoconnect(); if (result[0] === false) { setOpen(true); - if (openConnect) await window.api.invoke('window-open', 'connect'); + if (openConnect) await window.api.window.open('connect'); } else { setOpen(false); } diff --git a/src/renderer/views/preferences/index.tsx b/src/renderer/views/preferences/index.tsx index 46e11c8..7440058 100644 --- a/src/renderer/views/preferences/index.tsx +++ b/src/renderer/views/preferences/index.tsx @@ -18,7 +18,7 @@ export default function PreferencesView() { const height = containerRef.current?.clientHeight; const width = containerRef.current?.clientWidth; if (height && width) { - window.api.invoke('window-set-size', 'preferences', width, height + 30); + window.api.window.setSize('preferences', width, height + 30); } }, [value]);