diff --git a/CHANGELOG.md b/CHANGELOG.md index 73f67759..339e1289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ Release date: TBD #### All Platforms - Now "Saved" indicators appear for each preferences section. [#500](https://github.com/mattermost/desktop/issues/500) + - Added the dialog to reopen the application when it quits unexpectedly. + [#626](https://github.com/mattermost/desktop/pull/626) #### Windows - Added the feature to open the application via `mattermost://` link. diff --git a/src/main.js b/src/main.js index 4dbd0f33..9a178a63 100644 --- a/src/main.js +++ b/src/main.js @@ -10,15 +10,18 @@ const { systemPreferences, session } = require('electron'); +const os = require('os'); +const path = require('path'); const isDev = require('electron-is-dev'); const installExtension = require('electron-devtools-installer'); const squirrelStartup = require('./main/squirrelStartup'); +const CriticalErrorHandler = require('./main/CriticalErrorHandler'); const protocols = require('../electron-builder.json').protocols; -process.on('uncaughtException', (error) => { - console.error(error); -}); +const criticalErrorHandler = new CriticalErrorHandler(); + +process.on('uncaughtException', criticalErrorHandler.processUncaughtExceptionHandler.bind(criticalErrorHandler)); global.willAppQuit = false; @@ -27,9 +30,6 @@ if (squirrelStartup()) { global.willAppQuit = true; } -const os = require('os'); -const path = require('path'); - var settings = require('./common/settings'); var certificateStore = require('./main/certificateStore').load(path.resolve(app.getPath('userData'), 'certificate.json')); const {createMainWindow} = require('./main/mainWindow'); @@ -172,7 +172,7 @@ if (app.makeSingleInstance((commandLine/*, workingDirectory*/) => { } } })) { - app.quit(); + app.exit(); } function shouldShowTrayIcon() { @@ -304,6 +304,10 @@ app.on('certificate-error', (event, webContents, url, error, certificate, callba } }); +app.on('gpu-process-crashed', () => { + throw new Error('The GPU process has crached'); +}); + const loginCallbackMap = new Map(); ipcMain.on('login-credentials', (event, request, user, password) => { @@ -392,11 +396,10 @@ app.on('ready', () => { // when you should delete the corresponding element. mainWindow = null; }); - mainWindow.on('unresponsive', () => { - console.log('The application has become unresponsive.'); - }); + criticalErrorHandler.setMainWindow(mainWindow); + mainWindow.on('unresponsive', criticalErrorHandler.windowUnresponsiveHandler.bind(criticalErrorHandler)); mainWindow.webContents.on('crashed', () => { - console.log('The application has crashed.'); + throw new Error('webContents \'crashed\' event has been emitted'); }); ipcMain.on('notified', () => { @@ -468,7 +471,7 @@ app.on('ready', () => { } } - if (trayIcon) { + if (trayIcon && !trayIcon.isDestroyed()) { if (arg.mentionCount > 0) { trayIcon.setImage(trayImages.mention); if (process.platform === 'darwin') { diff --git a/src/main/CriticalErrorHandler.js b/src/main/CriticalErrorHandler.js new file mode 100644 index 00000000..9d767925 --- /dev/null +++ b/src/main/CriticalErrorHandler.js @@ -0,0 +1,100 @@ +const {app, dialog} = require('electron'); +const {spawn} = require('child_process'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const BUTTON_OK = 'OK'; +const BUTTON_SHOW_DETAILS = 'Show Details'; +const BUTTON_REOPEN = 'Reopen'; + +function createErrorReport(err) { + return `Application: ${app.getName()} ${app.getVersion()}\n` + + `Platform: ${os.type()} ${os.release()} ${os.arch()}\n` + + `${err.stack}`; +} + +function openDetachedExternal(url) { + const spawnOption = {detached: true, stdio: 'ignore'}; + switch (process.platform) { + case 'win32': + return spawn('cmd', ['/C', 'start', url], spawnOption); + case 'darwin': + return spawn('open', [url], spawnOption); + case 'linux': + return spawn('xdg-open', [url], spawnOption); + default: + return null; + } +} + +function bindWindowToShowMessageBox(win) { + if (win && win.isVisible()) { + return dialog.showMessageBox.bind(null, win); + } + return dialog.showMessageBox; +} + +class CriticalErrorHandler { + constructor() { + this.mainWindow = null; + } + + setMainWindow(mainWindow) { + this.mainWindow = mainWindow; + } + + windowUnresponsiveHandler() { + const result = dialog.showMessageBox(this.mainWindow, { + type: 'warning', + title: app.getName(), + message: 'The window is no longer responsive.\nDo you wait until the window becomes responsive again?', + buttons: ['No', 'Yes'], + defaultId: 0 + }); + if (result === 0) { + throw new Error('BrowserWindow \'unresponsive\' event has been emitted'); + } + } + + processUncaughtExceptionHandler(err) { + const file = path.join(app.getPath('userData'), `uncaughtException-${Date.now()}.txt`); + const report = createErrorReport(err); + fs.writeFileSync(file, report.replace(new RegExp('\\n', 'g'), os.EOL)); + + if (app.isReady()) { + const buttons = [BUTTON_SHOW_DETAILS, BUTTON_OK, BUTTON_REOPEN]; + if (process.platform === 'darwin') { + buttons.reverse(); + } + const showMessageBox = bindWindowToShowMessageBox(this.mainWindow); + const result = showMessageBox({ + type: 'error', + title: app.getName(), + message: `The ${app.getName()} app quit unexpectedly. Click "Show Details" to learn more or "Reopen" to open the application again.\n\n Internal error: ${err.message}`, + buttons, + defaultId: buttons.indexOf(BUTTON_REOPEN), + noLink: true + }); + switch (result) { + case buttons.indexOf(BUTTON_SHOW_DETAILS): + { + const child = openDetachedExternal(file); + if (child) { + child.on('error', (spawnError) => { + console.log(spawnError); + }); + child.unref(); + } + } + break; + case buttons.indexOf(BUTTON_REOPEN): + app.relaunch(); + break; + } + } + throw err; + } +} + +module.exports = CriticalErrorHandler;