|
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(); |