import { ipcMain, ipcRenderer, WebContents } from 'electron'; import log from 'electron-log'; import * as uuid from 'uuid'; import { IGuiSettingsState } from './gui-settings-state'; import { IAppUpgradeInfo, ICurrentAppVersionInfo } from '../main/index'; import { AccountToken, IAccountData, ILocation, IRelayList, ISettings, RelaySettingsUpdate, TunnelStateTransition, } from './daemon-rpc-types'; export interface IAppStateSnapshot { locale: string; isConnected: boolean; autoStart: boolean; accountHistory: AccountToken[]; tunnelState: TunnelStateTransition; settings: ISettings; location?: ILocation; relays: IRelayList; currentVersion: ICurrentAppVersionInfo; upgradeVersion: IAppUpgradeInfo; guiSettings: IGuiSettingsState; } interface ISender { notify(webContents: WebContents, value: T): void; } interface ISenderVoid { notify(webContents: WebContents): void; } interface IReceiver { listen(fn: (value: T) => void): void; } interface ITunnelMethods extends IReceiver { connect(): Promise; disconnect(): Promise; } interface ITunnelHandlers extends ISender { handleConnect(fn: () => Promise): void; handleDisconnect(fn: () => Promise): void; } interface ISettingsMethods extends IReceiver { setAllowLan(allowLan: boolean): Promise; setEnableIpv6(enableIpv6: boolean): Promise; setBlockWhenDisconnected(block: boolean): Promise; setOpenVpnMssfix(mssfix?: number): Promise; updateRelaySettings(update: RelaySettingsUpdate): Promise; } interface ISettingsHandlers extends ISender { handleAllowLan(fn: (allowLan: boolean) => Promise): void; handleEnableIpv6(fn: (enableIpv6: boolean) => Promise): void; handleBlockWhenDisconnected(fn: (block: boolean) => Promise): void; handleOpenVpnMssfix(fn: (mssfix?: number) => Promise): void; handleUpdateRelaySettings(fn: (update: RelaySettingsUpdate) => Promise): void; } interface IGuiSettingsMethods extends IReceiver { setAutoConnect(autoConnect: boolean): void; setStartMinimized(startMinimized: boolean): void; setMonochromaticIcon(monochromaticIcon: boolean): void; } interface IGuiSettingsHandlers extends ISender { handleAutoConnect(fn: (autoConnect: boolean) => void): void; handleStartMinimized(fn: (startMinimized: boolean) => void): void; handleMonochromaticIcon(fn: (monochromaticIcon: boolean) => void): void; } interface IAccountHandlers { handleSet(fn: (token: AccountToken) => Promise): void; handleUnset(fn: () => Promise): void; handleGetData(fn: (token: AccountToken) => Promise): void; } interface IAccountMethods { set(token: AccountToken): Promise; unset(): Promise; getData(token: AccountToken): Promise; } interface IAccountHistoryHandlers extends ISender { handleRemoveItem(fn: (token: AccountToken) => Promise): void; } interface IAccountHistoryMethods extends IReceiver { removeItem(token: AccountToken): Promise; } interface IAutoStartMethods extends IReceiver { set(autoStart: boolean): Promise; } interface IAutoStartHandlers extends ISender { handleSet(fn: (value: boolean) => Promise): void; } /// Events names 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 SETTINGS_CHANGED = 'settings-changed'; const SET_ALLOW_LAN = 'set-allow-lan'; const SET_ENABLE_IPV6 = 'set-enable-ipv6'; const SET_BLOCK_WHEN_DISCONNECTED = 'set-block-when-disconnected'; const SET_OPENVPN_MSSFIX = 'set-openvpn-mssfix'; const UPDATE_RELAY_SETTINGS = 'update-relay-settings'; 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_AUTO_CONNECT = 'set-auto-connect'; const SET_MONOCHROMATIC_ICON = 'set-monochromatic-icon'; const SET_START_MINIMIZED = 'set-start-minimized'; const GET_APP_STATE = 'get-app-state'; const ACCOUNT_HISTORY_CHANGED = 'account-history-changed'; const REMOVE_ACCOUNT_HISTORY_ITEM = 'remove-account-history-item'; const SET_ACCOUNT = 'set-account'; const UNSET_ACCOUNT = 'unset-account'; const GET_ACCOUNT_DATA = 'get-account-data'; const AUTO_START_CHANGED = 'auto-start-changed'; const SET_AUTO_START = 'set-auto-start'; /// 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 daemonConnected: IReceiver = { listen: listen(DAEMON_CONNECTED), }; public static daemonDisconnected: IReceiver = { listen: listen(DAEMON_DISCONNECTED), }; public static tunnel: ITunnelMethods = { listen: listen(TUNNEL_STATE_CHANGED), connect: requestSender(CONNECT_TUNNEL), disconnect: requestSender(DISCONNECT_TUNNEL), }; public static settings: ISettingsMethods = { listen: listen(SETTINGS_CHANGED), setAllowLan: requestSender(SET_ALLOW_LAN), setEnableIpv6: requestSender(SET_ENABLE_IPV6), setBlockWhenDisconnected: requestSender(SET_BLOCK_WHEN_DISCONNECTED), setOpenVpnMssfix: requestSender(SET_OPENVPN_MSSFIX), updateRelaySettings: requestSender(UPDATE_RELAY_SETTINGS), }; public static location: IReceiver = { listen: listen(LOCATION_CHANGED), }; public static relays: IReceiver = { listen: listen(RELAYS_CHANGED), }; public static currentVersion: IReceiver = { listen: listen(CURRENT_VERSION_CHANGED), }; public static upgradeVersion: IReceiver = { listen: listen(UPGRADE_VERSION_CHANGED), }; public static guiSettings: IGuiSettingsMethods = { listen: listen(GUI_SETTINGS_CHANGED), setAutoConnect: set(SET_AUTO_CONNECT), setMonochromaticIcon: set(SET_MONOCHROMATIC_ICON), setStartMinimized: set(SET_START_MINIMIZED), }; public static autoStart: IAutoStartMethods = { listen: listen(AUTO_START_CHANGED), set: requestSender(SET_AUTO_START), }; public static account: IAccountMethods = { set: requestSender(SET_ACCOUNT), unset: requestSender(UNSET_ACCOUNT), getData: requestSender(GET_ACCOUNT_DATA), }; public static accountHistory: IAccountHistoryMethods = { listen: listen(ACCOUNT_HISTORY_CHANGED), removeItem: requestSender(REMOVE_ACCOUNT_HISTORY_ITEM), }; } export class IpcMainEventChannel { public static state = { handleGet(fn: () => IAppStateSnapshot) { ipcMain.on(GET_APP_STATE, (event: Electron.Event) => { event.returnValue = fn(); }); }, }; public static daemonConnected: ISenderVoid = { notify: senderVoid(DAEMON_CONNECTED), }; public static daemonDisconnected: ISender = { notify: sender(DAEMON_DISCONNECTED), }; public static tunnel: ITunnelHandlers = { notify: sender(TUNNEL_STATE_CHANGED), handleConnect: requestHandler(CONNECT_TUNNEL), handleDisconnect: requestHandler(DISCONNECT_TUNNEL), }; public static location: ISender = { notify: sender(LOCATION_CHANGED), }; public static settings: ISettingsHandlers = { notify: sender(SETTINGS_CHANGED), handleAllowLan: requestHandler(SET_ALLOW_LAN), handleEnableIpv6: requestHandler(SET_ENABLE_IPV6), handleBlockWhenDisconnected: requestHandler(SET_BLOCK_WHEN_DISCONNECTED), handleOpenVpnMssfix: requestHandler(SET_OPENVPN_MSSFIX), handleUpdateRelaySettings: requestHandler(UPDATE_RELAY_SETTINGS), }; public static relays: ISender = { notify: sender(RELAYS_CHANGED), }; public static currentVersion: ISender = { notify: sender(CURRENT_VERSION_CHANGED), }; public static upgradeVersion: ISender = { notify: sender(UPGRADE_VERSION_CHANGED), }; public static guiSettings: IGuiSettingsHandlers = { notify: sender(GUI_SETTINGS_CHANGED), handleAutoConnect: handler(SET_AUTO_CONNECT), handleMonochromaticIcon: handler(SET_MONOCHROMATIC_ICON), handleStartMinimized: handler(SET_START_MINIMIZED), }; public static autoStart: IAutoStartHandlers = { notify: sender(AUTO_START_CHANGED), handleSet: requestHandler(SET_AUTO_START), }; public static account: IAccountHandlers = { handleSet: requestHandler(SET_ACCOUNT), handleUnset: requestHandler(UNSET_ACCOUNT), handleGetData: requestHandler(GET_ACCOUNT_DATA), }; public static accountHistory: IAccountHistoryHandlers = { notify: sender(ACCOUNT_HISTORY_CHANGED), handleRemoveItem: requestHandler(REMOVE_ACCOUNT_HISTORY_ITEM), }; } function listen(event: string): (fn: (value: T) => void) => void { return (fn: (value: T) => void) => { ipcRenderer.on(event, (_event: Electron.Event, newState: T) => fn(newState)); }; } function set(event: string): (value: T) => void { return (newValue: T) => { ipcRenderer.send(event, newValue); }; } function sender(event: string): (webContents: WebContents, value: T) => void { return (webContents: WebContents, value: T) => { webContents.send(event, value); }; } function senderVoid(event: string): (webContents: WebContents) => void { return (webContents: WebContents) => { webContents.send(event); }; } function handler(event: string): (handlerFn: (value: T) => void) => void { return (handlerFn: (value: T) => void) => { ipcMain.on(event, (_event: Electron.Event, newValue: T) => { handlerFn(newValue); }); }; } type RequestResult = { type: 'success'; value: T } | { type: 'error'; message: string }; function requestHandler(event: string): (fn: (...args: any[]) => Promise) => void { return (fn: (...args: any[]) => Promise) => { ipcMain.on(event, async (ipcEvent: Electron.Event, requestId: string, ...args: any[]) => { const responseEvent = `${event}-${requestId}`; try { const result: RequestResult = { 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 = { 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); } } }); }; } function requestSender(event: string): (...args: any[]) => Promise { return (...args: any[]): Promise => { return new Promise((resolve: (result: T) => void, reject: (error: Error) => void) => { const requestId = uuid.v4(); const responseEvent = `${event}-${requestId}`; ipcRenderer.once(responseEvent, (_ipcEvent: Electron.Event, result: RequestResult) => { switch (result.type) { case 'error': reject(new Error(result.message)); break; case 'success': resolve(result.value); break; } }); ipcRenderer.send(event, requestId, ...args); }); }; }