diff --git a/toolkit/components/crashes/CrashManager.jsm b/toolkit/components/crashes/CrashManager.jsm index 6fb4a01af881..c014f7c6dc12 100644 --- a/toolkit/components/crashes/CrashManager.jsm +++ b/toolkit/components/crashes/CrashManager.jsm @@ -346,19 +346,62 @@ this.CrashManager.prototype = Object.freeze({ let decoder = new TextDecoder(); data = decoder.decode(data); - let sepIndex = data.indexOf("\n"); - if (sepIndex == -1) { - return this.EVENT_FILE_ERROR_MALFORMED; + let type, time, payload; + let start = 0; + for (let i = 0; i < 2; i++) { + let index = data.indexOf("\n", start); + if (index == -1) { + return this.EVENT_FILE_ERROR_MALFORMED; + } + + let sub = data.substring(start, index); + switch (i) { + case 0: + type = sub; + break; + case 1: + time = sub; + try { + time = parseInt(time, 10); + } catch (ex) { + return this.EVENT_FILE_ERROR_MALFORMED; + } + } + + start = index + 1; } + let date = new Date(time * 1000); + let payload = data.substring(start); - let type = data.substring(0, sepIndex); - let payload = data.substring(sepIndex + 1); - - return this._handleEventFilePayload(entry, type, payload); + return this._handleEventFilePayload(store, entry, type, date, payload); }.bind(this)); }, - _handleEventFilePayload: function (entry, type, payload) { + _handleEventFilePayload: function (store, entry, type, date, payload) { + // The payload types and formats are documented in docs/crash-events.rst. + // Do not change the format of an existing type. Instead, invent a new + // type. + + let eventMap = { + "crash.main.1": "addMainProcessCrash", + "crash.plugin.1": "addPluginCrash", + "hang.plugin.1": "addPluginHang", + }; + + if (type in eventMap) { + let lines = payload.split("\n"); + if (lines.length > 1) { + this._log.warn("Multiple lines unexpected in payload for " + + entry.path); + return this.EVENT_FILE_ERROR_MALFORMED; + } + + store[eventMap[type]](payload, date); + return this.EVENT_FILE_SUCCESS; + } + + // DO NOT ADD NEW TYPES WITHOUT DOCUMENTING! + return this.EVENT_FILE_ERROR_UNKNOWN_EVENT; }, @@ -494,6 +537,10 @@ function CrashStore(storeDir, telemetrySizeKey) { } CrashStore.prototype = Object.freeze({ + TYPE_MAIN_CRASH: "main-crash", + TYPE_PLUGIN_CRASH: "plugin-crash", + TYPE_PLUGIN_HANG: "plugin-hang", + /** * Load data from disk. * @@ -681,6 +728,87 @@ CrashStore.prototype = Object.freeze({ return null; }, + + _ensureCrashRecord: function (id) { + if (!this._data.crashes.has(id)) { + this._data.crashes.set(id, { + id: id, + type: null, + crashDate: null, + }); + } + + return this._data.crashes.get(id); + }, + + /** + * Record the occurrence of a crash in the main process. + * + * @param id (string) Crash ID. Likely a UUID. + * @param date (Date) When the crash occurred. + */ + addMainProcessCrash: function (id, date) { + let r = this._ensureCrashRecord(id); + r.type = this.TYPE_MAIN_CRASH; + r.crashDate = date; + }, + + /** + * Record the occurrence of a crash in a plugin process. + * + * @param id (string) Crash ID. Likely a UUID. + * @param date (Date) When the crash occurred. + */ + addPluginCrash: function (id, date) { + let r = this._ensureCrashRecord(id); + r.type = this.TYPE_PLUGIN_CRASH; + r.crashDate = date; + }, + + /** + * Record the occurrence of a hang in a plugin process. + * + * @param id (string) Crash ID. Likely a UUID. + * @param date (Date) When the hang was reported. + */ + addPluginHang: function (id, date) { + let r = this._ensureCrashRecord(id); + r.type = this.TYPE_PLUGIN_HANG; + r.crashDate = date; + }, + + get mainProcessCrashes() { + let crashes = []; + for (let crash of this.crashes) { + if (crash.isMainProcessCrash) { + crashes.push(crash); + } + } + + return crashes; + }, + + get pluginCrashes() { + let crashes = []; + for (let crash of this.crashes) { + if (crash.isPluginCrash) { + crashes.push(crash); + } + } + + return crashes; + }, + + get pluginHangs() { + let crashes = []; + for (let crash of this.crashes) { + if (crash.isPluginHang) { + crashes.push(crash); + } + } + + return crashes; + }, }); /** @@ -718,6 +846,22 @@ CrashRecord.prototype = Object.freeze({ // We currently only have 1 date, so this is easy. return this._o.crashDate; }, + + get type() { + return this._o.type; + }, + + get isMainProcessCrash() { + return this._o.type == CrashStore.prototype.TYPE_MAIN_CRASH; + }, + + get isPluginCrash() { + return this._o.type == CrashStore.prototype.TYPE_PLUGIN_CRASH; + }, + + get isPluginHang() { + return this._o.type == CrashStore.prototype.TYPE_PLUGIN_HANG; + }, }); /** diff --git a/toolkit/components/crashes/CrashManagerTest.jsm b/toolkit/components/crashes/CrashManagerTest.jsm index 46b8b34cf497..34125b987d71 100644 --- a/toolkit/components/crashes/CrashManagerTest.jsm +++ b/toolkit/components/crashes/CrashManagerTest.jsm @@ -87,10 +87,12 @@ this.TestingCrashManager.prototype = { }); }, - createEventsFile: function (filename, name, content, index=0, date=new Date()) { + createEventsFile: function (filename, type, date, content, index=0) { let path = OS.Path.join(this._eventsDirs[index], filename); - let data = name + "\n" + content; + let data = type + "\n" + + Math.floor(date.getTime() / 1000) + "\n" + + content; let encoder = new TextEncoder(); let array = encoder.encode(data); @@ -105,21 +107,22 @@ this.TestingCrashManager.prototype = { * * We can probably delete this once we have actual events defined. */ - _handleEventFilePayload: function (entry, type, payload) { + _handleEventFilePayload: function (store, entry, type, date, payload) { if (type == "test.1") { if (payload == "malformed") { return this.EVENT_FILE_ERROR_MALFORMED; } else if (payload == "success") { return this.EVENT_FILE_SUCCESS; } else { - // Payload is crash ID. Create a duommy record. - this._store._data.crashes.set(payload, {id: payload, crashDate: entry.date}); - - return this.EVENT_FILE_SUCCESS; + return this.EVENT_FILE_ERROR_UNKNOWN_EVENT; } } - return CrashManager.prototype._handleEventFilePayload.call(this, type, + return CrashManager.prototype._handleEventFilePayload.call(this, + store, + entry, + type, + date, payload); }, }; diff --git a/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js b/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js index c050461e7d8d..3cda6678d125 100644 --- a/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js +++ b/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js @@ -12,6 +12,8 @@ Cu.import("resource://gre/modules/osfile.jsm", this); Cu.import("resource://testing-common/CrashManagerTest.jsm", this); +const DUMMY_DATE = new Date(1391043519000); + function run_test() { do_get_profile(); run_next_test(); @@ -111,9 +113,9 @@ add_task(function* test_store_expires() { // Ensure discovery of unprocessed events files works. add_task(function* test_unprocessed_events_files() { let m = yield getManager(); - yield m.createEventsFile("1", "test.1", "foo", 0); - yield m.createEventsFile("2", "test.1", "bar", 0); - yield m.createEventsFile("1", "test.1", "baz", 1); + yield m.createEventsFile("1", "test.1", new Date(), "foo", 0); + yield m.createEventsFile("2", "test.1", new Date(), "bar", 0); + yield m.createEventsFile("1", "test.1", new Date(), "baz", 1); let paths = yield m._getUnprocessedEventsFiles(); Assert.equal(paths.length, 3); @@ -133,7 +135,7 @@ add_task(function* test_aggregate_events_locking() { add_task(function* test_malformed_files_deleted() { let m = yield getManager(); - yield m.createEventsFile("1", "test.1", "malformed"); + yield m.createEventsFile("1", "crash.main.1", new Date(), "foo\nbar"); let count = yield m.aggregateEventsFiles(); Assert.equal(count, 1); @@ -148,8 +150,8 @@ add_task(function* test_malformed_files_deleted() { add_task(function* test_aggregate_ignore_unknown_events() { let m = yield getManager(); - yield m.createEventsFile("1", "test.1", "success"); - yield m.createEventsFile("2", "foobar.1", "dummy"); + yield m.createEventsFile("1", "crash.main.1", DUMMY_DATE, "id1"); + yield m.createEventsFile("2", "foobar.1", new Date(), "dummy"); let count = yield m.aggregateEventsFiles(); Assert.equal(count, 2); @@ -165,8 +167,8 @@ add_task(function* test_prune_old() { let m = yield getManager(); let oldDate = new Date(Date.now() - 86400000); let newDate = new Date(Date.now() - 10000); - yield m.createEventsFile("1", "test.1", "id1", 0, oldDate); - yield m.createEventsFile("2", "test.1", "id2", 0, newDate); + yield m.createEventsFile("1", "crash.main.1", oldDate, "id1"); + yield m.createEventsFile("2", "crash.plugin.1", newDate, "id2"); yield m.aggregateEventsFiles(); @@ -190,13 +192,69 @@ add_task(function* test_prune_old() { add_task(function* test_schedule_maintenance() { let m = yield getManager(); - yield m.createEventsFile("1", "test.1", "id1"); + yield m.createEventsFile("1", "crash.main.1", DUMMY_DATE, "id1"); let oldDate = new Date(Date.now() - m.PURGE_OLDER_THAN_DAYS * 2 * 24 * 60 * 60 * 1000); - yield m.createEventsFile("2", "test.1", "id2", 0, oldDate); + yield m.createEventsFile("2", "crash.main.1", oldDate, "id2"); yield m.scheduleMaintenance(25); let crashes = yield m.getCrashes(); Assert.equal(crashes.length, 1); Assert.equal(crashes[0].id, "id1"); }); + +add_task(function* test_main_crash_event_file() { + let m = yield getManager(); + yield m.createEventsFile("1", "crash.main.1", DUMMY_DATE, "id1"); + let count = yield m.aggregateEventsFiles(); + Assert.equal(count, 1); + + let crashes = yield m.getCrashes(); + Assert.equal(crashes.length, 1); + Assert.equal(crashes[0].id, "id1"); + Assert.equal(crashes[0].type, "main-crash"); + Assert.deepEqual(crashes[0].crashDate, DUMMY_DATE); + + count = yield m.aggregateEventsFiles(); + Assert.equal(count, 0); +}); + +add_task(function* test_multiline_crash_id_rejected() { + let m = yield getManager(); + yield m.createEventsFile("1", "crash.main.1", DUMMY_DATE, "id1\nid2"); + yield m.aggregateEventsFiles(); + let crashes = yield m.getCrashes(); + Assert.equal(crashes.length, 0); +}); + +add_task(function* test_plugin_crash_event_file() { + let m = yield getManager(); + yield m.createEventsFile("1", "crash.plugin.1", DUMMY_DATE, "id1"); + let count = yield m.aggregateEventsFiles(); + Assert.equal(count, 1); + + let crashes = yield m.getCrashes(); + Assert.equal(crashes.length, 1); + Assert.equal(crashes[0].id, "id1"); + Assert.equal(crashes[0].type, "plugin-crash"); + Assert.deepEqual(crashes[0].crashDate, DUMMY_DATE); + + count = yield m.aggregateEventsFiles(); + Assert.equal(count, 0); +}); + +add_task(function* test_plugin_hang_event_file() { + let m = yield getManager(); + yield m.createEventsFile("1", "hang.plugin.1", DUMMY_DATE, "id1"); + let count = yield m.aggregateEventsFiles(); + Assert.equal(count, 1); + + let crashes = yield m.getCrashes(); + Assert.equal(crashes.length, 1); + Assert.equal(crashes[0].id, "id1"); + Assert.equal(crashes[0].type, "plugin-hang"); + Assert.deepEqual(crashes[0].crashDate, DUMMY_DATE); + + count = yield m.aggregateEventsFiles(); + Assert.equal(count, 0); +}); diff --git a/toolkit/components/crashes/tests/xpcshell/test_crash_store.js b/toolkit/components/crashes/tests/xpcshell/test_crash_store.js index 4f0287877597..b62b0ad7aaac 100644 --- a/toolkit/components/crashes/tests/xpcshell/test_crash_store.js +++ b/toolkit/components/crashes/tests/xpcshell/test_crash_store.js @@ -25,10 +25,6 @@ function getStore() { yield OS.File.makeDir(storeDir, {unixMode: OS.Constants.libc.S_IRWXU}); let s = new CrashStore(storeDir); - s._addCrash = (id, date) => { - s._data.crashes.set(id, {id: id, crashDate: date}); - } - yield s.load(); return s; @@ -49,8 +45,7 @@ add_task(function test_add_crash() { Assert.equal(s.crashesCount, 0); let d = new Date(Date.now() - 5000); - // TODO use official APIs once they are implemented. - s._addCrash("id1", d); + s.addMainProcessCrash("id1", d); Assert.equal(s.crashesCount, 1); @@ -61,7 +56,7 @@ add_task(function test_add_crash() { Assert.equal(c.id, "id1", "ID set properly."); Assert.equal(c.crashDate.getTime(), d.getTime(), "Date set."); - s._addCrash("id2", new Date()); + s.addMainProcessCrash("id2", new Date()); Assert.equal(s.crashesCount, 2); }); @@ -72,8 +67,8 @@ add_task(function test_save_load() { let d1 = new Date(); let d2 = new Date(d1.getTime() - 10000); - s._addCrash("id1", d1); - s._addCrash("id2", d2); + s.addMainProcessCrash("id1", d1); + s.addMainProcessCrash("id2", d2); yield s.save(); @@ -102,3 +97,87 @@ add_task(function test_corrupt_json() { Assert.ok(s.corruptDate); Assert.equal(date.getTime(), s.corruptDate.getTime()); }); + +add_task(function* test_add_main_crash() { + let s = yield getStore(); + + s.addMainProcessCrash("id1", new Date()); + Assert.equal(s.crashesCount, 1); + + let c = s.crashes[0]; + Assert.ok(c.crashDate); + Assert.equal(c.type, bsp.CrashStore.prototype.TYPE_MAIN_CRASH); + Assert.ok(c.isMainProcessCrash); + + s.addMainProcessCrash("id2", new Date()); + Assert.equal(s.crashesCount, 2); + + // Duplicate. + s.addMainProcessCrash("id1", new Date()); + Assert.equal(s.crashesCount, 2); + + Assert.equal(s.mainProcessCrashes.length, 2); +}); + +add_task(function* test_add_plugin_crash() { + let s = yield getStore(); + + s.addPluginCrash("id1", new Date()); + Assert.equal(s.crashesCount, 1); + + let c = s.crashes[0]; + Assert.ok(c.crashDate); + Assert.equal(c.type, bsp.CrashStore.prototype.TYPE_PLUGIN_CRASH); + Assert.ok(c.isPluginCrash); + + s.addPluginCrash("id2", new Date()); + Assert.equal(s.crashesCount, 2); + + s.addPluginCrash("id1", new Date()); + Assert.equal(s.crashesCount, 2); + + Assert.equal(s.pluginCrashes.length, 2); +}); + +add_task(function* test_add_plugin_hang() { + let s = yield getStore(); + + s.addPluginHang("id1", new Date()); + Assert.equal(s.crashesCount, 1); + + let c = s.crashes[0]; + Assert.ok(c.crashDate); + Assert.equal(c.type, bsp.CrashStore.prototype.TYPE_PLUGIN_HANG); + Assert.ok(c.isPluginHang); + + s.addPluginHang("id2", new Date()); + Assert.equal(s.crashesCount, 2); + + s.addPluginHang("id1", new Date()); + Assert.equal(s.crashesCount, 2); + + Assert.equal(s.pluginHangs.length, 2); +}); + +add_task(function* test_add_mixed_types() { + let s = yield getStore(); + + s.addMainProcessCrash("main", new Date()); + s.addPluginCrash("pcrash", new Date()); + s.addPluginHang("phang", new Date()); + + Assert.equal(s.crashesCount, 3); + + yield s.save(); + + s._data.crashes.clear(); + Assert.equal(s.crashesCount, 0); + + yield s.load(); + + Assert.equal(s.crashesCount, 3); + + Assert.equal(s.mainProcessCrashes.length, 1); + Assert.equal(s.pluginCrashes.length, 1); + Assert.equal(s.pluginHangs.length, 1); +});