diff options
| -rw-r--r-- | app/app.js | 25 | ||||
| -rw-r--r-- | app/lib/backend.js | 9 | ||||
| -rw-r--r-- | app/lib/ipc-facade.js | 5 | ||||
| -rw-r--r-- | app/main.js | 16 | ||||
| -rw-r--r-- | app/shutdown-handler.js | 92 | ||||
| -rw-r--r-- | app/tray-icon-controller.js | 2 |
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; |
