Mailspring/app/src/error-logger.js

298 lines
9.1 KiB
JavaScript

// This file cannot be Coffeescript because it loads before the
// Coffeescript interpreter. Note that it runs in both browser and
// renderer processes.
var ErrorLogger, _, fs, path, app, os, remote;
os = require('os');
fs = require('fs-plus');
path = require('path');
let ipcRenderer = null;
if (process.type === 'renderer') {
ipcRenderer = require('electron').ipcRenderer;
remote = require('electron').remote;
app = remote.app;
} else {
app = require('electron').app;
}
var appVersion = app.getVersion();
var crashReporter = require('electron').crashReporter;
var RavenErrorReporter = require('./error-logger-extensions/raven-error-reporter');
// A globally available ErrorLogger that can report errors to various
// sources and enhance error functionality.
//
// This runs in both the backend browser process and each and every
// renderer process.
//
// This is available as `global.errorLogger` in the backend browser
// process.
//
// It is available at `AppEnv.errorLogger` in each renderer process.
// You should almost always use `AppEnv.reportError` in the renderer
// processes instead of manually accessing the `errorLogger`
//
// The errorLogger will report errors to a log file as well as to 3rd
// party reporting services if enabled.
module.exports = ErrorLogger = (function() {
function ErrorLogger(args) {
this.reportError = this.reportError.bind(this);
this.inSpecMode = args.inSpecMode;
this.inDevMode = args.inDevMode;
this.resourcePath = args.resourcePath;
this._startCrashReporter();
this._extendErrorObject();
this._extendNativeConsole();
this.extensions = [
new RavenErrorReporter({
inSpecMode: args.inSpecMode,
inDevMode: args.inDevMode,
resourcePath: args.resourcePath,
}),
];
if (this.inSpecMode) {
return;
}
this._cleanOldLogFiles();
this._setupNewLogFile();
this._hookProcessOutputsToLogFile();
}
/////////////////////////////////////////////////////////////////////
/////////////////////////// PUBLIC METHODS //////////////////////////
/////////////////////////////////////////////////////////////////////
ErrorLogger.prototype.reportError = function(error, extra = {}) {
if (this.inSpecMode) {
return;
}
if (!error) {
error = { stack: '' };
}
this._appendLog(error.stack);
if (extra) {
this._appendLog(extra);
}
if (process.type === 'renderer') {
var errorJSON = '{}';
try {
errorJSON = JSON.stringify(error);
} catch (err) {
var recoveredError = new Error();
recoveredError.stack = error.stack;
recoveredError.message = `Recovered Error: ${error.message}`;
errorJSON = JSON.stringify(recoveredError);
}
var extraJSON;
try {
extraJSON = JSON.stringify(extra);
} catch (err) {
extraJSON = '{}';
}
/**
* We synchronously send all errors to the backend main process.
*
* This is important because errors can frequently happen right
* before a renderer window is closing. Since error reporting hits
* APIs and is asynchronous it's possible for the window to be
* destroyed before the report makes it.
*
* This is a rare use of `sendSync` to ensure the command has made
* it before the window closes.
*/
ipcRenderer.sendSync('report-error', { errorJSON: errorJSON, extra: extraJSON });
} else {
this._notifyExtensions('reportError', error, extra);
}
console.error(error, extra);
};
ErrorLogger.prototype.openLogs = function() {
var shell = require('electron').shell;
shell.openItem(this._logPath());
};
/////////////////////////////////////////////////////////////////////
////////////////////////// PRIVATE METHODS //////////////////////////
/////////////////////////////////////////////////////////////////////
ErrorLogger.prototype._startCrashReporter = function(args) {
crashReporter.start({
productName: 'Mailspring',
companyName: 'Mailspring',
submitURL: `https://id.getmailspring.com/report-crash?ver=${appVersion}&platform=${process.platform}`,
uploadToServer: true,
autoSubmit: true,
});
};
ErrorLogger.prototype._extendNativeConsole = function(args) {
console.debug = this._consoleDebug.bind(this);
if (process.type === 'browser' && process.platform === 'darwin') {
var nslog = require('nslog');
console.log = nslog;
console.error = nslog;
}
};
// globally define Error.toJSON. This allows us to pass errors via IPC
// and through the Action Bridge. Note:they are not re-inflated into
// Error objects automatically.
ErrorLogger.prototype._extendErrorObject = function(args) {
Object.defineProperty(Error.prototype, 'toJSON', {
value: function() {
var alt = {};
Object.getOwnPropertyNames(this).forEach(function(key) {
alt[key] = this[key];
}, this);
return alt;
},
configurable: true,
});
};
ErrorLogger.prototype._logPath = function() {
var tmpPath = app.getPath('temp');
var logpid = process.pid;
if (process.type === 'renderer') {
logpid = remote.process.pid + '.' + process.pid;
}
return path.join(tmpPath, 'Mailspring-' + logpid + '.log');
};
// If we're the browser process, remove log files that are more than
// two days old. These log files get pretty big because we're logging
// so verbosely.
ErrorLogger.prototype._cleanOldLogFiles = function() {
if (process.type === 'browser') {
var tmpPath = app.getPath('temp');
fs.readdir(tmpPath, function(err, files) {
if (err) {
console.error(err);
return;
}
var logFilter = new RegExp('Mailspring-[.0-9]*.log$');
files.forEach(function(file) {
if (logFilter.test(file) === true) {
var filepath = path.join(tmpPath, file);
fs.stat(filepath, function(err, stats) {
if (!err && stats) {
var lastModified = new Date(stats.mtime);
var fileAge = Date.now() - lastModified.getTime();
if (fileAge > 1000 * 60 * 60 * 24 * 2) {
// two days
fs.unlink(filepath, () => {});
}
}
});
}
});
});
}
};
ErrorLogger.prototype._setupNewLogFile = function() {
// Open a file write stream to log output from this process
console.log('Streaming log data to ' + this._logPath());
this.loghost = os.hostname();
this.logstream = fs.createWriteStream(this._logPath(), {
flags: 'a',
encoding: 'utf8',
fd: null,
mode: 666,
});
};
ErrorLogger.prototype._hookProcessOutputsToLogFile = function() {
var self = this;
// Override stdout and stderr to pipe their output to the file
// in addition to calling through to the existing implementation
function hook_process_output(channel, callback) {
var old_write = process[channel].write;
process[channel].write = (function(write) {
return function(string, encoding, fd) {
write.apply(process[channel], arguments);
callback(string, encoding, fd);
};
})(process[channel].write);
// Return a function that can be used to undo this change
return function() {
process[channel].write = old_write;
};
}
hook_process_output('stdout', function(string, encoding, fd) {
self._appendLog.apply(self, [string]);
});
hook_process_output('stderr', function(string, encoding, fd) {
self._appendLog.apply(self, [string]);
});
};
ErrorLogger.prototype._notifyExtensions = function() {
var command, args;
command = arguments[0];
args = 2 <= arguments.length ? Array.prototype.slice.call(arguments, 1) : [];
for (var i = 0; i < this.extensions.length; i++) {
const extension = this.extensions[i];
extension[command].apply(extension, args);
}
};
// Create a new console.debug option, which takes `true` (print)
// or `false`, don't print in console as the first parameter.
// This makes it easy for developers to turn on and off
// "verbose console" mode.
ErrorLogger.prototype._consoleDebug = function() {
var args = [];
var showIt = arguments[0];
for (var ii = 1; ii < arguments.length; ii++) {
args.push(arguments[ii]);
}
if (this.inDevMode === true && showIt === true) {
console.log.apply(console, args);
}
this._appendLog.apply(this, [args]);
};
ErrorLogger.prototype._appendLog = function(obj) {
if (this.inSpecMode) {
return;
}
try {
var message =
JSON.stringify({
host: this.loghost,
timestamp: new Date().toISOString(),
payload: obj,
}) + '\n';
this.logstream.write(message, 'utf8', function(err) {
if (err) {
console.error('ErrorLogger: Unable to write to the log stream!' + err.toString());
}
});
} catch (err) {
console.error('ErrorLogger: Unable to write to the log stream.' + err.toString());
}
};
return ErrorLogger;
})();