toolkit/crashreporter/CrashSubmit.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/crashreporter/CrashSubmit.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,478 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +Components.utils.import("resource://gre/modules/Services.jsm");
     1.9 +Components.utils.import("resource://gre/modules/KeyValueParser.jsm");
    1.10 +
    1.11 +this.EXPORTED_SYMBOLS = [
    1.12 +  "CrashSubmit"
    1.13 +];
    1.14 +
    1.15 +const Cc = Components.classes;
    1.16 +const Ci = Components.interfaces;
    1.17 +const STATE_START = Ci.nsIWebProgressListener.STATE_START;
    1.18 +const STATE_STOP = Ci.nsIWebProgressListener.STATE_STOP;
    1.19 +
    1.20 +const SUCCESS = "success";
    1.21 +const FAILED  = "failed";
    1.22 +const SUBMITTING = "submitting";
    1.23 +
    1.24 +let reportURL = null;
    1.25 +let strings = null;
    1.26 +let myListener = null;
    1.27 +
    1.28 +function parseINIStrings(file) {
    1.29 +  var factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].
    1.30 +                getService(Ci.nsIINIParserFactory);
    1.31 +  var parser = factory.createINIParser(file);
    1.32 +  var obj = {};
    1.33 +  var en = parser.getKeys("Strings");
    1.34 +  while (en.hasMore()) {
    1.35 +    var key = en.getNext();
    1.36 +    obj[key] = parser.getString("Strings", key);
    1.37 +  }
    1.38 +  return obj;
    1.39 +}
    1.40 +
    1.41 +// Since we're basically re-implementing part of the crashreporter
    1.42 +// client here, we'll just steal the strings we need from crashreporter.ini
    1.43 +function getL10nStrings() {
    1.44 +  let dirSvc = Cc["@mozilla.org/file/directory_service;1"].
    1.45 +               getService(Ci.nsIProperties);
    1.46 +  let path = dirSvc.get("GreD", Ci.nsIFile);
    1.47 +  path.append("crashreporter.ini");
    1.48 +  if (!path.exists()) {
    1.49 +    // see if we're on a mac
    1.50 +    path = path.parent;
    1.51 +    path.append("crashreporter.app");
    1.52 +    path.append("Contents");
    1.53 +    path.append("MacOS");
    1.54 +    path.append("crashreporter.ini");
    1.55 +    if (!path.exists()) {
    1.56 +      // very bad, but I don't know how to recover
    1.57 +      return;
    1.58 +    }
    1.59 +  }
    1.60 +  let crstrings = parseINIStrings(path);
    1.61 +  strings = {
    1.62 +    'crashid': crstrings.CrashID,
    1.63 +    'reporturl': crstrings.CrashDetailsURL
    1.64 +  };
    1.65 +
    1.66 +  path = dirSvc.get("XCurProcD", Ci.nsIFile);
    1.67 +  path.append("crashreporter-override.ini");
    1.68 +  if (path.exists()) {
    1.69 +    crstrings = parseINIStrings(path);
    1.70 +    if ('CrashID' in crstrings)
    1.71 +      strings['crashid'] = crstrings.CrashID;
    1.72 +    if ('CrashDetailsURL' in crstrings)
    1.73 +      strings['reporturl'] = crstrings.CrashDetailsURL;
    1.74 +  }
    1.75 +}
    1.76 +
    1.77 +function getPendingDir() {
    1.78 +  let directoryService = Cc["@mozilla.org/file/directory_service;1"].
    1.79 +                         getService(Ci.nsIProperties);
    1.80 +  let pendingDir = directoryService.get("UAppData", Ci.nsIFile);
    1.81 +  pendingDir.append("Crash Reports");
    1.82 +  pendingDir.append("pending");
    1.83 +  return pendingDir;
    1.84 +}
    1.85 +
    1.86 +function getPendingMinidump(id) {
    1.87 +  let pendingDir = getPendingDir();
    1.88 +  let dump = pendingDir.clone();
    1.89 +  let extra = pendingDir.clone();
    1.90 +  dump.append(id + ".dmp");
    1.91 +  extra.append(id + ".extra");
    1.92 +  return [dump, extra];
    1.93 +}
    1.94 +
    1.95 +function getAllPendingMinidumpsIDs() {
    1.96 +  let minidumps = [];
    1.97 +  let pendingDir = getPendingDir();
    1.98 +
    1.99 +  if (!(pendingDir.exists() && pendingDir.isDirectory()))
   1.100 +    return [];
   1.101 +  let entries = pendingDir.directoryEntries;
   1.102 +
   1.103 +  while (entries.hasMoreElements()) {
   1.104 +    let entry = entries.getNext().QueryInterface(Ci.nsIFile);
   1.105 +    if (entry.isFile()) {
   1.106 +      let matches = entry.leafName.match(/(.+)\.extra$/);
   1.107 +      if (matches)
   1.108 +        minidumps.push(matches[1]);
   1.109 +    }
   1.110 +  }
   1.111 +
   1.112 +  return minidumps;
   1.113 +}
   1.114 +
   1.115 +function pruneSavedDumps() {
   1.116 +  const KEEP = 10;
   1.117 +
   1.118 +  let pendingDir = getPendingDir();
   1.119 +  if (!(pendingDir.exists() && pendingDir.isDirectory()))
   1.120 +    return;
   1.121 +  let entries = pendingDir.directoryEntries;
   1.122 +  let entriesArray = [];
   1.123 +
   1.124 +  while (entries.hasMoreElements()) {
   1.125 +    let entry = entries.getNext().QueryInterface(Ci.nsIFile);
   1.126 +    if (entry.isFile()) {
   1.127 +      let matches = entry.leafName.match(/(.+)\.extra$/);
   1.128 +      if (matches)
   1.129 +	entriesArray.push(entry);
   1.130 +    }
   1.131 +  }
   1.132 +
   1.133 +  entriesArray.sort(function(a,b) {
   1.134 +    let dateA = a.lastModifiedTime;
   1.135 +    let dateB = b.lastModifiedTime;
   1.136 +    if (dateA < dateB)
   1.137 +      return -1;
   1.138 +    if (dateB < dateA)
   1.139 +      return 1;
   1.140 +    return 0;
   1.141 +  });
   1.142 +
   1.143 +  if (entriesArray.length > KEEP) {
   1.144 +    for (let i = 0; i < entriesArray.length - KEEP; ++i) {
   1.145 +      let extra = entriesArray[i];
   1.146 +      let matches = extra.leafName.match(/(.+)\.extra$/);
   1.147 +      if (matches) {
   1.148 +        let dump = extra.clone();
   1.149 +        dump.leafName = matches[1] + '.dmp';
   1.150 +        dump.remove(false);
   1.151 +        extra.remove(false);
   1.152 +      }
   1.153 +    }
   1.154 +  }
   1.155 +}
   1.156 +
   1.157 +function addFormEntry(doc, form, name, value) {
   1.158 +  var input = doc.createElement("input");
   1.159 +  input.type = "hidden";
   1.160 +  input.name = name;
   1.161 +  input.value = value;
   1.162 +  form.appendChild(input);
   1.163 +}
   1.164 +
   1.165 +function writeSubmittedReport(crashID, viewURL) {
   1.166 +  let directoryService = Cc["@mozilla.org/file/directory_service;1"].
   1.167 +                           getService(Ci.nsIProperties);
   1.168 +  let reportFile = directoryService.get("UAppData", Ci.nsIFile);
   1.169 +  reportFile.append("Crash Reports");
   1.170 +  reportFile.append("submitted");
   1.171 +  if (!reportFile.exists())
   1.172 +    reportFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0700);
   1.173 +  reportFile.append(crashID + ".txt");
   1.174 +  var fstream = Cc["@mozilla.org/network/file-output-stream;1"].
   1.175 +                createInstance(Ci.nsIFileOutputStream);
   1.176 +  // open, write, truncate
   1.177 +  fstream.init(reportFile, -1, -1, 0);
   1.178 +  var os = Cc["@mozilla.org/intl/converter-output-stream;1"].
   1.179 +           createInstance(Ci.nsIConverterOutputStream);
   1.180 +  os.init(fstream, "UTF-8", 0, 0x0000);
   1.181 +
   1.182 +  var data = strings.crashid.replace("%s", crashID);
   1.183 +  if (viewURL)
   1.184 +     data += "\n" + strings.reporturl.replace("%s", viewURL);
   1.185 +
   1.186 +  os.writeString(data);
   1.187 +  os.close();
   1.188 +  fstream.close();
   1.189 +}
   1.190 +
   1.191 +// the Submitter class represents an individual submission.
   1.192 +function Submitter(id, submitSuccess, submitError, noThrottle,
   1.193 +                   extraExtraKeyVals) {
   1.194 +  this.id = id;
   1.195 +  this.successCallback = submitSuccess;
   1.196 +  this.errorCallback = submitError;
   1.197 +  this.noThrottle = noThrottle;
   1.198 +  this.additionalDumps = [];
   1.199 +  this.extraKeyVals = extraExtraKeyVals || {};
   1.200 +}
   1.201 +
   1.202 +Submitter.prototype = {
   1.203 +  submitSuccess: function Submitter_submitSuccess(ret)
   1.204 +  {
   1.205 +    if (!ret.CrashID) {
   1.206 +      this.notifyStatus(FAILED);
   1.207 +      this.cleanup();
   1.208 +      return;
   1.209 +    }
   1.210 +
   1.211 +    // Write out the details file to submitted/
   1.212 +    writeSubmittedReport(ret.CrashID, ret.ViewURL);
   1.213 +
   1.214 +    // Delete from pending dir
   1.215 +    try {
   1.216 +      this.dump.remove(false);
   1.217 +      this.extra.remove(false);
   1.218 +      for (let i of this.additionalDumps) {
   1.219 +        i.dump.remove(false);
   1.220 +      }
   1.221 +    }
   1.222 +    catch (ex) {
   1.223 +      // report an error? not much the user can do here.
   1.224 +    }
   1.225 +
   1.226 +    this.notifyStatus(SUCCESS, ret);
   1.227 +    this.cleanup();
   1.228 +  },
   1.229 +
   1.230 +  cleanup: function Submitter_cleanup() {
   1.231 +    // drop some references just to be nice
   1.232 +    this.successCallback = null;
   1.233 +    this.errorCallback = null;
   1.234 +    this.iframe = null;
   1.235 +    this.dump = null;
   1.236 +    this.extra = null;
   1.237 +    this.additionalDumps = null;
   1.238 +    // remove this object from the list of active submissions
   1.239 +    let idx = CrashSubmit._activeSubmissions.indexOf(this);
   1.240 +    if (idx != -1)
   1.241 +      CrashSubmit._activeSubmissions.splice(idx, 1);
   1.242 +  },
   1.243 +
   1.244 +  submitForm: function Submitter_submitForm()
   1.245 +  {
   1.246 +    if (!('ServerURL' in this.extraKeyVals)) {
   1.247 +      return false;
   1.248 +    }
   1.249 +    let serverURL = this.extraKeyVals.ServerURL;
   1.250 +
   1.251 +    // Override the submission URL from the environment or prefs.
   1.252 +
   1.253 +    var envOverride = Cc['@mozilla.org/process/environment;1'].
   1.254 +      getService(Ci.nsIEnvironment).get("MOZ_CRASHREPORTER_URL");
   1.255 +    if (envOverride != '') {
   1.256 +      serverURL = envOverride;
   1.257 +    }
   1.258 +    else if ('PluginHang' in this.extraKeyVals) {
   1.259 +      try {
   1.260 +        serverURL = Services.prefs.
   1.261 +          getCharPref("toolkit.crashreporter.pluginHangSubmitURL");
   1.262 +      } catch(e) { }
   1.263 +    }
   1.264 +
   1.265 +    let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
   1.266 +              .createInstance(Ci.nsIXMLHttpRequest);
   1.267 +    xhr.open("POST", serverURL, true);
   1.268 +
   1.269 +    let formData = Cc["@mozilla.org/files/formdata;1"]
   1.270 +                   .createInstance(Ci.nsIDOMFormData);
   1.271 +    // add the data
   1.272 +    for (let [name, value] in Iterator(this.extraKeyVals)) {
   1.273 +      if (name != "ServerURL") {
   1.274 +        formData.append(name, value);
   1.275 +      }
   1.276 +    }
   1.277 +    if (this.noThrottle) {
   1.278 +      // tell the server not to throttle this, since it was manually submitted
   1.279 +      formData.append("Throttleable", "0");
   1.280 +    }
   1.281 +    // add the minidumps
   1.282 +    formData.append("upload_file_minidump", File(this.dump.path));
   1.283 +    if (this.additionalDumps.length > 0) {
   1.284 +      let names = [];
   1.285 +      for (let i of this.additionalDumps) {
   1.286 +        names.push(i.name);
   1.287 +        formData.append("upload_file_minidump_"+i.name,
   1.288 +                        File(i.dump.path));
   1.289 +      }
   1.290 +    }
   1.291 +
   1.292 +    let self = this;
   1.293 +    xhr.addEventListener("readystatechange", function (aEvt) {
   1.294 +      if (xhr.readyState == 4) {
   1.295 +        if (xhr.status != 200) {
   1.296 +          self.notifyStatus(FAILED);
   1.297 +          self.cleanup();
   1.298 +        } else {
   1.299 +          let ret = parseKeyValuePairs(xhr.responseText);
   1.300 +          self.submitSuccess(ret);
   1.301 +        }
   1.302 +      }
   1.303 +    }, false);
   1.304 +
   1.305 +    xhr.send(formData);
   1.306 +    return true;
   1.307 +  },
   1.308 +
   1.309 +  notifyStatus: function Submitter_notify(status, ret)
   1.310 +  {
   1.311 +    let propBag = Cc["@mozilla.org/hash-property-bag;1"].
   1.312 +                  createInstance(Ci.nsIWritablePropertyBag2);
   1.313 +    propBag.setPropertyAsAString("minidumpID", this.id);
   1.314 +    if (status == SUCCESS) {
   1.315 +      propBag.setPropertyAsAString("serverCrashID", ret.CrashID);
   1.316 +    }
   1.317 +
   1.318 +    let extraKeyValsBag = Cc["@mozilla.org/hash-property-bag;1"].
   1.319 +                          createInstance(Ci.nsIWritablePropertyBag2);
   1.320 +    for (let key in this.extraKeyVals) {
   1.321 +      extraKeyValsBag.setPropertyAsAString(key, this.extraKeyVals[key]);
   1.322 +    }
   1.323 +    propBag.setPropertyAsInterface("extra", extraKeyValsBag);
   1.324 +
   1.325 +    Services.obs.notifyObservers(propBag, "crash-report-status", status);
   1.326 +
   1.327 +    switch (status) {
   1.328 +      case SUCCESS:
   1.329 +        if (this.successCallback)
   1.330 +          this.successCallback(this.id, ret);
   1.331 +        break;
   1.332 +      case FAILED:
   1.333 +        if (this.errorCallback)
   1.334 +          this.errorCallback(this.id);
   1.335 +        break;
   1.336 +      default:
   1.337 +        // no callbacks invoked.
   1.338 +    }
   1.339 +  },
   1.340 +
   1.341 +  submit: function Submitter_submit()
   1.342 +  {
   1.343 +    let [dump, extra] = getPendingMinidump(this.id);
   1.344 +    if (!dump.exists() || !extra.exists()) {
   1.345 +      this.notifyStatus(FAILED);
   1.346 +      this.cleanup();
   1.347 +      return false;
   1.348 +    }
   1.349 +
   1.350 +    let extraKeyVals = parseKeyValuePairsFromFile(extra);
   1.351 +    for (let key in extraKeyVals) {
   1.352 +      if (!(key in this.extraKeyVals)) {
   1.353 +        this.extraKeyVals[key] = extraKeyVals[key];
   1.354 +      }
   1.355 +    }
   1.356 +
   1.357 +    let additionalDumps = [];
   1.358 +    if ("additional_minidumps" in this.extraKeyVals) {
   1.359 +      let names = this.extraKeyVals.additional_minidumps.split(',');
   1.360 +      for (let name of names) {
   1.361 +        let [dump, extra] = getPendingMinidump(this.id + "-" + name);
   1.362 +        if (!dump.exists()) {
   1.363 +          this.notifyStatus(FAILED);
   1.364 +          this.cleanup();
   1.365 +          return false;
   1.366 +        }
   1.367 +        additionalDumps.push({'name': name, 'dump': dump});
   1.368 +      }
   1.369 +    }
   1.370 +
   1.371 +    this.notifyStatus(SUBMITTING);
   1.372 +
   1.373 +    this.dump = dump;
   1.374 +    this.extra = extra;
   1.375 +    this.additionalDumps = additionalDumps;
   1.376 +
   1.377 +    if (!this.submitForm()) {
   1.378 +       this.notifyStatus(FAILED);
   1.379 +       this.cleanup();
   1.380 +       return false;
   1.381 +    }
   1.382 +    return true;
   1.383 +  }
   1.384 +};
   1.385 +
   1.386 +//===================================
   1.387 +// External API goes here
   1.388 +this.CrashSubmit = {
   1.389 +  /**
   1.390 +   * Submit the crash report named id.dmp from the "pending" directory.
   1.391 +   *
   1.392 +   * @param id
   1.393 +   *        Filename (minus .dmp extension) of the minidump to submit.
   1.394 +   * @param params
   1.395 +   *        An object containing any of the following optional parameters:
   1.396 +   *        - submitSuccess
   1.397 +   *          A function that will be called if the report is submitted
   1.398 +   *          successfully with two parameters: the id that was passed
   1.399 +   *          to this function, and an object containing the key/value
   1.400 +   *          data returned from the server in its properties.
   1.401 +   *        - submitError
   1.402 +   *          A function that will be called with one parameter if the
   1.403 +   *          report fails to submit: the id that was passed to this
   1.404 +   *          function.
   1.405 +   *        - noThrottle
   1.406 +   *          If true, this crash report should be submitted with
   1.407 +   *          an extra parameter of "Throttleable=0" indicating that
   1.408 +   *          it should be processed right away. This should be set
   1.409 +   *          when the report is being submitted and the user expects
   1.410 +   *          to see the results immediately. Defaults to false.
   1.411 +   *        - extraExtraKeyVals
   1.412 +   *          An object whose key-value pairs will be merged with the data from
   1.413 +   *          the ".extra" file submitted with the report.  The properties of
   1.414 +   *          this object will override properties of the same name in the
   1.415 +   *          .extra file.
   1.416 +   *
   1.417 +   * @return true if the submission began successfully, or false if
   1.418 +   *         it failed for some reason. (If the dump file does not
   1.419 +   *         exist, for example.)
   1.420 +   */
   1.421 +  submit: function CrashSubmit_submit(id, params)
   1.422 +  {
   1.423 +    params = params || {};
   1.424 +    let submitSuccess = null;
   1.425 +    let submitError = null;
   1.426 +    let noThrottle = false;
   1.427 +    let extraExtraKeyVals = null;
   1.428 +
   1.429 +    if ('submitSuccess' in params)
   1.430 +      submitSuccess = params.submitSuccess;
   1.431 +    if ('submitError' in params)
   1.432 +      submitError = params.submitError;
   1.433 +    if ('noThrottle' in params)
   1.434 +      noThrottle = params.noThrottle;
   1.435 +    if ('extraExtraKeyVals' in params)
   1.436 +      extraExtraKeyVals = params.extraExtraKeyVals;
   1.437 +
   1.438 +    let submitter = new Submitter(id,
   1.439 +                                  submitSuccess,
   1.440 +                                  submitError,
   1.441 +                                  noThrottle,
   1.442 +                                  extraExtraKeyVals);
   1.443 +    CrashSubmit._activeSubmissions.push(submitter);
   1.444 +    return submitter.submit();
   1.445 +  },
   1.446 +
   1.447 +  /**
   1.448 +   * Delete the minidup from the "pending" directory.
   1.449 +   *
   1.450 +   * @param id
   1.451 +   *        Filename (minus .dmp extension) of the minidump to delete.
   1.452 +   */
   1.453 +  delete: function CrashSubmit_delete(id) {
   1.454 +    let [dump, extra] = getPendingMinidump(id);
   1.455 +    dump.QueryInterface(Ci.nsIFile).remove(false);
   1.456 +    extra.QueryInterface(Ci.nsIFile).remove(false);
   1.457 +  },
   1.458 +
   1.459 +  /**
   1.460 +   * Get the list of pending crash IDs.
   1.461 +   *
   1.462 +   * @return an array of string, each being an ID as
   1.463 +   *         expected to be passed to submit()
   1.464 +   */
   1.465 +  pendingIDs: function CrashSubmit_pendingIDs() {
   1.466 +    return getAllPendingMinidumpsIDs();
   1.467 +  },
   1.468 +
   1.469 +  /**
   1.470 +   * Prune the saved dumps.
   1.471 +   */
   1.472 +  pruneSavedDumps: function CrashSubmit_pruneSavedDumps() {
   1.473 +    pruneSavedDumps();
   1.474 +  },
   1.475 +
   1.476 +  // List of currently active submit objects
   1.477 +  _activeSubmissions: []
   1.478 +};
   1.479 +
   1.480 +// Run this when first loaded
   1.481 +getL10nStrings();

mercurial