Electron-401 (fixes crash reporter) (#334)

- fix crash reporter
- fix typo
- Add support to generate crash report
- Add logic to store globally and return
- PR review fixes
- Add logic to validate file mime type
This commit is contained in:
Kiran Niranjan 2018-04-11 13:13:59 +00:00 committed by Vishwas Shashidhar
parent 2de86c8003
commit 3be2986c7e
12 changed files with 276 additions and 80 deletions

View File

@ -7,6 +7,7 @@ const fs = require('fs');
const log = require('../log.js'); const log = require('../log.js');
const logLevels = require('../enums/logLevels.js'); const logLevels = require('../enums/logLevels.js');
const buildNumber = require('../../package.json').buildNumber; const buildNumber = require('../../package.json').buildNumber;
const { initCrashReporterMain, initCrashReporterRenderer } = require('../crashReporter.js');
let aboutWindow; let aboutWindow;
@ -80,9 +81,27 @@ function openAboutWindow(windowName) {
}); });
aboutWindow.webContents.on('did-finish-load', () => { aboutWindow.webContents.on('did-finish-load', () => {
// initialize crash reporter
initCrashReporterMain({ process: 'about app window' });
initCrashReporterRenderer(aboutWindow, { process: 'render | about app window' });
aboutWindow.webContents.send('buildNumber', buildNumber || '0'); aboutWindow.webContents.send('buildNumber', buildNumber || '0');
}); });
aboutWindow.webContents.on('crashed', function () {
const options = {
type: 'error',
title: 'Renderer Process Crashed',
message: 'Oops! Looks like we have had a crash.',
buttons: ['Close']
};
electron.dialog.showMessageBox(options, function () {
if (aboutWindow && !aboutWindow.isDestroyed()) {
aboutWindow.close();
}
});
});
aboutWindow.on('close', () => { aboutWindow.on('close', () => {
destroyWindow(); destroyWindow();
}); });

View File

@ -1,5 +1,5 @@
'use strict'; 'use strict';
const { remote, ipcRenderer } = require('electron'); const { remote, ipcRenderer, crashReporter } = require('electron');
renderDom(); renderDom();
@ -24,4 +24,10 @@ ipcRenderer.on('buildNumber', (event, buildNumber) => {
if (versionText) { if (versionText) {
versionText.innerHTML = version ? `Version ${version} (${version}.${buildNumber})` : 'N/A'; versionText.innerHTML = version ? `Version ${version} (${version}.${buildNumber})` : 'N/A';
} }
});
ipcRenderer.on('register-crash-reporter', (event, arg) => {
if (arg && typeof arg === 'object') {
crashReporter.start(arg);
}
}); });

View File

@ -8,6 +8,7 @@ const fs = require('fs');
const log = require('../log.js'); const log = require('../log.js');
const logLevels = require('../enums/logLevels.js'); const logLevels = require('../enums/logLevels.js');
const { isMac } = require('../utils/misc'); const { isMac } = require('../utils/misc');
const { initCrashReporterMain, initCrashReporterRenderer } = require('../crashReporter.js');
let basicAuthWindow; let basicAuthWindow;
@ -95,10 +96,26 @@ function openBasicAuthWindow(windowName, hostname, isValidCredentials, clearSett
}); });
basicAuthWindow.webContents.on('did-finish-load', () => { basicAuthWindow.webContents.on('did-finish-load', () => {
// initialize crash reporter
initCrashReporterMain({ process: 'basic auth window' });
initCrashReporterRenderer(basicAuthWindow, { process: 'render | basic auth window' });
basicAuthWindow.webContents.send('hostname', hostname); basicAuthWindow.webContents.send('hostname', hostname);
basicAuthWindow.webContents.send('isValidCredentials', isValidCredentials); basicAuthWindow.webContents.send('isValidCredentials', isValidCredentials);
}); });
basicAuthWindow.webContents.on('crashed', function () {
const options = {
type: 'error',
title: 'Renderer Process Crashed',
message: 'Oops! Looks like we have had a crash.',
buttons: ['Close']
};
electron.dialog.showMessageBox(options, function () {
closeAuthWindow(true);
});
});
basicAuthWindow.on('close', () => { basicAuthWindow.on('close', () => {
destroyWindow(); destroyWindow();
}); });

View File

@ -1,6 +1,5 @@
'use strict'; 'use strict';
const electron = require('electron'); const { ipcRenderer, crashReporter } = require('electron');
const ipc = electron.ipcRenderer;
renderDom(); renderDom();
@ -26,7 +25,7 @@ function loadContent() {
if (cancel) { if (cancel) {
cancel.addEventListener('click', () => { cancel.addEventListener('click', () => {
ipc.send('close-basic-auth'); ipcRenderer.send('close-basic-auth');
}); });
} }
} }
@ -39,14 +38,14 @@ function submitForm() {
let password = document.getElementById('password').value; let password = document.getElementById('password').value;
if (username && password) { if (username && password) {
ipc.send('login', { username, password }); ipcRenderer.send('login', { username, password });
} }
} }
/** /**
* Updates the hosts name * Updates the hosts name
*/ */
ipc.on('hostname', (event, host) => { ipcRenderer.on('hostname', (event, host) => {
let hostname = document.getElementById('hostname'); let hostname = document.getElementById('hostname');
if (hostname){ if (hostname){
@ -57,10 +56,16 @@ ipc.on('hostname', (event, host) => {
/** /**
* Triggered if user credentials are invalid * Triggered if user credentials are invalid
*/ */
ipc.on('isValidCredentials', (event, isValidCredentials) => { ipcRenderer.on('isValidCredentials', (event, isValidCredentials) => {
let credentialsError = document.getElementById('credentialsError'); let credentialsError = document.getElementById('credentialsError');
if (credentialsError){ if (credentialsError){
credentialsError.style.display = isValidCredentials ? 'none' : 'block' credentialsError.style.display = isValidCredentials ? 'none' : 'block'
} }
});
ipcRenderer.on('register-crash-reporter', (event, arg) => {
if (arg && typeof arg === 'object') {
crashReporter.start(arg);
}
}); });

85
js/crashReporter.js Normal file
View File

@ -0,0 +1,85 @@
const { crashReporter } = require('electron');
const { getMultipleConfigField } = require('./config.js');
const log = require('./log.js');
const logLevels = require('./enums/logLevels.js');
const configFields = ['url', 'crashReporter'];
let crashReporterData;
/**
* Method that returns all the required field for crash reporter
*
* @param extras {object}
* @return {Promise<any>}
*/
function getCrashReporterConfig(extras) {
return new Promise((resolve, reject) => {
if (crashReporterData && crashReporterData.companyName) {
crashReporterData.extra = Object.assign(crashReporterData.extra, extras);
resolve(crashReporterData);
return;
}
getMultipleConfigField(configFields)
.then((data) => {
if (!data && !data.crashReporter && !data.crashReporter.companyName) {
reject('Company name cannot be empty');
return;
}
crashReporterData = {
companyName: data.crashReporter.companyName,
submitURL: data.crashReporter.submitURL,
uploadToServer: data.crashReporter.uploadToServer,
extra: Object.assign(
{podUrl: data.url},
extras
)
};
resolve(crashReporterData);
})
.catch((err) => log.send(
logLevels.ERROR,
'Unable to initialize crash reporter failed to read config file. Error is -> ' + err
));
})
}
function initCrashReporterMain(extras) {
getCrashReporterConfig(extras).then((mainCrashReporterData) => {
try {
crashReporter.start(mainCrashReporterData);
} catch (err) {
log.send(logLevels.ERROR, 'Failed to start crash reporter main process. Error is -> ' + err);
}
}).catch((err) => log.send(
logLevels.ERROR,
'Unable to initialize crash reporter for main process. Error is -> ' + err
));
}
/**
* Method to initialize crash reporter for renderer process
*
* @param browserWindow {Electron.BrowserWindow}
* @param extras {Object}
*/
function initCrashReporterRenderer(browserWindow, extras) {
if (browserWindow && browserWindow.webContents && !browserWindow.isDestroyed()) {
getCrashReporterConfig(extras).then((rendererCrashReporterData) => {
browserWindow.webContents.send('register-crash-reporter', rendererCrashReporterData);
}).catch((err) => log.send(
logLevels.ERROR,
'Unable to initialize crash reporter for renderer process. Error is -> ' + err
));
}
}
module.exports = {
initCrashReporterMain,
initCrashReporterRenderer,
};

View File

@ -8,6 +8,7 @@ const fs = require('fs');
const log = require('../log.js'); const log = require('../log.js');
const logLevels = require('../enums/logLevels.js'); const logLevels = require('../enums/logLevels.js');
const { isMac, isWindowsOS } = require('./../utils/misc.js'); const { isMac, isWindowsOS } = require('./../utils/misc.js');
const { initCrashReporterMain, initCrashReporterRenderer } = require('../crashReporter.js');
let screenPickerWindow; let screenPickerWindow;
let preloadWindow; let preloadWindow;
@ -91,9 +92,27 @@ function openScreenPickerWindow(eventSender, sources, id) {
}); });
screenPickerWindow.webContents.on('did-finish-load', () => { screenPickerWindow.webContents.on('did-finish-load', () => {
// initialize crash reporter
initCrashReporterMain({ process: 'desktop capture window' });
initCrashReporterRenderer(screenPickerWindow, { process: 'render | desktop capture window' });
screenPickerWindow.webContents.send('desktop-capturer-sources', sources, isWindowsOS); screenPickerWindow.webContents.send('desktop-capturer-sources', sources, isWindowsOS);
}); });
screenPickerWindow.webContents.on('crashed', function () {
const options = {
type: 'error',
title: 'Renderer Process Crashed',
message: 'Oops! Looks like we have had a crash.',
buttons: ['Close']
};
electron.dialog.showMessageBox(options, function () {
if (screenPickerWindow && !screenPickerWindow.isDestroyed()) {
screenPickerWindow.close();
}
});
});
screenPickerWindow.on('close', () => { screenPickerWindow.on('close', () => {
destroyWindow(); destroyWindow();
}); });

View File

@ -1,5 +1,5 @@
'use strict'; 'use strict';
const { ipcRenderer } = require('electron'); const { ipcRenderer, crashReporter } = require('electron');
const screenRegExp = new RegExp(/^Screen \d+$/gmi); const screenRegExp = new RegExp(/^Screen \d+$/gmi);
@ -265,4 +265,10 @@ function updateSelectedSource(index) {
function closeScreenPickerWindow() { function closeScreenPickerWindow() {
document.removeEventListener('keyUp', handleKeyUpPress.bind(this), true); document.removeEventListener('keyUp', handleKeyUpPress.bind(this), true);
ipcRenderer.send('close-screen-picker'); ipcRenderer.send('close-screen-picker');
} }
ipcRenderer.on('register-crash-reporter', (event, arg) => {
if (arg && typeof arg === 'object') {
crashReporter.start(arg);
}
});

View File

@ -129,7 +129,8 @@ const template = [{
{ {
label: isMac ? 'Show Logs in Finder' : 'Show Logs in Explorer', label: isMac ? 'Show Logs in Finder' : 'Show Logs in Explorer',
click() { click() {
const FILE_EXTENSIONS = [ '.log' ];
const MAC_LOGS_PATH = '/Library/Logs/Symphony/'; const MAC_LOGS_PATH = '/Library/Logs/Symphony/';
const WINDOWS_LOGS_PATH = '\\AppData\\Roaming\\Symphony\\'; const WINDOWS_LOGS_PATH = '\\AppData\\Roaming\\Symphony\\';
@ -146,7 +147,7 @@ const template = [{
let destination = electron.app.getPath('downloads') + destPath + timestamp + '.zip'; let destination = electron.app.getPath('downloads') + destPath + timestamp + '.zip';
archiveHandler.generateArchiveForDirectory(source, destination) archiveHandler.generateArchiveForDirectory(source, destination, FILE_EXTENSIONS)
.then(() => { .then(() => {
electron.shell.showItemInFolder(destination); electron.shell.showItemInFolder(destination);
}) })
@ -157,10 +158,29 @@ const template = [{
} }
}, },
{ {
label: 'Open Crashes Directory', label: isMac ? 'Show crash dump in Finder' : 'Show crash dump in Explorer',
click() { click() {
const crashesDirectory = electron.crashReporter.getCrashesDirectory() + '/completed'; const FILE_EXTENSIONS = isMac ? [ '.dmp' ] : [ '.dmp', '.txt' ];
electron.shell.showItemInFolder(crashesDirectory); const crashesDirectory = electron.crashReporter.getCrashesDirectory();
let source = isMac ? crashesDirectory + '/completed' : crashesDirectory;
if (!fs.existsSync(source) || fs.readdirSync(source).length === 0) {
electron.dialog.showErrorBox('Failed!', 'No crashes available to share');
return;
}
let destPath = isMac ? '/crashes_symphony_' : '\\crashes_symphony_';
let timestamp = new Date().getTime();
let destination = electron.app.getPath('downloads') + destPath + timestamp + '.zip';
archiveHandler.generateArchiveForDirectory(source, destination, FILE_EXTENSIONS)
.then(() => {
electron.shell.showItemInFolder(destination);
})
.catch((err) => {
electron.dialog.showErrorBox('Failed!', 'Unable to generate crash report due to -> ' + err);
});
} }
} }
] ]

View File

@ -130,19 +130,6 @@ function createAPI() {
*/ */
ScreenSnippet: remote.require('./screenSnippet/index.js').ScreenSnippet, ScreenSnippet: remote.require('./screenSnippet/index.js').ScreenSnippet,
/**
* Provides API to crash the renderer process that calls this function
* Is only used for demos.
*/
crashRendererProcess: function () {
// For practical purposes, we don't allow
// this method to work in non-dev environments
if (!process.env.ELECTRON_DEV) {
return;
}
process.crash();
},
/** /**
* Provides api for client side searching * Provides api for client side searching
* using the SymphonySearchEngine library * using the SymphonySearchEngine library
@ -394,9 +381,13 @@ function createAPI() {
}); });
/**
* an event triggered by the main process to start crash reporter
* in the render thread
*/
local.ipcRenderer.on('register-crash-reporter', (event, arg) => { local.ipcRenderer.on('register-crash-reporter', (event, arg) => {
if (arg) { if (arg && typeof arg === 'object') {
crashReporter.start({companyName: arg.companyName, submitURL: arg.submitURL, uploadToServer: arg.uploadToServer, extra: {'process': arg.process, podUrl: arg.podUrl}}); crashReporter.start(arg);
} }
}); });

View File

@ -3,11 +3,25 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const archiver = require('archiver'); const archiver = require('archiver');
const log = require('../log.js');
const logLevels = require('../enums/logLevels.js');
const mmm = require('mmmagic');
const Magic = mmm.Magic;
const magic = new Magic(mmm.MAGIC_MIME_TYPE);
function generateArchiveForDirectory(source, destination) { /**
* Archives files in the source directory
* that matches the given file extension
*
* @param source {String} source path
* @param destination {String} destination path
* @param fileExtensions {Array} array of file ext
* @return {Promise<any>}
*/
function generateArchiveForDirectory(source, destination, fileExtensions) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let output = fs.createWriteStream(destination); let output = fs.createWriteStream(destination);
let archive = archiver('zip', {zlib: {level: 9}}); let archive = archiver('zip', {zlib: {level: 9}});
@ -20,20 +34,55 @@ function generateArchiveForDirectory(source, destination) {
}); });
archive.pipe(output); archive.pipe(output);
let files = fs.readdirSync(source); let files = fs.readdirSync(source);
files.forEach((file) => {
if (path.extname(file) === '.log') { let filtered = files.filter((file) => fileExtensions.indexOf(path.extname(file)) !== -1);
archive.file(source + '/' + file, { name: 'logs/' + file }); mapMimeType(filtered, source)
} .then((mappedData) => {
}); if (mappedData.length > 0) {
mappedData.map((data) => {
archive.finalize(); switch (data.mimeType) {
case 'text/plain':
if (path.extname(data.file) === '.txt') {
archive.file(source + '/' + data.file, { name: 'crashes/' + data.file });
} else {
archive.file(source + '/' + data.file, { name: 'logs/' + data.file });
}
break;
case 'application/x-dmp':
archive.file(source + '/' + data.file, { name: 'crashes/' + data.file });
break;
default:
break;
}
});
}
archive.finalize();
})
.catch((err) => {
log.send(logLevels.ERROR, 'Failed to find mime type. Error is -> ' + err);
});
}); });
} }
function mapMimeType(files, source) {
return Promise.all(files.map((file) => {
return new Promise((resolve, reject) => {
return magic.detectFile(source + '/' + file, (err, result) => {
if (err) {
return reject(err);
}
return resolve({file: file, mimeType: result});
});
});
}))
.then((data) => data)
.catch((err) => log.send(logLevels.ERROR, 'Failed to find mime type. Error is -> ' + err));
}
module.exports = { module.exports = {
generateArchiveForDirectory: generateArchiveForDirectory generateArchiveForDirectory: generateArchiveForDirectory
}; };

View File

@ -4,7 +4,6 @@ const fs = require('fs');
const electron = require('electron'); const electron = require('electron');
const app = electron.app; const app = electron.app;
const globalShortcut = electron.globalShortcut; const globalShortcut = electron.globalShortcut;
const crashReporter = electron.crashReporter;
const BrowserWindow = electron.BrowserWindow; const BrowserWindow = electron.BrowserWindow;
const path = require('path'); const path = require('path');
const nodeURL = require('url'); const nodeURL = require('url');
@ -24,6 +23,7 @@ const { getConfigField, updateConfigField, getGlobalConfigField } = require('./c
const { isMac, isNodeEnv, isWindows10, isWindowsOS } = require('./utils/misc'); const { isMac, isNodeEnv, isWindows10, isWindowsOS } = require('./utils/misc');
const { deleteIndexFolder } = require('./search/search.js'); const { deleteIndexFolder } = require('./search/search.js');
const { isWhitelisted } = require('./utils/whitelistHandler'); const { isWhitelisted } = require('./utils/whitelistHandler');
const { initCrashReporterMain, initCrashReporterRenderer } = require('./crashReporter.js');
// show dialog when certificate errors occur // show dialog when certificate errors occur
require('./dialogs/showCertError.js'); require('./dialogs/showCertError.js');
@ -188,6 +188,10 @@ function doCreateMainWindow(initialUrl, initialBounds, isCustomTitleBar) {
// content can be cached and will still finish load but // content can be cached and will still finish load but
// we might not have network connectivity, so warn the user. // we might not have network connectivity, so warn the user.
mainWindow.webContents.on('did-finish-load', function () { mainWindow.webContents.on('did-finish-load', function () {
// Initialize crash reporter
initCrashReporterMain({ process: 'main window' });
initCrashReporterRenderer(mainWindow, { process: 'render | main window' });
url = mainWindow.webContents.getURL(); url = mainWindow.webContents.getURL();
if (isCustomTitleBarEnabled) { if (isCustomTitleBarEnabled) {
mainWindow.webContents.insertCSS(fs.readFileSync(path.join(__dirname, '/windowsTitleBar/style.css'), 'utf8').toString()); mainWindow.webContents.insertCSS(fs.readFileSync(path.join(__dirname, '/windowsTitleBar/style.css'), 'utf8').toString());
@ -318,23 +322,6 @@ function doCreateMainWindow(initialUrl, initialBounds, isCustomTitleBar) {
}); });
}); });
getConfigField('url')
.then(initializeCrashReporter)
.catch(app.quit);
function initializeCrashReporter(podUrl) {
getConfigField('crashReporter')
.then((crashReporterConfig) => {
log.send(logLevels.INFO, 'Initializing crash reporter on the main window!');
crashReporter.start({ companyName: crashReporterConfig.companyName, submitURL: crashReporterConfig.submitURL, uploadToServer: crashReporterConfig.uploadToServer, extra: { 'process': 'renderer / main window', podUrl: podUrl } });
log.send(logLevels.INFO, 'initialized crash reporter on the main window!');
mainWindow.webContents.send('register-crash-reporter', { companyName: crashReporterConfig.companyName, submitURL: crashReporterConfig.submitURL, uploadToServer: crashReporterConfig.uploadToServer, process: 'preload script / main window renderer' });
})
.catch((err) => {
log.send(logLevels.ERROR, 'Unable to initialize crash reporter in the main window. Error is -> ' + err);
});
}
// open external links in default browser - a tag with href='_blank' or window.open // open external links in default browser - a tag with href='_blank' or window.open
mainWindow.webContents.on('new-window', handleNewWindow); mainWindow.webContents.on('new-window', handleNewWindow);
@ -420,19 +407,8 @@ function doCreateMainWindow(initialUrl, initialBounds, isCustomTitleBar) {
browserWin.setMenu(null); browserWin.setMenu(null);
} }
getConfigField('url') initCrashReporterMain({ process: 'pop-out window' });
.then((podUrl) => { initCrashReporterRenderer(browserWin, { process: 'render | pop-out window' });
getConfigField('crashReporter')
.then((crashReporterConfig) => {
crashReporter.start({ companyName: crashReporterConfig.companyName, submitURL: crashReporterConfig.submitURL, uploadToServer: crashReporterConfig.uploadToServer, extra: { 'process': 'renderer / child window', podUrl: podUrl } });
log.send(logLevels.INFO, 'initialized crash reporter on a child window!');
browserWin.webContents.send('register-crash-reporter', { companyName: crashReporterConfig.companyName, submitURL: crashReporterConfig.submitURL, uploadToServer: crashReporterConfig.uploadToServer, process: 'preload script / child window renderer' });
})
.catch((err) => {
log.send(logLevels.ERROR, 'Unable to initialize crash reporter in the child window. Error is -> ' + err);
});
})
.catch(app.quit);
browserWin.winName = frameName; browserWin.winName = frameName;
browserWin.setAlwaysOnTop(alwaysOnTop); browserWin.setAlwaysOnTop(alwaysOnTop);
@ -452,7 +428,7 @@ function doCreateMainWindow(initialUrl, initialBounds, isCustomTitleBar) {
browserWin.webContents.removeListener('crashed', handleChildWindowCrashEvent); browserWin.webContents.removeListener('crashed', handleChildWindowCrashEvent);
}); });
let handleChildWindowCrashEvent = () => { let handleChildWindowCrashEvent = (e) => {
const options = { const options = {
type: 'error', type: 'error',
title: 'Renderer Process Crashed', title: 'Renderer Process Crashed',
@ -460,14 +436,16 @@ function doCreateMainWindow(initialUrl, initialBounds, isCustomTitleBar) {
buttons: ['Reload', 'Close'] buttons: ['Reload', 'Close']
}; };
electron.dialog.showMessageBox(options, function (index) { let childBrowserWindow = BrowserWindow.fromWebContents(e.sender);
if (index === 0) { if (childBrowserWindow && !childBrowserWindow.isDestroyed()) {
browserWin.reload(); electron.dialog.showMessageBox(childBrowserWindow, options, function (index) {
} if (index === 0) {
else { childBrowserWindow.reload();
browserWin.close(); } else {
} childBrowserWindow.close();
}); }
});
}
}; };
browserWin.webContents.on('crashed', handleChildWindowCrashEvent); browserWin.webContents.on('crashed', handleChildWindowCrashEvent);

View File

@ -122,6 +122,7 @@
"lodash.isequal": "4.5.0", "lodash.isequal": "4.5.0",
"lodash.omit": "4.5.0", "lodash.omit": "4.5.0",
"lodash.pick": "4.4.0", "lodash.pick": "4.4.0",
"mmmagic": "0.5.0",
"parse-domain": "2.0.0", "parse-domain": "2.0.0",
"ref": "1.3.5", "ref": "1.3.5",
"shell-path": "2.1.0", "shell-path": "2.1.0",