toolkit/crashreporter/CrashSubmit.jsm

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:1d20a21e559b
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 Components.utils.import("resource://gre/modules/Services.jsm");
6 Components.utils.import("resource://gre/modules/KeyValueParser.jsm");
7
8 this.EXPORTED_SYMBOLS = [
9 "CrashSubmit"
10 ];
11
12 const Cc = Components.classes;
13 const Ci = Components.interfaces;
14 const STATE_START = Ci.nsIWebProgressListener.STATE_START;
15 const STATE_STOP = Ci.nsIWebProgressListener.STATE_STOP;
16
17 const SUCCESS = "success";
18 const FAILED = "failed";
19 const SUBMITTING = "submitting";
20
21 let reportURL = null;
22 let strings = null;
23 let myListener = null;
24
25 function parseINIStrings(file) {
26 var factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].
27 getService(Ci.nsIINIParserFactory);
28 var parser = factory.createINIParser(file);
29 var obj = {};
30 var en = parser.getKeys("Strings");
31 while (en.hasMore()) {
32 var key = en.getNext();
33 obj[key] = parser.getString("Strings", key);
34 }
35 return obj;
36 }
37
38 // Since we're basically re-implementing part of the crashreporter
39 // client here, we'll just steal the strings we need from crashreporter.ini
40 function getL10nStrings() {
41 let dirSvc = Cc["@mozilla.org/file/directory_service;1"].
42 getService(Ci.nsIProperties);
43 let path = dirSvc.get("GreD", Ci.nsIFile);
44 path.append("crashreporter.ini");
45 if (!path.exists()) {
46 // see if we're on a mac
47 path = path.parent;
48 path.append("crashreporter.app");
49 path.append("Contents");
50 path.append("MacOS");
51 path.append("crashreporter.ini");
52 if (!path.exists()) {
53 // very bad, but I don't know how to recover
54 return;
55 }
56 }
57 let crstrings = parseINIStrings(path);
58 strings = {
59 'crashid': crstrings.CrashID,
60 'reporturl': crstrings.CrashDetailsURL
61 };
62
63 path = dirSvc.get("XCurProcD", Ci.nsIFile);
64 path.append("crashreporter-override.ini");
65 if (path.exists()) {
66 crstrings = parseINIStrings(path);
67 if ('CrashID' in crstrings)
68 strings['crashid'] = crstrings.CrashID;
69 if ('CrashDetailsURL' in crstrings)
70 strings['reporturl'] = crstrings.CrashDetailsURL;
71 }
72 }
73
74 function getPendingDir() {
75 let directoryService = Cc["@mozilla.org/file/directory_service;1"].
76 getService(Ci.nsIProperties);
77 let pendingDir = directoryService.get("UAppData", Ci.nsIFile);
78 pendingDir.append("Crash Reports");
79 pendingDir.append("pending");
80 return pendingDir;
81 }
82
83 function getPendingMinidump(id) {
84 let pendingDir = getPendingDir();
85 let dump = pendingDir.clone();
86 let extra = pendingDir.clone();
87 dump.append(id + ".dmp");
88 extra.append(id + ".extra");
89 return [dump, extra];
90 }
91
92 function getAllPendingMinidumpsIDs() {
93 let minidumps = [];
94 let pendingDir = getPendingDir();
95
96 if (!(pendingDir.exists() && pendingDir.isDirectory()))
97 return [];
98 let entries = pendingDir.directoryEntries;
99
100 while (entries.hasMoreElements()) {
101 let entry = entries.getNext().QueryInterface(Ci.nsIFile);
102 if (entry.isFile()) {
103 let matches = entry.leafName.match(/(.+)\.extra$/);
104 if (matches)
105 minidumps.push(matches[1]);
106 }
107 }
108
109 return minidumps;
110 }
111
112 function pruneSavedDumps() {
113 const KEEP = 10;
114
115 let pendingDir = getPendingDir();
116 if (!(pendingDir.exists() && pendingDir.isDirectory()))
117 return;
118 let entries = pendingDir.directoryEntries;
119 let entriesArray = [];
120
121 while (entries.hasMoreElements()) {
122 let entry = entries.getNext().QueryInterface(Ci.nsIFile);
123 if (entry.isFile()) {
124 let matches = entry.leafName.match(/(.+)\.extra$/);
125 if (matches)
126 entriesArray.push(entry);
127 }
128 }
129
130 entriesArray.sort(function(a,b) {
131 let dateA = a.lastModifiedTime;
132 let dateB = b.lastModifiedTime;
133 if (dateA < dateB)
134 return -1;
135 if (dateB < dateA)
136 return 1;
137 return 0;
138 });
139
140 if (entriesArray.length > KEEP) {
141 for (let i = 0; i < entriesArray.length - KEEP; ++i) {
142 let extra = entriesArray[i];
143 let matches = extra.leafName.match(/(.+)\.extra$/);
144 if (matches) {
145 let dump = extra.clone();
146 dump.leafName = matches[1] + '.dmp';
147 dump.remove(false);
148 extra.remove(false);
149 }
150 }
151 }
152 }
153
154 function addFormEntry(doc, form, name, value) {
155 var input = doc.createElement("input");
156 input.type = "hidden";
157 input.name = name;
158 input.value = value;
159 form.appendChild(input);
160 }
161
162 function writeSubmittedReport(crashID, viewURL) {
163 let directoryService = Cc["@mozilla.org/file/directory_service;1"].
164 getService(Ci.nsIProperties);
165 let reportFile = directoryService.get("UAppData", Ci.nsIFile);
166 reportFile.append("Crash Reports");
167 reportFile.append("submitted");
168 if (!reportFile.exists())
169 reportFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0700);
170 reportFile.append(crashID + ".txt");
171 var fstream = Cc["@mozilla.org/network/file-output-stream;1"].
172 createInstance(Ci.nsIFileOutputStream);
173 // open, write, truncate
174 fstream.init(reportFile, -1, -1, 0);
175 var os = Cc["@mozilla.org/intl/converter-output-stream;1"].
176 createInstance(Ci.nsIConverterOutputStream);
177 os.init(fstream, "UTF-8", 0, 0x0000);
178
179 var data = strings.crashid.replace("%s", crashID);
180 if (viewURL)
181 data += "\n" + strings.reporturl.replace("%s", viewURL);
182
183 os.writeString(data);
184 os.close();
185 fstream.close();
186 }
187
188 // the Submitter class represents an individual submission.
189 function Submitter(id, submitSuccess, submitError, noThrottle,
190 extraExtraKeyVals) {
191 this.id = id;
192 this.successCallback = submitSuccess;
193 this.errorCallback = submitError;
194 this.noThrottle = noThrottle;
195 this.additionalDumps = [];
196 this.extraKeyVals = extraExtraKeyVals || {};
197 }
198
199 Submitter.prototype = {
200 submitSuccess: function Submitter_submitSuccess(ret)
201 {
202 if (!ret.CrashID) {
203 this.notifyStatus(FAILED);
204 this.cleanup();
205 return;
206 }
207
208 // Write out the details file to submitted/
209 writeSubmittedReport(ret.CrashID, ret.ViewURL);
210
211 // Delete from pending dir
212 try {
213 this.dump.remove(false);
214 this.extra.remove(false);
215 for (let i of this.additionalDumps) {
216 i.dump.remove(false);
217 }
218 }
219 catch (ex) {
220 // report an error? not much the user can do here.
221 }
222
223 this.notifyStatus(SUCCESS, ret);
224 this.cleanup();
225 },
226
227 cleanup: function Submitter_cleanup() {
228 // drop some references just to be nice
229 this.successCallback = null;
230 this.errorCallback = null;
231 this.iframe = null;
232 this.dump = null;
233 this.extra = null;
234 this.additionalDumps = null;
235 // remove this object from the list of active submissions
236 let idx = CrashSubmit._activeSubmissions.indexOf(this);
237 if (idx != -1)
238 CrashSubmit._activeSubmissions.splice(idx, 1);
239 },
240
241 submitForm: function Submitter_submitForm()
242 {
243 if (!('ServerURL' in this.extraKeyVals)) {
244 return false;
245 }
246 let serverURL = this.extraKeyVals.ServerURL;
247
248 // Override the submission URL from the environment or prefs.
249
250 var envOverride = Cc['@mozilla.org/process/environment;1'].
251 getService(Ci.nsIEnvironment).get("MOZ_CRASHREPORTER_URL");
252 if (envOverride != '') {
253 serverURL = envOverride;
254 }
255 else if ('PluginHang' in this.extraKeyVals) {
256 try {
257 serverURL = Services.prefs.
258 getCharPref("toolkit.crashreporter.pluginHangSubmitURL");
259 } catch(e) { }
260 }
261
262 let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
263 .createInstance(Ci.nsIXMLHttpRequest);
264 xhr.open("POST", serverURL, true);
265
266 let formData = Cc["@mozilla.org/files/formdata;1"]
267 .createInstance(Ci.nsIDOMFormData);
268 // add the data
269 for (let [name, value] in Iterator(this.extraKeyVals)) {
270 if (name != "ServerURL") {
271 formData.append(name, value);
272 }
273 }
274 if (this.noThrottle) {
275 // tell the server not to throttle this, since it was manually submitted
276 formData.append("Throttleable", "0");
277 }
278 // add the minidumps
279 formData.append("upload_file_minidump", File(this.dump.path));
280 if (this.additionalDumps.length > 0) {
281 let names = [];
282 for (let i of this.additionalDumps) {
283 names.push(i.name);
284 formData.append("upload_file_minidump_"+i.name,
285 File(i.dump.path));
286 }
287 }
288
289 let self = this;
290 xhr.addEventListener("readystatechange", function (aEvt) {
291 if (xhr.readyState == 4) {
292 if (xhr.status != 200) {
293 self.notifyStatus(FAILED);
294 self.cleanup();
295 } else {
296 let ret = parseKeyValuePairs(xhr.responseText);
297 self.submitSuccess(ret);
298 }
299 }
300 }, false);
301
302 xhr.send(formData);
303 return true;
304 },
305
306 notifyStatus: function Submitter_notify(status, ret)
307 {
308 let propBag = Cc["@mozilla.org/hash-property-bag;1"].
309 createInstance(Ci.nsIWritablePropertyBag2);
310 propBag.setPropertyAsAString("minidumpID", this.id);
311 if (status == SUCCESS) {
312 propBag.setPropertyAsAString("serverCrashID", ret.CrashID);
313 }
314
315 let extraKeyValsBag = Cc["@mozilla.org/hash-property-bag;1"].
316 createInstance(Ci.nsIWritablePropertyBag2);
317 for (let key in this.extraKeyVals) {
318 extraKeyValsBag.setPropertyAsAString(key, this.extraKeyVals[key]);
319 }
320 propBag.setPropertyAsInterface("extra", extraKeyValsBag);
321
322 Services.obs.notifyObservers(propBag, "crash-report-status", status);
323
324 switch (status) {
325 case SUCCESS:
326 if (this.successCallback)
327 this.successCallback(this.id, ret);
328 break;
329 case FAILED:
330 if (this.errorCallback)
331 this.errorCallback(this.id);
332 break;
333 default:
334 // no callbacks invoked.
335 }
336 },
337
338 submit: function Submitter_submit()
339 {
340 let [dump, extra] = getPendingMinidump(this.id);
341 if (!dump.exists() || !extra.exists()) {
342 this.notifyStatus(FAILED);
343 this.cleanup();
344 return false;
345 }
346
347 let extraKeyVals = parseKeyValuePairsFromFile(extra);
348 for (let key in extraKeyVals) {
349 if (!(key in this.extraKeyVals)) {
350 this.extraKeyVals[key] = extraKeyVals[key];
351 }
352 }
353
354 let additionalDumps = [];
355 if ("additional_minidumps" in this.extraKeyVals) {
356 let names = this.extraKeyVals.additional_minidumps.split(',');
357 for (let name of names) {
358 let [dump, extra] = getPendingMinidump(this.id + "-" + name);
359 if (!dump.exists()) {
360 this.notifyStatus(FAILED);
361 this.cleanup();
362 return false;
363 }
364 additionalDumps.push({'name': name, 'dump': dump});
365 }
366 }
367
368 this.notifyStatus(SUBMITTING);
369
370 this.dump = dump;
371 this.extra = extra;
372 this.additionalDumps = additionalDumps;
373
374 if (!this.submitForm()) {
375 this.notifyStatus(FAILED);
376 this.cleanup();
377 return false;
378 }
379 return true;
380 }
381 };
382
383 //===================================
384 // External API goes here
385 this.CrashSubmit = {
386 /**
387 * Submit the crash report named id.dmp from the "pending" directory.
388 *
389 * @param id
390 * Filename (minus .dmp extension) of the minidump to submit.
391 * @param params
392 * An object containing any of the following optional parameters:
393 * - submitSuccess
394 * A function that will be called if the report is submitted
395 * successfully with two parameters: the id that was passed
396 * to this function, and an object containing the key/value
397 * data returned from the server in its properties.
398 * - submitError
399 * A function that will be called with one parameter if the
400 * report fails to submit: the id that was passed to this
401 * function.
402 * - noThrottle
403 * If true, this crash report should be submitted with
404 * an extra parameter of "Throttleable=0" indicating that
405 * it should be processed right away. This should be set
406 * when the report is being submitted and the user expects
407 * to see the results immediately. Defaults to false.
408 * - extraExtraKeyVals
409 * An object whose key-value pairs will be merged with the data from
410 * the ".extra" file submitted with the report. The properties of
411 * this object will override properties of the same name in the
412 * .extra file.
413 *
414 * @return true if the submission began successfully, or false if
415 * it failed for some reason. (If the dump file does not
416 * exist, for example.)
417 */
418 submit: function CrashSubmit_submit(id, params)
419 {
420 params = params || {};
421 let submitSuccess = null;
422 let submitError = null;
423 let noThrottle = false;
424 let extraExtraKeyVals = null;
425
426 if ('submitSuccess' in params)
427 submitSuccess = params.submitSuccess;
428 if ('submitError' in params)
429 submitError = params.submitError;
430 if ('noThrottle' in params)
431 noThrottle = params.noThrottle;
432 if ('extraExtraKeyVals' in params)
433 extraExtraKeyVals = params.extraExtraKeyVals;
434
435 let submitter = new Submitter(id,
436 submitSuccess,
437 submitError,
438 noThrottle,
439 extraExtraKeyVals);
440 CrashSubmit._activeSubmissions.push(submitter);
441 return submitter.submit();
442 },
443
444 /**
445 * Delete the minidup from the "pending" directory.
446 *
447 * @param id
448 * Filename (minus .dmp extension) of the minidump to delete.
449 */
450 delete: function CrashSubmit_delete(id) {
451 let [dump, extra] = getPendingMinidump(id);
452 dump.QueryInterface(Ci.nsIFile).remove(false);
453 extra.QueryInterface(Ci.nsIFile).remove(false);
454 },
455
456 /**
457 * Get the list of pending crash IDs.
458 *
459 * @return an array of string, each being an ID as
460 * expected to be passed to submit()
461 */
462 pendingIDs: function CrashSubmit_pendingIDs() {
463 return getAllPendingMinidumpsIDs();
464 },
465
466 /**
467 * Prune the saved dumps.
468 */
469 pruneSavedDumps: function CrashSubmit_pruneSavedDumps() {
470 pruneSavedDumps();
471 },
472
473 // List of currently active submit objects
474 _activeSubmissions: []
475 };
476
477 // Run this when first loaded
478 getL10nStrings();

mercurial