toolkit/crashreporter/CrashSubmit.jsm

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     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/. */
     5 Components.utils.import("resource://gre/modules/Services.jsm");
     6 Components.utils.import("resource://gre/modules/KeyValueParser.jsm");
     8 this.EXPORTED_SYMBOLS = [
     9   "CrashSubmit"
    10 ];
    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;
    17 const SUCCESS = "success";
    18 const FAILED  = "failed";
    19 const SUBMITTING = "submitting";
    21 let reportURL = null;
    22 let strings = null;
    23 let myListener = null;
    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 }
    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   };
    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 }
    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 }
    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 }
    92 function getAllPendingMinidumpsIDs() {
    93   let minidumps = [];
    94   let pendingDir = getPendingDir();
    96   if (!(pendingDir.exists() && pendingDir.isDirectory()))
    97     return [];
    98   let entries = pendingDir.directoryEntries;
   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   }
   109   return minidumps;
   110 }
   112 function pruneSavedDumps() {
   113   const KEEP = 10;
   115   let pendingDir = getPendingDir();
   116   if (!(pendingDir.exists() && pendingDir.isDirectory()))
   117     return;
   118   let entries = pendingDir.directoryEntries;
   119   let entriesArray = [];
   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   }
   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   });
   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 }
   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 }
   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);
   179   var data = strings.crashid.replace("%s", crashID);
   180   if (viewURL)
   181      data += "\n" + strings.reporturl.replace("%s", viewURL);
   183   os.writeString(data);
   184   os.close();
   185   fstream.close();
   186 }
   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 }
   199 Submitter.prototype = {
   200   submitSuccess: function Submitter_submitSuccess(ret)
   201   {
   202     if (!ret.CrashID) {
   203       this.notifyStatus(FAILED);
   204       this.cleanup();
   205       return;
   206     }
   208     // Write out the details file to submitted/
   209     writeSubmittedReport(ret.CrashID, ret.ViewURL);
   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     }
   223     this.notifyStatus(SUCCESS, ret);
   224     this.cleanup();
   225   },
   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   },
   241   submitForm: function Submitter_submitForm()
   242   {
   243     if (!('ServerURL' in this.extraKeyVals)) {
   244       return false;
   245     }
   246     let serverURL = this.extraKeyVals.ServerURL;
   248     // Override the submission URL from the environment or prefs.
   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     }
   262     let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
   263               .createInstance(Ci.nsIXMLHttpRequest);
   264     xhr.open("POST", serverURL, true);
   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     }
   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);
   302     xhr.send(formData);
   303     return true;
   304   },
   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     }
   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);
   322     Services.obs.notifyObservers(propBag, "crash-report-status", status);
   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   },
   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     }
   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     }
   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     }
   368     this.notifyStatus(SUBMITTING);
   370     this.dump = dump;
   371     this.extra = extra;
   372     this.additionalDumps = additionalDumps;
   374     if (!this.submitForm()) {
   375        this.notifyStatus(FAILED);
   376        this.cleanup();
   377        return false;
   378     }
   379     return true;
   380   }
   381 };
   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;
   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;
   435     let submitter = new Submitter(id,
   436                                   submitSuccess,
   437                                   submitError,
   438                                   noThrottle,
   439                                   extraExtraKeyVals);
   440     CrashSubmit._activeSubmissions.push(submitter);
   441     return submitter.submit();
   442   },
   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   },
   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   },
   466   /**
   467    * Prune the saved dumps.
   468    */
   469   pruneSavedDumps: function CrashSubmit_pruneSavedDumps() {
   470     pruneSavedDumps();
   471   },
   473   // List of currently active submit objects
   474   _activeSubmissions: []
   475 };
   477 // Run this when first loaded
   478 getL10nStrings();

mercurial