summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2018-11-16 14:33:36 +0100
committerAndrej Mihajlov <and@mullvad.net>2018-11-20 10:48:22 +0100
commit07b0c8747a23e7973b26e65e4015f4c8a81f0758 (patch)
treeabc48cdacdc890e85de5c87861866109a29e80d7
parenta9d5f93836279dd7c1b14ddcf7449422bd7dd788 (diff)
downloadmullvadvpn-07b0c8747a23e7973b26e65e4015f4c8a81f0758.tar.xz
mullvadvpn-07b0c8747a23e7973b26e65e4015f4c8a81f0758.zip
Move notifications and version fetch to the main process
-rw-r--r--gui/flow-libs/electron.js.flow37
-rw-r--r--gui/flow-libs/notification.js.flow51
-rwxr-xr-xgui/packages/desktop/electron-builder.yml1
-rw-r--r--gui/packages/desktop/src/main/index.js185
-rw-r--r--gui/packages/desktop/src/main/notification-controller.js (renamed from gui/packages/desktop/src/renderer/lib/notification-controller.js)52
-rw-r--r--gui/packages/desktop/src/main/window-controller.js4
-rw-r--r--gui/packages/desktop/src/renderer/app.js116
7 files changed, 266 insertions, 180 deletions
diff --git a/gui/flow-libs/electron.js.flow b/gui/flow-libs/electron.js.flow
index bc37e5a524..52df468272 100644
--- a/gui/flow-libs/electron.js.flow
+++ b/gui/flow-libs/electron.js.flow
@@ -197,6 +197,43 @@ declare module 'electron' {
popup(browserWindow?: BrowserWindow, options?: PopupOptions): void;
}
+ // https://electronjs.org/docs/api/structures/notification-action
+ declare type NotificationAction = {
+ type: 'button',
+ text?: string
+ };
+
+ // https://electronjs.org/docs/api/notification
+ declare type NotificationOptions = {
+ title?: string,
+ subtitle?: string,
+ body: string,
+ silent?: boolean,
+ icon?: string | NativeImage,
+ hasReply?: boolean,
+ replyPlaceholder?: string,
+ sound?: string,
+ actions?: Array<NotificationAction>,
+ closeButtonText?: string
+ };
+
+ declare class Notification extends EventEmitter {
+ static isSupported(): boolean;
+ constructor(options: NotificationOptions): void;
+ show(): void;
+ close(): void;
+ title: string;
+ subtitle: string;
+ body: string;
+ silent: boolean;
+ hasReply: boolean;
+ replyPlaceholder: string;
+ sound: string;
+ actions: Array<NotificationAction>,
+ closeButtonText: string;
+ }
+
+
// http://electron.atom.io/docs/api/app
declare class App extends EventEmitter {
diff --git a/gui/flow-libs/notification.js.flow b/gui/flow-libs/notification.js.flow
deleted file mode 100644
index bf5dda3af2..0000000000
--- a/gui/flow-libs/notification.js.flow
+++ /dev/null
@@ -1,51 +0,0 @@
-/* Notification */
-type NotificationPermission = 'default' | 'denied' | 'granted';
-type NotificationDirection = 'auto' | 'ltr' | 'rtl';
-type VibratePattern = number | Array<number>;
-type NotificationAction = { action: string, title: string, icon?: string };
-type NotificationOptions = {
- dir: NotificationDirection,
- lang: string,
- body: string,
- tag: string,
- image: string,
- icon: string,
- badge: string,
- sound: string,
- vibrate: VibratePattern,
- timestamp: number,
- renotify: boolean,
- silent: boolean,
- requireInteraction: boolean,
- data: ?any,
- actions: Array<NotificationAction>,
-};
-
-declare class Notification extends EventTarget {
- constructor(title: string, options?: $Shape<NotificationOptions>): void;
- static permission: NotificationPermission;
- static requestPermission(
- callback?: (perm: NotificationPermission) => mixed,
- ): Promise<NotificationPermission>;
- static maxActions: number;
- onclick: (evt: Event) => any;
- onerror: (evt: Event) => any;
- title: string;
- dir: NotificationDirection;
- lang: string;
- body: string;
- tag: string;
- image: string;
- icon: string;
- badge: string;
- sound: string;
- vibrate: Array<number>;
- timestamp: number;
- renotify: boolean;
- silent: boolean;
- requireInteraction: boolean;
- data: any;
- actions: Array<NotificationAction>;
-
- close(): void;
-}
diff --git a/gui/packages/desktop/electron-builder.yml b/gui/packages/desktop/electron-builder.yml
index d08895d848..c5124623cf 100755
--- a/gui/packages/desktop/electron-builder.yml
+++ b/gui/packages/desktop/electron-builder.yml
@@ -37,6 +37,7 @@ mac:
category: public.app-category.tools
extendInfo:
LSUIElement: true
+ NSUserNotificationAlertStyle: alert
extraResources:
- from: ../../../target/release/mullvad
to: .
diff --git a/gui/packages/desktop/src/main/index.js b/gui/packages/desktop/src/main/index.js
index 6de9df40a5..3c1d76459e 100644
--- a/gui/packages/desktop/src/main/index.js
+++ b/gui/packages/desktop/src/main/index.js
@@ -8,6 +8,7 @@ import mkdirp from 'mkdirp';
import uuid from 'uuid';
import { app, screen, BrowserWindow, ipcMain, Tray, Menu, nativeImage } from 'electron';
+import NotificationController from './notification-controller';
import WindowController from './window-controller';
import TrayIconController from './tray-icon-controller';
@@ -20,19 +21,38 @@ import {
defaultSettings,
defaultTunnelStateTransition,
} from './daemon-rpc';
-import type { RelayList, TunnelState, TunnelStateTransition, Settings } from './daemon-rpc';
+import type {
+ AppVersionInfo,
+ RelayList,
+ Settings,
+ TunnelState,
+ TunnelStateTransition,
+} from './daemon-rpc';
import ReconnectionBackoff from './reconnection-backoff';
import { resolveBin } from './proc';
const RELAY_LIST_UPDATE_INTERVAL = 60 * 60 * 1000;
+const VERSION_UPDATE_INTERVAL = 24 * 60 * 60 * 1000;
const DAEMON_RPC_PATH =
process.platform === 'win32' ? '//./pipe/Mullvad VPN' : '/var/run/mullvad-vpn';
type AppQuitStage = 'unready' | 'initiated' | 'ready';
+export type CurrentAppVersionInfo = {
+ gui: string,
+ daemon: string,
+ isConsistent: boolean,
+};
+
+export type AppUpgradeInfo = {
+ nextUpgrade: ?string,
+ upToDate: boolean,
+} & AppVersionInfo;
+
const ApplicationMain = {
+ _notificationController: new NotificationController(),
_windowController: (null: ?WindowController),
_trayIconController: (null: ?TrayIconController),
@@ -46,9 +66,27 @@ const ApplicationMain = {
_tunnelState: defaultTunnelStateTransition(),
_settings: defaultSettings(),
+
_relays: ({ countries: [] }: RelayList),
_relaysInterval: (null: ?IntervalID),
+ _currentVersion: ({
+ daemon: '',
+ gui: '',
+ isConsistent: true,
+ }: CurrentAppVersionInfo),
+
+ _upgradeVersion: ({
+ currentIsSupported: true,
+ latest: {
+ latestStable: '',
+ latest: '',
+ },
+ nextUpgrade: null,
+ upToDate: true,
+ }: AppUpgradeInfo),
+ _latestVersionInterval: (null: ?IntervalID),
+
run() {
// Since electron's GPU blacklists are broken, GPU acceleration won't work on older distros
if (process.platform === 'linux') {
@@ -288,8 +326,30 @@ const ApplicationMain = {
return this._recoverFromBootstrapError(error);
}
+ // fetch the daemon's version
+ try {
+ this._setDaemonVersion(await this._daemonRpc.getCurrentVersion());
+ } catch (error) {
+ log.error(`Failed to fetch the daemon's version: ${error.message}`);
+
+ return this._recoverFromBootstrapError(error);
+ }
+
+ // fetch the latest version info in background
+ this._fetchLatestVersion();
+
// start periodic updates
this._startRelaysPeriodicUpdates();
+ this._startLatestVersionPeriodicUpdates();
+
+ // notify user about inconsistent version
+ if (
+ process.env.NODE_ENV !== 'development' &&
+ !this._shouldSuppressNotifications() &&
+ !this._currentVersion.isConsistent
+ ) {
+ this._notificationController.notifyInconsistentVersion();
+ }
// reset the reconnect backoff when connection established.
this._reconnectBackoff.reset();
@@ -310,6 +370,7 @@ const ApplicationMain = {
// stop periodic updates
this._stopRelaysPeriodicUpdates();
+ this._stopLatestVersionPeriodicUpdates();
// notify renderer process
if (this._windowController) {
@@ -375,11 +436,17 @@ const ApplicationMain = {
},
_setTunnelState(newState: TunnelStateTransition) {
+ const windowController = this._windowController;
+
this._tunnelState = newState;
this._updateTrayIcon(newState.state);
- if (this._windowController) {
- this._windowController.send('tunnel-state-changed', newState);
+ if (!this._shouldSuppressNotifications()) {
+ this._notificationController.notifyTunnelState(newState);
+ }
+
+ if (windowController) {
+ windowController.send('tunnel-state-changed', newState);
}
},
@@ -422,6 +489,109 @@ const ApplicationMain = {
}
},
+ _setDaemonVersion(daemonVersion: string) {
+ const guiVersion = app.getVersion().replace('.0', '');
+ const versionInfo = {
+ daemon: daemonVersion,
+ gui: guiVersion,
+ isConsistent: daemonVersion === guiVersion,
+ };
+
+ this._currentVersion = versionInfo;
+
+ // notify renderer
+ if (this._windowController) {
+ this._windowController.send('current-version-changed', versionInfo);
+ }
+ },
+
+ _setLatestVersion(latestVersionInfo: AppVersionInfo) {
+ function isBeta(version: string) {
+ return version.includes('-');
+ }
+
+ function nextUpgrade(current: string, latest: string, latestStable: string): ?string {
+ if (isBeta(current)) {
+ return current === latest ? null : latest;
+ } else {
+ return current === latestStable ? null : latestStable;
+ }
+ }
+
+ function checkIfLatest(current: string, latest: string, latestStable: string): boolean {
+ // perhaps -beta?
+ if (isBeta(current)) {
+ return current === latest;
+ } else {
+ // must be stable
+ return current === latestStable;
+ }
+ }
+
+ const currentVersionInfo = this._currentVersion;
+ const latestVersion = latestVersionInfo.latest.latest;
+ const latestStableVersion = latestVersionInfo.latest.latestStable;
+
+ // the reason why we rely on daemon version here is because daemon obtains the version info
+ // based on its built-in version information
+ const isUpToDate = checkIfLatest(currentVersionInfo.daemon, latestVersion, latestStableVersion);
+ const upgradeVersion = nextUpgrade(
+ currentVersionInfo.daemon,
+ latestVersion,
+ latestStableVersion,
+ );
+
+ const upgradeInfo = {
+ ...latestVersionInfo,
+ nextUpgrade: upgradeVersion,
+ upToDate: isUpToDate,
+ };
+
+ this._upgradeVersion = upgradeInfo;
+
+ // notify user to update the app if it became unsupported
+ if (
+ process.env.NODE_ENV !== 'development' &&
+ !this._shouldSuppressNotifications() &&
+ currentVersionInfo.isConsistent &&
+ !latestVersionInfo.currentIsSupported &&
+ upgradeVersion
+ ) {
+ this._notificationController.notifyUnsupportedVersion(upgradeVersion);
+ }
+
+ if (this._windowController) {
+ this._windowController.send('upgrade-version-changed', upgradeInfo);
+ }
+ },
+
+ async _fetchLatestVersion() {
+ try {
+ this._setLatestVersion(await this._daemonRpc.getVersionInfo());
+ } catch (error) {
+ console.error(`Failed to request the version info: ${error.message}`);
+ }
+ },
+
+ _startLatestVersionPeriodicUpdates() {
+ const handler = () => {
+ this._fetchLatestVersion();
+ };
+ this._latestVersionInterval = setInterval(handler, VERSION_UPDATE_INTERVAL);
+ },
+
+ _stopLatestVersionPeriodicUpdates() {
+ if (this._latestVersionInterval) {
+ clearInterval(this._latestVersionInterval);
+
+ this._latestVersionInterval = null;
+ }
+ },
+
+ _shouldSuppressNotifications() {
+ return this._windowController && this._windowController.isVisible();
+ },
+
_updateTrayIcon(tunnelState: TunnelState) {
const iconTypes: { [TunnelState]: TrayIconType } = {
connected: 'secured',
@@ -436,9 +606,12 @@ const ApplicationMain = {
},
_registerWindowListener(windowController: WindowController) {
- const window = windowController.window;
+ windowController.window.on('show', () => {
+ // cancel notifications when window appears
+ this._notificationController.cancelPendingNotifications();
- window.on('show', () => window.webContents.send('window-shown'));
+ windowController.send('window-shown');
+ });
},
_registerIpcListeners() {
@@ -470,6 +643,8 @@ const ApplicationMain = {
this._tunnelState,
this._settings,
this._relays,
+ this._currentVersion,
+ this._upgradeVersion,
);
});
diff --git a/gui/packages/desktop/src/renderer/lib/notification-controller.js b/gui/packages/desktop/src/main/notification-controller.js
index 06118b3562..da77836063 100644
--- a/gui/packages/desktop/src/renderer/lib/notification-controller.js
+++ b/gui/packages/desktop/src/main/notification-controller.js
@@ -1,10 +1,10 @@
// @flow
-import { remote } from 'electron';
+import { shell, Notification } from 'electron';
import log from 'electron-log';
-import config from '../../config';
+import config from '../config';
-import type { TunnelStateTransition } from './daemon-rpc-proxy';
+import type { TunnelStateTransition } from './daemon-rpc';
export default class NotificationController {
_lastTunnelStateNotification: ?Notification;
@@ -48,41 +48,33 @@ export default class NotificationController {
}
notifyInconsistentVersion() {
- if (remote.getCurrentWindow().isVisible()) {
- return;
- }
-
this._presentNotificationOnce('inconsistent-version', () => {
- const notification = new Notification(remote.app.getName(), {
+ const notification = new Notification({
body: 'Inconsistent internal version information, please restart the app',
silent: true,
});
- this._addPendingNotification(notification);
+ this._scheduleNotification(notification);
});
}
notifyUnsupportedVersion(upgradeVersion: string) {
- if (remote.getCurrentWindow().isVisible()) {
- return;
- }
-
this._presentNotificationOnce('unsupported-version', () => {
- const notification = new Notification(remote.app.getName(), {
+ const notification = new Notification({
body: `You are running an unsupported app version. Please upgrade to ${upgradeVersion} now to ensure your security`,
silent: true,
});
- notification.addEventListener('click', () => {
- remote.shell.openExternal(config.links.download);
+ notification.on('click', () => {
+ shell.openExternal(config.links.download);
});
- this._addPendingNotification(notification);
+ this._scheduleNotification(notification);
});
}
cancelPendingNotifications() {
for (const notification of this._pendingNotifications) {
- this._closeNotification(notification);
+ notification.close();
}
}
@@ -90,21 +82,21 @@ export default class NotificationController {
const lastNotification = this._lastTunnelStateNotification;
const sameAsLastNotification = lastNotification && lastNotification.body === message;
- if (sameAsLastNotification || remote.getCurrentWindow().isVisible()) {
+ if (sameAsLastNotification) {
return;
}
- const newNotification = new Notification(remote.app.getName(), {
+ const newNotification = new Notification({
body: message,
silent: true,
});
if (lastNotification) {
- this._closeNotification(lastNotification);
+ lastNotification.close();
}
this._lastTunnelStateNotification = newNotification;
- this._addPendingNotification(newNotification);
+ this._scheduleNotification(newNotification);
}
_presentNotificationOnce(notificationName: string, presentNotification: () => void) {
@@ -115,22 +107,14 @@ export default class NotificationController {
}
}
- _closeNotification(notification: Notification) {
- // If the notification is closed too soon, it might still get shown. If that happens, close()
- // should be called again so that it is closed immediately.
- // Tracking issue: https://github.com/electron/electron/issues/12887
- notification.addEventListener('show', () => {
- notification.close();
- });
+ _scheduleNotification(notification: Notification) {
+ this._addPendingNotification(notification);
- notification.close();
+ notification.show();
}
_addPendingNotification(notification: Notification) {
- // Quirk: chromium postpones the 'close' event until new notifications pump the queue or window
- // becomes visible. It's possible that there is going to be one stale notification in
- // `_pendingNotifications` array but that shouldn't be a big deal.
- notification.addEventListener('close', () => {
+ notification.on('close', () => {
this._removePendingNotification(notification);
});
diff --git a/gui/packages/desktop/src/main/window-controller.js b/gui/packages/desktop/src/main/window-controller.js
index 0943364dab..faabe63c91 100644
--- a/gui/packages/desktop/src/main/window-controller.js
+++ b/gui/packages/desktop/src/main/window-controller.js
@@ -175,6 +175,10 @@ export default class WindowController {
}
}
+ isVisible(): boolean {
+ return this._window.isVisible();
+ }
+
send(event: string, ...data: Array<mixed>): void {
this._window.webContents.send(event, ...data);
}
diff --git a/gui/packages/desktop/src/renderer/app.js b/gui/packages/desktop/src/renderer/app.js
index fd863ccb50..1c456fd7d5 100644
--- a/gui/packages/desktop/src/renderer/app.js
+++ b/gui/packages/desktop/src/renderer/app.js
@@ -1,7 +1,7 @@
// @flow
import log from 'electron-log';
-import { remote, webFrame, ipcRenderer } from 'electron';
+import { webFrame, ipcRenderer } from 'electron';
import * as React from 'react';
import { bindActionCreators } from 'redux';
import { Provider } from 'react-redux';
@@ -14,7 +14,6 @@ import { createMemoryHistory } from 'history';
import { InvalidAccountError } from '../main/errors';
import makeRoutes from './routes';
-import NotificationController from './lib/notification-controller';
import configureStore from './redux/store';
import accountActions from './redux/account/actions';
@@ -24,6 +23,8 @@ import versionActions from './redux/version/actions';
import userInterfaceActions from './redux/userinterface/actions';
import type { WindowShapeParameters } from '../main/window-controller';
+import type { CurrentAppVersionInfo, AppUpgradeInfo } from '../main';
+
import type {
AccountToken,
Settings,
@@ -43,7 +44,6 @@ import DaemonRpcProxy, {
import type { ReduxStore } from './redux/store';
export default class AppRenderer {
- _notificationController = new NotificationController();
_memoryHistory = createMemoryHistory();
_reduxStore: ReduxStore;
_reduxActions: *;
@@ -93,8 +93,6 @@ export default class AppRenderer {
if (this._connectedToDaemon) {
this.updateAccountExpiry();
}
-
- this._notificationController.cancelPendingNotifications();
});
ipcRenderer.on('daemon-connected', () => {
@@ -117,6 +115,17 @@ export default class AppRenderer {
this._setRelays(newRelays);
});
+ ipcRenderer.on(
+ 'current-version-changed',
+ (_event: Event, currentVersion: CurrentAppVersionInfo) => {
+ this._setCurrentVersion(currentVersion);
+ },
+ );
+
+ ipcRenderer.on('upgrade-version-changed', (_event: Event, upgradeVersion: AppUpgradeInfo) => {
+ this._setUpgradeVersion(upgradeVersion);
+ });
+
// Request the initial state from main process
ipcRenderer.on(
'get-state-reply',
@@ -126,10 +135,14 @@ export default class AppRenderer {
tunnelState: TunnelStateTransition,
settings: Settings,
relays: RelayList,
+ currentVersion: CurrentAppVersionInfo,
+ upgradeVersion: AppUpgradeInfo,
) => {
this._setTunnelState(tunnelState);
this._setSettings(settings);
this._setRelays(relays);
+ this._setCurrentVersion(currentVersion);
+ this._setUpgradeVersion(upgradeVersion);
if (isConnected) {
this._onDaemonConnected();
@@ -356,88 +369,10 @@ export default class AppRenderer {
actions.settings.updateAutoConnect(autoConnect);
}
- async _getAppComponentsVersions() {
- const daemonVersion = await this._daemonRpc.getCurrentVersion();
- const guiVersion = remote.app.getVersion().replace('.0', '');
- return {
- daemon: daemonVersion,
- gui: guiVersion,
- isConsistent: daemonVersion === guiVersion,
- };
- }
-
- async _fetchCurrentVersion() {
- const actions = this._reduxActions;
- const versions = await this._getAppComponentsVersions();
-
- // notify user about inconsistent version
- if (process.env.NODE_ENV !== 'development' && !versions.isConsistent) {
- this._notificationController.notifyInconsistentVersion();
- }
-
- actions.version.updateVersion(versions.gui, versions.isConsistent);
- }
-
- async _fetchLatestVersionInfo() {
- function isBeta(version: string) {
- return version.includes('-');
- }
-
- function nextUpgrade(current: string, latest: string, latestStable: string): ?string {
- if (isBeta(current)) {
- return current === latest ? null : latest;
- } else {
- return current === latestStable ? null : latestStable;
- }
- }
-
- function checkIfLatest(current: string, latest: string, latestStable: string): boolean {
- // perhaps -beta?
- if (isBeta(current)) {
- return current === latest;
- } else {
- // must be stable
- return current === latestStable;
- }
- }
-
- const versions = await this._getAppComponentsVersions();
- const versionInfo = await this._daemonRpc.getVersionInfo();
- const latestVersion = versionInfo.latest.latest;
- const latestStableVersion = versionInfo.latest.latestStable;
-
- // the reason why we rely on daemon version here is because daemon obtains the version info
- // based on its built-in version information
- const isUpToDate = checkIfLatest(versions.daemon, latestVersion, latestStableVersion);
- const upgradeVersion = nextUpgrade(versions.daemon, latestVersion, latestStableVersion);
-
- // notify user to update the app if it became unsupported
- if (
- process.env.NODE_ENV !== 'development' &&
- versions.isConsistent &&
- !versionInfo.currentIsSupported &&
- upgradeVersion
- ) {
- this._notificationController.notifyUnsupportedVersion(upgradeVersion);
- }
-
- this._reduxActions.version.updateLatest({
- ...versionInfo,
- nextUpgrade: upgradeVersion,
- upToDate: isUpToDate,
- });
- }
-
async _onDaemonConnected() {
this._connectedToDaemon = true;
try {
- await this._fetchCurrentVersion();
- } catch (error) {
- log.error(`Cannot fetch the current version: ${error.message}`);
- }
-
- try {
await this._fetchAccountHistory();
} catch (error) {
log.error(`Cannot fetch the account history: ${error.message}`);
@@ -445,12 +380,6 @@ export default class AppRenderer {
// set the appropriate start view
await this._setStartView();
-
- try {
- await this._fetchLatestVersionInfo();
- } catch (error) {
- log.error(`Cannot fetch the latest version information: ${error.message}`);
- }
}
_onDaemonDisconnected(error: ?Error) {
@@ -521,7 +450,6 @@ export default class AppRenderer {
this._updateConnectionStatus(tunnelState);
this._updateUserLocation(tunnelState.state);
- this._notificationController.notifyTunnelState(tunnelState);
}
_setSettings(newSettings: Settings) {
@@ -537,6 +465,14 @@ export default class AppRenderer {
this._setRelaySettings(newSettings.relaySettings);
}
+ _setCurrentVersion(versionInfo: CurrentAppVersionInfo) {
+ this._reduxActions.version.updateVersion(versionInfo.gui, versionInfo.isConsistent);
+ }
+
+ _setUpgradeVersion(upgradeVersion: AppUpgradeInfo) {
+ this._reduxActions.version.updateLatest(upgradeVersion);
+ }
+
async _updateUserLocation(tunnelState: TunnelState) {
if (['connected', 'connecting', 'disconnected'].includes(tunnelState)) {
try {