diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2020-12-09 13:52:48 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2020-12-09 13:52:48 +0100 |
| commit | ae3cae40c16b01683b2a562ac48799699c711ca2 (patch) | |
| tree | 88b3aae528c977fe0a86f4a2f8974668dced84d8 /gui/src/shared | |
| parent | 83bdea1ba486c2625e99625e1948730359d01d22 (diff) | |
| parent | d53403ee74a89643c3f3ff201128b32fc760798f (diff) | |
| download | mullvadvpn-ae3cae40c16b01683b2a562ac48799699c711ca2.tar.xz mullvadvpn-ae3cae40c16b01683b2a562ac48799699c711ca2.zip | |
Merge branch 'improve-ipc'
Diffstat (limited to 'gui/src/shared')
| -rw-r--r-- | gui/src/shared/account-expiry.ts | 7 | ||||
| -rw-r--r-- | gui/src/shared/ipc-event-channel.ts | 658 | ||||
| -rw-r--r-- | gui/src/shared/ipc-helpers.ts | 184 | ||||
| -rw-r--r-- | gui/src/shared/string-helpers.ts | 3 |
4 files changed, 330 insertions, 522 deletions
diff --git a/gui/src/shared/account-expiry.ts b/gui/src/shared/account-expiry.ts index 4511797f3c..a76eb6ecc4 100644 --- a/gui/src/shared/account-expiry.ts +++ b/gui/src/shared/account-expiry.ts @@ -1,6 +1,7 @@ import moment from 'moment'; import { sprintf } from 'sprintf-js'; import { messages } from './gettext'; +import { capitalize } from './string-helpers'; type DateArgument = string | Date | moment.Moment; @@ -47,9 +48,5 @@ export function formatRemainingTime( { duration }, ); - return shouldCapitalizeFirstLetter ? capitalizeFirstLetter(remaining) : remaining; -} - -function capitalizeFirstLetter(inputString: string): string { - return inputString.charAt(0).toUpperCase() + inputString.slice(1); + return shouldCapitalizeFirstLetter ? capitalize(remaining) : remaining; } diff --git a/gui/src/shared/ipc-event-channel.ts b/gui/src/shared/ipc-event-channel.ts index 587397467c..11bbeef31f 100644 --- a/gui/src/shared/ipc-event-channel.ts +++ b/gui/src/shared/ipc-event-channel.ts @@ -1,9 +1,3 @@ -import { ipcMain, ipcRenderer, WebContents } from 'electron'; -import log from 'electron-log'; -import * as uuid from 'uuid'; - -import { IGuiSettingsState } from './gui-settings-state'; - import { ICurrentAppVersionInfo } from '../main/index'; import { IWindowShapeParameters } from '../main/window-controller'; import ISplitTunnelingApplication from '../shared/linux-split-tunneling-application'; @@ -23,6 +17,20 @@ import { TunnelState, VoucherResponse, } from './daemon-rpc-types'; +import { IGuiSettingsState } from './gui-settings-state'; +import { + createIpcMain, + createIpcRenderer, + invoke, + invokeSync, + notifyRenderer, + send, +} from './ipc-helpers'; + +export interface IRelayListPair { + relays: IRelayList; + bridges: IRelayList; +} export interface IAppStateSnapshot { locale: string; @@ -40,515 +48,131 @@ export interface IAppStateSnapshot { wireguardPublicKey?: IWireguardPublicKey; } -export interface IRelayListPair { - relays: IRelayList; - bridges: IRelayList; -} - -interface ISender<T> { - notify(webContents: WebContents, value: T): void; -} - -interface ISenderVoid { - notify(webContents: WebContents): void; -} - -interface IReceiver<T> { - listen(fn: (value: T) => void): void; -} - -interface ITunnelMethods extends IReceiver<TunnelState> { - connect(): Promise<void>; - disconnect(): Promise<void>; - reconnect(): Promise<void>; -} - -interface ITunnelHandlers extends ISender<TunnelState> { - handleConnect(fn: () => Promise<void>): void; - handleDisconnect(fn: () => Promise<void>): void; - handleReconnect(fn: () => Promise<void>): void; -} - -interface ISettingsMethods extends IReceiver<ISettings> { - setAllowLan(allowLan: boolean): Promise<void>; - setShowBetaReleases(showBetaReleases: boolean): Promise<void>; - setEnableIpv6(enableIpv6: boolean): Promise<void>; - setBlockWhenDisconnected(block: boolean): Promise<void>; - setBridgeState(state: BridgeState): Promise<void>; - setOpenVpnMssfix(mssfix?: number): Promise<void>; - setWireguardMtu(mtu?: number): Promise<void>; - updateRelaySettings(update: RelaySettingsUpdate): Promise<void>; - updateBridgeSettings(bridgeSettings: BridgeSettings): Promise<void>; - setDnsOptions(dns: IDnsOptions): Promise<void>; -} - -interface ISettingsHandlers extends ISender<ISettings> { - handleAllowLan(fn: (allowLan: boolean) => Promise<void>): void; - handleShowBetaReleases(fn: (showBetaReleases: boolean) => Promise<void>): void; - handleEnableIpv6(fn: (enableIpv6: boolean) => Promise<void>): void; - handleBlockWhenDisconnected(fn: (block: boolean) => Promise<void>): void; - handleBridgeState(fn: (state: BridgeState) => Promise<void>): void; - handleOpenVpnMssfix(fn: (mssfix?: number) => Promise<void>): void; - handleWireguardMtu(fn: (mtu?: number) => Promise<void>): void; - handleUpdateRelaySettings(fn: (update: RelaySettingsUpdate) => Promise<void>): void; - handleUpdateBridgeSettings(fn: (bridgeSettings: BridgeSettings) => Promise<void>): void; - handleDnsOptions(fn: (dns: IDnsOptions) => Promise<void>): void; -} - -interface IGuiSettingsMethods extends IReceiver<IGuiSettingsState> { - setEnableSystemNotifications(flag: boolean): void; - setAutoConnect(autoConnect: boolean): void; - setStartMinimized(startMinimized: boolean): void; - setMonochromaticIcon(monochromaticIcon: boolean): void; - setPreferredLocale(locale: string): void; - setUnpinnedWindow(unpinnedWindow: boolean): void; -} - -interface IGuiSettingsHandlers extends ISender<IGuiSettingsState> { - handleEnableSystemNotifications(fn: (flag: boolean) => void): void; - handleAutoConnect(fn: (autoConnect: boolean) => void): void; - handleStartMinimized(fn: (startMinimized: boolean) => void): void; - handleMonochromaticIcon(fn: (monochromaticIcon: boolean) => void): void; - handleSetPreferredLocale(fn: (locale: string) => void): void; - handleSetUnpinnedWindow(fn: (unpinnedWindow: boolean) => void): void; -} - -interface IAccountHandlers extends ISender<IAccountData | undefined> { - handleCreate(fn: () => Promise<string>): void; - handleLogin(fn: (token: AccountToken) => Promise<void>): void; - handleLogout(fn: () => Promise<void>): void; - handleWwwAuthToken(fn: () => Promise<string>): void; - handleSubmitVoucher(fn: (voucherCode: string) => Promise<VoucherResponse>): void; -} - -interface IAccountMethods extends IReceiver<IAccountData | undefined> { - create(): Promise<string>; - login(token: AccountToken): Promise<void>; - logout(): Promise<void>; - getWwwAuthToken(): Promise<string>; - submitVoucher(voucherCode: string): Promise<VoucherResponse>; -} - -interface IAccountHistoryHandlers extends ISender<AccountToken[]> { - handleRemoveItem(fn: (token: AccountToken) => Promise<void>): void; -} - -interface IAccountHistoryMethods extends IReceiver<AccountToken[]> { - removeItem(token: AccountToken): Promise<void>; -} - -interface IAutoStartMethods extends IReceiver<boolean> { - set(autoStart: boolean): Promise<void>; -} - -interface IAutoStartHandlers extends ISender<boolean> { - handleSet(fn: (value: boolean) => Promise<void>): void; -} - -interface IWireguardKeyMethods extends IReceiver<IWireguardPublicKey | undefined> { - listenKeygenEvents(fn: (event: KeygenEvent) => void): void; - generateKey(): Promise<KeygenEvent>; - verifyKey(): Promise<boolean>; -} - -interface IWireguardKeyHandlers extends ISender<IWireguardPublicKey | undefined> { - notifyKeygenEvent(webContents: WebContents, event: KeygenEvent): void; - handleGenerateKey(fn: () => Promise<KeygenEvent>): void; - handleVerifyKey(fn: () => Promise<boolean>): void; -} - -interface ISplitTunnelingMethods { - getApplications(): Promise<ISplitTunnelingApplication[]>; - launchApplication(application: ISplitTunnelingApplication | string): Promise<void>; -} - -interface ISplitTunnelingHandlers { - handleGetApplications(fn: () => Promise<ISplitTunnelingApplication[]>): void; - handleLaunchApplication( - fn: (application: ISplitTunnelingApplication | string) => Promise<void>, - ): void; -} - -/// Events names - -const LOCALE_CHANGED = 'locale-changed'; -const WINDOW_SHAPE_CHANGED = 'window-shape-changed'; -const WINDOW_FOCUS_CHANGED = 'window-focus'; - -const DAEMON_CONNECTED = 'daemon-connected'; -const DAEMON_DISCONNECTED = 'daemon-disconnected'; - -const TUNNEL_STATE_CHANGED = 'tunnel-state-changed'; -const CONNECT_TUNNEL = 'connect-tunnel'; -const DISCONNECT_TUNNEL = 'disconnect-tunnel'; -const RECONNECT_TUNNEL = 'reconnect-tunnel'; - -const SETTINGS_CHANGED = 'settings-changed'; -const SET_ALLOW_LAN = 'set-allow-lan'; -const SET_SHOW_BETA_RELEASES = 'set-show-beta-releases'; -const SET_ENABLE_IPV6 = 'set-enable-ipv6'; -const SET_BLOCK_WHEN_DISCONNECTED = 'set-block-when-disconnected'; -const SET_BRIDGE_STATE = 'set-bridge-state'; -const SET_OPENVPN_MSSFIX = 'set-openvpn-mssfix'; -const SET_WIREGUARD_MTU = 'set-wireguard-mtu'; -const UPDATE_RELAY_SETTINGS = 'update-relay-settings'; -const UPDATE_BRIDGE_SETTINGS = 'update-bridge-location'; -const SET_DNS_OPTIONS = 'set-dns-options'; - -const LOCATION_CHANGED = 'location-changed'; -const RELAYS_CHANGED = 'relays-changed'; -const CURRENT_VERSION_CHANGED = 'current-version-changed'; -const UPGRADE_VERSION_CHANGED = 'upgrade-version-changed'; - -const GUI_SETTINGS_CHANGED = 'gui-settings-changed'; -const SET_ENABLE_SYSTEM_NOTIFICATIONS = 'set-enable-system-notifications'; -const SET_AUTO_CONNECT = 'set-auto-connect'; -const SET_MONOCHROMATIC_ICON = 'set-monochromatic-icon'; -const SET_START_MINIMIZED = 'set-start-minimized'; -const SET_PREFERRED_LOCALE = 'set-preferred-locale'; -const SET_UNPINNED_WINDOW = 'set-unpinned-window'; - -const GET_APP_STATE = 'get-app-state'; - -const ACCOUNT_HISTORY_CHANGED = 'account-history-changed'; -const REMOVE_ACCOUNT_HISTORY_ITEM = 'remove-account-history-item'; - -const CREATE_NEW_ACCOUNT = 'create-new-account'; -const DO_LOGIN = 'do-login'; -const DO_LOGOUT = 'do-logout'; -const DO_GET_WWW_AUTH_TOKEN = 'do-get-www-auth-token'; -const ACCOUNT_DATA_CHANGED = 'account-data-changed'; -const REDEEM_VOUCHER = 'redeem-voucher'; - -const AUTO_START_CHANGED = 'auto-start-changed'; -const SET_AUTO_START = 'set-auto-start'; - -const WIREGUARD_KEY_SET = 'wireguard-key-change-event'; -const WIREGUARD_KEYGEN_EVENT = 'wireguard-keygen-event'; -const GENERATE_WIREGUARD_KEY = 'generate-wireguard-key'; -const VERIFY_WIREGUARD_KEY = 'verify-wireguard-key'; - -const SPLIT_TUNNELING_GET_APPLICATIONS = 'split-tunneling-get-applications'; -const SPLIT_TUNNELING_LAUNCH_APPLICATION = 'split-tunneling-launch-application'; - -/// Typed IPC event channel -/// -/// Static methods are meant to be provide the way to send the events from a renderer process, while -/// instance methods are meant to be used from a main process. -/// -export class IpcRendererEventChannel { - public static state = { - /// Synchronously sends the IPC request and returns the app state snapshot - get(): IAppStateSnapshot { - return ipcRenderer.sendSync(GET_APP_STATE); - }, - }; - - public static locale: IReceiver<string> = { - listen: listen(LOCALE_CHANGED), - }; - - public static windowShape: IReceiver<IWindowShapeParameters> = { - listen: listen(WINDOW_SHAPE_CHANGED), - }; - - public static windowFocus: IReceiver<boolean> = { - listen: listen(WINDOW_FOCUS_CHANGED), - }; - - public static daemonConnected: IReceiver<void> = { - listen: listen(DAEMON_CONNECTED), - }; - - public static daemonDisconnected: IReceiver<string | undefined> = { - listen: listen(DAEMON_DISCONNECTED), - }; - - public static tunnel: ITunnelMethods = { - listen: listen(TUNNEL_STATE_CHANGED), - connect: requestSender(CONNECT_TUNNEL), - disconnect: requestSender(DISCONNECT_TUNNEL), - reconnect: requestSender(RECONNECT_TUNNEL), - }; - - public static settings: ISettingsMethods = { - listen: listen(SETTINGS_CHANGED), - setAllowLan: requestSender(SET_ALLOW_LAN), - setShowBetaReleases: requestSender(SET_SHOW_BETA_RELEASES), - setEnableIpv6: requestSender(SET_ENABLE_IPV6), - setBlockWhenDisconnected: requestSender(SET_BLOCK_WHEN_DISCONNECTED), - setBridgeState: requestSender(SET_BRIDGE_STATE), - setOpenVpnMssfix: requestSender(SET_OPENVPN_MSSFIX), - setWireguardMtu: requestSender(SET_WIREGUARD_MTU), - updateRelaySettings: requestSender(UPDATE_RELAY_SETTINGS), - updateBridgeSettings: requestSender(UPDATE_BRIDGE_SETTINGS), - setDnsOptions: requestSender(SET_DNS_OPTIONS), - }; - - public static location: IReceiver<ILocation> = { - listen: listen(LOCATION_CHANGED), - }; - - public static relays: IReceiver<IRelayListPair> = { - listen: listen(RELAYS_CHANGED), - }; - - public static currentVersion: IReceiver<ICurrentAppVersionInfo> = { - listen: listen(CURRENT_VERSION_CHANGED), - }; - - public static upgradeVersion: IReceiver<IAppVersionInfo> = { - listen: listen(UPGRADE_VERSION_CHANGED), - }; - - public static guiSettings: IGuiSettingsMethods = { - listen: listen(GUI_SETTINGS_CHANGED), - setEnableSystemNotifications: set(SET_ENABLE_SYSTEM_NOTIFICATIONS), - setAutoConnect: set(SET_AUTO_CONNECT), - setMonochromaticIcon: set(SET_MONOCHROMATIC_ICON), - setStartMinimized: set(SET_START_MINIMIZED), - setPreferredLocale: set(SET_PREFERRED_LOCALE), - setUnpinnedWindow: set(SET_UNPINNED_WINDOW), - }; - - public static autoStart: IAutoStartMethods = { - listen: listen(AUTO_START_CHANGED), - set: requestSender(SET_AUTO_START), - }; - - public static account: IAccountMethods = { - listen: listen(ACCOUNT_DATA_CHANGED), - create: requestSender(CREATE_NEW_ACCOUNT), - login: requestSender(DO_LOGIN), - logout: requestSender(DO_LOGOUT), - getWwwAuthToken: requestSender(DO_GET_WWW_AUTH_TOKEN), - submitVoucher: requestSender(REDEEM_VOUCHER), - }; - - public static accountHistory: IAccountHistoryMethods = { - listen: listen(ACCOUNT_HISTORY_CHANGED), - removeItem: requestSender(REMOVE_ACCOUNT_HISTORY_ITEM), - }; - - public static wireguardKeys: IWireguardKeyMethods = { - listen: listen(WIREGUARD_KEY_SET), - listenKeygenEvents: listen(WIREGUARD_KEYGEN_EVENT), - generateKey: requestSender(GENERATE_WIREGUARD_KEY), - verifyKey: requestSender(VERIFY_WIREGUARD_KEY), - }; - - public static splitTunneling: ISplitTunnelingMethods = { - getApplications: requestSender(SPLIT_TUNNELING_GET_APPLICATIONS), - launchApplication: requestSender(SPLIT_TUNNELING_LAUNCH_APPLICATION), - }; -} - -export class IpcMainEventChannel { - public static state = { - handleGet(fn: () => IAppStateSnapshot) { - ipcMain.on(GET_APP_STATE, (event: Electron.IpcMainEvent) => { - event.returnValue = fn(); - }); - }, - }; - - public static locale: ISender<string> = { - notify: sender(LOCALE_CHANGED), - }; - - public static windowShape: ISender<IWindowShapeParameters> = { - notify: sender(WINDOW_SHAPE_CHANGED), - }; - - public static windowFocus: ISender<boolean> = { - notify: sender(WINDOW_FOCUS_CHANGED), - }; - - public static daemonConnected: ISenderVoid = { - notify: senderVoid(DAEMON_CONNECTED), - }; - - public static daemonDisconnected: ISender<string | undefined> = { - notify: sender(DAEMON_DISCONNECTED), - }; - - public static tunnel: ITunnelHandlers = { - notify: sender(TUNNEL_STATE_CHANGED), - handleConnect: requestHandler(CONNECT_TUNNEL), - handleDisconnect: requestHandler(DISCONNECT_TUNNEL), - handleReconnect: requestHandler(RECONNECT_TUNNEL), - }; - - public static location: ISender<ILocation> = { - notify: sender(LOCATION_CHANGED), - }; +// The different types of requests are: +// * send<ArgumentType>(), which is used for one-way communication from the renderer process to the +// main process. The main channel will have a property named 'handle<PropertyName>' and the +// renderer will have a property named the same as the one specified. +// * invoke<ArgumentType, ReturnType>(), which is used for two-way communication from the renderer +// process to the main process. The naming is the same as `send<A>()`. +// * invokeSync<ArgumentType, ReturnType>(), same as `invoke<A, R>()` but synchronous. +// * notifyRenderer<ArgumentType>(), which is used for one-way communication from the main process +// to the renderer process. The renderer ipc channel will have a property named +// `listen<PropertyName>` and the main channel will have a property named `notify<PropertyName>`. +// +// Example: +// const ipc = { +// groupOfCalls: { +// first: send<boolean>(), +// second: request<boolean, number>(), +// third: requestSync<boolean, number>(), +// fourth: notifyRenderer<boolean>(), +// }, +// }; +// +// createIpcMain(ipc) +// => { +// groupOfCalls: { +// handleFirst: (fn: (arg: boolean) => void) => void, +// handleSecond: (fn: (arg: boolean) => Promise<number>) => void, +// handleThird: (fn: (arg: boolean) => number) => void, +// notifyFourth: (arg: boolean) => void, +// }, +// +// createIpcRenderer(ipc) +// => { +// groupOfCalls: { +// first: (arg: boolean) => void, +// second: (arg: boolean) => Promise<number>, +// third: (arg: boolean) => number, +// listenFourth: (fn: (arg: boolean) => void) => void, +// }, +// } +const ipc = { + state: { + get: invokeSync<void, IAppStateSnapshot>(), + }, + locale: { + '': notifyRenderer<string>(), + }, + windowShape: { + '': notifyRenderer<IWindowShapeParameters>(), + }, + windowFocus: { + '': notifyRenderer<boolean>(), + }, + daemonConnected: { + '': notifyRenderer<void>(), + }, + daemonDisconnected: { + '': notifyRenderer<string | undefined>(), + }, + location: { + '': notifyRenderer<ILocation>(), + }, + relays: { + '': notifyRenderer<IRelayListPair>(), + }, + currentVersion: { + '': notifyRenderer<ICurrentAppVersionInfo>(), + }, + upgradeVersion: { + '': notifyRenderer<IAppVersionInfo>(), + }, + tunnel: { + '': notifyRenderer<TunnelState>(), + connect: invoke<void, void>(), + disconnect: invoke<void, void>(), + reconnect: invoke<void, void>(), + }, + settings: { + '': notifyRenderer<ISettings>(), + setAllowLan: invoke<boolean, void>(), + setShowBetaReleases: invoke<boolean, void>(), + setEnableIpv6: invoke<boolean, void>(), + setBlockWhenDisconnected: invoke<boolean, void>(), + setBridgeState: invoke<BridgeState, void>(), + setOpenVpnMssfix: invoke<number | undefined, void>(), + setWireguardMtu: invoke<number | undefined, void>(), + updateRelaySettings: invoke<RelaySettingsUpdate, void>(), + updateBridgeSettings: invoke<BridgeSettings, void>(), + setDnsOptions: invoke<IDnsOptions, void>(), + }, + guiSettings: { + '': notifyRenderer<IGuiSettingsState>(), + setEnableSystemNotifications: send<boolean>(), + setAutoConnect: send<boolean>(), + setStartMinimized: send<boolean>(), + setMonochromaticIcon: send<boolean>(), + setPreferredLocale: send<string>(), + setUnpinnedWindow: send<boolean>(), + }, + account: { + '': notifyRenderer<IAccountData | undefined>(), + create: invoke<void, string>(), + login: invoke<AccountToken, void>(), + logout: invoke<void, void>(), + getWwwAuthToken: invoke<void, string>(), + submitVoucher: invoke<string, VoucherResponse>(), + }, + accountHistory: { + '': notifyRenderer<AccountToken[]>(), + removeItem: invoke<AccountToken, void>(), + }, + autoStart: { + '': notifyRenderer<boolean>(), + set: invoke<boolean, void>(), + }, + wireguardKeys: { + publicKey: notifyRenderer<IWireguardPublicKey | undefined>(), + keygenEvent: notifyRenderer<KeygenEvent>(), + generateKey: invoke<void, KeygenEvent>(), + verifyKey: invoke<void, boolean>(), + }, + splitTunneling: { + getApplications: invoke<void, ISplitTunnelingApplication[]>(), + launchApplication: invoke<ISplitTunnelingApplication | string, void>(), + }, +}; - public static settings: ISettingsHandlers = { - notify: sender(SETTINGS_CHANGED), - handleAllowLan: requestHandler(SET_ALLOW_LAN), - handleShowBetaReleases: requestHandler(SET_SHOW_BETA_RELEASES), - handleEnableIpv6: requestHandler(SET_ENABLE_IPV6), - handleBlockWhenDisconnected: requestHandler(SET_BLOCK_WHEN_DISCONNECTED), - handleBridgeState: requestHandler(SET_BRIDGE_STATE), - handleOpenVpnMssfix: requestHandler(SET_OPENVPN_MSSFIX), - handleWireguardMtu: requestHandler(SET_WIREGUARD_MTU), - handleUpdateRelaySettings: requestHandler(UPDATE_RELAY_SETTINGS), - handleUpdateBridgeSettings: requestHandler(UPDATE_BRIDGE_SETTINGS), - handleDnsOptions: requestHandler(SET_DNS_OPTIONS), - }; - - public static relays: ISender<IRelayListPair> = { - notify: sender(RELAYS_CHANGED), - }; - - public static currentVersion: ISender<ICurrentAppVersionInfo> = { - notify: sender(CURRENT_VERSION_CHANGED), - }; - - public static upgradeVersion: ISender<IAppVersionInfo> = { - notify: sender(UPGRADE_VERSION_CHANGED), - }; - - public static guiSettings: IGuiSettingsHandlers = { - notify: sender(GUI_SETTINGS_CHANGED), - handleEnableSystemNotifications: handler(SET_ENABLE_SYSTEM_NOTIFICATIONS), - handleAutoConnect: handler(SET_AUTO_CONNECT), - handleMonochromaticIcon: handler(SET_MONOCHROMATIC_ICON), - handleStartMinimized: handler(SET_START_MINIMIZED), - handleSetPreferredLocale: handler(SET_PREFERRED_LOCALE), - handleSetUnpinnedWindow: handler(SET_UNPINNED_WINDOW), - }; - - public static autoStart: IAutoStartHandlers = { - notify: sender<boolean>(AUTO_START_CHANGED), - handleSet: requestHandler(SET_AUTO_START), - }; - - public static account: IAccountHandlers = { - notify: sender<IAccountData | undefined>(ACCOUNT_DATA_CHANGED), - handleCreate: requestHandler(CREATE_NEW_ACCOUNT), - handleLogin: requestHandler(DO_LOGIN), - handleLogout: requestHandler(DO_LOGOUT), - handleWwwAuthToken: requestHandler(DO_GET_WWW_AUTH_TOKEN), - handleSubmitVoucher: requestHandler<VoucherResponse>(REDEEM_VOUCHER), - }; - - public static accountHistory: IAccountHistoryHandlers = { - notify: sender<AccountToken[]>(ACCOUNT_HISTORY_CHANGED), - handleRemoveItem: requestHandler(REMOVE_ACCOUNT_HISTORY_ITEM), - }; - - public static wireguardKeys: IWireguardKeyHandlers = { - notify: sender<IWireguardPublicKey | undefined>(WIREGUARD_KEY_SET), - notifyKeygenEvent: sender<KeygenEvent>(WIREGUARD_KEYGEN_EVENT), - handleGenerateKey: requestHandler(GENERATE_WIREGUARD_KEY), - handleVerifyKey: requestHandler(VERIFY_WIREGUARD_KEY), - }; - - public static splitTunneling: ISplitTunnelingHandlers = { - handleGetApplications: requestHandler(SPLIT_TUNNELING_GET_APPLICATIONS), - handleLaunchApplication: requestHandler(SPLIT_TUNNELING_LAUNCH_APPLICATION), - }; -} - -function listen<T>(event: string): (fn: (value: T) => void) => void { - return (fn: (value: T) => void) => { - ipcRenderer.on(event, (_event: Electron.IpcRendererEvent, newState: T) => fn(newState)); - }; -} - -function set<T>(event: string): (value: T) => void { - return (newValue: T) => { - ipcRenderer.send(event, newValue); - }; -} - -function sender<T>(event: string): (webContents: WebContents, value: T) => void { - return (webContents: WebContents, value: T) => { - if (webContents.isDestroyed()) { - log.error(`sender(${event}): webContents is already destroyed!`); - } else { - webContents.send(event, value); - } - }; -} - -function senderVoid(event: string): (webContents: WebContents) => void { - return (webContents: WebContents) => { - if (webContents.isDestroyed()) { - log.error(`senderVoid(${event}): webContents is already destroyed!`); - } else { - webContents.send(event); - } - }; -} - -function handler<T>(event: string): (handlerFn: (value: T) => void) => void { - return (handlerFn: (value: T) => void) => { - ipcMain.on(event, (_event: Electron.IpcMainEvent, newValue: T) => { - handlerFn(newValue); - }); - }; -} - -type RequestResult<T> = { type: 'success'; value: T } | { type: 'error'; message: string }; - -// The Electron API uses the `any` type. -/* eslint-disable @typescript-eslint/no-explicit-any */ -function requestHandler<T>(event: string): (fn: (...args: any[]) => Promise<T>) => void { - return (fn: (...args: any[]) => Promise<T>) => { - ipcMain.on( - event, - async (ipcEvent: Electron.IpcMainEvent, requestId: string, ...args: any[]) => { - const responseEvent = `${event}-${requestId}`; - try { - const result: RequestResult<T> = { type: 'success', value: await fn(...args) }; - - if (ipcEvent.sender.isDestroyed()) { - log.debug(`Cannot send the reply for ${responseEvent} since the sender was destroyed.`); - } else { - ipcEvent.sender.send(responseEvent, result); - } - } catch (error) { - const result: RequestResult<T> = { type: 'error', message: error.message || '' }; - - if (ipcEvent.sender.isDestroyed()) { - log.debug(`Cannot send the reply for ${responseEvent} since the sender was destroyed.`); - } else { - ipcEvent.sender.send(responseEvent, result); - } - } - }, - ); - }; -} -/* eslint-enable @typescript-eslint/no-explicit-any */ - -// The Electron API uses the `any` type. -/* eslint-disable @typescript-eslint/no-explicit-any */ -function requestSender<T>(event: string): (...args: any[]) => Promise<T> { - return (...args: any[]): Promise<T> => { - return new Promise((resolve: (result: T) => void, reject: (error: Error) => void) => { - const requestId = uuid.v4(); - const responseEvent = `${event}-${requestId}`; - - ipcRenderer.once( - responseEvent, - (_ipcEvent: Electron.IpcRendererEvent, result: RequestResult<T>) => { - switch (result.type) { - case 'error': - reject(new Error(result.message)); - break; - - case 'success': - resolve(result.value); - break; - } - }, - ); - - ipcRenderer.send(event, requestId, ...args); - }); - }; -} -/* eslint-enable @typescript-eslint/no-explicit-any */ +export const IpcMainEventChannel = createIpcMain(ipc); +export const IpcRendererEventChannel = createIpcRenderer(ipc); diff --git a/gui/src/shared/ipc-helpers.ts b/gui/src/shared/ipc-helpers.ts new file mode 100644 index 0000000000..1c1465b09b --- /dev/null +++ b/gui/src/shared/ipc-helpers.ts @@ -0,0 +1,184 @@ +import { ipcMain, ipcRenderer, WebContents } from 'electron'; +import log from 'electron-log'; +import { capitalize } from './string-helpers'; + +type Handler<T, R> = (callback: (arg: T) => R) => void; +type Sender<T, R> = (arg: T) => R; +type Notifier<T> = (webContents: WebContents, arg: T) => void; +type Listener<T> = (callback: (arg: T) => void) => void; + +interface IpcCall<T, R> { + direction: 'renderer-to-main' | 'main-to-renderer'; + send: (event: string) => Notifier<T> | Sender<T, R>; + receive: (event: string) => Listener<T> | Handler<T, R>; +} + +interface MainToRenderer<T> extends IpcCall<T, never> { + direction: 'main-to-renderer'; + send: (event: string) => Notifier<T>; + receive: (event: string) => Listener<T>; +} + +interface RendererToMain<T, R> extends IpcCall<T, R> { + direction: 'renderer-to-main'; + send: (event: string) => Sender<T, R>; + receive: (event: string) => Handler<T, R>; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AnyIpcCall = IpcCall<any, any>; +type Schema = Record<string, Record<string, AnyIpcCall>>; + +// Renames all IPC calls, e.g. `callName` to either `notifyCallName` or `handleCallName` depending +// on direction. +type IpcMainKey<N extends string, I extends AnyIpcCall> = I['direction'] extends 'main-to-renderer' + ? `notify${Capitalize<N>}` + : `handle${Capitalize<N>}`; + +// Selects either the send or receive function depending on direction. +type IpcMainFn<I extends AnyIpcCall> = I['direction'] extends 'main-to-renderer' + ? ReturnType<I['send']> + : ReturnType<I['receive']>; + +// Renames all receiving IPC calls, e.g. `callName` to `listenCallName`. +type IpcRendererKey< + N extends string, + I extends AnyIpcCall +> = I['direction'] extends 'main-to-renderer' ? `listen${Capitalize<N>}` : N; + +// Selects either the send or receive function depending on direction. +type IpcRendererFn<I extends AnyIpcCall> = I['direction'] extends 'main-to-renderer' + ? ReturnType<I['receive']> + : ReturnType<I['send']>; + +// Transforms the provided schema to the correct type for the main event channel. +type IpcMain<S extends Schema> = { + [G in keyof S]: { + [K in keyof S[G] as IpcMainKey<string & K, S[G][K]>]: IpcMainFn<S[G][K]>; + }; +}; + +// Transforms the provided schema to the correct type for the renderer event channel. +type IpcRenderer<S extends Schema> = { + [G in keyof S]: { + [K in keyof S[G] as IpcRendererKey<string & K, S[G][K]>]: IpcRendererFn<S[G][K]>; + }; +}; + +// Preforms the transformation of the main event channel in accordance with the above types. +export function createIpcMain<S extends Schema>(ipc: S): IpcMain<S> { + return createIpc(ipc, (event, key, spec) => { + const capitalizedKey = capitalize(key); + const newKey = + spec.direction === 'main-to-renderer' ? `notify${capitalizedKey}` : `handle${capitalizedKey}`; + const newValue = spec.direction === 'main-to-renderer' ? spec.send(event) : spec.receive(event); + + return [newKey, newValue]; + }); +} + +// Preforms the transformation of the renderer event channel in accordance with the above types. +export function createIpcRenderer<S extends Schema>(ipc: S): IpcRenderer<S> { + return createIpc(ipc, (event, key, spec) => { + const newKey = spec.direction === 'main-to-renderer' ? `listen${capitalize(key)}` : key; + const newValue = spec.direction === 'main-to-renderer' ? spec.receive(event) : spec.send(event); + + return [newKey, newValue]; + }); +} + +function createIpc<S extends Schema, T, R extends IpcMain<S> | IpcRenderer<S>>( + ipc: S, + fn: (event: string, key: string, spec: AnyIpcCall) => [newKey: string, newValue: T], +): R { + return Object.fromEntries( + Object.entries(ipc).map(([groupKey, group]) => { + const newGroup = Object.fromEntries( + Object.entries(group).map(([key, spec]) => fn(`${groupKey}-${key}`, key, spec)), + ); + return [groupKey, newGroup]; + }), + ) as R; +} + +// Sends a request from the renderer process to the main process without any possibility to respond. +export function send<T>(): RendererToMain<T, void> { + return { + direction: 'renderer-to-main', + send: (event: string) => (newValue: T) => ipcRenderer.send(event, newValue), + receive: (event: string) => (handlerFn: (value: T) => void) => { + ipcMain.on(event, (_event, newValue: T) => { + handlerFn(newValue); + }); + }, + }; +} + +// Sends a synchronous request from the renderer process to the main process. +export function invokeSync<T, R>(): RendererToMain<T, R> { + return { + direction: 'renderer-to-main', + send: (event: string) => (newValue: T) => ipcRenderer.sendSync(event, newValue), + receive: (event: string) => (handlerFn: (value: T) => R) => { + ipcMain.on(event, (ipcEvent, newValue: T) => { + ipcEvent.returnValue = handlerFn(newValue); + }); + }, + }; +} + +// Sends an asynchronous request from the renderer process to the main process. +export function invoke<T, R>(): RendererToMain<T, Promise<R>> { + return { + direction: 'renderer-to-main', + send: invokeImpl, + receive: handle, + }; +} + +// Sends a request from the main process to the renderer process without any possibility to respond. +export function notifyRenderer<T>(): MainToRenderer<T> { + return { + direction: 'main-to-renderer', + send: notifyRendererImpl, + receive: (event: string) => (fn: (value: T) => void) => { + ipcRenderer.on(event, (_event, newState: T) => fn(newState)); + }, + }; +} + +function notifyRendererImpl<T>(event: string): Notifier<T> { + return (webContents: WebContents, value: T) => { + if (webContents.isDestroyed()) { + log.error(`sender(${event}): webContents is already destroyed!`); + } else { + webContents.send(event, value); + } + }; +} + +type RequestResult<T> = { type: 'success'; value: T } | { type: 'error'; message: string }; + +function invokeImpl<T, R>(event: string): Sender<T, Promise<R>> { + return async (arg: T): Promise<R> => { + const result: RequestResult<R> = await ipcRenderer.invoke(event, arg); + switch (result.type) { + case 'error': + throw new Error(result.message); + case 'success': + return result.value; + } + }; +} + +function handle<T, R>(event: string): Handler<T, Promise<R>> { + return (fn: (arg: T) => Promise<R>) => { + ipcMain.handle(event, async (_ipcEvent, arg: T) => { + try { + return { type: 'success', value: await fn(arg) }; + } catch (error) { + return { type: 'error', message: error.message || '' }; + } + }); + }; +} diff --git a/gui/src/shared/string-helpers.ts b/gui/src/shared/string-helpers.ts new file mode 100644 index 0000000000..30a2ac9d58 --- /dev/null +++ b/gui/src/shared/string-helpers.ts @@ -0,0 +1,3 @@ +export function capitalize(inputString: string): string { + return inputString.charAt(0).toUpperCase() + inputString.slice(1); +} |
