Skip to content

Commit

Permalink
Add a crash reporter to diagnose CLI crashes. Update the service endp…
Browse files Browse the repository at this point in the history
…oint URL.

All crashes get submitted by default. The user has can turn off crash reporting if he wants to.
Add lots of tests. Thanks a lot @rwaldron.
  • Loading branch information
tikurahul committed Apr 13, 2016
1 parent d60f777 commit fac3975
Show file tree
Hide file tree
Showing 7 changed files with 659 additions and 0 deletions.
37 changes: 37 additions & 0 deletions bin/tessel-2.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ updateNotifier({

// Internal
var controller = require('../lib/controller');
var CrashReporter = require('../lib/crash_reporter');
var init = require('../lib/init');
var logs = require('../lib/logs');
var drivers = require('./tessel-install-drivers');
Expand Down Expand Up @@ -77,6 +78,42 @@ parser.command('install-drivers')
})
.help('Install drivers');

parser.command('crash-reporter')
.callback(opts => {
// t2 crash-reporter --on
if (opts.on) {
return CrashReporter.on().then(() => {
// t2 crash-reporter --on --test
if (opts.test) {
CrashReporter.test().then(module.exports.closeSuccessfulCommand);
}
}).then(module.exports.closeSuccessfulCommand, module.exports.closeFailedCommand);
} else if (opts.off) {
// t2 crash-reporter --on
return CrashReporter.off()
.then(module.exports.closeSuccessfulCommand, module.exports.closeFailedCommand);
}

// t2 crash-reporter --test
if (opts.test) {
// not handling failures, as we want to trigger a crash
CrashReporter.test()
.then(module.exports.closeSuccessfulCommand);
}
})
.option('off', {
flag: true,
help: 'Disable the Crash Reporter.'
})
.option('on', {
flag: true,
help: 'Enable the Crash Reporter.'
})
.option('test', {
flag: true,
help: 'Test the Crash Reporter.'
});

parser.command('provision')
.callback(callControllerCallback('provisionTessel'))
.option('force', {
Expand Down
110 changes: 110 additions & 0 deletions lib/crash_reporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// System Objects
var os = require('os');

// Third Party Dependencies
var request = require('request');
var tags = require('common-tags');

// Internal
var logs = require('./logs');
var packageJson = require('../package.json');
var Preferences = require('./preferences');

// the value of the crash reporter preference
// the value has to be one of 'on' or 'off'
var CRASH_REPORTER_PREFERENCE = 'crash.reporter.preference';
var SUBMIT_CRASH_URL = 'http://crash-reporter.tessel.io/crashes/submit';

// override for testing
if (process.env.DEV_MODE === 'true') {
SUBMIT_CRASH_URL = 'http://localhost:8080/crashes/submit';
}

var CrashReporter = {};

CrashReporter.off = function() {
return Preferences.write(CRASH_REPORTER_PREFERENCE, 'off')
.catch(error => {
// do nothing
// do not crash the crash reporter :)
logs.err('Error turning off crash reporter preferences', error);
});
};

CrashReporter.on = function() {
return Preferences.write(CRASH_REPORTER_PREFERENCE, 'on')
.catch(error => {
// do nothing
// do not crash the crash reporter :)
logs.err('Error turning on crash reporter preferences', error);
});
};

CrashReporter.submit = function(report) {
return Preferences.read(CRASH_REPORTER_PREFERENCE, 'on')
.then(value => {
if (value === 'on') {
var labels = tags.stripIndent `
${packageJson.name},
CLI version: ${packageJson.version},
Node version: ${process.version},
OS platform: ${os.platform()},
OS release: ${os.release()}
`;

CrashReporter.post(labels, report)
.then(() => {
logs.info('Done !');
})
.catch(error => {
logs.err('Error submitting crash report', error);
});
}
})
.catch(error => {
// do nothing
// do not crash the crash reporter :)
logs.err('Error submitting crash report', error);
});
};

CrashReporter.post = function(labels, report) {
return new Promise((resolve, reject) => {
request.post({
url: SUBMIT_CRASH_URL,
form: {
crash: report,
labels: labels,
f: 'json'
}
}, (error, httpResponse, body) => {
try {
if (error) {
reject(error);
} else {
var json = JSON.parse(body);
if (json.error) {
reject(json.error);
} else {
resolve();
}
}
} catch (exception) {
reject(exception);
}
});
});
};

CrashReporter.test = () => {
return Promise.reject(new Error('Testing the crash reporter'));
};

var onError = error => {
return CrashReporter.submit(error.stack);
};

process.on('unhandledRejection', onError);
process.on('uncaughtException', onError);

module.exports = CrashReporter;
70 changes: 70 additions & 0 deletions lib/preferences.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// System Objects
var path = require('path');

// Third Party Dependencies
var fs = require('fs-extra');

// Internal
var logs = require('./logs');


var home = (process.platform.startsWith('win')) ? process.env.HOMEPATH : process.env.HOME;
var preferencesJson = path.join(home, '.tessel', 'preferences.json');
var Preferences = {};

Preferences.read = function(key, defaultValue) {
return Preferences.load().then(contents => {
if (contents) {
return contents[key] || defaultValue;
} else {
return defaultValue;
}
})
.catch(error => {
logs.err('Error reading preference', key, error);
return defaultValue;
});
};

Preferences.write = function(key, value) {
return new Promise((resolve, reject) => {
Preferences.load()
.then(contents => {
contents = contents || {};
contents[key] = value;
fs.writeFile(preferencesJson, JSON.stringify(contents), function(error) {
if (error) {
logs.err('Error writing preference', key, value);
reject(error);
} else {
resolve();
}
});
})
.catch(error => {
reject(error);
});
});
};

Preferences.load = function() {
return new Promise((resolve, reject) => {
fs.exists(preferencesJson, (exists) => {
if (exists) {
fs.readFile(preferencesJson, (error, data) => {
if (error) {
reject(error);
} else {
resolve(JSON.parse(data));
}
});
} else {
// we don't have any local preferences
// return falsy value
resolve();
}
});
});
};

module.exports = Preferences;
2 changes: 2 additions & 0 deletions test/common/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ global.provision = require('../../lib/tessel/provision');
global.deployLists = require('../../lib/tessel/deploy-lists');

// ./lib/*
global.CrashReporter = require('../../lib/crash_reporter');
global.Preferences = require('../../lib/preferences');
global.controller = require('../../lib/controller');
global.discover = require('../../lib/discover');
global.logs = require('../../lib/logs');
Expand Down
134 changes: 134 additions & 0 deletions test/unit/bin-tessel-2.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
// expected default command options are protected from regressions.
// This should be used as a guide for reviewing new tessel-centric
// additions to the cli command set.

/*global CrashReporter */

var defaults = {
timeout: {
abbr: 't',
Expand Down Expand Up @@ -679,3 +682,134 @@ exports['--output true/false'] = {
});
},
};


exports['Tessel (cli: crash-reporter)'] = {
setUp: function(done) {
this.sandbox = sinon.sandbox.create();
this.warn = this.sandbox.stub(logs, 'warn');
this.info = this.sandbox.stub(logs, 'info');

this.closeSuccessful = this.sandbox.stub(cli, 'closeSuccessfulCommand');
this.closeFailed = this.sandbox.stub(cli, 'closeFailedCommand');

this.crOn = this.sandbox.stub(CrashReporter, 'on').returns(Promise.resolve());
this.crOff = this.sandbox.stub(CrashReporter, 'off').returns(Promise.resolve());
this.crPost = this.sandbox.stub(CrashReporter, 'post').returns(Promise.resolve());
this.crSubmit = this.sandbox.stub(CrashReporter, 'submit').returns(Promise.resolve());
this.crTest = this.sandbox.stub(CrashReporter, 'test').returns(Promise.resolve());

done();
},

tearDown: function(done) {
this.sandbox.restore();
done();
},

callThroughNoOptions: function(test) {
test.expect(3);

cli(['crash-reporter']);

test.equal(this.crOn.callCount, 0);
test.equal(this.crOff.callCount, 0);
test.equal(this.crTest.callCount, 0);


test.done();
},

on: function(test) {
test.expect(3);

cli(['crash-reporter', '--on=true']);

test.equal(this.crOn.callCount, 1);
test.equal(this.crOff.callCount, 0);
test.equal(this.crTest.callCount, 0);


test.done();
},

off: function(test) {
test.expect(3);

cli(['crash-reporter', '--off=true']);

test.equal(this.crOn.callCount, 0);
test.equal(this.crOff.callCount, 1);
test.equal(this.crTest.callCount, 0);


test.done();
},

test: function(test) {
test.expect(3);

cli(['crash-reporter', '--test=true']);

test.equal(this.crOn.callCount, 0);
test.equal(this.crOff.callCount, 0);
test.equal(this.crTest.callCount, 1);


test.done();
},

onAndTest: function(test) {
test.expect(3);

var resolve = Promise.resolve();
this.crOn.restore();
this.crOn = this.sandbox.stub(CrashReporter, 'on').returns(resolve);

cli(['crash-reporter', ' ', '--on', ' ', '--test', ' ']);

resolve.then(() => {
test.equal(this.crOn.callCount, 1);
test.equal(this.crOff.callCount, 0);
test.equal(this.crTest.callCount, 1);

test.done();
});
},

onNoTestThrough: function(test) {
test.expect(3);

var resolve = Promise.resolve();
this.crOn.restore();
this.crOn = this.sandbox.stub(CrashReporter, 'on').returns(resolve);

cli(['crash-reporter', ' ', '--on', ' ']);

resolve.then(() => {
test.equal(this.crOn.callCount, 1);
test.equal(this.crOff.callCount, 0);
test.equal(this.crTest.callCount, 0);

test.done();
});
},

unsuccessful: function(test) {
test.expect(3);

var resolve = Promise.resolve();
this.crOn.restore();
this.crOn = this.sandbox.stub(CrashReporter, 'on').returns(resolve);

cli(['crash-reporter', ' ', '--on', ' ']);

resolve.then(() => {
test.equal(this.crOn.callCount, 1);
test.equal(this.crOff.callCount, 0);
test.equal(this.crTest.callCount, 0);
test.done();
});
},

};

0 comments on commit fac3975

Please sign in to comment.