summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--app/app.js25
-rw-r--r--app/lib/backend.js9
-rw-r--r--app/lib/ipc-facade.js5
-rw-r--r--app/main.js16
-rw-r--r--app/shutdown-handler.js92
-rw-r--r--app/tray-icon-controller.js2
6 files changed, 101 insertions, 48 deletions
diff --git a/app/app.js b/app/app.js
index 8e9cc76fb7..055972fdf5 100644
--- a/app/app.js
+++ b/app/app.js
@@ -11,6 +11,8 @@ import makeRoutes from './routes';
import configureStore from './redux/store';
import { Backend, BackendError } from './lib/backend';
+import { setShutdownHandler } from './shutdown-handler';
+
import type { ConnectionState } from './redux/connection/reducers';
import type { TrayIconType } from './tray-icon-controller';
@@ -40,32 +42,15 @@ ipcRenderer.on('backend-info', async (_event, args) => {
}
});
-ipcRenderer.on('shutdown', () => {
- log.info('Been told by the node process to shutdown');
- backend.shutdown().catch((e) => {
- log.warn('Unable to shut down the backend', e.message);
- });
-});
-
-ipcRenderer.on('disconnect', () => {
- log.info('Been told by the node process to disconnect the tunnel');
- backend.disconnect().catch((e) => {
- log.warn('Unable to disconnect the tunnel', e.message);
- });
-});
-
-ipcRenderer.on('app-shutdown', async () => {
- log.info('Been told by the renderer process that the app is shutting down');
+setShutdownHandler(async () => {
+ log.info('Executing a shutdown handler');
- // The shutdown behaviour may have to be different on mobile platforms
try {
await backend.disconnect();
+ log.info('Disconnected the tunnel');
} catch (e) {
log.error(`Failed to shutdown tunnel: ${e.message}`);
}
-
- // no matter what, don't block the frontend from shutting down, I guess.
- ipcRenderer.send('daemon-shutdown', true);
});
//////////////////////////////////////////////////////////////////////////
diff --git a/app/lib/backend.js b/app/lib/backend.js
index dfe4fdd2c3..c9f9969af3 100644
--- a/app/lib/backend.js
+++ b/app/lib/backend.js
@@ -294,15 +294,6 @@ export class Backend {
}
}
- async shutdown() {
- try {
- await this._ensureAuthenticated();
- await this._ipc.shutdown();
- } catch (e) {
- log.error('Failed to shutdown: ', e.message);
- }
- }
-
async updateRelaySettings(relaySettings: RelaySettingsUpdate) {
try {
await this._ensureAuthenticated();
diff --git a/app/lib/ipc-facade.js b/app/lib/ipc-facade.js
index 0aa1d48d3f..103ef4d931 100644
--- a/app/lib/ipc-facade.js
+++ b/app/lib/ipc-facade.js
@@ -188,7 +188,6 @@ export interface IpcFacade {
getAllowLan(): Promise<boolean>;
connect(): Promise<void>;
disconnect(): Promise<void>;
- shutdown(): Promise<void>;
getLocation(): Promise<Location>;
getState(): Promise<BackendState>;
registerStateListener((BackendState) => void): void;
@@ -286,10 +285,6 @@ export class RealIpc implements IpcFacade {
return this._ipc.send('disconnect').then(this._ignoreResponse);
}
- shutdown(): Promise<void> {
- return this._ipc.send('shutdown').then(this._ignoreResponse);
- }
-
getLocation(): Promise<Location> {
// send the IPC with 30s timeout since the backend will wait
// for a HTTP request before replying
diff --git a/app/main.js b/app/main.js
index b6fefc519e..313c0f7cde 100644
--- a/app/main.js
+++ b/app/main.js
@@ -14,6 +14,7 @@ import { canTrustRpcAddressFile } from './lib/rpc-file-security';
import { execFile } from 'child_process';
import uuid from 'uuid';
+import { ShutdownCoordinator } from './shutdown-handler';
import type { TrayIconType } from './tray-icon-controller';
// The name for application directory used for
@@ -24,7 +25,6 @@ const ApplicationMain = {
_windowController: (null: ?WindowController),
_trayIconController: (null: ?TrayIconController),
- _readyToQuit: false,
_logFilePath: '',
_connectionFilePollInterval: (null: ?IntervalID),
@@ -105,6 +105,8 @@ const ApplicationMain = {
const window = this._createWindow();
const tray = this._createTray();
+ const _shutdownCoordinator = new ShutdownCoordinator(window.webContents);
+
const windowController = new WindowController(window, tray);
const trayIconController = new TrayIconController(tray, 'unsecured');
@@ -118,13 +120,6 @@ const ApplicationMain = {
this._windowController = windowController;
this._trayIconController = trayIconController;
- app.on('before-quit', (event) => {
- if (!this._readyToQuit) {
- event.preventDefault();
- window.webContents.send('app-shutdown');
- }
- });
-
if (process.env.NODE_ENV === 'development') {
await this._installDevTools();
@@ -152,11 +147,6 @@ const ApplicationMain = {
this._pollConnectionInfoFile();
});
- ipcMain.on('daemon-shutdown', (isTunnelDown: boolean) => {
- this._readyToQuit = isTunnelDown;
- app.quit();
- });
-
ipcMain.on('show-window', () => {
const windowController = this._windowController;
if (windowController) {
diff --git a/app/shutdown-handler.js b/app/shutdown-handler.js
new file mode 100644
index 0000000000..afd10f61b2
--- /dev/null
+++ b/app/shutdown-handler.js
@@ -0,0 +1,92 @@
+// @flow
+import { app, ipcMain, ipcRenderer } from 'electron';
+import { log } from './lib/platform';
+import type { WebContents } from 'electron';
+
+// The timeout before the shutdown is enforced
+const SHUTDOWN_TIMEOUT = 3000;
+
+/**
+ * Shutdown coordinator postpones the application shutdown until ShutdownHandler finished the
+ * shutdown sequence.
+ *
+ * ShutdownCoordinator can only be created from main process.
+ */
+class ShutdownCoordinator {
+ _canQuit = false;
+ _isQuitting = false;
+ _webContents: WebContents;
+ _shutdownTimeout: ?TimeoutID;
+
+ constructor(webContents: WebContents) {
+ this._webContents = webContents;
+
+ app.on('before-quit', this._onBeforeQuit);
+ ipcMain.on('app-shutdown-reply', this._onShutdownReply);
+ }
+
+ dispose() {
+ app
+ .removeListener('before-quit', this._onBeforeQuit)
+ .removeListener('app-shutdown-reply', this._onShutdownReply);
+ }
+
+ _onBeforeQuit = (event) => {
+ if (!this._canQuit) {
+ // make sure we don't call the shutdown handler twice
+ if (!this._isQuitting) {
+ this._isQuitting = true;
+
+ // start timer to force shutdown if the renderer process is not able to handle shutdown in
+ // a timely manner.
+ this._shutdownTimeout = setTimeout(this._onShutdownTimeout, SHUTDOWN_TIMEOUT);
+
+ this._webContents.send('app-shutdown');
+ }
+
+ event.preventDefault();
+ }
+ };
+
+ _onShutdownReply = () => {
+ const shutdownTimeout = this._shutdownTimeout;
+ if (shutdownTimeout) {
+ clearTimeout(shutdownTimeout);
+ }
+ this._grantShutdown();
+ };
+
+ _onShutdownTimeout = () => {
+ log.warn('It took longer than expected to shutdown. Forcing shutdown now.');
+ this._grantShutdown();
+ };
+
+ _grantShutdown() {
+ this._canQuit = true;
+ app.quit();
+ }
+}
+
+/**
+ * Executes the provided closure when application is about to quit.
+ * It works in tandem with ShutdownCoordinator and can only be used in renderer process.
+ * The shutdown will be postponed until the Promise returned from closure is resolved.
+ * ShutdownCoordinator will force the shutdown if the returned Promise is not resolved within
+ * the allocated time (see SHUTDOWN_TIMEOUT).
+ */
+function setShutdownHandler(handler: () => Promise<void>) {
+ ipcRenderer.once('app-shutdown', async (event) => {
+ try {
+ await handler();
+ } catch (error) {
+ log.warn(`An error occurred in shutdown handler: ${error.message}`);
+ }
+ event.sender.send('app-shutdown-reply');
+ });
+}
+
+if (ipcMain) {
+ module.exports = { ShutdownCoordinator };
+} else {
+ module.exports = { setShutdownHandler };
+}
diff --git a/app/tray-icon-controller.js b/app/tray-icon-controller.js
index b4871c42d2..c32421b20f 100644
--- a/app/tray-icon-controller.js
+++ b/app/tray-icon-controller.js
@@ -19,7 +19,7 @@ export default class TrayIconController {
this._iconType = initialType;
}
- destroy() {
+ dispose() {
if (this._animation) {
this._animation.stop();
this._animation = null;