diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2018-11-15 13:34:50 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2018-11-16 11:12:41 +0100 |
| commit | 113d529db2d769aa327a366b73bb9520f4e0c18c (patch) | |
| tree | fba4a1c2f91f8c984f9002435d3ed1c870a286c3 /gui | |
| parent | 5c44ad01b85928c1bbee6054daa053540c1c1970 (diff) | |
| download | mullvadvpn-113d529db2d769aa327a366b73bb9520f4e0c18c.tar.xz mullvadvpn-113d529db2d769aa327a366b73bb9520f4e0c18c.zip | |
Move tunnel and settings management from renderer to main
Diffstat (limited to 'gui')
7 files changed, 216 insertions, 317 deletions
diff --git a/gui/packages/desktop/src/main/daemon-rpc.js b/gui/packages/desktop/src/main/daemon-rpc.js index 1811fa1e36..ef904560f0 100644 --- a/gui/packages/desktop/src/main/daemon-rpc.js +++ b/gui/packages/desktop/src/main/daemon-rpc.js @@ -638,3 +638,28 @@ function transformObjectKeys(object: Object, keyTransformer: (string) => string) } return object; } + +export function defaultSettings(): Settings { + return { + accountToken: null, + allowLan: false, + autoConnect: false, + relaySettings: { + normal: { + location: 'any', + tunnel: 'any', + }, + }, + tunnelOptions: { + enableIpv6: false, + openvpn: { + mssfix: null, + }, + proxy: null, + }, + }; +} + +export function defaultTunnelStateTransition(): TunnelStateTransition { + return { state: 'disconnected' }; +} diff --git a/gui/packages/desktop/src/main/index.js b/gui/packages/desktop/src/main/index.js index 4ba94831e7..365cf3e613 100644 --- a/gui/packages/desktop/src/main/index.js +++ b/gui/packages/desktop/src/main/index.js @@ -8,15 +8,23 @@ import mkdirp from 'mkdirp'; import uuid from 'uuid'; import { app, screen, BrowserWindow, ipcMain, Tray, Menu, nativeImage } from 'electron'; -import TrayIconController from './tray-icon-controller'; import WindowController from './window-controller'; -import { DaemonRpc, ConnectionObserver, SubscriptionListener } from './daemon-rpc'; -import type { TunnelStateTransition, Settings } from './daemon-rpc'; -import ReconnectionBackoff from './reconnection-backoff'; -import { resolveBin } from './proc'; +import TrayIconController from './tray-icon-controller'; import type { TrayIconType } from './tray-icon-controller'; +import { + DaemonRpc, + ConnectionObserver, + SubscriptionListener, + defaultSettings, + defaultTunnelStateTransition, +} from './daemon-rpc'; +import type { TunnelState, TunnelStateTransition, Settings } from './daemon-rpc'; + +import ReconnectionBackoff from './reconnection-backoff'; +import { resolveBin } from './proc'; + const DAEMON_RPC_PATH = process.platform === 'win32' ? '//./pipe/Mullvad VPN' : '/var/run/mullvad-vpn'; @@ -34,6 +42,9 @@ const ApplicationMain = { _oldLogFilePath: (null: ?string), _quitStage: ('unready': AppQuitStage), + _tunnelState: defaultTunnelStateTransition(), + _settings: defaultSettings(), + run() { // Since electron's GPU blacklists are broken, GPU acceleration won't work on older distros if (process.platform === 'linux') { @@ -237,38 +248,39 @@ const ApplicationMain = { async _onDaemonConnected() { this._connectedToDaemon = true; - // reset the reconnect backoff when connection established. - this._reconnectBackoff.reset(); + // subscribe to events + try { + await this._subscribeEvents(); + } catch (error) { + log.error(`Failed to subscribe: ${error.message}`); - if (this._windowController) { - this._windowController.send('daemon-connected'); + return this._recoverFromBootstrapError(error); } - const stateListener = new SubscriptionListener( - (newState: TunnelStateTransition) => { - this._onStateChange(newState); - }, - (error: Error) => { - log.error(`Cannot deserialize the new state: ${error.message}`); - }, - ); + // fetch the tunnel state + try { + this._setTunnelState(await this._daemonRpc.getState()); + } catch (error) { + log.error(`Failed to fetch the tunnel state: ${error.message}`); - const settingsListener = new SubscriptionListener( - (newSettings: Settings) => { - this._onSettingsChange(newSettings); - }, - (error: Error) => { - log.error(`Cannot deserialize the new settings: ${error.message}`); - }, - ); + return this._recoverFromBootstrapError(error); + } + // fetch settings try { - await Promise.all([ - this._daemonRpc.subscribeStateListener(stateListener), - this._daemonRpc.subscribeSettingsListener(settingsListener), - ]); + this._setSettings(await this._daemonRpc.getSettings()); } catch (error) { - log.error(`Failed to subscribe: ${error.message}`); + log.error(`Failed to fetch settings: ${error.message}`); + + return this._recoverFromBootstrapError(error); + } + + // reset the reconnect backoff when connection established. + this._reconnectBackoff.reset(); + + // notify renderer + if (this._windowController) { + this._windowController.send('daemon-connected'); } }, @@ -277,9 +289,7 @@ const ApplicationMain = { if (error) { log.debug(`Lost connection to daemon: ${error.message}`); - this._reconnectBackoff.attempt(() => { - this._connectToDaemon(); - }); + this._reconnectToDaemon(); } else { log.info(`Disconnected from the daemon`); } @@ -295,18 +305,75 @@ const ApplicationMain = { this._daemonRpc.connect({ path: DAEMON_RPC_PATH }); }, - _onStateChange(newState: TunnelStateTransition) { + _reconnectToDaemon() { + this._reconnectBackoff.attempt(() => { + this._connectToDaemon(); + }); + }, + + _recoverFromBootstrapError(_error: ?Error) { + // Attempt to reconnect to daemon if the program fails to fetch settings, tunnel state or + // subscribe for RPC events. + this._daemonRpc.disconnect(); + + this._reconnectToDaemon(); + }, + + async _subscribeEvents(): Promise<void> { + const stateListener = new SubscriptionListener( + (newState: TunnelStateTransition) => { + this._setTunnelState(newState); + }, + (error: Error) => { + log.error(`Cannot deserialize the new state: ${error.message}`); + }, + ); + + const settingsListener = new SubscriptionListener( + (newSettings: Settings) => { + this._setSettings(newSettings); + }, + (error: Error) => { + log.error(`Cannot deserialize the new settings: ${error.message}`); + }, + ); + + await Promise.all([ + this._daemonRpc.subscribeStateListener(stateListener), + this._daemonRpc.subscribeSettingsListener(settingsListener), + ]); + }, + + _setTunnelState(newState: TunnelStateTransition) { + this._tunnelState = newState; + this._updateTrayIcon(newState.state); + if (this._windowController) { - this._windowController.send('state-changed', newState); + this._windowController.send('tunnel-state-changed', newState); } }, - _onSettingsChange(newSettings: Settings) { + _setSettings(newSettings: Settings) { + this._settings = newSettings; + if (this._windowController) { this._windowController.send('settings-changed', newSettings); } }, + _updateTrayIcon(tunnelState: TunnelState) { + const iconTypes: { [TunnelState]: TrayIconType } = { + connected: 'secured', + connecting: 'securing', + blocked: 'securing', + }; + const type = iconTypes[tunnelState] || 'unsecured'; + + if (this._trayIconController) { + this._trayIconController.animateToIcon(type); + } + }, + _registerWindowListener(windowController: WindowController) { const window = windowController.window; @@ -315,16 +382,16 @@ const ApplicationMain = { _registerIpcListeners() { ipcMain.on('daemon-rpc-call', async (event, id: string, method: string, payload: any) => { - log.debug(`Got daemon-rpc-call: ${id} ${method} ${payload}`); + log.debug(`Got daemon-rpc-call: ${id} ${method}`); try { // $FlowFixMe: flow does not like index accessors. const result = await this._daemonRpc[method](payload); - log.debug(`Reply to ${id} ${method} with success: ${JSON.stringify(result)}`); + log.debug(`Send daemon-rpc-reply-${id} ${method} with success`); event.sender.send(`daemon-rpc-reply-${id}`, result); } catch (error) { - log.debug(`Reply to ${id} ${method} with error: ${error.message}`); + log.debug(`Send daemon-rpc-reply-${id} ${method} with error: ${error.message}`); event.sender.send(`daemon-rpc-reply-${id}`, undefined, { className: error.constructor.name || '', data: { @@ -335,8 +402,13 @@ const ApplicationMain = { } }); - ipcMain.on('daemon-connection-status', (event) => { - event.sender.send('daemon-connection-status-reply', this._connectedToDaemon); + ipcMain.on('get-state', (event) => { + event.sender.send( + 'get-state-reply', + this._connectedToDaemon, + this._tunnelState, + this._settings, + ); }); ipcMain.on('show-window', () => { @@ -346,20 +418,6 @@ const ApplicationMain = { } }); - ipcMain.on('hide-window', () => { - const windowController = this._windowController; - if (windowController) { - windowController.hide(); - } - }); - - ipcMain.on('change-tray-icon', (_event: any, type: TrayIconType) => { - const trayIconController = this._trayIconController; - if (trayIconController) { - trayIconController.animateToIcon(type); - } - }); - ipcMain.on('collect-logs', (event, requestId, toRedact) => { const reportPath = path.join(app.getPath('temp'), uuid.v4() + '.log'); const executable = resolveBin('problem-report'); diff --git a/gui/packages/desktop/src/renderer/app.js b/gui/packages/desktop/src/renderer/app.js index cd9d003659..e21a64aafb 100644 --- a/gui/packages/desktop/src/renderer/app.js +++ b/gui/packages/desktop/src/renderer/app.js @@ -23,9 +23,6 @@ import settingsActions from './redux/settings/actions'; import versionActions from './redux/version/actions'; import userInterfaceActions from './redux/userinterface/actions'; -import SettingsProxy from './lib/subscription-proxy/settings-proxy'; -import TunnelStateProxy from './lib/subscription-proxy/tunnel-state-proxy'; - import type { WindowShapeParameters } from '../main/window-controller'; import type { AccountToken, @@ -38,10 +35,12 @@ import type { AccountData, } from './lib/daemon-rpc-proxy'; -import DaemonRpcProxy from './lib/daemon-rpc-proxy'; +import DaemonRpcProxy, { + defaultSettings, + defaultTunnelStateTransition, +} from './lib/daemon-rpc-proxy'; import type { ReduxStore } from './redux/store'; -import type { TrayIconType } from '../main/tray-icon-controller'; const RELAY_LIST_UPDATE_INTERVAL = 60 * 60 * 1000; @@ -68,12 +67,10 @@ export default class AppRenderer { this._updateRelayLocations(relayList); }, ); - _tunnelStateProxy = new TunnelStateProxy(this._daemonRpc, (tunnelState) => { - this._setTunnelState(tunnelState); - }); - _settingsProxy = new SettingsProxy(this._daemonRpc, (settings) => { - this._setSettings(settings); - }); + + _tunnelState = defaultTunnelStateTransition(); + _settings = defaultSettings(); + _connectedToDaemon = false; constructor() { @@ -118,14 +115,33 @@ export default class AppRenderer { this._onDaemonDisconnected(errorMessage ? new Error(errorMessage) : null); }); - // Request the initial daemon connection status - ipcRenderer.once('daemon-connection-status-reply', (isConnected: boolean) => { - if (isConnected) { - this._onDaemonConnected(); - } + ipcRenderer.on('tunnel-state-changed', (_event: Event, newState: TunnelStateTransition) => { + this._setTunnelState(newState); }); - ipcRenderer.send('daemon-connection-status'); + ipcRenderer.on('settings-changed', (_event: Event, newSettings: Settings) => { + this._setSettings(newSettings); + }); + + // Request the initial state from main process + ipcRenderer.on( + 'get-state-reply', + ( + _event: Event, + isConnected: boolean, + tunnelState: TunnelStateTransition, + settings: Settings, + ) => { + this._setTunnelState(tunnelState); + this._setSettings(settings); + + if (isConnected) { + this._onDaemonConnected(); + } + }, + ); + + ipcRenderer.send('get-state'); // disable pinch to zoom webFrame.setVisualZoomLevelLimits(1, 1); @@ -216,10 +232,8 @@ export default class AppRenderer { async connectTunnel() { const actions = this._reduxActions; - const tunnelState = await this._tunnelStateProxy.fetch(); - // connect only if tunnel is disconnected or blocked. - if (tunnelState.state === 'disconnected' || tunnelState.state === 'blocked') { + if (this._tunnelState.state === 'disconnected' || this._tunnelState.state === 'blocked') { // switch to connecting state immediately to prevent a lag that may be caused by RPC // communication delay actions.connection.connecting(null); @@ -283,10 +297,8 @@ export default class AppRenderer { } async updateAccountExpiry() { - const settings = await this._settingsProxy.fetch(); - - if (settings && settings.accountToken) { - this._accountDataCache.fetch(settings.accountToken); + if (this._settings.accountToken) { + this._accountDataCache.fetch(this._settings.accountToken); } } @@ -428,45 +440,6 @@ export default class AppRenderer { this._connectedToDaemon = true; try { - await this._runPrimaryApplicationFlow(); - } catch (error) { - log.error(`An error was raised when running the primary application flow: ${error.message}`); - } - } - - _onDaemonDisconnected(error: ?Error) { - const actions = this._reduxActions; - - this._relayListCache.stopUpdating(); - - // recover connection on error - if (error) { - // only send to the connecting to daemon view if the daemon was - // connnected previously - if (this._connectedToDaemon) { - actions.history.replace('/'); - } - } - - this._connectedToDaemon = false; - } - - async _runPrimaryApplicationFlow() { - // fetch initial state and subscribe for changes - try { - await this._settingsProxy.fetch(); - } catch (error) { - log.error(`Cannot fetch the initial settings: ${error.message}`); - } - - try { - await this._tunnelStateProxy.fetch(); - } catch (error) { - log.error(`Cannot fetch the initial tunnel state: ${error.message}`); - } - - // fetch the rest of data - try { await this._fetchCurrentVersion(); } catch (error) { log.error(`Cannot fetch the current version: ${error.message}`); @@ -488,33 +461,29 @@ export default class AppRenderer { } catch (error) { log.error(`Cannot fetch the latest version information: ${error.message}`); } + } - // auto connect the tunnel on startup - // note: disabled when developing - if (process.env.NODE_ENV !== 'development') { - const settings = await this._settingsProxy.fetch(); + _onDaemonDisconnected(error: ?Error) { + const actions = this._reduxActions; - // only connect if account is set in the daemon - if (settings.accountToken) { - try { - log.debug('Autoconnect the tunnel'); - await this.connectTunnel(); - } catch (error) { - log.error(`Failed to autoconnect the tunnel: ${error.message}`); - } - } else { - log.debug('Skip autoconnect because account token is not set'); + this._relayListCache.stopUpdating(); + + // recover connection on error + if (error) { + // only send to the connecting to daemon view if the daemon was + // connnected previously + if (this._connectedToDaemon) { + actions.history.replace('/'); } - } else { - log.debug('Skip autoconnect in development'); } + + this._connectedToDaemon = false; } async _setStartView() { const actions = this._reduxActions; const history = this._memoryHistory; - const settings = await this._settingsProxy.fetch(); - const accountToken = settings.accountToken; + const accountToken = this._settings.accountToken; if (accountToken) { log.debug(`Account token is set. Showing the tunnel view.`); @@ -560,13 +529,16 @@ export default class AppRenderer { _setTunnelState(tunnelState: TunnelStateTransition) { log.debug(`Tunnel state: ${tunnelState.state}`); + this._tunnelState = tunnelState; + this._updateConnectionStatus(tunnelState); this._updateUserLocation(tunnelState.state); - this._updateTrayIcon(tunnelState.state); this._notificationController.notifyTunnelState(tunnelState); } _setSettings(newSettings: Settings) { + this._settings = newSettings; + const reduxSettings = this._reduxActions.settings; reduxSettings.updateAllowLan(newSettings.allowLan); @@ -616,17 +588,6 @@ export default class AppRenderer { break; } } - - _updateTrayIcon(tunnelState: TunnelState) { - const iconTypes: { [TunnelState]: TrayIconType } = { - connected: 'secured', - connecting: 'securing', - blocked: 'securing', - }; - const type = iconTypes[tunnelState] || 'unsecured'; - - ipcRenderer.send('change-tray-icon', type); - } } type AccountVerification = { status: 'verified' } | { status: 'deferred', error: Error }; diff --git a/gui/packages/desktop/src/renderer/lib/daemon-rpc-proxy.js b/gui/packages/desktop/src/renderer/lib/daemon-rpc-proxy.js index df2c5b72b6..b3d326a5a6 100644 --- a/gui/packages/desktop/src/renderer/lib/daemon-rpc-proxy.js +++ b/gui/packages/desktop/src/renderer/lib/daemon-rpc-proxy.js @@ -4,6 +4,7 @@ import { ipcRenderer } from 'electron'; import log from 'electron-log'; import uuid from 'uuid'; +// Re-export types export type { AccountToken, AccountData, @@ -27,17 +28,12 @@ export type { DaemonRpcProtocol, } from '../../main/daemon-rpc'; -export { ConnectionObserver, SubscriptionListener } from '../../main/daemon-rpc'; - -import { - NoCreditError, - NoInternetError, - NoDaemonError, - InvalidAccountError, - CommunicationError, -} from '../../main/errors'; - -import { TimeOutError, RemoteError } from '../../main/jsonrpc-client'; +export { + ConnectionObserver, + SubscriptionListener, + defaultSettings, + defaultTunnelStateTransition, +} from '../../main/daemon-rpc'; import type { AccountToken, @@ -50,8 +46,19 @@ import type { Settings, Location, } from '../../main/daemon-rpc'; + import { ConnectionObserver, SubscriptionListener } from '../../main/daemon-rpc'; +import { + NoCreditError, + NoInternetError, + NoDaemonError, + InvalidAccountError, + CommunicationError, +} from '../../main/errors'; + +import { TimeOutError, RemoteError } from '../../main/jsonrpc-client'; + type ErrorInfo = { className: string, data: Object, @@ -192,17 +199,16 @@ export default class DaemonRpcProxy implements DaemonRpcProtocol { ipcRenderer.once( `daemon-rpc-reply-${id}`, (_event: Event, result: R, errorInfo: ?ErrorInfo) => { - log.debug( - `Got daemon-rpc-reply: ${id} ${method} ${JSON.stringify(result)} ${JSON.stringify( - errorInfo, - )}`, - ); - if (errorInfo) { const error = this._deserializeError(errorInfo.className, errorInfo.data); - log.debug(`Deserialized an error to instance of ${error.constructor.name}`); + + log.debug( + `Got daemon-rpc-reply-${id} ${method} with error: ${JSON.stringify(errorInfo)}`, + ); + reject(error); } else { + log.debug(`Got daemon-rpc-reply-${id} ${method} with success`); resolve(result); } }, diff --git a/gui/packages/desktop/src/renderer/lib/subscription-proxy/base-proxy.js b/gui/packages/desktop/src/renderer/lib/subscription-proxy/base-proxy.js deleted file mode 100644 index 5d34ff8839..0000000000 --- a/gui/packages/desktop/src/renderer/lib/subscription-proxy/base-proxy.js +++ /dev/null @@ -1,118 +0,0 @@ -// @flow - -import log from 'electron-log'; -import { ConnectionObserver, SubscriptionListener, ResponseParseError } from '../daemon-rpc-proxy'; -import type { DaemonRpcProtocol } from '../daemon-rpc-proxy'; - -export default class BaseSubscriptionProxy<T> { - _rpc: DaemonRpcProtocol; - _connectionObserver = new ConnectionObserver( - () => {}, - () => { - this._didDisconnectFromDaemon(); - }, - ); - - _isSubscribed = false; - _executingPromise: ?Promise<T>; - - _value: ?T; - _onUpdate: (T) => void; - - constructor(rpc: DaemonRpcProtocol, onUpdate: (T) => void) { - this._rpc = rpc; - this._onUpdate = onUpdate; - - rpc.addConnectionObserver(this._connectionObserver); - } - - async fetch(): Promise<T> { - // return the cached promise if there is an ongoing fetch - if (this._executingPromise) { - return this._executingPromise; - } - - // return the value if it's available - if (this._value) { - return this._value; - } - - // subscribe if needed and fetch the initial state. - const fetchPromise = this._subscribeAndFetchValue(); - - // cache the fetch promise - this._executingPromise = fetchPromise; - - try { - const value = await fetchPromise; - - // cache the initial value - this._value = value; - - // notify the delegate upon initial fetch - this._onUpdate(value); - - return value; - } catch (error) { - throw error; - } finally { - // unset the cached fetch promise - this._executingPromise = null; - } - } - - async _subscribeAndFetchValue(): Promise<T> { - if (!this._isSubscribed) { - await this._subscribeValueListener(); - this._isSubscribed = true; - } - - // request the initial value - return await this.constructor.requestValue(this._rpc); - } - - static subscribeValueListener( - _rpc: DaemonRpcProtocol, - _listener: SubscriptionListener<T>, - ): Promise<void> { - throw new Error( - `Override static ${this.constructor.name}.subscribeValueListener() in subclasses`, - ); - } - - static requestValue(_rpc: DaemonRpcProtocol): Promise<T> { - throw new Error(`Override static ${this.constructor.name}.requestValue() in subclasses`); - } - - _subscribeValueListener(): Promise<void> { - const listener = new SubscriptionListener( - (value: T) => { - this._didReceiveUpdate(value); - }, - (error: Error) => { - let reason = ''; - - if (error instanceof ResponseParseError) { - const validationError = error.validationError; - if (validationError) { - reason = ` Reason: ${validationError.message}`; - } - } - - log.error(`Failed to deserialize the payload: ${error.message}.${reason}`); - }, - ); - return this.constructor.subscribeValueListener(this._rpc, listener); - } - - _didReceiveUpdate(updatedValue: T) { - this._value = updatedValue; - this._onUpdate(updatedValue); - } - - _didDisconnectFromDaemon() { - this._isSubscribed = false; - this._executingPromise = null; - this._value = null; - } -} diff --git a/gui/packages/desktop/src/renderer/lib/subscription-proxy/settings-proxy.js b/gui/packages/desktop/src/renderer/lib/subscription-proxy/settings-proxy.js deleted file mode 100644 index c7fb9b3b3b..0000000000 --- a/gui/packages/desktop/src/renderer/lib/subscription-proxy/settings-proxy.js +++ /dev/null @@ -1,15 +0,0 @@ -// @flow - -import BaseSubscriptionProxy from './base-proxy'; -import { SubscriptionListener } from '../daemon-rpc-proxy'; -import type { DaemonRpcProtocol, Settings } from '../daemon-rpc-proxy'; - -export default class SettingsProxy extends BaseSubscriptionProxy<Settings> { - static subscribeValueListener(rpc: DaemonRpcProtocol, listener: SubscriptionListener<Settings>) { - return rpc.subscribeSettingsListener(listener); - } - - static requestValue(rpc: DaemonRpcProtocol): Promise<Settings> { - return rpc.getSettings(); - } -} diff --git a/gui/packages/desktop/src/renderer/lib/subscription-proxy/tunnel-state-proxy.js b/gui/packages/desktop/src/renderer/lib/subscription-proxy/tunnel-state-proxy.js deleted file mode 100644 index 4c1afa66d6..0000000000 --- a/gui/packages/desktop/src/renderer/lib/subscription-proxy/tunnel-state-proxy.js +++ /dev/null @@ -1,18 +0,0 @@ -// @flow - -import BaseSubscriptionProxy from './base-proxy'; -import { SubscriptionListener } from '../daemon-rpc-proxy'; -import type { DaemonRpcProtocol, TunnelStateTransition } from '../daemon-rpc-proxy'; - -export default class TunnelStateProxy extends BaseSubscriptionProxy<TunnelStateTransition> { - static subscribeValueListener( - rpc: DaemonRpcProtocol, - listener: SubscriptionListener<TunnelStateTransition>, - ) { - return rpc.subscribeStateListener(listener); - } - - static requestValue(rpc: DaemonRpcProtocol): Promise<TunnelStateTransition> { - return rpc.getState(); - } -} |
