зеркало из https://github.com/mozilla/gecko-dev.git
Merge mozilla-central to mozilla-inbound. CLOSED TREE
This commit is contained in:
Коммит
bb4780fb18
|
@ -1735,19 +1735,6 @@ pref("extensions.screenshots.disabled", false);
|
|||
// disable uploading to the server.
|
||||
pref("extensions.screenshots.upload-disabled", false);
|
||||
|
||||
// Preferences for BrowserErrorReporter.jsm
|
||||
// Only collect errors on Nightly, and specifically not local builds
|
||||
#if defined(NIGHTLY_BUILD) && MOZ_UPDATE_CHANNEL != default
|
||||
pref("browser.chrome.errorReporter.enabled", true);
|
||||
#else
|
||||
pref("browser.chrome.errorReporter.enabled", false);
|
||||
#endif
|
||||
pref("browser.chrome.errorReporter.sampleRate", "0.001");
|
||||
pref("browser.chrome.errorReporter.publicKey", "c709cb7a2c0b4f0882fcc84a5af161ec");
|
||||
pref("browser.chrome.errorReporter.projectId", "339");
|
||||
pref("browser.chrome.errorReporter.submitUrl", "https://sentry.prod.mozaws.net/api/339/store/");
|
||||
pref("browser.chrome.errorReporter.logLevel", "Error");
|
||||
|
||||
// URL for Learn More link for browser error logging in preferences
|
||||
pref("browser.chrome.errorReporter.infoURL",
|
||||
"https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/nightly-error-collection");
|
||||
|
|
|
@ -384,7 +384,6 @@ XPCOMUtils.defineLazyModuleGetters(this, {
|
|||
Blocklist: "resource://gre/modules/Blocklist.jsm",
|
||||
BookmarkHTMLUtils: "resource://gre/modules/BookmarkHTMLUtils.jsm",
|
||||
BookmarkJSONUtils: "resource://gre/modules/BookmarkJSONUtils.jsm",
|
||||
BrowserErrorReporter: "resource:///modules/BrowserErrorReporter.jsm",
|
||||
BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.jsm",
|
||||
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
|
||||
ContentClick: "resource:///modules/ContentClick.jsm",
|
||||
|
@ -658,16 +657,6 @@ BrowserGlue.prototype = {
|
|||
return this.pingCentre;
|
||||
},
|
||||
|
||||
/**
|
||||
* Lazily initialize BrowserErrorReporter
|
||||
*/
|
||||
get browserErrorReporter() {
|
||||
Object.defineProperty(this, "browserErrorReporter", {
|
||||
value: new BrowserErrorReporter(),
|
||||
});
|
||||
return this.browserErrorReporter;
|
||||
},
|
||||
|
||||
_sendMainPingCentrePing() {
|
||||
let newTabSetting;
|
||||
let homePageSetting;
|
||||
|
@ -1455,12 +1444,6 @@ BrowserGlue.prototype = {
|
|||
AutoCompletePopup.uninit();
|
||||
DateTimePickerParent.uninit();
|
||||
|
||||
// Browser errors are only collected on Nightly, but telemetry for
|
||||
// them is collected on all channels.
|
||||
if (AppConstants.MOZ_DATA_REPORTING) {
|
||||
this.browserErrorReporter.uninit();
|
||||
}
|
||||
|
||||
Normandy.uninit();
|
||||
},
|
||||
|
||||
|
@ -1503,12 +1486,6 @@ BrowserGlue.prototype = {
|
|||
}
|
||||
this._windowsWereRestored = true;
|
||||
|
||||
// Browser errors are only collected on Nightly, but telemetry for
|
||||
// them is collected on all channels.
|
||||
if (AppConstants.MOZ_DATA_REPORTING) {
|
||||
this.browserErrorReporter.init();
|
||||
}
|
||||
|
||||
BrowserUsageTelemetry.init();
|
||||
SearchTelemetry.init();
|
||||
|
||||
|
|
|
@ -130,9 +130,6 @@ if (AppConstants.MOZ_DATA_REPORTING) {
|
|||
}
|
||||
|
||||
// Data Choices tab
|
||||
if (AppConstants.NIGHTLY_BUILD) {
|
||||
Preferences.add({ id: "browser.chrome.errorReporter.enabled", type: "bool" });
|
||||
}
|
||||
if (AppConstants.MOZ_CRASHREPORTER) {
|
||||
Preferences.add({ id: "browser.crashReports.unsubmittedCheck.autoSubmit2", type: "bool" });
|
||||
}
|
||||
|
@ -406,9 +403,6 @@ var gPrivacyPane = {
|
|||
|
||||
if (AppConstants.MOZ_DATA_REPORTING) {
|
||||
this.initDataCollection();
|
||||
if (AppConstants.NIGHTLY_BUILD) {
|
||||
this.initCollectBrowserErrors();
|
||||
}
|
||||
if (AppConstants.MOZ_CRASHREPORTER) {
|
||||
this.initSubmitCrashes();
|
||||
}
|
||||
|
@ -1485,11 +1479,6 @@ var gPrivacyPane = {
|
|||
"dataCollectionPrivacyNotice");
|
||||
},
|
||||
|
||||
initCollectBrowserErrors() {
|
||||
this._setupLearnMoreLink("browser.chrome.errorReporter.infoURL",
|
||||
"collectBrowserErrorsLearnMore");
|
||||
},
|
||||
|
||||
initSubmitCrashes() {
|
||||
this._setupLearnMoreLink("toolkit.crashreporter.infoURL",
|
||||
"crashReporterLearnMore");
|
||||
|
|
|
@ -687,18 +687,6 @@
|
|||
data-l10n-id="collection-health-report-disabled"/>
|
||||
#endif
|
||||
|
||||
#ifdef NIGHTLY_BUILD
|
||||
<hbox align="center">
|
||||
<checkbox id="collectBrowserErrorsBox"
|
||||
class="tail-with-learn-more"
|
||||
preference="browser.chrome.errorReporter.enabled"
|
||||
data-l10n-id="collection-browser-errors"
|
||||
flex="1"/>
|
||||
<label id="collectBrowserErrorsLearnMore"
|
||||
class="learnMore text-link" data-l10n-id="collection-browser-errors-link"/>
|
||||
</hbox>
|
||||
#endif
|
||||
|
||||
#ifdef MOZ_CRASHREPORTER
|
||||
<hbox align="center">
|
||||
<checkbox id="automaticallySubmitCrashesBox"
|
||||
|
|
|
@ -70,11 +70,6 @@ skip-if = !e10s
|
|||
skip-if = e10s
|
||||
[browser_permissions_urlFieldHidden.js]
|
||||
[browser_proxy_backup.js]
|
||||
[browser_privacypane.js]
|
||||
run-if = nightly_build
|
||||
# browser_privacypane.js only has Browser Error collection tests currently,
|
||||
# which is disabled outside Nightly. Remove this once non-Nightly tests are
|
||||
# added.
|
||||
[browser_privacypane_2.js]
|
||||
[browser_privacypane_3.js]
|
||||
[browser_sanitizeOnShutdown_prefLocked.js]
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test the initial value of Browser Error collection checkbox
|
||||
add_task(async function testBrowserErrorInitialValue() {
|
||||
// Skip if non-Nightly since the checkbox will be missing.
|
||||
if (!AppConstants.NIGHTLY_BUILD) {
|
||||
return;
|
||||
}
|
||||
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["browser.chrome.errorReporter.enabled", true]],
|
||||
});
|
||||
await openPreferencesViaOpenPreferencesAPI("privacy-reports", {leaveOpen: true});
|
||||
|
||||
let doc = gBrowser.contentDocument;
|
||||
ok(
|
||||
doc.querySelector("#collectBrowserErrorsBox").checked,
|
||||
"Checkbox for collecting browser errors should be checked when the pref is true"
|
||||
);
|
||||
|
||||
BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
||||
await SpecialPowers.popPrefEnv();
|
||||
});
|
||||
|
||||
// Test that the Learn More link is set to the correct, formatted URL from a
|
||||
// pref value
|
||||
add_task(async function testBrowserErrorLearnMore() {
|
||||
// Skip if non-Nightly since the checkbox will be missing.
|
||||
if (!AppConstants.NIGHTLY_BUILD) {
|
||||
return;
|
||||
}
|
||||
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["browser.chrome.errorReporter.infoURL", "https://example.com/%NAME%/"]],
|
||||
});
|
||||
await openPreferencesViaOpenPreferencesAPI("privacy-reports", {leaveOpen: true});
|
||||
|
||||
let doc = gBrowser.contentDocument;
|
||||
is(
|
||||
doc.querySelector("#collectBrowserErrorsLearnMore").href,
|
||||
`https://example.com/${Services.appinfo.name}/`,
|
||||
"Learn More link for browser error collection should have an href set by a pref"
|
||||
);
|
||||
|
||||
BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
||||
await SpecialPowers.popPrefEnv();
|
||||
});
|
|
@ -1,48 +0,0 @@
|
|||
.. _browsererrorreporter:
|
||||
|
||||
=======================
|
||||
Browser Error Reporter
|
||||
=======================
|
||||
|
||||
The `BrowserErrorReporter.jsm <https://dxr.mozilla.org/mozilla-central/source/browser/modules/BrowserErrorReporter.jsm>`_ module collects errors logged to the Browser Console and sends them to a remote error aggregation service.
|
||||
|
||||
.. note::
|
||||
This module and the related service is a prototype and will be removed from Firefox in later 2018.
|
||||
|
||||
Opt-out
|
||||
=======
|
||||
Collection is enabled by default in the Nightly channel, except for local builds, where it is disabled. It is not available outside of the Nightly channel.
|
||||
|
||||
To opt-out of collection:
|
||||
|
||||
1. Open ``about:preferences``.
|
||||
2. Select the Privacy and Security panel and go to the Nightly Data Collection and Use section.
|
||||
3. Uncheck "Allow Nightly to send browser error reports (including error messages) to Mozilla".
|
||||
|
||||
Collected Error Data
|
||||
====================
|
||||
Errors are first sampled at the rate specified by the ``browser.chrome.errorReporter.sampleRate`` preference.
|
||||
|
||||
The payload sent to the remote collection service contains the following info:
|
||||
|
||||
- Firefox version number
|
||||
- Firefox update channel (usually "Nightly")
|
||||
- Firefox build ID
|
||||
- Revision used to build Firefox
|
||||
- Timestamp when the error occurred
|
||||
- A project ID specified by the ``browser.chrome.errorReporter.projectId`` preference
|
||||
- The error message
|
||||
- The filename that the error was thrown from
|
||||
- A stacktrace, if available, of the code being executed when the error was thrown
|
||||
|
||||
Privacy-sensitive info
|
||||
======================
|
||||
Error reports may contain sensitive information about the user:
|
||||
|
||||
- Error messages may inadvertently contain personal info depending on the code that generated the error.
|
||||
- Filenames in the stack trace may contain add-on IDs of currently-installed add-ons. They may also contain local filesystem paths.
|
||||
|
||||
.. seealso::
|
||||
|
||||
`Browser Error Collection wiki page <https://wiki.mozilla.org/Firefox/BrowserErrorCollection>`_
|
||||
Wiki page with up-to-date information on error collection and how we restrict access to the collected data.
|
|
@ -9,4 +9,3 @@ This is the nascent documentation of the Firefox front-end code.
|
|||
|
||||
AddressBar
|
||||
BrowserUsageTelemetry
|
||||
BrowserErrorReporter
|
||||
|
|
|
@ -950,11 +950,6 @@ addon-recommendations-link = Learn more
|
|||
# or builds with no Telemetry support available.
|
||||
collection-health-report-disabled = Data reporting is disabled for this build configuration
|
||||
|
||||
collection-browser-errors =
|
||||
.label = Allow { -brand-short-name } to send browser error reports (including error messages) to { -vendor-short-name }
|
||||
.accesskey = b
|
||||
collection-browser-errors-link = Learn more
|
||||
|
||||
collection-backlogged-crash-reports =
|
||||
.label = Allow { -brand-short-name } to send backlogged crash reports on your behalf
|
||||
.accesskey = c
|
||||
|
|
|
@ -1,474 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
|
||||
ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
ChromeUtils.import("resource://gre/modules/Timer.jsm");
|
||||
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
ChromeUtils.defineModuleGetter(this, "Log", "resource://gre/modules/Log.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "UpdateUtils", "resource://gre/modules/UpdateUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyGlobalGetters(this, ["fetch", "URL"]);
|
||||
|
||||
var EXPORTED_SYMBOLS = ["BrowserErrorReporter"];
|
||||
|
||||
const CONTEXT_LINES = 5;
|
||||
const ERROR_PREFIX_RE = /^[^\W]+:/m;
|
||||
const PREF_ENABLED = "browser.chrome.errorReporter.enabled";
|
||||
const PREF_LOG_LEVEL = "browser.chrome.errorReporter.logLevel";
|
||||
const PREF_PROJECT_ID = "browser.chrome.errorReporter.projectId";
|
||||
const PREF_PUBLIC_KEY = "browser.chrome.errorReporter.publicKey";
|
||||
const PREF_SAMPLE_RATE = "browser.chrome.errorReporter.sampleRate";
|
||||
const PREF_SUBMIT_URL = "browser.chrome.errorReporter.submitUrl";
|
||||
const RECENT_BUILD_AGE = 1000 * 60 * 60 * 24 * 7; // 7 days
|
||||
const SDK_NAME = "firefox-error-reporter";
|
||||
const SDK_VERSION = "1.0.0";
|
||||
const TELEMETRY_ERROR_COLLECTED = "browser.errors.collected_count";
|
||||
const TELEMETRY_ERROR_COLLECTED_FILENAME = "browser.errors.collected_count_by_filename";
|
||||
const TELEMETRY_ERROR_COLLECTED_STACK = "browser.errors.collected_with_stack_count";
|
||||
const TELEMETRY_ERROR_REPORTED = "browser.errors.reported_success_count";
|
||||
const TELEMETRY_ERROR_REPORTED_FAIL = "browser.errors.reported_failure_count";
|
||||
const TELEMETRY_ERROR_SAMPLE_RATE = "browser.errors.sample_rate";
|
||||
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIScriptError#Categories
|
||||
const REPORTED_CATEGORIES = new Set([
|
||||
"XPConnect JavaScript",
|
||||
"component javascript",
|
||||
"chrome javascript",
|
||||
"chrome registration",
|
||||
"XBL",
|
||||
"XBL Prototype Handler",
|
||||
"XBL Content Sink",
|
||||
"xbl javascript",
|
||||
"FrameConstructor",
|
||||
]);
|
||||
|
||||
const PLATFORM_NAMES = {
|
||||
linux: "Linux",
|
||||
win: "Windows",
|
||||
macosx: "macOS",
|
||||
android: "Android",
|
||||
};
|
||||
|
||||
// Filename URI regexes that we are okay with reporting to Telemetry. URIs not
|
||||
// matching these patterns may contain local file paths.
|
||||
const TELEMETRY_REPORTED_PATTERNS = new Set([
|
||||
/^resource:\/\/(?:\/|gre|devtools)/,
|
||||
/^chrome:\/\/(?:global|browser|devtools)/,
|
||||
]);
|
||||
|
||||
// Mapping of regexes to sample rates; if the regex matches the module an error
|
||||
// is thrown from, the matching sample rate is used instead of the default.
|
||||
// In case of a conflict, the first matching rate by insertion order is used.
|
||||
const MODULE_SAMPLE_RATES = new Map([
|
||||
[/^(?:chrome|resource):\/\/devtools/, 1],
|
||||
[/^moz-extension:\/\//, 0],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Collects nsIScriptError messages logged to the browser console and reports
|
||||
* them to a remotely-hosted error collection service.
|
||||
*
|
||||
* This is a PROTOTYPE; it will be removed in the future and potentially
|
||||
* replaced with a more robust implementation. It is meant to only collect
|
||||
* errors from Nightly (and local builds if enabled for development purposes)
|
||||
* and has not been reviewed for use outside of Nightly.
|
||||
*
|
||||
* The outgoing requests are designed to be compatible with Sentry. See
|
||||
* https://docs.sentry.io/clientdev/ for details on the data format that Sentry
|
||||
* expects.
|
||||
*
|
||||
* Errors may contain PII, such as in messages or local file paths in stack
|
||||
* traces; see bug 1426482 for privacy review and server-side mitigation.
|
||||
*/
|
||||
class BrowserErrorReporter {
|
||||
/**
|
||||
* Generate a Date object corresponding to the date in the appBuildId.
|
||||
*/
|
||||
static getAppBuildIdDate() {
|
||||
const appBuildId = Services.appinfo.appBuildID;
|
||||
const buildYear = Number.parseInt(appBuildId.slice(0, 4));
|
||||
// Date constructor uses 0-indexed months
|
||||
const buildMonth = Number.parseInt(appBuildId.slice(4, 6)) - 1;
|
||||
const buildDay = Number.parseInt(appBuildId.slice(6, 8));
|
||||
return new Date(buildYear, buildMonth, buildDay);
|
||||
}
|
||||
|
||||
constructor(options = {}) {
|
||||
// Test arguments for mocks and changing behavior
|
||||
const defaultOptions = {
|
||||
fetch: defaultFetch,
|
||||
now: null,
|
||||
chromeOnly: true,
|
||||
sampleRates: MODULE_SAMPLE_RATES,
|
||||
registerListener: () => Services.console.registerListener(this),
|
||||
unregisterListener: () => Services.console.unregisterListener(this),
|
||||
};
|
||||
for (const [key, defaultValue] of Object.entries(defaultOptions)) {
|
||||
this[key] = key in options ? options[key] : defaultValue;
|
||||
}
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "appBuildIdDate", BrowserErrorReporter.getAppBuildIdDate);
|
||||
|
||||
// Values that don't change between error reports.
|
||||
this.requestBodyTemplate = {
|
||||
logger: "javascript",
|
||||
platform: "javascript",
|
||||
release: Services.appinfo.appBuildID,
|
||||
environment: UpdateUtils.getUpdateChannel(false),
|
||||
contexts: {
|
||||
os: {
|
||||
name: PLATFORM_NAMES[AppConstants.platform],
|
||||
version: (
|
||||
Cc["@mozilla.org/network/protocol;1?name=http"]
|
||||
.getService(Ci.nsIHttpProtocolHandler)
|
||||
.oscpu
|
||||
),
|
||||
},
|
||||
browser: {
|
||||
name: "Firefox",
|
||||
version: Services.appinfo.version,
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
changeset: AppConstants.SOURCE_REVISION_URL,
|
||||
},
|
||||
sdk: {
|
||||
name: SDK_NAME,
|
||||
version: SDK_VERSION,
|
||||
},
|
||||
};
|
||||
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
this,
|
||||
"collectionEnabled",
|
||||
PREF_ENABLED,
|
||||
false,
|
||||
this.handleEnabledPrefChanged.bind(this),
|
||||
);
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
this,
|
||||
"sampleRatePref",
|
||||
PREF_SAMPLE_RATE,
|
||||
"0.0",
|
||||
this.handleSampleRatePrefChanged.bind(this),
|
||||
);
|
||||
|
||||
// Prefix mappings for the mangleFilePaths transform.
|
||||
this.manglePrefixes = options.manglePrefixes || {
|
||||
greDir: Services.dirsvc.get("GreD", Ci.nsIFile),
|
||||
profileDir: Services.dirsvc.get("ProfD", Ci.nsIFile),
|
||||
};
|
||||
// File paths are encoded by nsIURI, so let's do the same for the prefixes
|
||||
// we're comparing them to.
|
||||
for (const [name, prefixFile] of Object.entries(this.manglePrefixes)) {
|
||||
let filePath = Services.io.newFileURI(prefixFile).filePath;
|
||||
|
||||
// filePath might not have a trailing slash in some cases
|
||||
if (!filePath.endsWith("/")) {
|
||||
filePath += "/";
|
||||
}
|
||||
|
||||
this.manglePrefixes[name] = filePath;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily-created logger
|
||||
*/
|
||||
get logger() {
|
||||
const logger = Log.repository.getLogger("BrowserErrorReporter");
|
||||
logger.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
|
||||
logger.manageLevelFromPref(PREF_LOG_LEVEL);
|
||||
|
||||
Object.defineProperty(this, "logger", {value: logger});
|
||||
return this.logger;
|
||||
}
|
||||
|
||||
init() {
|
||||
if (this.collectionEnabled) {
|
||||
this.registerListener();
|
||||
|
||||
// Processing already-logged messages in case any errors occurred before
|
||||
// startup.
|
||||
for (const message of Services.console.getMessageArray()) {
|
||||
this.observe(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uninit() {
|
||||
try {
|
||||
this.unregisterListener();
|
||||
} catch (err) {} // It probably wasn't registered.
|
||||
}
|
||||
|
||||
handleEnabledPrefChanged(prefName, previousValue, newValue) {
|
||||
if (newValue) {
|
||||
this.registerListener();
|
||||
} else {
|
||||
try {
|
||||
this.unregisterListener();
|
||||
} catch (err) {} // It probably wasn't registered.
|
||||
}
|
||||
}
|
||||
|
||||
handleSampleRatePrefChanged(prefName, previousValue, newValue) {
|
||||
Services.telemetry.scalarSet(TELEMETRY_ERROR_SAMPLE_RATE, newValue);
|
||||
}
|
||||
|
||||
errorCollectedFilenameKey(filename) {
|
||||
for (const pattern of TELEMETRY_REPORTED_PATTERNS) {
|
||||
if (filename.match(pattern)) {
|
||||
return filename;
|
||||
}
|
||||
}
|
||||
|
||||
// WebExtensions get grouped separately from other errors
|
||||
if (filename.startsWith("moz-extension://")) {
|
||||
return "MOZEXTENSION";
|
||||
}
|
||||
|
||||
return "FILTERED";
|
||||
}
|
||||
|
||||
isRecentBuild() {
|
||||
// The local clock is not reliable, but this method doesn't need to be
|
||||
// perfect.
|
||||
const now = this.now || new Date();
|
||||
return (now - this.appBuildIdDate) <= RECENT_BUILD_AGE;
|
||||
}
|
||||
|
||||
observe(message) {
|
||||
if (message instanceof Ci.nsIScriptError) {
|
||||
ChromeUtils.idleDispatch(() => this.handleMessage(message));
|
||||
}
|
||||
}
|
||||
|
||||
async handleMessage(message) {
|
||||
const isWarning = message.flags & message.warningFlag;
|
||||
const isFromChrome = REPORTED_CATEGORIES.has(message.category);
|
||||
if ((this.chromeOnly && !isFromChrome) || isWarning) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Record that we collected an error prior to applying the sample rate
|
||||
Services.telemetry.scalarAdd(TELEMETRY_ERROR_COLLECTED, 1);
|
||||
if (message.stack) {
|
||||
Services.telemetry.scalarAdd(TELEMETRY_ERROR_COLLECTED_STACK, 1);
|
||||
}
|
||||
if (message.sourceName) {
|
||||
const key = this.errorCollectedFilenameKey(message.sourceName);
|
||||
Services.telemetry.keyedScalarAdd(TELEMETRY_ERROR_COLLECTED_FILENAME, key.slice(0, 69), 1);
|
||||
}
|
||||
|
||||
// We do not collect errors on non-Nightly channels, just telemetry.
|
||||
// Also, old builds should not send errors to Sentry
|
||||
if (!AppConstants.NIGHTLY_BUILD || !this.isRecentBuild()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sample the amount of errors we send out
|
||||
let sampleRate = Number.parseFloat(this.sampleRatePref);
|
||||
for (const [regex, rate] of this.sampleRates) {
|
||||
if (message.sourceName.match(regex)) {
|
||||
sampleRate = rate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!Number.isFinite(sampleRate) || (Math.random() >= sampleRate)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const exceptionValue = {};
|
||||
const requestBody = {
|
||||
...this.requestBodyTemplate,
|
||||
timestamp: new Date().toISOString().slice(0, -1), // Remove trailing "Z"
|
||||
project: Services.prefs.getCharPref(PREF_PROJECT_ID),
|
||||
exception: {
|
||||
values: [exceptionValue],
|
||||
},
|
||||
};
|
||||
|
||||
const transforms = [
|
||||
addErrorMessage,
|
||||
addStacktrace,
|
||||
addModule,
|
||||
mangleExtensionUrls,
|
||||
this.mangleFilePaths.bind(this),
|
||||
tagExtensionErrors,
|
||||
];
|
||||
for (const transform of transforms) {
|
||||
await transform(message, exceptionValue, requestBody);
|
||||
}
|
||||
|
||||
const url = new URL(Services.prefs.getCharPref(PREF_SUBMIT_URL));
|
||||
url.searchParams.set("sentry_client", `${SDK_NAME}/${SDK_VERSION}`);
|
||||
url.searchParams.set("sentry_version", "7");
|
||||
url.searchParams.set("sentry_key", Services.prefs.getCharPref(PREF_PUBLIC_KEY));
|
||||
|
||||
try {
|
||||
await this.fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
// Sentry throws an auth error without a referrer specified.
|
||||
referrer: "https://fake.mozilla.org",
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
Services.telemetry.scalarAdd(TELEMETRY_ERROR_REPORTED, 1);
|
||||
this.logger.debug(`Sent error "${message.errorMessage}" successfully.`);
|
||||
} catch (error) {
|
||||
Services.telemetry.scalarAdd(TELEMETRY_ERROR_REPORTED_FAIL, 1);
|
||||
this.logger.warn(`Failed to send error "${message.errorMessage}": ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alters file: and jar: paths to remove leading file paths that may contain
|
||||
* user-identifying or platform-specific paths.
|
||||
*
|
||||
* prefixes is a mapping of replacementName -> filePath, where filePath is a
|
||||
* path on the filesystem that should be replaced, and replacementName is the
|
||||
* text that will replace it.
|
||||
*/
|
||||
mangleFilePaths(message, exceptionValue) {
|
||||
exceptionValue.module = this._transformFilePath(exceptionValue.module);
|
||||
for (const frame of exceptionValue.stacktrace.frames) {
|
||||
frame.module = this._transformFilePath(frame.module);
|
||||
}
|
||||
}
|
||||
|
||||
_transformFilePath(path) {
|
||||
try {
|
||||
const uri = Services.io.newURI(path);
|
||||
if (uri.schemeIs("jar")) {
|
||||
return uri.filePath;
|
||||
}
|
||||
if (uri.schemeIs("file")) {
|
||||
for (const [name, prefix] of Object.entries(this.manglePrefixes)) {
|
||||
if (uri.filePath.startsWith(prefix)) {
|
||||
return uri.filePath.replace(prefix, `[${name}]/`);
|
||||
}
|
||||
}
|
||||
|
||||
return "[UNKNOWN_LOCAL_FILEPATH]";
|
||||
}
|
||||
} catch (err) {}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
function defaultFetch(...args) {
|
||||
// Do not make network requests while running in automation
|
||||
if (Cu.isInAutomation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fetch(...args);
|
||||
}
|
||||
|
||||
function addErrorMessage(message, exceptionValue) {
|
||||
// Parse the error type from the message if present (e.g. "TypeError: Whoops").
|
||||
let errorMessage = message.errorMessage;
|
||||
let errorName = "Error";
|
||||
if (message.errorMessage.match(ERROR_PREFIX_RE)) {
|
||||
const parts = message.errorMessage.split(":");
|
||||
errorName = parts[0];
|
||||
errorMessage = parts.slice(1).join(":").trim();
|
||||
}
|
||||
|
||||
exceptionValue.type = errorName;
|
||||
exceptionValue.value = errorMessage;
|
||||
}
|
||||
|
||||
async function addStacktrace(message, exceptionValue) {
|
||||
const frames = [];
|
||||
let frame = message.stack;
|
||||
// Avoid an infinite loop by limiting traces to 100 frames.
|
||||
while (frame && frames.length < 100) {
|
||||
const normalizedFrame = {
|
||||
function: frame.functionDisplayName,
|
||||
module: frame.source,
|
||||
lineno: frame.line,
|
||||
colno: frame.column,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(frame.source);
|
||||
const sourceCode = await response.text();
|
||||
const sourceLines = sourceCode.split(/\r?\n/);
|
||||
// HTML pages and some inline event handlers have 0 as their line number
|
||||
let lineIndex = Math.max(frame.line - 1, 0);
|
||||
|
||||
// XBL line numbers are off by one, and pretty much every XML file with JS
|
||||
// in it is an XBL file.
|
||||
if (frame.source.endsWith(".xml") && lineIndex > 0) {
|
||||
lineIndex--;
|
||||
}
|
||||
|
||||
normalizedFrame.context_line = sourceLines[lineIndex];
|
||||
normalizedFrame.pre_context = sourceLines.slice(
|
||||
Math.max(lineIndex - CONTEXT_LINES, 0),
|
||||
lineIndex,
|
||||
);
|
||||
normalizedFrame.post_context = sourceLines.slice(
|
||||
lineIndex + 1,
|
||||
Math.min(lineIndex + 1 + CONTEXT_LINES, sourceLines.length),
|
||||
);
|
||||
} catch (err) {
|
||||
// Could be a fetch issue, could be a line index issue. Not much we can
|
||||
// do to recover in either case.
|
||||
}
|
||||
|
||||
frames.push(normalizedFrame);
|
||||
frame = frame.parent;
|
||||
}
|
||||
// Frames are sent in order from oldest to newest.
|
||||
frames.reverse();
|
||||
|
||||
exceptionValue.stacktrace = {frames};
|
||||
}
|
||||
|
||||
function addModule(message, exceptionValue) {
|
||||
exceptionValue.module = message.sourceName;
|
||||
}
|
||||
|
||||
function mangleExtensionUrls(message, exceptionValue) {
|
||||
const extensions = new Map();
|
||||
for (let extension of WebExtensionPolicy.getActiveExtensions()) {
|
||||
extensions.set(extension.mozExtensionHostname, extension);
|
||||
}
|
||||
|
||||
// Replaces any instances of moz-extension:// URLs with internal UUIDs to use
|
||||
// the add-on ID instead.
|
||||
function mangleExtURL(string, anchored = true) {
|
||||
if (!string) {
|
||||
return string;
|
||||
}
|
||||
|
||||
const re = new RegExp(`${anchored ? "^" : ""}moz-extension://([^/]+)/`, "g");
|
||||
return string.replace(re, (m0, m1) => {
|
||||
const id = extensions.has(m1) ? extensions.get(m1).id : m1;
|
||||
return `moz-extension://${id}/`;
|
||||
});
|
||||
}
|
||||
|
||||
exceptionValue.value = mangleExtURL(exceptionValue.value, false);
|
||||
exceptionValue.module = mangleExtURL(exceptionValue.module);
|
||||
for (const frame of exceptionValue.stacktrace.frames) {
|
||||
frame.module = mangleExtURL(frame.module);
|
||||
}
|
||||
}
|
||||
|
||||
function tagExtensionErrors(message, exceptionValue, requestBody) {
|
||||
requestBody.tags.isExtensionError = !!(
|
||||
exceptionValue.module && exceptionValue.module.startsWith("moz-extension://")
|
||||
);
|
||||
}
|
|
@ -125,7 +125,6 @@ XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
|
|||
EXTRA_JS_MODULES += [
|
||||
'AboutNewTab.jsm',
|
||||
'AsyncTabSwitcher.jsm',
|
||||
'BrowserErrorReporter.jsm',
|
||||
'BrowserUsageTelemetry.jsm',
|
||||
'BrowserWindowTracker.jsm',
|
||||
'ContentClick.jsm',
|
||||
|
|
|
@ -4,15 +4,6 @@ support-files =
|
|||
prefs =
|
||||
browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar=false
|
||||
|
||||
[browser_BrowserErrorReporter.js]
|
||||
skip-if = (verify && !debug && (os == 'mac' || os == 'win'))
|
||||
support-files =
|
||||
head_BrowserErrorReporter.js
|
||||
[browser_BrowserErrorReporter_nightly.js]
|
||||
skip-if = !nightly_build || (verify && !debug && (os == 'mac' || os == 'win'))
|
||||
support-files =
|
||||
head_BrowserErrorReporter.js
|
||||
browser_BrowserErrorReporter.html
|
||||
[browser_BrowserWindowTracker.js]
|
||||
[browser_ContentSearch.js]
|
||||
support-files =
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test page</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
// Line and column numbers are significant and used in tests, make sure to
|
||||
// update the tests if you make any changes to this file!
|
||||
function madeToFail() {
|
||||
madeToFail2();
|
||||
}
|
||||
function madeToFail2() {
|
||||
throw new Error("testFetchArguments error");
|
||||
}
|
||||
madeToFail();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,202 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
// This file contains BrowserErrorReporter tests that don't depend on
|
||||
// errors being collected, which is only enabled on Nightly builds.
|
||||
|
||||
ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", this);
|
||||
ChromeUtils.import("resource:///modules/BrowserErrorReporter.jsm", this);
|
||||
ChromeUtils.import("resource://gre/modules/AppConstants.jsm", this);
|
||||
ChromeUtils.import("resource://gre/modules/FileUtils.jsm", this);
|
||||
ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", this);
|
||||
|
||||
/* global sinon */
|
||||
Services.scriptloader.loadSubScript(new URL("head_BrowserErrorReporter.js", gTestPath).href, this);
|
||||
|
||||
add_task(async function testInitPrefDisabled() {
|
||||
let listening = false;
|
||||
const reporter = new BrowserErrorReporter({
|
||||
registerListener() {
|
||||
listening = true;
|
||||
},
|
||||
});
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
[PREF_ENABLED, false],
|
||||
]});
|
||||
|
||||
reporter.init();
|
||||
ok(!listening, "Reporter does not listen for errors if the enabled pref is false.");
|
||||
});
|
||||
|
||||
add_task(async function testInitUninitPrefEnabled() {
|
||||
let listening = false;
|
||||
const reporter = new BrowserErrorReporter({
|
||||
registerListener() {
|
||||
listening = true;
|
||||
},
|
||||
unregisterListener() {
|
||||
listening = false;
|
||||
},
|
||||
});
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
[PREF_ENABLED, true],
|
||||
]});
|
||||
|
||||
reporter.init();
|
||||
ok(listening, "Reporter listens for errors if the enabled pref is true.");
|
||||
|
||||
reporter.uninit();
|
||||
ok(!listening, "Reporter does not listen for errors after uninit.");
|
||||
});
|
||||
|
||||
add_task(async function testEnabledPrefWatcher() {
|
||||
let listening = false;
|
||||
const reporter = new BrowserErrorReporter({
|
||||
registerListener() {
|
||||
listening = true;
|
||||
},
|
||||
unregisterListener() {
|
||||
listening = false;
|
||||
},
|
||||
now: BrowserErrorReporter.getAppBuildIdDate(),
|
||||
});
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
[PREF_ENABLED, false],
|
||||
]});
|
||||
|
||||
reporter.init();
|
||||
ok(!listening, "Reporter does not collect errors if the enable pref is false.");
|
||||
|
||||
Services.console.logMessage(createScriptError({message: "Shouldn't report"}));
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
[PREF_ENABLED, true],
|
||||
]});
|
||||
ok(listening, "Reporter collects errors if the enabled pref switches to true.");
|
||||
});
|
||||
|
||||
add_task(async function testScalars() {
|
||||
// Do not bother testing telemetry scalars if they're already expired.
|
||||
if (SCALARS_EXPIRED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchStub = sinon.stub();
|
||||
const reporter = new BrowserErrorReporter({
|
||||
fetch: fetchStub,
|
||||
sampleRates: new Map(),
|
||||
now: BrowserErrorReporter.getAppBuildIdDate(),
|
||||
});
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
[PREF_ENABLED, true],
|
||||
[PREF_SAMPLE_RATE, "1.0"],
|
||||
]});
|
||||
|
||||
Services.telemetry.clearScalars();
|
||||
|
||||
const messages = [
|
||||
createScriptError({message: "No name"}),
|
||||
createScriptError({message: "Also no name", sourceName: "resource://gre/modules/Foo.jsm"}),
|
||||
createScriptError({message: "More no name", sourceName: "resource://gre/modules/Bar.jsm"}),
|
||||
createScriptError({message: "Yeah sures", sourceName: "unsafe://gre/modules/Bar.jsm"}),
|
||||
createScriptError({message: "Addon", sourceName: "moz-extension://foo/Bar.jsm"}),
|
||||
createScriptError({
|
||||
message: "long",
|
||||
sourceName: "resource://gre/modules/long/long/long/long/long/long/long/long/long/long/",
|
||||
}),
|
||||
{message: "Not a scripterror instance."},
|
||||
createScriptError({message: "Whatever", stack: [frame()]}),
|
||||
];
|
||||
|
||||
// Use handleMessage to avoid errors from other code messing up our counts.
|
||||
for (const message of messages) {
|
||||
await reporter.handleMessage(message);
|
||||
}
|
||||
|
||||
const scalars = Services.telemetry.getSnapshotForScalars("main", false).parent;
|
||||
is(
|
||||
scalars[TELEMETRY_ERROR_COLLECTED],
|
||||
7,
|
||||
`${TELEMETRY_ERROR_COLLECTED} is incremented when an error is collected.`,
|
||||
);
|
||||
is(
|
||||
scalars[TELEMETRY_ERROR_COLLECTED_STACK],
|
||||
1,
|
||||
`${TELEMETRY_ERROR_REPORTED_FAIL} is incremented when an error with a stack trace is collected.`,
|
||||
);
|
||||
|
||||
const keyedScalars = Services.telemetry.getSnapshotForKeyedScalars("main", false).parent;
|
||||
Assert.deepEqual(
|
||||
keyedScalars[TELEMETRY_ERROR_COLLECTED_FILENAME],
|
||||
{
|
||||
"FILTERED": 1,
|
||||
"MOZEXTENSION": 1,
|
||||
"resource://gre/modules/Foo.jsm": 1,
|
||||
"resource://gre/modules/Bar.jsm": 1,
|
||||
// Cut off at 70-character limit
|
||||
"resource://gre/modules/long/long/long/long/long/long/long/long/long/l": 1,
|
||||
},
|
||||
`${TELEMETRY_ERROR_COLLECTED_FILENAME} is incremented when an error is collected.`,
|
||||
);
|
||||
|
||||
resetConsole();
|
||||
});
|
||||
|
||||
add_task(async function testCollectedFilenameScalar() {
|
||||
// Do not bother testing telemetry scalars if they're already expired.
|
||||
if (SCALARS_EXPIRED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchStub = sinon.stub();
|
||||
const reporter = new BrowserErrorReporter({
|
||||
fetch: fetchStub,
|
||||
now: BrowserErrorReporter.getAppBuildIdDate(),
|
||||
});
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
[PREF_ENABLED, true],
|
||||
[PREF_SAMPLE_RATE, "1.0"],
|
||||
]});
|
||||
|
||||
const testCases = [
|
||||
["chrome://unknown/module.jsm", false],
|
||||
["resource://unknown/module.jsm", false],
|
||||
["unknown://unknown/module.jsm", false],
|
||||
|
||||
["resource://gre/modules/Foo.jsm", true],
|
||||
["resource:///modules/Foo.jsm", true],
|
||||
["chrome://global/Foo.jsm", true],
|
||||
["chrome://browser/Foo.jsm", true],
|
||||
["chrome://devtools/Foo.jsm", true],
|
||||
];
|
||||
|
||||
for (const [filename, shouldMatch] of testCases) {
|
||||
Services.telemetry.clearScalars();
|
||||
|
||||
// Use handleMessage to avoid errors from other code messing up our counts.
|
||||
await reporter.handleMessage(createScriptError({
|
||||
message: "Fine",
|
||||
sourceName: filename,
|
||||
}));
|
||||
|
||||
const keyedScalars = (
|
||||
Services.telemetry.getSnapshotForKeyedScalars("main", false).parent
|
||||
);
|
||||
|
||||
let matched = null;
|
||||
if (shouldMatch) {
|
||||
matched = keyedScalars[TELEMETRY_ERROR_COLLECTED_FILENAME][filename] === 1;
|
||||
} else {
|
||||
matched = keyedScalars[TELEMETRY_ERROR_COLLECTED_FILENAME].FILTERED === 1;
|
||||
}
|
||||
|
||||
ok(
|
||||
matched,
|
||||
shouldMatch
|
||||
? `${TELEMETRY_ERROR_COLLECTED_FILENAME} logs a key for ${filename}.`
|
||||
: `${TELEMETRY_ERROR_COLLECTED_FILENAME} logs a FILTERED key for ${filename}.`,
|
||||
);
|
||||
}
|
||||
|
||||
resetConsole();
|
||||
});
|
|
@ -1,548 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
// This file contains BrowserErrorReporter tests that depend on errors
|
||||
// being collected, which is only enabled on Nightly builds.
|
||||
|
||||
ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", this);
|
||||
ChromeUtils.import("resource:///modules/BrowserErrorReporter.jsm", this);
|
||||
ChromeUtils.import("resource://gre/modules/AppConstants.jsm", this);
|
||||
ChromeUtils.import("resource://gre/modules/FileUtils.jsm", this);
|
||||
ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", this);
|
||||
|
||||
/* global sinon */
|
||||
Services.scriptloader.loadSubScript(new URL("head_BrowserErrorReporter.js", gTestPath).href, this);
|
||||
|
||||
add_task(async function testInitPastMessages() {
|
||||
const fetchSpy = sinon.spy();
|
||||
const reporter = new BrowserErrorReporter({
|
||||
fetch: fetchSpy,
|
||||
registerListener: noop,
|
||||
unregisterListener: noop,
|
||||
now: BrowserErrorReporter.getAppBuildIdDate(),
|
||||
});
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
[PREF_ENABLED, true],
|
||||
[PREF_SAMPLE_RATE, "1.0"],
|
||||
]});
|
||||
|
||||
resetConsole();
|
||||
Services.console.logMessage(createScriptError({message: "Logged before init"}));
|
||||
reporter.init();
|
||||
|
||||
// Include ok() to satisfy mochitest warning for test without any assertions
|
||||
const errorWasLogged = await TestUtils.waitForCondition(
|
||||
() => fetchPassedError(fetchSpy, "Logged before init"),
|
||||
"Waiting for message to be logged",
|
||||
);
|
||||
ok(errorWasLogged, "Reporter collects errors logged before initialization.");
|
||||
|
||||
reporter.uninit();
|
||||
});
|
||||
|
||||
add_task(async function testNonErrorLogs() {
|
||||
const fetchSpy = sinon.spy();
|
||||
const reporter = new BrowserErrorReporter({
|
||||
fetch: fetchSpy,
|
||||
now: BrowserErrorReporter.getAppBuildIdDate(),
|
||||
});
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
[PREF_ENABLED, true],
|
||||
[PREF_SAMPLE_RATE, "1.0"],
|
||||
]});
|
||||
|
||||
await reporter.handleMessage({message: "Not a scripterror instance."});
|
||||
ok(
|
||||
!fetchPassedError(fetchSpy, "Not a scripterror instance."),
|
||||
"Reporter does not collect normal log messages or warnings.",
|
||||
);
|
||||
|
||||
await reporter.handleMessage(createScriptError({
|
||||
message: "Warning message",
|
||||
flags: Ci.nsIScriptError.warningFlag,
|
||||
}));
|
||||
ok(
|
||||
!fetchPassedError(fetchSpy, "Warning message"),
|
||||
"Reporter does not collect normal log messages or warnings.",
|
||||
);
|
||||
|
||||
await reporter.handleMessage(createScriptError({
|
||||
message: "Non-chrome category",
|
||||
category: "totally from a website",
|
||||
}));
|
||||
ok(
|
||||
!fetchPassedError(fetchSpy, "Non-chrome category"),
|
||||
"Reporter does not collect normal log messages or warnings.",
|
||||
);
|
||||
|
||||
await reporter.handleMessage(createScriptError({message: "Is error"}));
|
||||
ok(
|
||||
fetchPassedError(fetchSpy, "Is error"),
|
||||
"Reporter collects error messages.",
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function testSampling() {
|
||||
const fetchSpy = sinon.spy();
|
||||
const reporter = new BrowserErrorReporter({
|
||||
fetch: fetchSpy,
|
||||
now: BrowserErrorReporter.getAppBuildIdDate(),
|
||||
});
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
[PREF_ENABLED, true],
|
||||
[PREF_SAMPLE_RATE, "1.0"],
|
||||
]});
|
||||
|
||||
await reporter.handleMessage(createScriptError({message: "Should log"}));
|
||||
ok(
|
||||
fetchPassedError(fetchSpy, "Should log"),
|
||||
"A 1.0 sample rate will cause the reporter to always collect errors.",
|
||||
);
|
||||
|
||||
await reporter.handleMessage(createScriptError({message: "undefined", sourceName: undefined}));
|
||||
ok(
|
||||
fetchPassedError(fetchSpy, "undefined"),
|
||||
"A missing sourceName doesn't break reporting.",
|
||||
);
|
||||
|
||||
await reporter.handleMessage(createScriptError({
|
||||
message: "mozextension",
|
||||
sourceName: "moz-extension://Bar/Foo.jsm",
|
||||
}));
|
||||
ok(
|
||||
!fetchPassedError(fetchSpy, "mozextension"),
|
||||
"moz-extension:// paths are sampled at 0% even if the default rate is 1.0.",
|
||||
);
|
||||
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
[PREF_SAMPLE_RATE, "0.0"],
|
||||
]});
|
||||
await reporter.handleMessage(createScriptError({message: "Shouldn't log"}));
|
||||
ok(
|
||||
!fetchPassedError(fetchSpy, "Shouldn't log"),
|
||||
"A 0.0 sample rate will cause the reporter to never collect errors.",
|
||||
);
|
||||
|
||||
await reporter.handleMessage(createScriptError({
|
||||
message: "chromedevtools",
|
||||
sourceName: "chrome://devtools/Foo.jsm",
|
||||
}));
|
||||
ok(
|
||||
fetchPassedError(fetchSpy, "chromedevtools"),
|
||||
"chrome://devtools/ paths are sampled at 100% even if the default rate is 0.0.",
|
||||
);
|
||||
|
||||
await reporter.handleMessage(createScriptError({
|
||||
message: "resourcedevtools",
|
||||
sourceName: "resource://devtools/Foo.jsm",
|
||||
}));
|
||||
ok(
|
||||
fetchPassedError(fetchSpy, "resourcedevtools"),
|
||||
"resource://devtools/ paths are sampled at 100% even if the default rate is 0.0.",
|
||||
);
|
||||
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
[PREF_SAMPLE_RATE, ")fasdf"],
|
||||
]});
|
||||
await reporter.handleMessage(createScriptError({message: "Also shouldn't log"}));
|
||||
ok(
|
||||
!fetchPassedError(fetchSpy, "Also shouldn't log"),
|
||||
"An invalid sample rate will cause the reporter to never collect errors.",
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function testNameMessage() {
|
||||
const fetchSpy = sinon.spy();
|
||||
const reporter = new BrowserErrorReporter({
|
||||
fetch: fetchSpy,
|
||||
now: BrowserErrorReporter.getAppBuildIdDate(),
|
||||
});
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
[PREF_ENABLED, true],
|
||||
[PREF_SAMPLE_RATE, "1.0"],
|
||||
]});
|
||||
|
||||
await reporter.handleMessage(createScriptError({message: "No name"}));
|
||||
let call = fetchCallForMessage(fetchSpy, "No name");
|
||||
let body = JSON.parse(call.args[1].body);
|
||||
is(
|
||||
body.exception.values[0].type,
|
||||
"Error",
|
||||
"Reporter uses a generic type when no name is in the message.",
|
||||
);
|
||||
is(
|
||||
body.exception.values[0].value,
|
||||
"No name",
|
||||
"Reporter uses error message as the exception value.",
|
||||
);
|
||||
|
||||
await reporter.handleMessage(createScriptError({message: "FooError: Has name"}));
|
||||
call = fetchCallForMessage(fetchSpy, "Has name");
|
||||
body = JSON.parse(call.args[1].body);
|
||||
is(
|
||||
body.exception.values[0].type,
|
||||
"FooError",
|
||||
"Reporter uses the error type from the message.",
|
||||
);
|
||||
is(
|
||||
body.exception.values[0].value,
|
||||
"Has name",
|
||||
"Reporter uses error message as the value parameter.",
|
||||
);
|
||||
|
||||
await reporter.handleMessage(createScriptError({message: "FooError: Has :extra: colons"}));
|
||||
call = fetchCallForMessage(fetchSpy, "Has :extra: colons");
|
||||
body = JSON.parse(call.args[1].body);
|
||||
is(
|
||||
body.exception.values[0].type,
|
||||
"FooError",
|
||||
"Reporter uses the error type from the message.",
|
||||
);
|
||||
is(
|
||||
body.exception.values[0].value,
|
||||
"Has :extra: colons",
|
||||
"Reporter uses error message as the value parameter.",
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function testRecentBuild() {
|
||||
// Create date that is guaranteed to be a month newer than the build date.
|
||||
const nowDate = BrowserErrorReporter.getAppBuildIdDate();
|
||||
nowDate.setMonth(nowDate.getMonth() + 1);
|
||||
|
||||
const fetchSpy = sinon.spy();
|
||||
const reporter = new BrowserErrorReporter({
|
||||
fetch: fetchSpy,
|
||||
now: nowDate,
|
||||
});
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
[PREF_ENABLED, true],
|
||||
[PREF_SAMPLE_RATE, "1.0"],
|
||||
]});
|
||||
|
||||
await reporter.handleMessage(createScriptError({message: "Is error"}));
|
||||
ok(
|
||||
!fetchPassedError(fetchSpy, "Is error"),
|
||||
"Reporter does not collect errors from builds older than a week.",
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function testFetchArguments() {
|
||||
const fetchSpy = sinon.spy();
|
||||
const reporter = new BrowserErrorReporter({
|
||||
fetch: fetchSpy,
|
||||
now: BrowserErrorReporter.getAppBuildIdDate(),
|
||||
});
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
[PREF_ENABLED, true],
|
||||
[PREF_SAMPLE_RATE, "1.0"],
|
||||
[PREF_PROJECT_ID, "123"],
|
||||
[PREF_PUBLIC_KEY, "foobar"],
|
||||
[PREF_SUBMIT_URL, "https://errors.example.com/api/123/store/"],
|
||||
]});
|
||||
|
||||
resetConsole();
|
||||
reporter.init();
|
||||
const testPageUrl = (
|
||||
"chrome://mochitests/content/browser/browser/modules/test/browser/" +
|
||||
"browser_BrowserErrorReporter.html"
|
||||
);
|
||||
|
||||
SimpleTest.expectUncaughtException();
|
||||
await BrowserTestUtils.withNewTab(testPageUrl, async () => {
|
||||
const call = await TestUtils.waitForCondition(
|
||||
() => fetchCallForMessage(fetchSpy, "testFetchArguments error"),
|
||||
"Wait for error from browser_BrowserErrorReporter.html to be logged",
|
||||
);
|
||||
const body = JSON.parse(call.args[1].body);
|
||||
const url = new URL(call.args[0]);
|
||||
|
||||
is(url.origin, "https://errors.example.com", "Reporter builds API url from DSN pref.");
|
||||
is(url.pathname, "/api/123/store/", "Reporter builds API url from DSN pref.");
|
||||
is(
|
||||
url.searchParams.get("sentry_client"),
|
||||
"firefox-error-reporter/1.0.0",
|
||||
"Reporter identifies itself in the outgoing request",
|
||||
);
|
||||
is(url.searchParams.get("sentry_version"), "7", "Reporter is compatible with Sentry 7.");
|
||||
is(url.searchParams.get("sentry_key"), "foobar", "Reporter pulls API key from DSN pref.");
|
||||
is(body.project, "123", "Reporter pulls project ID from DSN pref.");
|
||||
is(
|
||||
body.tags.changeset,
|
||||
AppConstants.SOURCE_REVISION_URL,
|
||||
"Reporter pulls changeset tag from AppConstants",
|
||||
);
|
||||
is(call.args[1].referrer, "https://fake.mozilla.org", "Reporter uses a fake referer.");
|
||||
|
||||
const response = await fetch(testPageUrl);
|
||||
const pageText = await response.text();
|
||||
const pageLines = pageText.split("\n");
|
||||
Assert.deepEqual(
|
||||
body.exception,
|
||||
{
|
||||
values: [
|
||||
{
|
||||
type: "Error",
|
||||
value: "testFetchArguments error",
|
||||
module: testPageUrl,
|
||||
stacktrace: {
|
||||
frames: [
|
||||
{
|
||||
function: null,
|
||||
module: testPageUrl,
|
||||
lineno: 17,
|
||||
colno: 7,
|
||||
pre_context: pageLines.slice(11, 16),
|
||||
context_line: pageLines[16],
|
||||
post_context: pageLines.slice(17, 22),
|
||||
},
|
||||
{
|
||||
function: "madeToFail",
|
||||
module: testPageUrl,
|
||||
lineno: 12,
|
||||
colno: 9,
|
||||
pre_context: pageLines.slice(6, 11),
|
||||
context_line: pageLines[11],
|
||||
post_context: pageLines.slice(12, 17),
|
||||
},
|
||||
{
|
||||
function: "madeToFail2",
|
||||
module: testPageUrl,
|
||||
lineno: 15,
|
||||
colno: 15,
|
||||
pre_context: pageLines.slice(9, 14),
|
||||
context_line: pageLines[14],
|
||||
post_context: pageLines.slice(15, 20),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"Reporter builds stack trace from scriptError correctly.",
|
||||
);
|
||||
});
|
||||
|
||||
reporter.uninit();
|
||||
});
|
||||
|
||||
add_task(async function testAddonIDMangle() {
|
||||
const fetchSpy = sinon.spy();
|
||||
const reporter = new BrowserErrorReporter({
|
||||
fetch: fetchSpy,
|
||||
chromeOnly: false,
|
||||
sampleRates: new Map(),
|
||||
now: BrowserErrorReporter.getAppBuildIdDate(),
|
||||
});
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
[PREF_ENABLED, true],
|
||||
[PREF_SAMPLE_RATE, "1.0"],
|
||||
]});
|
||||
resetConsole();
|
||||
reporter.init();
|
||||
|
||||
// Create and install test add-on
|
||||
const id = "browsererrorcollection@example.com";
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
applications: {
|
||||
gecko: { id },
|
||||
},
|
||||
},
|
||||
background() {
|
||||
throw new Error("testAddonIDMangle error");
|
||||
},
|
||||
});
|
||||
await extension.startup();
|
||||
|
||||
// Just in case the error hasn't been thrown before add-on startup.
|
||||
const call = await TestUtils.waitForCondition(
|
||||
() => fetchCallForMessage(fetchSpy, "testAddonIDMangle error"),
|
||||
`Wait for error from ${id} to be logged`,
|
||||
);
|
||||
const body = JSON.parse(call.args[1].body);
|
||||
const stackFrame = body.exception.values[0].stacktrace.frames[0];
|
||||
ok(
|
||||
stackFrame.module.startsWith(`moz-extension://${id}/`),
|
||||
"Stack frame filenames use the proper add-on ID instead of internal UUIDs.",
|
||||
);
|
||||
|
||||
await extension.unload();
|
||||
reporter.uninit();
|
||||
});
|
||||
|
||||
add_task(async function testExtensionTag() {
|
||||
const fetchSpy = sinon.spy();
|
||||
const reporter = new BrowserErrorReporter({
|
||||
fetch: fetchSpy,
|
||||
chromeOnly: false,
|
||||
sampleRates: new Map(),
|
||||
now: BrowserErrorReporter.getAppBuildIdDate(),
|
||||
});
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
[PREF_ENABLED, true],
|
||||
[PREF_SAMPLE_RATE, "1.0"],
|
||||
]});
|
||||
resetConsole();
|
||||
reporter.init();
|
||||
|
||||
// Create and install test add-on
|
||||
const id = "browsererrorcollection@example.com";
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
applications: {
|
||||
gecko: { id },
|
||||
},
|
||||
},
|
||||
background() {
|
||||
throw new Error("testExtensionTag error");
|
||||
},
|
||||
});
|
||||
await extension.startup();
|
||||
|
||||
// Just in case the error hasn't been thrown before add-on startup.
|
||||
let call = await TestUtils.waitForCondition(
|
||||
() => fetchCallForMessage(fetchSpy, "testExtensionTag error"),
|
||||
`Wait for error from ${id} to be logged`,
|
||||
);
|
||||
let body = JSON.parse(call.args[1].body);
|
||||
ok(body.tags.isExtensionError, "Errors from extensions have an isExtensionError=true tag.");
|
||||
|
||||
await extension.unload();
|
||||
reporter.uninit();
|
||||
|
||||
await reporter.handleMessage(createScriptError({message: "testExtensionTag not from extension"}));
|
||||
call = fetchCallForMessage(fetchSpy, "testExtensionTag not from extension");
|
||||
body = JSON.parse(call.args[1].body);
|
||||
is(body.tags.isExtensionError, false, "Normal errors have an isExtensionError=false tag.");
|
||||
});
|
||||
|
||||
add_task(async function testScalars() {
|
||||
// Do not bother testing telemetry scalars if they're already expired.
|
||||
if (SCALARS_EXPIRED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchStub = sinon.stub();
|
||||
const reporter = new BrowserErrorReporter({
|
||||
fetch: fetchStub,
|
||||
now: BrowserErrorReporter.getAppBuildIdDate(),
|
||||
});
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
[PREF_ENABLED, true],
|
||||
[PREF_SAMPLE_RATE, "1.0"],
|
||||
]});
|
||||
|
||||
Services.telemetry.clearScalars();
|
||||
|
||||
// Basic count
|
||||
await reporter.handleMessage(createScriptError({message: "No name"}));
|
||||
|
||||
// Sample rate affects counts
|
||||
await SpecialPowers.pushPrefEnv({set: [[PREF_SAMPLE_RATE, "0.0"]]});
|
||||
await reporter.handleMessage(createScriptError({message: "Additionally no name"}));
|
||||
|
||||
// Failed fetches should be counted too
|
||||
await SpecialPowers.pushPrefEnv({set: [[PREF_SAMPLE_RATE, "1.0"]]});
|
||||
fetchStub.rejects(new Error("Could not report"));
|
||||
await reporter.handleMessage(createScriptError({message: "Maybe name?"}));
|
||||
|
||||
const scalars = Services.telemetry.getSnapshotForScalars("main", false).parent;
|
||||
is(
|
||||
scalars[TELEMETRY_ERROR_COLLECTED],
|
||||
3,
|
||||
`${TELEMETRY_ERROR_COLLECTED} is incremented when an error is collected.`,
|
||||
);
|
||||
is(
|
||||
scalars[TELEMETRY_ERROR_SAMPLE_RATE],
|
||||
"1.0",
|
||||
`${TELEMETRY_ERROR_SAMPLE_RATE} contains the last sample rate used.`,
|
||||
);
|
||||
is(
|
||||
scalars[TELEMETRY_ERROR_REPORTED],
|
||||
1,
|
||||
`${TELEMETRY_ERROR_REPORTED} is incremented when an error is reported.`,
|
||||
);
|
||||
is(
|
||||
scalars[TELEMETRY_ERROR_REPORTED_FAIL],
|
||||
1,
|
||||
`${TELEMETRY_ERROR_REPORTED_FAIL} is incremented when an error fails to be reported.`,
|
||||
);
|
||||
|
||||
resetConsole();
|
||||
});
|
||||
|
||||
add_task(async function testFilePathMangle() {
|
||||
const fetchSpy = sinon.spy();
|
||||
const reporter = new BrowserErrorReporter({fetch: fetchSpy});
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
[PREF_ENABLED, true],
|
||||
[PREF_SAMPLE_RATE, "1.0"],
|
||||
]});
|
||||
|
||||
const greDir = Services.dirsvc.get("GreD", Ci.nsIFile).path;
|
||||
const profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile).path;
|
||||
|
||||
const message = createScriptError({
|
||||
message: "Whatever",
|
||||
sourceName: "file:///path/to/main.jsm",
|
||||
stack: [
|
||||
frame({source: "jar:file:///path/to/jar!/inside/jar.jsm"}),
|
||||
frame({source: `file://${greDir}/defaults/prefs/channel-prefs.js`}),
|
||||
frame({source: `file://${profileDir}/prefs.js`}),
|
||||
],
|
||||
});
|
||||
await reporter.handleMessage(message);
|
||||
|
||||
const call = fetchCallForMessage(fetchSpy, "Whatever");
|
||||
const body = JSON.parse(call.args[1].body);
|
||||
const exception = body.exception.values[0];
|
||||
is(exception.module, "[UNKNOWN_LOCAL_FILEPATH]", "Unrecognized local file paths are mangled");
|
||||
|
||||
// Stackframe order is reversed from what is in the message.
|
||||
const stackFrames = exception.stacktrace.frames;
|
||||
is(
|
||||
stackFrames[0].module, "[profileDir]/prefs.js",
|
||||
"Paths within the profile directory are preserved but mangled",
|
||||
);
|
||||
is(
|
||||
stackFrames[1].module, "[greDir]/defaults/prefs/channel-prefs.js",
|
||||
"Paths within the GRE directory are preserved but mangled",
|
||||
);
|
||||
is(
|
||||
stackFrames[2].module, "/inside/jar.jsm",
|
||||
"Paths within jarfiles are extracted from the full jar: URL",
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function testFilePathMangleWhitespace() {
|
||||
const fetchSpy = sinon.spy();
|
||||
|
||||
const greDir = Services.dirsvc.get("GreD", Ci.nsIFile);
|
||||
const whitespaceDir = greDir.clone();
|
||||
whitespaceDir.append("with whitespace");
|
||||
const manglePrefixes = {
|
||||
whitespace: whitespaceDir,
|
||||
};
|
||||
|
||||
const reporter = new BrowserErrorReporter({fetch: fetchSpy, manglePrefixes});
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
[PREF_ENABLED, true],
|
||||
[PREF_SAMPLE_RATE, "1.0"],
|
||||
]});
|
||||
|
||||
const message = createScriptError({
|
||||
message: "Whatever",
|
||||
sourceName: `file://${greDir.path}/with whitespace/remaining/file.jsm`,
|
||||
});
|
||||
await reporter.handleMessage(message);
|
||||
|
||||
const call = fetchCallForMessage(fetchSpy, "Whatever");
|
||||
const body = JSON.parse(call.args[1].body);
|
||||
const exception = body.exception.values[0];
|
||||
is(
|
||||
exception.module, "[whitespace]/remaining/file.jsm",
|
||||
"Prefixes with whitespace are correctly mangled",
|
||||
);
|
||||
});
|
|
@ -1,100 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
/* exported sinon */
|
||||
Services.scriptloader.loadSubScript("resource://testing-common/sinon-2.3.2.js");
|
||||
registerCleanupFunction(function() {
|
||||
delete window.sinon;
|
||||
});
|
||||
|
||||
const PREF_ENABLED = "browser.chrome.errorReporter.enabled";
|
||||
const PREF_PROJECT_ID = "browser.chrome.errorReporter.projectId";
|
||||
const PREF_PUBLIC_KEY = "browser.chrome.errorReporter.publicKey";
|
||||
const PREF_SAMPLE_RATE = "browser.chrome.errorReporter.sampleRate";
|
||||
const PREF_SUBMIT_URL = "browser.chrome.errorReporter.submitUrl";
|
||||
const TELEMETRY_ERROR_COLLECTED = "browser.errors.collected_count";
|
||||
const TELEMETRY_ERROR_COLLECTED_FILENAME = "browser.errors.collected_count_by_filename";
|
||||
const TELEMETRY_ERROR_COLLECTED_STACK = "browser.errors.collected_with_stack_count";
|
||||
const TELEMETRY_ERROR_REPORTED = "browser.errors.reported_success_count";
|
||||
const TELEMETRY_ERROR_REPORTED_FAIL = "browser.errors.reported_failure_count";
|
||||
const TELEMETRY_ERROR_SAMPLE_RATE = "browser.errors.sample_rate";
|
||||
|
||||
const currentVersion = Services.appinfo.platformVersion;
|
||||
const expiringVersion = "64.0";
|
||||
const SCALARS_EXPIRED = Services.vc.compare(currentVersion, expiringVersion) === 1;
|
||||
|
||||
add_task(async function testSetup() {
|
||||
const canRecordExtended = Services.telemetry.canRecordExtended;
|
||||
Services.telemetry.canRecordExtended = true;
|
||||
registerCleanupFunction(() => Services.telemetry.canRecordExtended = canRecordExtended);
|
||||
});
|
||||
|
||||
function createScriptError(options = {}) {
|
||||
let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
|
||||
scriptError.init(
|
||||
options.message || "",
|
||||
"sourceName" in options ? options.sourceName : null,
|
||||
options.sourceLine || null,
|
||||
options.lineNumber || null,
|
||||
options.columnNumber || null,
|
||||
options.flags || Ci.nsIScriptError.errorFlag,
|
||||
options.category || "chrome javascript",
|
||||
);
|
||||
|
||||
// You can't really set the stack of a scriptError in JS, so we shadow it instead.
|
||||
if (options.stack) {
|
||||
scriptError = Object.create(scriptError, {
|
||||
stack: {
|
||||
value: createStack(options.stack),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return scriptError;
|
||||
}
|
||||
|
||||
function createStack(frames) {
|
||||
for (let k = 0; k < frames.length - 1; k++) {
|
||||
frames[k].parent = frames[k + 1];
|
||||
}
|
||||
return frames[0];
|
||||
}
|
||||
|
||||
function frame(options = {}) {
|
||||
return Object.assign({
|
||||
functionDisplayName: "fooFunction",
|
||||
source: "resource://modules/BrowserErrorReporter.jsm",
|
||||
line: 5,
|
||||
column: 10,
|
||||
}, options);
|
||||
}
|
||||
|
||||
function noop() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
// Clears the console of any previous messages. Should be called at the end of
|
||||
// each test that logs to the console.
|
||||
function resetConsole() {
|
||||
Services.console.logStringMessage("");
|
||||
Services.console.reset();
|
||||
}
|
||||
|
||||
// Finds the fetch spy call for an error with a matching message.
|
||||
function fetchCallForMessage(fetchSpy, message) {
|
||||
for (const call of fetchSpy.getCalls()) {
|
||||
const body = JSON.parse(call.args[1].body);
|
||||
if (body.exception.values[0].value.includes(message)) {
|
||||
return call;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper to test if a fetch spy was called with the given error message.
|
||||
// Used in tests where unrelated JS errors from other code are logged.
|
||||
function fetchPassedError(fetchSpy, message) {
|
||||
return fetchCallForMessage(fetchSpy, message) !== null;
|
||||
}
|
|
@ -424,12 +424,7 @@ void BrowsingContext::Blur(ErrorResult& aError) {
|
|||
Nullable<WindowProxyHolder> BrowsingContext::GetTop(ErrorResult& aError) {
|
||||
// We never return null or throw an error, but the implementation in
|
||||
// nsGlobalWindow does and we need to use the same signature.
|
||||
BrowsingContext* bc = this;
|
||||
BrowsingContext* parent;
|
||||
while ((parent = bc->mParent)) {
|
||||
bc = parent;
|
||||
}
|
||||
return WindowProxyHolder(bc);
|
||||
return WindowProxyHolder(TopLevelBrowsingContext());
|
||||
}
|
||||
|
||||
void BrowsingContext::GetOpener(JSContext* aCx,
|
||||
|
|
|
@ -120,8 +120,8 @@ partial interface HTMLMediaElement {
|
|||
attribute boolean mozPreservesPitch;
|
||||
|
||||
// NB: for internal use with the video controls:
|
||||
[Func="IsChromeOrXBL"] attribute boolean mozAllowCasting;
|
||||
[Func="IsChromeOrXBL"] attribute boolean mozIsCasting;
|
||||
[Func="IsChromeOrXBLOrUAWidget"] attribute boolean mozAllowCasting;
|
||||
[Func="IsChromeOrXBLOrUAWidget"] attribute boolean mozIsCasting;
|
||||
|
||||
// Mozilla extension: stream capture
|
||||
[Throws]
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<!DOCTYPE html>
|
||||
<style>
|
||||
body {
|
||||
background-color: limegreen;
|
||||
}
|
||||
.A {
|
||||
transform: scale(0.01) perspective(1000px);
|
||||
transform-origin: 0% 0%;
|
||||
filter: grayscale(40%);
|
||||
}
|
||||
.B {
|
||||
background-color: black;
|
||||
width: 10000px;
|
||||
height: 5000px;
|
||||
border-radius: 2000px;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div class="A">
|
||||
<div class = "B"/>
|
||||
</div>
|
||||
</body>
|
|
@ -179,5 +179,6 @@ load 1508822.html
|
|||
load 1509099.html
|
||||
load 1513133.html
|
||||
load 1496194.html
|
||||
load 1505934-1.html
|
||||
load 1509123.html
|
||||
load texture-allocator-zero-region.html
|
||||
|
|
|
@ -86,6 +86,17 @@ pub const TILE_SIZE_WIDTH: i32 = 1024;
|
|||
pub const TILE_SIZE_HEIGHT: i32 = 256;
|
||||
const FRAMES_BEFORE_CACHING: usize = 2;
|
||||
|
||||
/// The maximum size per axis of texture cache item,
|
||||
/// in WorldPixel coordinates.
|
||||
// TODO(gw): This size is quite arbitrary - we should do some
|
||||
// profiling / telemetry to see when it makes sense
|
||||
// to cache a picture.
|
||||
const MAX_CACHE_SIZE: f32 = 2048.0;
|
||||
/// The maximum size per axis of a surface,
|
||||
/// in WorldPixel coordinates.
|
||||
const MAX_SURFACE_SIZE: f32 = 4096.0;
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GlobalTransformInfo {
|
||||
/// Current (quantized) value of the transform, that is
|
||||
|
@ -1278,6 +1289,11 @@ impl SurfaceInfo {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn fits_surface_size_limits(&self) -> bool {
|
||||
self.map_local_to_surface.bounds.size.width <= MAX_SURFACE_SIZE &&
|
||||
self.map_local_to_surface.bounds.size.height <= MAX_SURFACE_SIZE
|
||||
}
|
||||
|
||||
/// Take the set of child render tasks for this surface. This is
|
||||
/// used when constructing the render task tree.
|
||||
pub fn take_render_tasks(&mut self) -> Vec<RenderTaskId> {
|
||||
|
@ -1821,6 +1837,7 @@ impl PicturePrimitive {
|
|||
};
|
||||
|
||||
let state = PictureState {
|
||||
//TODO: check for MAX_CACHE_SIZE here?
|
||||
is_cacheable: true,
|
||||
map_local_to_pic,
|
||||
map_pic_to_world,
|
||||
|
@ -2020,38 +2037,12 @@ impl PicturePrimitive {
|
|||
let parent_raster_spatial_node_index = state.current_surface().raster_spatial_node_index;
|
||||
let surface_spatial_node_index = self.spatial_node_index;
|
||||
|
||||
// Check if there is perspective, and thus whether a new
|
||||
// rasterization root should be established.
|
||||
let xf = frame_context.clip_scroll_tree.get_relative_transform(
|
||||
parent_raster_spatial_node_index,
|
||||
surface_spatial_node_index,
|
||||
).expect("BUG: unable to get relative transform");
|
||||
|
||||
// TODO(gw): A temporary hack here to revert behavior to
|
||||
// always raster in screen-space. This is not
|
||||
// a problem yet, since we're not taking advantage
|
||||
// of this for caching yet. This is a workaround
|
||||
// for some existing issues with handling scale
|
||||
// when rasterizing in local space mode. Once
|
||||
// the fixes for those are in-place, we can
|
||||
// remove this hack!
|
||||
//let local_scale = raster_space.local_scale();
|
||||
// let wants_raster_root = xf.has_perspective_component() ||
|
||||
// local_scale.is_some();
|
||||
let establishes_raster_root = xf.has_perspective_component();
|
||||
|
||||
// TODO(gw): For now, we always raster in screen space. Soon,
|
||||
// we will be able to respect the requested raster
|
||||
// space, and/or override the requested raster root
|
||||
// if it makes sense to.
|
||||
let raster_space = RasterSpace::Screen;
|
||||
|
||||
let raster_spatial_node_index = if establishes_raster_root {
|
||||
surface_spatial_node_index
|
||||
} else {
|
||||
parent_raster_spatial_node_index
|
||||
};
|
||||
|
||||
let inflation_factor = match composite_mode {
|
||||
PictureCompositeMode::Filter(FilterOp::Blur(blur_radius)) => {
|
||||
// The amount of extra space needed for primitives inside
|
||||
|
@ -2063,32 +2054,58 @@ impl PicturePrimitive {
|
|||
}
|
||||
};
|
||||
|
||||
let surface_index = state.push_surface(
|
||||
let mut surface = {
|
||||
// Check if there is perspective, and thus whether a new
|
||||
// rasterization root should be established.
|
||||
let xf = frame_context.clip_scroll_tree.get_relative_transform(
|
||||
parent_raster_spatial_node_index,
|
||||
surface_spatial_node_index,
|
||||
).expect("BUG: unable to get relative transform");
|
||||
|
||||
let establishes_raster_root = xf.has_perspective_component();
|
||||
|
||||
SurfaceInfo::new(
|
||||
surface_spatial_node_index,
|
||||
raster_spatial_node_index,
|
||||
if establishes_raster_root {
|
||||
surface_spatial_node_index
|
||||
} else {
|
||||
parent_raster_spatial_node_index
|
||||
},
|
||||
inflation_factor,
|
||||
frame_context.screen_world_rect,
|
||||
&frame_context.clip_scroll_tree,
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
self.raster_config = Some(RasterConfig {
|
||||
composite_mode,
|
||||
surface_index,
|
||||
establishes_raster_root,
|
||||
});
|
||||
if surface_spatial_node_index != parent_raster_spatial_node_index &&
|
||||
!surface.fits_surface_size_limits()
|
||||
{
|
||||
// fall back to the parent raster root
|
||||
surface = SurfaceInfo::new(
|
||||
surface_spatial_node_index,
|
||||
parent_raster_spatial_node_index,
|
||||
inflation_factor,
|
||||
frame_context.screen_world_rect,
|
||||
&frame_context.clip_scroll_tree,
|
||||
);
|
||||
};
|
||||
|
||||
// If we have a cache key / descriptor for this surface,
|
||||
// update any transforms it cares about.
|
||||
if let Some(ref mut surface_desc) = self.surface_desc {
|
||||
surface_desc.update(
|
||||
surface_spatial_node_index,
|
||||
raster_spatial_node_index,
|
||||
surface.raster_spatial_node_index,
|
||||
frame_context.clip_scroll_tree,
|
||||
raster_space,
|
||||
);
|
||||
}
|
||||
|
||||
self.raster_config = Some(RasterConfig {
|
||||
composite_mode,
|
||||
establishes_raster_root: surface_spatial_node_index == surface.raster_spatial_node_index,
|
||||
surface_index: state.push_surface(surface),
|
||||
});
|
||||
}
|
||||
|
||||
Some(mem::replace(&mut self.prim_list.pictures, SmallVec::new()))
|
||||
|
@ -2154,7 +2171,7 @@ impl PicturePrimitive {
|
|||
let map_local_to_containing_block: SpaceMapper<LayoutPixel, LayoutPixel> = SpaceMapper::new_with_target(
|
||||
containing_block_index,
|
||||
cluster.spatial_node_index,
|
||||
LayoutRect::zero(), // bounds aren't going to be used for this mapping
|
||||
LayoutRect::zero(), // bounds aren't going to be used for this mapping
|
||||
&frame_context.clip_scroll_tree,
|
||||
);
|
||||
|
||||
|
@ -2341,10 +2358,6 @@ impl PicturePrimitive {
|
|||
// picture, and just draw a minimal portion of the picture
|
||||
// (clipped to screen bounds) to a temporary target each frame.
|
||||
|
||||
// TODO(gw): This size is quite arbitrary - we should do some
|
||||
// profiling / telemetry to see when it makes sense
|
||||
// to cache a picture.
|
||||
const MAX_CACHE_SIZE: f32 = 2048.0;
|
||||
let too_big_to_cache = unclipped.size.width > MAX_CACHE_SIZE ||
|
||||
unclipped.size.height > MAX_CACHE_SIZE;
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@ macro_rules! ft_dyn_fn {
|
|||
FT_Err_Unimplemented_Feature as FT_Error
|
||||
}
|
||||
lazy_static! {
|
||||
static ref func: unsafe extern "C" fn($($arg_type),*) -> FT_Error = {
|
||||
static ref FUNC: unsafe extern "C" fn($($arg_type),*) -> FT_Error = {
|
||||
unsafe {
|
||||
let cname = CString::new(stringify!($func_name)).unwrap();
|
||||
let ptr = dlsym(RTLD_DEFAULT, cname.as_ptr());
|
||||
|
@ -94,7 +94,7 @@ macro_rules! ft_dyn_fn {
|
|||
}
|
||||
};
|
||||
}
|
||||
(*func)($($arg_name),*)
|
||||
(*FUNC)($($arg_name),*)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -131,7 +131,7 @@ impl SpatialNode {
|
|||
origin_in_parent_reference_frame,
|
||||
invertible: true,
|
||||
};
|
||||
Self::new(pipeline_id, parent_index, SpatialNodeType:: ReferenceFrame(info))
|
||||
Self::new(pipeline_id, parent_index, SpatialNodeType::ReferenceFrame(info))
|
||||
}
|
||||
|
||||
pub fn new_sticky_frame(
|
||||
|
|
Двоичные данные
gfx/wr/wrench/reftests/split/same-plane.png
Двоичные данные
gfx/wr/wrench/reftests/split/same-plane.png
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 5.3 KiB После Ширина: | Высота: | Размер: 7.3 KiB |
|
@ -59,6 +59,9 @@ class jsjitExecutableAllocator(object):
|
|||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
return self.__next__()
|
||||
|
||||
def __next__(self):
|
||||
cur = self.index
|
||||
if cur >= self.max:
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
var g = newGlobal({newCompartment: true});
|
||||
g.parent = this;
|
||||
g.eval("new Debugger(parent).onExceptionUnwind = function () {};");
|
||||
|
||||
function* wrapNoThrow() {
|
||||
let iter = {
|
||||
[Symbol.iterator]() {
|
||||
return this;
|
||||
},
|
||||
next() {
|
||||
return { value: 10, done: false };
|
||||
},
|
||||
return() { return "invalid return value" }
|
||||
};
|
||||
for (const i of iter)
|
||||
yield i;
|
||||
}
|
||||
|
||||
function foo() {
|
||||
for (var i of [1,2,3]) {
|
||||
for (var j of [4,5,6]) {
|
||||
try {
|
||||
for (const i of wrapNoThrow()) break;
|
||||
} catch (e) {}
|
||||
}
|
||||
for (var j of [7,8,9]) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < 10; i++) {
|
||||
try {
|
||||
foo();
|
||||
} catch(e) {}
|
||||
}
|
|
@ -473,21 +473,26 @@ static inline jsbytecode* GetNextNonLoopEntryPc(jsbytecode* pc,
|
|||
return pc;
|
||||
}
|
||||
|
||||
static bool HasLiveStackValueAtDepth(JSScript* script, jsbytecode* pc,
|
||||
uint32_t stackDepth) {
|
||||
class NoOpTryNoteFilter {
|
||||
public:
|
||||
explicit NoOpTryNoteFilter() = default;
|
||||
bool operator()(const JSTryNote*) { return true; }
|
||||
};
|
||||
|
||||
class TryNoteIterAll : public TryNoteIter<NoOpTryNoteFilter> {
|
||||
public:
|
||||
TryNoteIterAll(JSContext* cx, JSScript* script, jsbytecode* pc)
|
||||
: TryNoteIter(cx, script, pc, NoOpTryNoteFilter()) {}
|
||||
};
|
||||
|
||||
static bool HasLiveStackValueAtDepth(JSContext* cx, HandleScript script,
|
||||
jsbytecode* pc, uint32_t stackDepth) {
|
||||
if (!script->hasTrynotes()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t pcOffset = script->pcToOffset(pc);
|
||||
|
||||
for (const JSTryNote& tn : script->trynotes()) {
|
||||
if (pcOffset < tn.start) {
|
||||
continue;
|
||||
}
|
||||
if (pcOffset >= tn.start + tn.length) {
|
||||
continue;
|
||||
}
|
||||
for (TryNoteIterAll tni(cx, script, pc); !tni.done(); ++tni) {
|
||||
const JSTryNote& tn = **tni;
|
||||
|
||||
switch (tn.kind) {
|
||||
case JSTRY_FOR_IN:
|
||||
|
@ -1012,7 +1017,8 @@ static bool InitFromBailout(JSContext* cx, size_t frameNo, HandleFunction fun,
|
|||
// iterators, however, so read them out. They will be closed by
|
||||
// HandleExceptionBaseline.
|
||||
MOZ_ASSERT(cx->realm()->isDebuggee());
|
||||
if (iter.moreFrames() || HasLiveStackValueAtDepth(script, pc, i + 1)) {
|
||||
if (iter.moreFrames() ||
|
||||
HasLiveStackValueAtDepth(cx, script, pc, i + 1)) {
|
||||
v = iter.read();
|
||||
} else {
|
||||
iter.skip();
|
||||
|
|
|
@ -793,7 +793,8 @@ NS_DEFINE_STATIC_IID_ACCESSOR(HttpBaseChannel, HTTP_BASE_CHANNEL_IID)
|
|||
template <class T>
|
||||
class HttpAsyncAborter {
|
||||
public:
|
||||
explicit HttpAsyncAborter(T *derived) : mThis(derived), mCallOnResume(0) {}
|
||||
explicit HttpAsyncAborter(T *derived)
|
||||
: mThis(derived), mCallOnResume(nullptr) {}
|
||||
|
||||
// Aborts channel: calls OnStart/Stop with provided status, removes channel
|
||||
// from loadGroup.
|
||||
|
@ -813,7 +814,7 @@ class HttpAsyncAborter {
|
|||
|
||||
protected:
|
||||
// Function to be called at resume time
|
||||
void (T::*mCallOnResume)(void);
|
||||
std::function<nsresult(T *)> mCallOnResume;
|
||||
};
|
||||
|
||||
template <class T>
|
||||
|
@ -838,7 +839,10 @@ inline void HttpAsyncAborter<T>::HandleAsyncAbort() {
|
|||
MOZ_LOG(
|
||||
gHttpLog, LogLevel::Debug,
|
||||
("Waiting until resume to do async notification [this=%p]\n", mThis));
|
||||
mCallOnResume = &T::HandleAsyncAbort;
|
||||
mCallOnResume = [](T *self) {
|
||||
self->HandleAsyncAbort();
|
||||
return NS_OK;
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -2347,9 +2347,17 @@ HttpChannelChild::Resume() {
|
|||
SendResume();
|
||||
}
|
||||
if (mCallOnResume) {
|
||||
rv = AsyncCall(mCallOnResume);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
mCallOnResume = nullptr;
|
||||
nsCOMPtr<nsIEventTarget> neckoTarget = GetNeckoTarget();
|
||||
MOZ_ASSERT(neckoTarget);
|
||||
|
||||
RefPtr<HttpChannelChild> self = this;
|
||||
std::function<nsresult(HttpChannelChild*)> callOnResume = nullptr;
|
||||
std::swap(callOnResume, mCallOnResume);
|
||||
rv = neckoTarget->Dispatch(
|
||||
NS_NewRunnableFunction(
|
||||
"net::HttpChannelChild::mCallOnResume",
|
||||
[callOnResume, self{std::move(self)}]() { callOnResume(self); }),
|
||||
NS_DISPATCH_NORMAL);
|
||||
}
|
||||
}
|
||||
if (mSynthesizedResponsePump) {
|
||||
|
@ -3813,7 +3821,7 @@ HttpChannelChild::LogMimeTypeMismatch(const nsACString& aMessageName,
|
|||
|
||||
nsAutoString url(aURL);
|
||||
nsAutoString contentType(aContentType);
|
||||
const char16_t* params[] = { url.get(), contentType.get() };
|
||||
const char16_t* params[] = {url.get(), contentType.get()};
|
||||
nsContentUtils::ReportToConsole(
|
||||
aWarning ? nsIScriptError::warningFlag : nsIScriptError::errorFlag,
|
||||
NS_LITERAL_CSTRING("MIMEMISMATCH"), doc,
|
||||
|
|
|
@ -439,7 +439,10 @@ nsresult nsHttpChannel::PrepareToConnect() {
|
|||
// We abandon the connection here if there was one.
|
||||
LOG(("Waiting until resume OnBeforeConnect [this=%p]\n", this));
|
||||
MOZ_ASSERT(!mCallOnResume);
|
||||
mCallOnResume = &nsHttpChannel::HandleOnBeforeConnect;
|
||||
mCallOnResume = [](nsHttpChannel *self) {
|
||||
self->HandleOnBeforeConnect();
|
||||
return NS_OK;
|
||||
};
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
@ -454,7 +457,10 @@ void nsHttpChannel::HandleContinueCancelledByTrackingProtection() {
|
|||
("Waiting until resume HandleContinueCancelledByTrackingProtection "
|
||||
"[this=%p]\n",
|
||||
this));
|
||||
mCallOnResume = &nsHttpChannel::HandleContinueCancelledByTrackingProtection;
|
||||
mCallOnResume = [](nsHttpChannel *self) {
|
||||
self->HandleContinueCancelledByTrackingProtection();
|
||||
return NS_OK;
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -469,7 +475,10 @@ void nsHttpChannel::HandleOnBeforeConnect() {
|
|||
|
||||
if (mSuspendCount) {
|
||||
LOG(("Waiting until resume OnBeforeConnect [this=%p]\n", this));
|
||||
mCallOnResume = &nsHttpChannel::HandleOnBeforeConnect;
|
||||
mCallOnResume = [](nsHttpChannel *self) {
|
||||
self->HandleOnBeforeConnect();
|
||||
return NS_OK;
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -593,7 +602,10 @@ nsresult nsHttpChannel::OnBeforeConnect() {
|
|||
// We abandon the connection here if there was one.
|
||||
LOG(("Waiting until resume OnBeforeConnect [this=%p]\n", this));
|
||||
MOZ_ASSERT(!mCallOnResume);
|
||||
mCallOnResume = &nsHttpChannel::OnBeforeConnectContinue;
|
||||
mCallOnResume = [](nsHttpChannel *self) {
|
||||
self->OnBeforeConnectContinue();
|
||||
return NS_OK;
|
||||
};
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
@ -606,7 +618,10 @@ void nsHttpChannel::OnBeforeConnectContinue() {
|
|||
|
||||
if (mSuspendCount) {
|
||||
LOG(("Waiting until resume OnBeforeConnect [this=%p]\n", this));
|
||||
mCallOnResume = &nsHttpChannel::OnBeforeConnectContinue;
|
||||
mCallOnResume = [](nsHttpChannel *self) {
|
||||
self->OnBeforeConnectContinue();
|
||||
return NS_OK;
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -774,14 +789,31 @@ nsresult nsHttpChannel::ContinueConnect() {
|
|||
}
|
||||
|
||||
// hit the net...
|
||||
return DoConnect();
|
||||
}
|
||||
|
||||
nsresult nsHttpChannel::DoConnect(nsAHttpConnection *aConn) {
|
||||
LOG(("nsHttpChannel::DoConnect [this=%p]\n", this));
|
||||
|
||||
nsresult rv = SetupTransaction();
|
||||
if (NS_FAILED(rv)) return rv;
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
// transfer ownership of connection to transaction
|
||||
if (aConn) {
|
||||
mTransaction->SetConnection(aConn);
|
||||
}
|
||||
|
||||
rv = gHttpHandler->InitiateTransaction(mTransaction, mPriority);
|
||||
if (NS_FAILED(rv)) return rv;
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = mTransactionPump->AsyncRead(this, nullptr);
|
||||
if (NS_FAILED(rv)) return rv;
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
uint32_t suspendCount = mSuspendCount;
|
||||
if (mAsyncResumePending) {
|
||||
|
@ -852,7 +884,10 @@ void nsHttpChannel::HandleAsyncRedirect() {
|
|||
|
||||
if (mSuspendCount) {
|
||||
LOG(("Waiting until resume to do async redirect [this=%p]\n", this));
|
||||
mCallOnResume = &nsHttpChannel::HandleAsyncRedirect;
|
||||
mCallOnResume = [](nsHttpChannel *self) {
|
||||
self->HandleAsyncRedirect();
|
||||
return NS_OK;
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -918,7 +953,10 @@ void nsHttpChannel::HandleAsyncNotModified() {
|
|||
|
||||
if (mSuspendCount) {
|
||||
LOG(("Waiting until resume to do async not-modified [this=%p]\n", this));
|
||||
mCallOnResume = &nsHttpChannel::HandleAsyncNotModified;
|
||||
mCallOnResume = [](nsHttpChannel *self) {
|
||||
self->HandleAsyncNotModified();
|
||||
return NS_OK;
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -938,7 +976,10 @@ void nsHttpChannel::HandleAsyncFallback() {
|
|||
|
||||
if (mSuspendCount) {
|
||||
LOG(("Waiting until resume to do async fallback [this=%p]\n", this));
|
||||
mCallOnResume = &nsHttpChannel::HandleAsyncFallback;
|
||||
mCallOnResume = [](nsHttpChannel *self) {
|
||||
self->HandleAsyncFallback();
|
||||
return NS_OK;
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2322,7 +2363,10 @@ nsresult nsHttpChannel::ContinueProcessResponse1() {
|
|||
if (mSuspendCount) {
|
||||
LOG(("Waiting until resume to finish processing response [this=%p]\n",
|
||||
this));
|
||||
mCallOnResume = &nsHttpChannel::AsyncContinueProcessResponse;
|
||||
mCallOnResume = [](nsHttpChannel *self) {
|
||||
self->AsyncContinueProcessResponse();
|
||||
return NS_OK;
|
||||
};
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
@ -2416,9 +2460,6 @@ nsresult nsHttpChannel::ContinueProcessResponse2(nsresult rv) {
|
|||
|
||||
uint32_t httpStatus = mResponseHead->Status();
|
||||
|
||||
bool successfulReval = false;
|
||||
bool partialContentUsed = false;
|
||||
|
||||
// handle different server response categories. Note that we handle
|
||||
// caching or not caching of error pages in
|
||||
// nsHttpResponseHead::MustValidate; if you change this switch, update that
|
||||
|
@ -2442,10 +2483,16 @@ nsresult nsHttpChannel::ContinueProcessResponse2(nsresult rv) {
|
|||
break;
|
||||
case 206:
|
||||
if (mCachedContentIsPartial) { // an internal byte range request...
|
||||
rv = ProcessPartialContent();
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
partialContentUsed = true;
|
||||
auto func = [](auto *self, nsresult aRv) {
|
||||
return self->ContinueProcessResponseAfterPartialContent(aRv);
|
||||
};
|
||||
rv = ProcessPartialContent(func);
|
||||
// Directly call ContinueProcessResponseAfterPartialContent if channel
|
||||
// is not suspended or ProcessPartialContent throws.
|
||||
if (!mSuspendCount || NS_FAILED(rv)) {
|
||||
return ContinueProcessResponseAfterPartialContent(rv);
|
||||
}
|
||||
return NS_OK;
|
||||
} else {
|
||||
mCacheInputStream.CloseAndRelease();
|
||||
rv = ProcessNormal();
|
||||
|
@ -2480,29 +2527,16 @@ nsresult nsHttpChannel::ContinueProcessResponse2(nsresult rv) {
|
|||
break;
|
||||
case 304:
|
||||
if (!ShouldBypassProcessNotModified()) {
|
||||
rv = ProcessNotModified();
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
successfulReval = true;
|
||||
break;
|
||||
}
|
||||
|
||||
LOG(("ProcessNotModified failed [rv=%" PRIx32 "]\n",
|
||||
static_cast<uint32_t>(rv)));
|
||||
|
||||
// We cannot read from the cache entry, it might be in an
|
||||
// incosistent state. Doom it and redirect the channel
|
||||
// to the same URI to reload from the network.
|
||||
mCacheInputStream.CloseAndRelease();
|
||||
if (mCacheEntry) {
|
||||
mCacheEntry->AsyncDoom(nullptr);
|
||||
mCacheEntry = nullptr;
|
||||
}
|
||||
|
||||
rv = StartRedirectChannelToURI(mURI,
|
||||
nsIChannelEventSink::REDIRECT_INTERNAL);
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
return NS_OK;
|
||||
auto func = [](auto *self, nsresult aRv) {
|
||||
return self->ContinueProcessResponseAfterNotModified(aRv);
|
||||
};
|
||||
rv = ProcessNotModified(func);
|
||||
// Directly call ContinueProcessResponseAfterNotModified if channel
|
||||
// is not suspended or ProcessNotModified throws.
|
||||
if (!mSuspendCount || NS_FAILED(rv)) {
|
||||
return ContinueProcessResponseAfterNotModified(rv);
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// Don't cache uninformative 304
|
||||
|
@ -2573,9 +2607,69 @@ nsresult nsHttpChannel::ContinueProcessResponse2(nsresult rv) {
|
|||
break;
|
||||
}
|
||||
|
||||
UpdateCacheDisposition(false, false);
|
||||
return rv;
|
||||
}
|
||||
|
||||
nsresult nsHttpChannel::ContinueProcessResponseAfterPartialContent(
|
||||
nsresult aRv) {
|
||||
LOG(
|
||||
("nsHttpChannel::ContinueProcessResponseAfterPartialContent "
|
||||
"[this=%p, rv=%" PRIx32 "]",
|
||||
this, static_cast<uint32_t>(aRv)));
|
||||
|
||||
UpdateCacheDisposition(false, NS_SUCCEEDED(aRv));
|
||||
return aRv;
|
||||
}
|
||||
|
||||
nsresult nsHttpChannel::ContinueProcessResponseAfterNotModified(nsresult aRv) {
|
||||
LOG(
|
||||
("nsHttpChannel::ContinueProcessResponseAfterNotModified "
|
||||
"[this=%p, rv=%" PRIx32 "]",
|
||||
this, static_cast<uint32_t>(aRv)));
|
||||
|
||||
if (NS_SUCCEEDED(aRv)) {
|
||||
mTransactionReplaced = true;
|
||||
UpdateCacheDisposition(true, false);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
LOG(("ProcessNotModified failed [rv=%" PRIx32 "]\n",
|
||||
static_cast<uint32_t>(aRv)));
|
||||
|
||||
// We cannot read from the cache entry, it might be in an
|
||||
// incosistent state. Doom it and redirect the channel
|
||||
// to the same URI to reload from the network.
|
||||
mCacheInputStream.CloseAndRelease();
|
||||
if (mCacheEntry) {
|
||||
mCacheEntry->AsyncDoom(nullptr);
|
||||
mCacheEntry = nullptr;
|
||||
}
|
||||
|
||||
nsresult rv =
|
||||
StartRedirectChannelToURI(mURI, nsIChannelEventSink::REDIRECT_INTERNAL);
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// Don't cache uninformative 304
|
||||
if (mCustomConditionalRequest) {
|
||||
CloseCacheEntry(false);
|
||||
}
|
||||
|
||||
if (ShouldBypassProcessNotModified() || NS_FAILED(rv)) {
|
||||
rv = ProcessNormal();
|
||||
}
|
||||
|
||||
UpdateCacheDisposition(false, false);
|
||||
return rv;
|
||||
}
|
||||
|
||||
void nsHttpChannel::UpdateCacheDisposition(bool aSuccessfulReval,
|
||||
bool aPartialContentUsed) {
|
||||
if (mRaceDelay && !mRaceCacheWithNetwork &&
|
||||
(mCachedContentIsPartial || mDidReval)) {
|
||||
if (successfulReval || partialContentUsed) {
|
||||
if (aSuccessfulReval || aPartialContentUsed) {
|
||||
AccumulateCategorical(
|
||||
Telemetry::LABELS_NETWORK_RACE_CACHE_VALIDATION::CachedContentUsed);
|
||||
} else {
|
||||
|
@ -2588,7 +2682,7 @@ nsresult nsHttpChannel::ContinueProcessResponse2(nsresult rv) {
|
|||
CacheDisposition cacheDisposition;
|
||||
if (!mDidReval) {
|
||||
cacheDisposition = kCacheMissed;
|
||||
} else if (successfulReval) {
|
||||
} else if (aSuccessfulReval) {
|
||||
cacheDisposition = kCacheHitViaReval;
|
||||
} else {
|
||||
cacheDisposition = kCacheMissedViaReval;
|
||||
|
@ -2612,7 +2706,6 @@ nsresult nsHttpChannel::ContinueProcessResponse2(nsresult rv) {
|
|||
Telemetry::Accumulate(Telemetry::HTTP_09_INFO, v09Info);
|
||||
}
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
nsresult nsHttpChannel::ContinueProcessResponse3(nsresult rv) {
|
||||
|
@ -2817,7 +2910,10 @@ void nsHttpChannel::HandleAsyncRedirectChannelToHttps() {
|
|||
if (mSuspendCount) {
|
||||
LOG(("Waiting until resume to do async redirect to https [this=%p]\n",
|
||||
this));
|
||||
mCallOnResume = &nsHttpChannel::HandleAsyncRedirectChannelToHttps;
|
||||
mCallOnResume = [](nsHttpChannel *self) {
|
||||
self->HandleAsyncRedirectChannelToHttps();
|
||||
return NS_OK;
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2849,7 +2945,10 @@ void nsHttpChannel::HandleAsyncAPIRedirect() {
|
|||
|
||||
if (mSuspendCount) {
|
||||
LOG(("Waiting until resume to do async API redirect [this=%p]\n", this));
|
||||
mCallOnResume = &nsHttpChannel::HandleAsyncAPIRedirect;
|
||||
mCallOnResume = [](nsHttpChannel *self) {
|
||||
self->HandleAsyncAPIRedirect();
|
||||
return NS_OK;
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -3217,7 +3316,9 @@ void nsHttpChannel::UntieByteRangeRequest() {
|
|||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||||
}
|
||||
|
||||
nsresult nsHttpChannel::ProcessPartialContent() {
|
||||
nsresult nsHttpChannel::ProcessPartialContent(
|
||||
const std::function<nsresult(nsHttpChannel *, nsresult)>
|
||||
&aContinueProcessResponseFunc) {
|
||||
// ok, we've just received a 206
|
||||
//
|
||||
// we need to stream whatever data is in the cache out first, and then
|
||||
|
@ -3317,15 +3418,16 @@ nsresult nsHttpChannel::ProcessPartialContent() {
|
|||
// to prevent duplicate OnStartRequest call on the target listener
|
||||
// in case this channel is canceled before it gets its OnStartRequest
|
||||
// from the http transaction.
|
||||
|
||||
// Now we continue reading the network response.
|
||||
} else {
|
||||
// the cached content is valid, although incomplete.
|
||||
mCachedContentIsValid = true;
|
||||
rv = ReadFromCache(false);
|
||||
return rv;
|
||||
}
|
||||
|
||||
return rv;
|
||||
// Now we continue reading the network response.
|
||||
// the cached content is valid, although incomplete.
|
||||
mCachedContentIsValid = true;
|
||||
return CallOrWaitForResume([aContinueProcessResponseFunc](auto *self) {
|
||||
nsresult rv = self->ReadFromCache(false);
|
||||
return aContinueProcessResponseFunc(self, rv);
|
||||
});
|
||||
}
|
||||
|
||||
nsresult nsHttpChannel::OnDoneReadingPartialCacheEntry(bool *streamDone) {
|
||||
|
@ -3391,7 +3493,9 @@ bool nsHttpChannel::ShouldBypassProcessNotModified() {
|
|||
return false;
|
||||
}
|
||||
|
||||
nsresult nsHttpChannel::ProcessNotModified() {
|
||||
nsresult nsHttpChannel::ProcessNotModified(
|
||||
const std::function<nsresult(nsHttpChannel *, nsresult)>
|
||||
&aContinueProcessResponseFunc) {
|
||||
nsresult rv;
|
||||
|
||||
LOG(("nsHttpChannel::ProcessNotModified [this=%p]\n", this));
|
||||
|
@ -3461,11 +3565,10 @@ nsresult nsHttpChannel::ProcessNotModified() {
|
|||
rv = mCacheEntry->SetValid();
|
||||
if (NS_FAILED(rv)) return rv;
|
||||
|
||||
rv = ReadFromCache(false);
|
||||
if (NS_FAILED(rv)) return rv;
|
||||
|
||||
mTransactionReplaced = true;
|
||||
return NS_OK;
|
||||
return CallOrWaitForResume([aContinueProcessResponseFunc](auto *self) {
|
||||
nsresult rv = self->ReadFromCache(false);
|
||||
return aContinueProcessResponseFunc(self, rv);
|
||||
});
|
||||
}
|
||||
|
||||
nsresult nsHttpChannel::ProcessFallback(bool *waitingForRedirectCallback) {
|
||||
|
@ -5915,7 +6018,10 @@ nsHttpChannel::CancelForTrackingProtection() {
|
|||
LOG(("Waiting until resume in Cancel [this=%p]\n", this));
|
||||
MOZ_ASSERT(!mCallOnResume);
|
||||
mTrackingProtectionCancellationPending = 1;
|
||||
mCallOnResume = &nsHttpChannel::HandleContinueCancelledByTrackingProtection;
|
||||
mCallOnResume = [](nsHttpChannel *self) {
|
||||
self->HandleContinueCancelledByTrackingProtection();
|
||||
return NS_OK;
|
||||
};
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
@ -6498,12 +6604,6 @@ nsresult nsHttpChannel::BeginConnectActual() {
|
|||
("Waiting for tracking protection cancellation in BeginConnectActual "
|
||||
"[this=%p]\n",
|
||||
this));
|
||||
MOZ_ASSERT(
|
||||
!mCallOnResume ||
|
||||
mCallOnResume ==
|
||||
&nsHttpChannel::HandleContinueCancelledByTrackingProtection,
|
||||
"We should be paused waiting for cancellation from tracking "
|
||||
"protection");
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
@ -6645,7 +6745,10 @@ nsresult nsHttpChannel::ContinueBeginConnectWithResult() {
|
|||
|
||||
if (mSuspendCount) {
|
||||
LOG(("Waiting until resume to do async connect [this=%p]\n", this));
|
||||
mCallOnResume = &nsHttpChannel::ContinueBeginConnect;
|
||||
mCallOnResume = [](nsHttpChannel *self) {
|
||||
self->ContinueBeginConnect();
|
||||
return NS_OK;
|
||||
};
|
||||
rv = NS_OK;
|
||||
} else if (mCanceled) {
|
||||
// We may have been cancelled already, by nsChannelClassifier in that
|
||||
|
@ -7414,56 +7517,93 @@ nsHttpChannel::OnStopRequest(nsIRequest *request, nsISupports *ctxt,
|
|||
// handle auth retry...
|
||||
if (authRetry) {
|
||||
mAuthRetryPending = false;
|
||||
status = DoAuthRetry(conn);
|
||||
if (NS_SUCCEEDED(status)) return NS_OK;
|
||||
}
|
||||
|
||||
// If DoAuthRetry failed, or if we have been cancelled since showing
|
||||
// the auth. dialog, then we need to send OnStartRequest now
|
||||
if (authRetry || (mAuthRetryPending && NS_FAILED(status))) {
|
||||
MOZ_ASSERT(NS_FAILED(status), "should have a failure code here");
|
||||
// NOTE: since we have a failure status, we can ignore the return
|
||||
// value from onStartRequest.
|
||||
LOG((" calling mListener->OnStartRequest [this=%p, listener=%p]\n", this,
|
||||
mListener.get()));
|
||||
if (mListener) {
|
||||
MOZ_ASSERT(!mOnStartRequestCalled,
|
||||
"We should not call OnStartRequest twice.");
|
||||
mListener->OnStartRequest(this, mListenerContext);
|
||||
mOnStartRequestCalled = true;
|
||||
} else {
|
||||
NS_WARNING("OnStartRequest skipped because of null listener");
|
||||
auto continueOSR = [authRetry, isFromNet, contentComplete,
|
||||
stickyConn{std::move(stickyConn)}](auto *self,
|
||||
nsresult aStatus) {
|
||||
return self->ContinueOnStopRequestAfterAuthRetry(
|
||||
aStatus, authRetry, isFromNet, contentComplete, stickyConn);
|
||||
};
|
||||
status = DoAuthRetry(conn, continueOSR);
|
||||
if (NS_SUCCEEDED(status)) {
|
||||
return NS_OK;
|
||||
}
|
||||
}
|
||||
return ContinueOnStopRequestAfterAuthRetry(status, authRetry, isFromNet,
|
||||
contentComplete, stickyConn);
|
||||
}
|
||||
|
||||
// if this transaction has been replaced, then bail.
|
||||
if (mTransactionReplaced) {
|
||||
LOG(("Transaction replaced\n"));
|
||||
// This was just the network check for a 304 response.
|
||||
mFirstResponseSource = RESPONSE_PENDING;
|
||||
return NS_OK;
|
||||
}
|
||||
return ContinueOnStopRequest(status, isFromNet, contentComplete);
|
||||
}
|
||||
|
||||
bool upgradeWebsocket = mUpgradeProtocolCallback && stickyConn &&
|
||||
mResponseHead &&
|
||||
((mResponseHead->Status() == 101 &&
|
||||
mResponseHead->Version() == HttpVersion::v1_1) ||
|
||||
(mResponseHead->Status() == 200 &&
|
||||
mResponseHead->Version() == HttpVersion::v2_0));
|
||||
nsresult nsHttpChannel::ContinueOnStopRequestAfterAuthRetry(
|
||||
nsresult aStatus, bool aAuthRetry, bool aIsFromNet, bool aContentComplete,
|
||||
nsAHttpConnection *aStickyConn) {
|
||||
LOG(
|
||||
("nsHttpChannel::ContinueOnStopRequestAfterAuthRetry "
|
||||
"[this=%p, aStatus=%" PRIx32
|
||||
" aAuthRetry=%d, aIsFromNet=%d, aStickyConn=%p]\n",
|
||||
this, static_cast<uint32_t>(aStatus), aAuthRetry, aIsFromNet,
|
||||
aStickyConn));
|
||||
|
||||
bool upgradeConnect = mUpgradeProtocolCallback && stickyConn &&
|
||||
(mCaps & NS_HTTP_CONNECT_ONLY) && mResponseHead &&
|
||||
mResponseHead->Status() == 200;
|
||||
if (aAuthRetry && NS_SUCCEEDED(aStatus)) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
if (upgradeWebsocket || upgradeConnect) {
|
||||
nsresult rv = gHttpHandler->ConnMgr()->CompleteUpgrade(
|
||||
stickyConn, mUpgradeProtocolCallback);
|
||||
if (NS_FAILED(rv)) {
|
||||
LOG((" CompleteUpgrade failed with %08x", static_cast<uint32_t>(rv)));
|
||||
}
|
||||
// If DoAuthRetry failed, or if we have been cancelled since showing
|
||||
// the auth. dialog, then we need to send OnStartRequest now
|
||||
if (aAuthRetry || (mAuthRetryPending && NS_FAILED(aStatus))) {
|
||||
MOZ_ASSERT(NS_FAILED(aStatus), "should have a failure code here");
|
||||
// NOTE: since we have a failure status, we can ignore the return
|
||||
// value from onStartRequest.
|
||||
LOG((" calling mListener->OnStartRequest [this=%p, listener=%p]\n", this,
|
||||
mListener.get()));
|
||||
if (mListener) {
|
||||
MOZ_ASSERT(!mOnStartRequestCalled,
|
||||
"We should not call OnStartRequest twice.");
|
||||
mListener->OnStartRequest(this, mListenerContext);
|
||||
mOnStartRequestCalled = true;
|
||||
} else {
|
||||
NS_WARNING("OnStartRequest skipped because of null listener");
|
||||
}
|
||||
}
|
||||
|
||||
// if this transaction has been replaced, then bail.
|
||||
if (mTransactionReplaced) {
|
||||
LOG(("Transaction replaced\n"));
|
||||
// This was just the network check for a 304 response.
|
||||
mFirstResponseSource = RESPONSE_PENDING;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
bool upgradeWebsocket = mUpgradeProtocolCallback && aStickyConn &&
|
||||
mResponseHead &&
|
||||
((mResponseHead->Status() == 101 &&
|
||||
mResponseHead->Version() == HttpVersion::v1_1) ||
|
||||
(mResponseHead->Status() == 200 &&
|
||||
mResponseHead->Version() == HttpVersion::v2_0));
|
||||
|
||||
bool upgradeConnect = mUpgradeProtocolCallback && aStickyConn &&
|
||||
(mCaps & NS_HTTP_CONNECT_ONLY) && mResponseHead &&
|
||||
mResponseHead->Status() == 200;
|
||||
|
||||
if (upgradeWebsocket || upgradeConnect) {
|
||||
nsresult rv = gHttpHandler->ConnMgr()->CompleteUpgrade(
|
||||
aStickyConn, mUpgradeProtocolCallback);
|
||||
if (NS_FAILED(rv)) {
|
||||
LOG((" CompleteUpgrade failed with %08x", static_cast<uint32_t>(rv)));
|
||||
}
|
||||
}
|
||||
|
||||
return ContinueOnStopRequest(aStatus, aIsFromNet, aContentComplete);
|
||||
}
|
||||
|
||||
nsresult nsHttpChannel::ContinueOnStopRequest(nsresult aStatus, bool aIsFromNet,
|
||||
bool aContentComplete) {
|
||||
LOG(
|
||||
("nsHttpChannel::ContinueOnStopRequest "
|
||||
"[this=%p aStatus=%" PRIx32 ", aIsFromNet=%d]\n",
|
||||
this, static_cast<uint32_t>(aStatus), aIsFromNet));
|
||||
|
||||
// HTTP_CHANNEL_DISPOSITION TELEMETRY
|
||||
enum ChannelDisposition {
|
||||
kHttpCanceled = 0,
|
||||
|
@ -7492,7 +7632,7 @@ nsHttpChannel::OnStopRequest(nsIRequest *request, nsISupports *ctxt,
|
|||
chanDisposition = kHttpDisk;
|
||||
upgradeChanDisposition =
|
||||
Telemetry::LABELS_HTTP_CHANNEL_DISPOSITION_UPGRADE::disk;
|
||||
} else if (NS_SUCCEEDED(status) && mResponseHead &&
|
||||
} else if (NS_SUCCEEDED(aStatus) && mResponseHead &&
|
||||
mResponseHead->Version() != HttpVersion::v0_9) {
|
||||
chanDisposition = kHttpNetOK;
|
||||
upgradeChanDisposition =
|
||||
|
@ -7540,7 +7680,7 @@ nsHttpChannel::OnStopRequest(nsIRequest *request, nsISupports *ctxt,
|
|||
Telemetry::Accumulate(Telemetry::HTTP_CHANNEL_DISPOSITION, chanDisposition);
|
||||
|
||||
// if needed, check cache entry has all data we expect
|
||||
if (mCacheEntry && mCachePump && mConcurrentCacheAccess && contentComplete) {
|
||||
if (mCacheEntry && mCachePump && mConcurrentCacheAccess && aContentComplete) {
|
||||
int64_t size, contentLength;
|
||||
nsresult rv = CheckPartial(mCacheEntry, &size, &contentLength);
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
|
@ -7571,7 +7711,7 @@ nsHttpChannel::OnStopRequest(nsIRequest *request, nsISupports *ctxt,
|
|||
}
|
||||
LOG((" but range request perform failed 0x%08" PRIx32,
|
||||
static_cast<uint32_t>(rv)));
|
||||
status = NS_ERROR_NET_INTERRUPT;
|
||||
aStatus = NS_ERROR_NET_INTERRUPT;
|
||||
} else {
|
||||
LOG((" but range request setup failed rv=0x%08" PRIx32
|
||||
", failing load",
|
||||
|
@ -7582,7 +7722,7 @@ nsHttpChannel::OnStopRequest(nsIRequest *request, nsISupports *ctxt,
|
|||
}
|
||||
|
||||
mIsPending = false;
|
||||
mStatus = status;
|
||||
mStatus = aStatus;
|
||||
|
||||
// perform any final cache operations before we close the cache entry.
|
||||
if (mCacheEntry && mRequestTimeInitialized) {
|
||||
|
@ -7598,7 +7738,7 @@ nsHttpChannel::OnStopRequest(nsIRequest *request, nsISupports *ctxt,
|
|||
}
|
||||
}
|
||||
|
||||
ReportRcwnStats(isFromNet);
|
||||
ReportRcwnStats(aIsFromNet);
|
||||
|
||||
// Register entry to the PerformanceStorage resource timing
|
||||
MaybeReportTimingData();
|
||||
|
@ -7608,7 +7748,7 @@ nsHttpChannel::OnStopRequest(nsIRequest *request, nsISupports *ctxt,
|
|||
MOZ_ASSERT(mOnStartRequestCalled,
|
||||
"OnStartRequest should be called before OnStopRequest");
|
||||
MOZ_ASSERT(!mOnStopRequestCalled, "We should not call OnStopRequest twice");
|
||||
mListener->OnStopRequest(this, mListenerContext, status);
|
||||
mListener->OnStopRequest(this, mListenerContext, aStatus);
|
||||
mOnStopRequestCalled = true;
|
||||
}
|
||||
|
||||
|
@ -7625,11 +7765,13 @@ nsHttpChannel::OnStopRequest(nsIRequest *request, nsISupports *ctxt,
|
|||
mAltDataCacheEntry = mCacheEntry;
|
||||
}
|
||||
|
||||
CloseCacheEntry(!contentComplete);
|
||||
CloseCacheEntry(!aContentComplete);
|
||||
|
||||
if (mOfflineCacheEntry) CloseOfflineCacheEntry();
|
||||
|
||||
if (mLoadGroup) mLoadGroup->RemoveRequest(this, nullptr, status);
|
||||
if (mLoadGroup) {
|
||||
mLoadGroup->RemoveRequest(this, nullptr, aStatus);
|
||||
}
|
||||
|
||||
// We don't need this info anymore
|
||||
CleanRedirectCacheChainIfNecessary();
|
||||
|
@ -8195,15 +8337,19 @@ nsHttpChannel::ResumeAt(uint64_t aStartPos, const nsACString &aEntityID) {
|
|||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult nsHttpChannel::DoAuthRetry(nsAHttpConnection *conn) {
|
||||
nsresult nsHttpChannel::DoAuthRetry(
|
||||
nsAHttpConnection *conn,
|
||||
const std::function<nsresult(nsHttpChannel *, nsresult)>
|
||||
&aContinueOnStopRequestFunc) {
|
||||
LOG(("nsHttpChannel::DoAuthRetry [this=%p]\n", this));
|
||||
|
||||
MOZ_ASSERT(!mTransaction, "should not have a transaction");
|
||||
nsresult rv;
|
||||
|
||||
// toggle mIsPending to allow nsIObserver implementations to modify
|
||||
// the request headers (bug 95044).
|
||||
mIsPending = false;
|
||||
// Note that we don't have to toggle |mIsPending| anymore. See the reasons
|
||||
// below.
|
||||
// 1. We can't suspend the channel during "http-on-modify-request"
|
||||
// when |mIsPending| is false.
|
||||
// 2. We don't check |mIsPending| in SetRequestHeader now.
|
||||
|
||||
// Reset mRequestObserversCalled because we've probably called the request
|
||||
// observers once already.
|
||||
|
@ -8218,6 +8364,19 @@ nsresult nsHttpChannel::DoAuthRetry(nsAHttpConnection *conn) {
|
|||
// notify "http-on-modify-request" observers
|
||||
CallOnModifyRequestObservers();
|
||||
|
||||
RefPtr<nsAHttpConnection> connRef(conn);
|
||||
return CallOrWaitForResume(
|
||||
[conn{std::move(connRef)}, aContinueOnStopRequestFunc](auto *self) {
|
||||
return self->ContinueDoAuthRetry(conn, aContinueOnStopRequestFunc);
|
||||
});
|
||||
}
|
||||
|
||||
nsresult nsHttpChannel::ContinueDoAuthRetry(
|
||||
nsAHttpConnection *aConn,
|
||||
const std::function<nsresult(nsHttpChannel *, nsresult)>
|
||||
&aContinueOnStopRequestFunc) {
|
||||
LOG(("nsHttpChannel::ContinueDoAuthRetry [this=%p]\n", this));
|
||||
|
||||
mIsPending = true;
|
||||
|
||||
// get rid of the old response headers
|
||||
|
@ -8226,7 +8385,9 @@ nsresult nsHttpChannel::DoAuthRetry(nsAHttpConnection *conn) {
|
|||
// rewind the upload stream
|
||||
if (mUploadStream) {
|
||||
nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(mUploadStream);
|
||||
if (seekable) seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0);
|
||||
if (seekable) {
|
||||
seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// always set sticky connection flag
|
||||
|
@ -8241,32 +8402,15 @@ nsresult nsHttpChannel::DoAuthRetry(nsAHttpConnection *conn) {
|
|||
mCaps &= ~NS_HTTP_CONNECTION_RESTARTABLE;
|
||||
}
|
||||
|
||||
// and create a new one...
|
||||
rv = SetupTransaction();
|
||||
if (NS_FAILED(rv)) return rv;
|
||||
// notify "http-on-before-connect" observers
|
||||
gHttpHandler->OnBeforeConnect(this);
|
||||
|
||||
// transfer ownership of connection to transaction
|
||||
if (conn) mTransaction->SetConnection(conn);
|
||||
|
||||
rv = gHttpHandler->InitiateTransaction(mTransaction, mPriority);
|
||||
if (NS_FAILED(rv)) return rv;
|
||||
|
||||
rv = mTransactionPump->AsyncRead(this, nullptr);
|
||||
if (NS_FAILED(rv)) return rv;
|
||||
|
||||
uint32_t suspendCount = mSuspendCount;
|
||||
if (mAsyncResumePending) {
|
||||
LOG(
|
||||
(" Suspend()'ing transaction pump once because of async resume pending"
|
||||
", sc=%u, pump=%p, this=%p",
|
||||
suspendCount, mTransactionPump.get(), this));
|
||||
++suspendCount;
|
||||
}
|
||||
while (suspendCount--) {
|
||||
mTransactionPump->Suspend();
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
RefPtr<nsAHttpConnection> connRef(aConn);
|
||||
return CallOrWaitForResume(
|
||||
[conn{std::move(connRef)}, aContinueOnStopRequestFunc](auto *self) {
|
||||
nsresult rv = self->DoConnect(conn);
|
||||
return aContinueOnStopRequestFunc(self, rv);
|
||||
});
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
@ -8809,6 +8953,23 @@ nsHttpChannel::SuspendInternal() {
|
|||
return NS_FAILED(rvTransaction) ? rvTransaction : rvCache;
|
||||
}
|
||||
|
||||
nsresult nsHttpChannel::CallOrWaitForResume(
|
||||
const std::function<nsresult(nsHttpChannel *)> &aFunc) {
|
||||
if (mCanceled) {
|
||||
MOZ_ASSERT(NS_FAILED(mStatus));
|
||||
return mStatus;
|
||||
}
|
||||
|
||||
if (mSuspendCount) {
|
||||
LOG(("Waiting until resume [this=%p]\n", this));
|
||||
MOZ_ASSERT(!mCallOnResume);
|
||||
mCallOnResume = aFunc;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
return aFunc(this);
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsHttpChannel::ResumeInternal() {
|
||||
NS_ENSURE_TRUE(mSuspendCount > 0, NS_ERROR_UNEXPECTED);
|
||||
|
@ -8828,8 +8989,8 @@ nsHttpChannel::ResumeInternal() {
|
|||
MOZ_ASSERT(!mAsyncResumePending);
|
||||
mAsyncResumePending = 1;
|
||||
|
||||
auto const callOnResume = mCallOnResume;
|
||||
mCallOnResume = nullptr;
|
||||
std::function<nsresult(nsHttpChannel *)> callOnResume = nullptr;
|
||||
std::swap(callOnResume, mCallOnResume);
|
||||
|
||||
RefPtr<nsHttpChannel> self(this);
|
||||
RefPtr<nsInputStreamPump> transactionPump = mTransactionPump;
|
||||
|
@ -8837,11 +8998,15 @@ nsHttpChannel::ResumeInternal() {
|
|||
|
||||
nsresult rv = NS_DispatchToCurrentThread(NS_NewRunnableFunction(
|
||||
"nsHttpChannel::CallOnResume",
|
||||
[callOnResume, self{std::move(self)},
|
||||
[callOnResume{std::move(callOnResume)}, self{std::move(self)},
|
||||
transactionPump{std::move(transactionPump)},
|
||||
cachePump{std::move(cachePump)}]() {
|
||||
MOZ_ASSERT(self->mAsyncResumePending);
|
||||
(self->*callOnResume)();
|
||||
nsresult rv = self->CallOrWaitForResume(callOnResume);
|
||||
if (NS_FAILED(rv)) {
|
||||
self->CloseCacheEntry(false);
|
||||
Unused << self->AsyncAbort(rv);
|
||||
}
|
||||
MOZ_ASSERT(self->mAsyncResumePending);
|
||||
|
||||
self->mAsyncResumePending = 0;
|
||||
|
|
|
@ -301,6 +301,12 @@ class nsHttpChannel final : public HttpBaseChannel,
|
|||
private:
|
||||
typedef nsresult (nsHttpChannel::*nsContinueRedirectionFunc)(nsresult result);
|
||||
|
||||
// Directly call |aFunc| if the channel is not canceled and not suspended.
|
||||
// Otherwise, set |aFunc| to |mCallOnResume| and wait until the channel
|
||||
// resumes.
|
||||
nsresult CallOrWaitForResume(
|
||||
const std::function<nsresult(nsHttpChannel *)> &aFunc);
|
||||
|
||||
bool RequestIsConditional();
|
||||
void HandleContinueCancelledByTrackingProtection();
|
||||
nsresult CancelInternal(nsresult status);
|
||||
|
@ -331,12 +337,17 @@ class nsHttpChannel final : public HttpBaseChannel,
|
|||
void AsyncContinueProcessResponse();
|
||||
MOZ_MUST_USE nsresult ContinueProcessResponse1();
|
||||
MOZ_MUST_USE nsresult ContinueProcessResponse2(nsresult);
|
||||
void UpdateCacheDisposition(bool aSuccessfulReval, bool aPartialContentUsed);
|
||||
MOZ_MUST_USE nsresult ContinueProcessResponse3(nsresult);
|
||||
MOZ_MUST_USE nsresult ProcessNormal();
|
||||
MOZ_MUST_USE nsresult ContinueProcessNormal(nsresult);
|
||||
void ProcessAltService();
|
||||
bool ShouldBypassProcessNotModified();
|
||||
MOZ_MUST_USE nsresult ProcessNotModified();
|
||||
MOZ_MUST_USE nsresult ProcessNotModified(
|
||||
const std::function<nsresult(nsHttpChannel *, nsresult)>
|
||||
&aContinueProcessResponseFunc);
|
||||
MOZ_MUST_USE nsresult ContinueProcessResponseAfterNotModified(nsresult aRv);
|
||||
|
||||
MOZ_MUST_USE nsresult AsyncProcessRedirection(uint32_t httpStatus);
|
||||
MOZ_MUST_USE nsresult ContinueProcessRedirection(nsresult);
|
||||
MOZ_MUST_USE nsresult ContinueProcessRedirectionAfterFallback(nsresult);
|
||||
|
@ -408,10 +419,25 @@ class nsHttpChannel final : public HttpBaseChannel,
|
|||
void ClearBogusContentEncodingIfNeeded();
|
||||
|
||||
// byte range request specific methods
|
||||
MOZ_MUST_USE nsresult ProcessPartialContent();
|
||||
MOZ_MUST_USE nsresult ProcessPartialContent(
|
||||
const std::function<nsresult(nsHttpChannel *, nsresult)>
|
||||
&aContinueProcessResponseFunc);
|
||||
MOZ_MUST_USE nsresult
|
||||
ContinueProcessResponseAfterPartialContent(nsresult aRv);
|
||||
MOZ_MUST_USE nsresult OnDoneReadingPartialCacheEntry(bool *streamDone);
|
||||
|
||||
MOZ_MUST_USE nsresult DoAuthRetry(nsAHttpConnection *);
|
||||
MOZ_MUST_USE nsresult
|
||||
DoAuthRetry(nsAHttpConnection *,
|
||||
const std::function<nsresult(nsHttpChannel *, nsresult)> &aOuter);
|
||||
MOZ_MUST_USE nsresult ContinueDoAuthRetry(
|
||||
nsAHttpConnection *aConn,
|
||||
const std::function<nsresult(nsHttpChannel *, nsresult)> &aOuter);
|
||||
MOZ_MUST_USE nsresult DoConnect(nsAHttpConnection *aConn = nullptr);
|
||||
MOZ_MUST_USE nsresult ContinueOnStopRequestAfterAuthRetry(
|
||||
nsresult aStatus, bool aAuthRetry, bool aIsFromNet, bool aContentComplete,
|
||||
nsAHttpConnection *aStickyConn);
|
||||
MOZ_MUST_USE nsresult ContinueOnStopRequest(nsresult status, bool aIsFromNet,
|
||||
bool aContentComplete);
|
||||
|
||||
void HandleAsyncRedirectChannelToHttps();
|
||||
MOZ_MUST_USE nsresult StartRedirectChannelToHttps();
|
||||
|
|
|
@ -0,0 +1,274 @@
|
|||
// This file tests async handling of a channel suspended in DoAuthRetry
|
||||
// notifying http-on-modify-request and http-on-before-connect observers.
|
||||
|
||||
var CC = Components.Constructor;
|
||||
|
||||
ChromeUtils.import("resource://testing-common/httpd.js");
|
||||
ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
|
||||
|
||||
// Turn off the authentication dialog blocking for this test.
|
||||
var prefs = Cc["@mozilla.org/preferences-service;1"].
|
||||
getService(Ci.nsIPrefBranch);
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "URL", function() {
|
||||
return "http://localhost:" + httpserv.identity.primaryPort;
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "PORT", function() {
|
||||
return httpserv.identity.primaryPort;
|
||||
});
|
||||
|
||||
var obs = Cc["@mozilla.org/observer-service;1"]
|
||||
.getService(Ci.nsIObserverService);
|
||||
|
||||
|
||||
var requestObserver = null;
|
||||
|
||||
function AuthPrompt()
|
||||
{}
|
||||
|
||||
AuthPrompt.prototype = {
|
||||
user: "guest",
|
||||
pass: "guest",
|
||||
|
||||
QueryInterface: function authprompt_qi(iid) {
|
||||
if (iid.equals(Ci.nsISupports) ||
|
||||
iid.equals(Ci.nsIAuthPrompt))
|
||||
return this;
|
||||
throw Cr.NS_ERROR_NO_INTERFACE;
|
||||
},
|
||||
|
||||
prompt: function ap1_prompt(title, text, realm, save, defaultText, result) {
|
||||
do_throw("unexpected prompt call");
|
||||
},
|
||||
|
||||
promptUsernameAndPassword:
|
||||
function promptUP(title, text, realm, savePW, user, pw)
|
||||
{
|
||||
user.value = this.user;
|
||||
pw.value = this.pass;
|
||||
|
||||
obs.addObserver(requestObserver, "http-on-before-connect");
|
||||
obs.addObserver(requestObserver, "http-on-modify-request");
|
||||
return true;
|
||||
},
|
||||
|
||||
promptPassword: function promptPW(title, text, realm, save, pwd) {
|
||||
do_throw("unexpected promptPassword call");
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function requestListenerObserver(suspendOnBeforeConnect, suspendOnModifyRequest)
|
||||
{
|
||||
this.suspendOnModifyRequest = suspendOnModifyRequest;
|
||||
this.suspendOnBeforeConnect = suspendOnBeforeConnect;
|
||||
}
|
||||
|
||||
requestListenerObserver.prototype = {
|
||||
suspendOnModifyRequest: false,
|
||||
suspendOnBeforeConnect: false,
|
||||
gotOnBeforeConnect: false,
|
||||
resumeOnBeforeConnect: false,
|
||||
gotOnModifyRequest: false,
|
||||
resumeOnModifyRequest: false,
|
||||
QueryInterface: function queryinterface(iid) {
|
||||
if (iid.equals(Ci.nsISupports) ||
|
||||
iid.equals(Ci.nsIObserver))
|
||||
return this;
|
||||
throw Cr.NS_ERROR_NO_INTERFACE;
|
||||
},
|
||||
|
||||
observe: function(subject, topic, data) {
|
||||
if (topic === "http-on-before-connect" &&
|
||||
subject instanceof Ci.nsIHttpChannel) {
|
||||
if (this.suspendOnBeforeConnect) {
|
||||
var chan = subject.QueryInterface(Ci.nsIHttpChannel);
|
||||
executeSoon(() => {
|
||||
this.resumeOnBeforeConnect = true;
|
||||
chan.resume();
|
||||
});
|
||||
this.gotOnBeforeConnect = true;
|
||||
chan.suspend();
|
||||
}
|
||||
}
|
||||
else if (topic === "http-on-modify-request" &&
|
||||
subject instanceof Ci.nsIHttpChannel) {
|
||||
if (this.suspendOnModifyRequest) {
|
||||
var chan = subject.QueryInterface(Ci.nsIHttpChannel);
|
||||
executeSoon(() => {
|
||||
this.resumeOnModifyRequest = true;
|
||||
chan.resume();
|
||||
});
|
||||
this.gotOnModifyRequest = true;
|
||||
chan.suspend();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function Requestor() {};
|
||||
|
||||
Requestor.prototype = {
|
||||
QueryInterface: function requestor_qi(iid) {
|
||||
if (iid.equals(Ci.nsISupports) ||
|
||||
iid.equals(Ci.nsIInterfaceRequestor))
|
||||
return this;
|
||||
throw Cr.NS_ERROR_NO_INTERFACE;
|
||||
},
|
||||
|
||||
getInterface: function requestor_gi(iid) {
|
||||
if (iid.equals(Ci.nsIAuthPrompt)) {
|
||||
// Allow the prompt to store state by caching it here
|
||||
if (!this.prompt)
|
||||
this.prompt = new AuthPrompt();
|
||||
return this.prompt;
|
||||
}
|
||||
|
||||
throw Cr.NS_ERROR_NO_INTERFACE;
|
||||
},
|
||||
|
||||
prompt: null
|
||||
};
|
||||
|
||||
var listener = {
|
||||
expectedCode: -1, // Uninitialized
|
||||
|
||||
onStartRequest: function test_onStartR(request, ctx) {
|
||||
try {
|
||||
if (!Components.isSuccessCode(request.status))
|
||||
do_throw("Channel should have a success code!");
|
||||
|
||||
if (!(request instanceof Ci.nsIHttpChannel))
|
||||
do_throw("Expecting an HTTP channel");
|
||||
|
||||
Assert.equal(request.responseStatus, this.expectedCode);
|
||||
// The request should be succeeded iff we expect 200
|
||||
Assert.equal(request.requestSucceeded, this.expectedCode == 200);
|
||||
|
||||
} catch (e) {
|
||||
do_throw("Unexpected exception: " + e);
|
||||
}
|
||||
throw Cr.NS_ERROR_ABORT;
|
||||
},
|
||||
|
||||
onDataAvailable: function test_ODA() {
|
||||
do_throw("Should not get any data!");
|
||||
},
|
||||
|
||||
onStopRequest: function test_onStopR(request, ctx, status) {
|
||||
Assert.equal(status, Cr.NS_ERROR_ABORT);
|
||||
if (requestObserver.suspendOnBeforeConnect) {
|
||||
Assert.ok(requestObserver.gotOnBeforeConnect && requestObserver.resumeOnBeforeConnect);
|
||||
}
|
||||
if (requestObserver.suspendOnModifyRequest) {
|
||||
Assert.ok(requestObserver.gotOnModifyRequest && requestObserver.resumeOnModifyRequest);
|
||||
}
|
||||
obs.removeObserver(requestObserver, "http-on-before-connect");
|
||||
obs.removeObserver(requestObserver, "http-on-modify-request");
|
||||
moveToNextTest();
|
||||
}
|
||||
};
|
||||
|
||||
function makeChan(url, loadingUrl) {
|
||||
var ios = Cc["@mozilla.org/network/io-service;1"].
|
||||
getService(Ci.nsIIOService);
|
||||
var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"]
|
||||
.getService(Ci.nsIScriptSecurityManager);
|
||||
var principal = ssm.createCodebasePrincipal(ios.newURI(loadingUrl), {});
|
||||
return NetUtil.newChannel(
|
||||
{ uri: url, loadingPrincipal: principal,
|
||||
securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
|
||||
contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER
|
||||
});
|
||||
}
|
||||
|
||||
var tests = [test_suspend_on_before_connect,
|
||||
test_suspend_on_modify_request,
|
||||
test_suspend_all];
|
||||
|
||||
var current_test = 0;
|
||||
|
||||
var httpserv = null;
|
||||
|
||||
function moveToNextTest() {
|
||||
if (current_test < (tests.length - 1)) {
|
||||
// First, gotta clear the auth cache
|
||||
Cc["@mozilla.org/network/http-auth-manager;1"]
|
||||
.getService(Ci.nsIHttpAuthManager)
|
||||
.clearAll();
|
||||
|
||||
current_test++;
|
||||
tests[current_test]();
|
||||
} else {
|
||||
do_test_pending();
|
||||
httpserv.stop(do_test_finished);
|
||||
}
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
httpserv = new HttpServer();
|
||||
|
||||
httpserv.registerPathHandler("/auth", authHandler);
|
||||
|
||||
httpserv.start(-1);
|
||||
|
||||
tests[0]();
|
||||
}
|
||||
|
||||
function test_suspend_on_auth(suspendOnBeforeConnect, suspendOnModifyRequest) {
|
||||
var chan = makeChan(URL + "/auth", URL);
|
||||
requestObserver =
|
||||
new requestListenerObserver(suspendOnBeforeConnect, suspendOnModifyRequest);
|
||||
chan.notificationCallbacks = new Requestor();
|
||||
listener.expectedCode = 200; // OK
|
||||
chan.asyncOpen2(listener);
|
||||
|
||||
do_test_pending();
|
||||
}
|
||||
|
||||
function test_suspend_on_before_connect()
|
||||
{
|
||||
test_suspend_on_auth(true, false);
|
||||
}
|
||||
|
||||
function test_suspend_on_modify_request()
|
||||
{
|
||||
test_suspend_on_auth(false, true);
|
||||
}
|
||||
|
||||
function test_suspend_all()
|
||||
{
|
||||
test_suspend_on_auth(true, true);
|
||||
}
|
||||
|
||||
|
||||
// PATH HANDLERS
|
||||
|
||||
// /auth
|
||||
function authHandler(metadata, response) {
|
||||
// btoa("guest:guest"), but that function is not available here
|
||||
var expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q=";
|
||||
|
||||
var body;
|
||||
if (metadata.hasHeader("Authorization") &&
|
||||
metadata.getHeader("Authorization") == expectedHeader)
|
||||
{
|
||||
response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
|
||||
response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
|
||||
|
||||
body = "success";
|
||||
}
|
||||
else
|
||||
{
|
||||
// didn't know guest:guest, failure
|
||||
response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
|
||||
response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
|
||||
|
||||
body = "failed";
|
||||
}
|
||||
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// This file tests async handling of a channel suspended in
|
||||
// notifying http-on-examine-merged-response observers.
|
||||
// Note that this test is developed based on test_bug482601.js.
|
||||
|
||||
|
||||
ChromeUtils.import("resource://testing-common/httpd.js");
|
||||
ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
|
||||
|
||||
var httpserv = null;
|
||||
var test_nr = 0;
|
||||
var buffer = "";
|
||||
var observerCalled = false;
|
||||
var channelResumed = false;
|
||||
|
||||
var observer = {
|
||||
QueryInterface: function (aIID) {
|
||||
if (aIID.equals(Ci.nsISupports) ||
|
||||
aIID.equals(Ci.nsIObserver))
|
||||
return this;
|
||||
throw Cr.NS_ERROR_NO_INTERFACE;
|
||||
},
|
||||
|
||||
observe: function(subject, topic, data) {
|
||||
if (topic === "http-on-examine-merged-response" &&
|
||||
subject instanceof Ci.nsIHttpChannel) {
|
||||
var chan = subject.QueryInterface(Ci.nsIHttpChannel);
|
||||
executeSoon(() => {
|
||||
Assert.equal(channelResumed,false);
|
||||
channelResumed = true;
|
||||
chan.resume();
|
||||
});
|
||||
Assert.equal(observerCalled,false);
|
||||
observerCalled = true;
|
||||
chan.suspend();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var listener = {
|
||||
onStartRequest: function (request, ctx) {
|
||||
buffer = "";
|
||||
},
|
||||
|
||||
onDataAvailable: function (request, ctx, stream, offset, count) {
|
||||
buffer = buffer.concat(read_stream(stream, count));
|
||||
},
|
||||
|
||||
onStopRequest: function (request, ctx, status) {
|
||||
Assert.equal(status, Cr.NS_OK);
|
||||
Assert.equal(buffer, "0123456789");
|
||||
Assert.equal(channelResumed, true);
|
||||
Assert.equal(observerCalled, true);
|
||||
test_nr++;
|
||||
do_timeout(0, do_test);
|
||||
}
|
||||
};
|
||||
|
||||
function run_test() {
|
||||
httpserv = new HttpServer();
|
||||
httpserv.registerPathHandler("/path/partial", path_partial);
|
||||
httpserv.registerPathHandler("/path/cached", path_cached);
|
||||
httpserv.start(-1);
|
||||
|
||||
var obs = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
|
||||
obs.addObserver(observer, "http-on-examine-merged-response");
|
||||
|
||||
do_timeout(0, do_test);
|
||||
do_test_pending();
|
||||
}
|
||||
|
||||
function do_test() {
|
||||
if (test_nr < tests.length) {
|
||||
tests[test_nr]();
|
||||
}
|
||||
else {
|
||||
var obs = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
|
||||
obs.removeObserver(observer, "http-on-examine-merged-response");
|
||||
httpserv.stop(do_test_finished);
|
||||
}
|
||||
}
|
||||
|
||||
var tests = [test_partial,
|
||||
test_cached];
|
||||
|
||||
function makeChan(url) {
|
||||
return NetUtil.newChannel({uri: url, loadUsingSystemPrincipal: true})
|
||||
.QueryInterface(Ci.nsIHttpChannel);
|
||||
}
|
||||
|
||||
function storeCache(aCacheEntry, aResponseHeads, aContent) {
|
||||
aCacheEntry.setMetaDataElement("request-method", "GET");
|
||||
aCacheEntry.setMetaDataElement("response-head", aResponseHeads);
|
||||
aCacheEntry.setMetaDataElement("charset", "ISO-8859-1");
|
||||
|
||||
var oStream = aCacheEntry.openOutputStream(0, aContent.length);
|
||||
var written = oStream.write(aContent, aContent.length);
|
||||
if (written != aContent.length) {
|
||||
do_throw("oStream.write has not written all data!\n" +
|
||||
" Expected: " + written + "\n" +
|
||||
" Actual: " + aContent.length + "\n");
|
||||
}
|
||||
oStream.close();
|
||||
aCacheEntry.close();
|
||||
}
|
||||
|
||||
function test_partial() {
|
||||
observerCalled = false;
|
||||
channelResumed = false;
|
||||
asyncOpenCacheEntry("http://localhost:" + httpserv.identity.primaryPort +
|
||||
"/path/partial",
|
||||
"disk", Ci.nsICacheStorage.OPEN_NORMALLY, null,
|
||||
test_partial2);
|
||||
}
|
||||
|
||||
function test_partial2(status, entry) {
|
||||
Assert.equal(status, Cr.NS_OK);
|
||||
storeCache(entry,
|
||||
"HTTP/1.1 200 OK\r\n" +
|
||||
"Date: Thu, 1 Jan 2009 00:00:00 GMT\r\n" +
|
||||
"Server: httpd.js\r\n" +
|
||||
"Last-Modified: Thu, 1 Jan 2009 00:00:00 GMT\r\n" +
|
||||
"Accept-Ranges: bytes\r\n" +
|
||||
"Content-Length: 10\r\n" +
|
||||
"Content-Type: text/plain\r\n",
|
||||
"0123");
|
||||
|
||||
observers_called = "";
|
||||
|
||||
var chan = makeChan("http://localhost:" + httpserv.identity.primaryPort +
|
||||
"/path/partial");
|
||||
chan.asyncOpen2(listener);
|
||||
}
|
||||
|
||||
function test_cached() {
|
||||
observerCalled = false;
|
||||
channelResumed = false;
|
||||
asyncOpenCacheEntry("http://localhost:" + httpserv.identity.primaryPort +
|
||||
"/path/cached",
|
||||
"disk", Ci.nsICacheStorage.OPEN_NORMALLY, null,
|
||||
test_cached2);
|
||||
}
|
||||
|
||||
function test_cached2(status, entry) {
|
||||
Assert.equal(status, Cr.NS_OK);
|
||||
storeCache(entry,
|
||||
"HTTP/1.1 200 OK\r\n" +
|
||||
"Date: Thu, 1 Jan 2009 00:00:00 GMT\r\n" +
|
||||
"Server: httpd.js\r\n" +
|
||||
"Last-Modified: Thu, 1 Jan 2009 00:00:00 GMT\r\n" +
|
||||
"Accept-Ranges: bytes\r\n" +
|
||||
"Content-Length: 10\r\n" +
|
||||
"Content-Type: text/plain\r\n",
|
||||
"0123456789");
|
||||
|
||||
observers_called = "";
|
||||
|
||||
var chan = makeChan("http://localhost:" + httpserv.identity.primaryPort +
|
||||
"/path/cached");
|
||||
chan.loadFlags = Ci.nsIRequest.VALIDATE_ALWAYS;
|
||||
chan.asyncOpen2(listener);
|
||||
}
|
||||
|
||||
// PATHS
|
||||
|
||||
// /path/partial
|
||||
function path_partial(metadata, response) {
|
||||
Assert.ok(metadata.hasHeader("If-Range"));
|
||||
Assert.equal(metadata.getHeader("If-Range"),
|
||||
"Thu, 1 Jan 2009 00:00:00 GMT");
|
||||
Assert.ok(metadata.hasHeader("Range"));
|
||||
Assert.equal(metadata.getHeader("Range"), "bytes=4-");
|
||||
|
||||
response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
|
||||
response.setHeader("Content-Range", "bytes 4-9/10", false);
|
||||
response.setHeader("Content-Type", "text/plain", false);
|
||||
response.setHeader("Last-Modified", "Thu, 1 Jan 2009 00:00:00 GMT");
|
||||
|
||||
var body = "456789";
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
}
|
||||
|
||||
// /path/cached
|
||||
function path_cached(metadata, response) {
|
||||
Assert.ok(metadata.hasHeader("If-Modified-Since"));
|
||||
Assert.equal(metadata.getHeader("If-Modified-Since"),
|
||||
"Thu, 1 Jan 2009 00:00:00 GMT");
|
||||
|
||||
response.setStatusLine(metadata.httpVersion, 304, "Not Modified");
|
||||
}
|
|
@ -425,3 +425,5 @@ run-sequentially = node server exceptions dont replay well
|
|||
skip-if = os == "android"
|
||||
[test_network_connectivity_service.js]
|
||||
skip-if = os == "android" # DNSv6 issues on android
|
||||
[test_suspend_channel_on_authRetry.js]
|
||||
[test_suspend_channel_on_examine_merged_response.js]
|
||||
|
|
|
@ -333,55 +333,20 @@ class RemoteSettingsClient extends EventEmitter {
|
|||
}
|
||||
} catch (e) {
|
||||
if (e.message.includes(INVALID_SIGNATURE)) {
|
||||
// Signature verification failed during synchronzation.
|
||||
// Signature verification failed during synchronization.
|
||||
reportStatus = UptakeTelemetry.STATUS.SIGNATURE_ERROR;
|
||||
// if sync fails with a signature error, it's likely that our
|
||||
// If sync fails with a signature error, it's likely that our
|
||||
// local data has been modified in some way.
|
||||
// We will attempt to fix this by retrieving the whole
|
||||
// remote collection.
|
||||
const payload = await fetchRemoteRecords(collection.bucket, collection.name, expectedTimestamp);
|
||||
try {
|
||||
await this._validateCollectionSignature(payload.data,
|
||||
payload.last_modified,
|
||||
collection,
|
||||
{ expectedTimestamp, ignoreLocal: true });
|
||||
syncResult = await this._retrySyncFromScratch(collection, expectedTimestamp);
|
||||
} catch (e) {
|
||||
// If the signature fails again, or if an error occured during wiping out the
|
||||
// local data, then we report it as a *signature retry* error.
|
||||
reportStatus = UptakeTelemetry.STATUS.SIGNATURE_RETRY_ERROR;
|
||||
throw e;
|
||||
}
|
||||
|
||||
// The signature is good (we haven't thrown).
|
||||
// Now we will Inspect what we had locally.
|
||||
const { data: oldData } = await collection.list({ order: "" }); // no need to sort.
|
||||
|
||||
// We build a sync result as if a diff-based sync was performed.
|
||||
syncResult = { created: [], updated: [], deleted: [] };
|
||||
|
||||
// If the remote last_modified is newer than the local last_modified,
|
||||
// replace the local data
|
||||
const localLastModified = await collection.db.getLastModified();
|
||||
if (payload.last_modified >= localLastModified) {
|
||||
const { data: newData } = payload;
|
||||
await collection.clear();
|
||||
await collection.loadDump(newData);
|
||||
|
||||
// Compare local and remote to populate the sync result
|
||||
const oldById = new Map(oldData.map(e => [e.id, e]));
|
||||
for (const r of newData) {
|
||||
const old = oldById.get(r.id);
|
||||
if (old) {
|
||||
if (old.last_modified != r.last_modified) {
|
||||
syncResult.updated.push({ old, new: r });
|
||||
}
|
||||
oldById.delete(r.id);
|
||||
} else {
|
||||
syncResult.created.push(r);
|
||||
}
|
||||
}
|
||||
// Records that remain in our map now are those missing from remote
|
||||
syncResult.deleted = Array.from(oldById.values());
|
||||
}
|
||||
|
||||
} else {
|
||||
// The sync has thrown, it can be related to metadata, network or a general error.
|
||||
if (e.message == MISSING_SIGNATURE) {
|
||||
|
@ -398,24 +363,11 @@ class RemoteSettingsClient extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
// Handle the obtained records (ie. apply locally through events).
|
||||
// Build the event data list. It should be filtered (ie. by application target)
|
||||
const { created: allCreated, updated: allUpdated, deleted: allDeleted } = syncResult;
|
||||
const [created, deleted, updatedFiltered] = await Promise.all(
|
||||
[allCreated, allDeleted, allUpdated.map(e => e.new)].map(this._filterEntries.bind(this))
|
||||
);
|
||||
// For updates, keep entries whose updated form matches the target.
|
||||
const updatedFilteredIds = new Set(updatedFiltered.map(e => e.id));
|
||||
const updated = allUpdated.filter(({ new: { id } }) => updatedFilteredIds.has(id));
|
||||
|
||||
const filteredSyncResult = await this._filterSyncResult(collection, syncResult);
|
||||
// If every changed entry is filtered, we don't even fire the event.
|
||||
if (created.length || updated.length || deleted.length) {
|
||||
// Read local collection of records (also filtered).
|
||||
const { data: allData } = await collection.list({ order: "" }); // no need to sort.
|
||||
const current = await this._filterEntries(allData);
|
||||
const payload = { data: { current, created, updated, deleted } };
|
||||
if (filteredSyncResult) {
|
||||
try {
|
||||
await this.emit("sync", payload);
|
||||
await this.emit("sync", { data: filteredSyncResult });
|
||||
} catch (e) {
|
||||
reportStatus = UptakeTelemetry.STATUS.APPLY_ERROR;
|
||||
throw e;
|
||||
|
@ -444,7 +396,7 @@ class RemoteSettingsClient extends EventEmitter {
|
|||
*
|
||||
* @param {Array<Object>} remoteRecords The list of changes to apply to the local database.
|
||||
* @param {int} timestamp The timestamp associated with the list of remote records.
|
||||
* @param {Collection} collection Kinto.js Collection instance.
|
||||
* @param {Collection} kintoCollection Kinto.js Collection instance.
|
||||
* @param {Object} options
|
||||
* @param {int} options.expectedTimestamp Cache busting of collection metadata
|
||||
* @param {Boolean} options.ignoreLocal When the signature verification is retried, since we refetch
|
||||
|
@ -478,6 +430,90 @@ class RemoteSettingsClient extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the whole list of records from the server, verify the signature again
|
||||
* and then compute a synchronization result as if the diff-based sync happened.
|
||||
* And eventually, wipe out the local data.
|
||||
*
|
||||
* @param {Collection} kintoCollection Kinto.js Collection instance.
|
||||
* @param {int} expectedTimestamp Cache busting of collection metadata
|
||||
*
|
||||
* @returns {Promise<Object>} the computed sync result.
|
||||
*/
|
||||
async _retrySyncFromScratch(kintoCollection, expectedTimestamp) {
|
||||
const payload = await fetchRemoteRecords(kintoCollection.bucket, kintoCollection.name, expectedTimestamp);
|
||||
await this._validateCollectionSignature(payload.data,
|
||||
payload.last_modified,
|
||||
kintoCollection,
|
||||
{ expectedTimestamp, ignoreLocal: true });
|
||||
|
||||
// The signature is good (we haven't thrown).
|
||||
// Now we will Inspect what we had locally.
|
||||
const { data: oldData } = await kintoCollection.list({ order: "" }); // no need to sort.
|
||||
|
||||
// We build a sync result as if a diff-based sync was performed.
|
||||
const syncResult = { created: [], updated: [], deleted: [] };
|
||||
|
||||
// If the remote last_modified is newer than the local last_modified,
|
||||
// replace the local data
|
||||
const localLastModified = await kintoCollection.db.getLastModified();
|
||||
if (payload.last_modified >= localLastModified) {
|
||||
const { data: newData } = payload;
|
||||
await kintoCollection.clear();
|
||||
await kintoCollection.loadDump(newData);
|
||||
|
||||
// Compare local and remote to populate the sync result
|
||||
const oldById = new Map(oldData.map(e => [e.id, e]));
|
||||
for (const r of newData) {
|
||||
const old = oldById.get(r.id);
|
||||
if (old) {
|
||||
if (old.last_modified != r.last_modified) {
|
||||
syncResult.updated.push({ old, new: r });
|
||||
}
|
||||
oldById.delete(r.id);
|
||||
} else {
|
||||
syncResult.created.push(r);
|
||||
}
|
||||
}
|
||||
// Records that remain in our map now are those missing from remote
|
||||
syncResult.deleted = Array.from(oldById.values());
|
||||
}
|
||||
return syncResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the filter func to filter the lists of changes obtained from synchronization,
|
||||
* and return them along with the filtered list of local records.
|
||||
*
|
||||
* If the filtered lists of changes are all empty, we return null (and thus don't
|
||||
* bother listing local DB).
|
||||
*
|
||||
* @param {Collection} kintoCollection Kinto.js Collection instance.
|
||||
* @param {Object} syncResult Synchronization result without filtering.
|
||||
*
|
||||
* @returns {Promise<Object>} the filtered list of local records, plus the filtered
|
||||
* list of created, updated and deleted records.
|
||||
*/
|
||||
async _filterSyncResult(kintoCollection, syncResult) {
|
||||
// Handle the obtained records (ie. apply locally through events).
|
||||
// Build the event data list. It should be filtered (ie. by application target)
|
||||
const { created: allCreated, updated: allUpdated, deleted: allDeleted } = syncResult;
|
||||
const [created, deleted, updatedFiltered] = await Promise.all(
|
||||
[allCreated, allDeleted, allUpdated.map(e => e.new)].map(this._filterEntries.bind(this))
|
||||
);
|
||||
// For updates, keep entries whose updated form matches the target.
|
||||
const updatedFilteredIds = new Set(updatedFiltered.map(e => e.id));
|
||||
const updated = allUpdated.filter(({ new: { id } }) => updatedFilteredIds.has(id));
|
||||
|
||||
if (!created.length && !updated.length && !deleted.length) {
|
||||
return null;
|
||||
}
|
||||
// Read local collection of records (also filtered).
|
||||
const { data: allData } = await kintoCollection.list({ order: "" }); // no need to sort.
|
||||
const current = await this._filterEntries(allData);
|
||||
return { created, updated, deleted, current };
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter entries for which calls to `this.filterFunc` returns null.
|
||||
*
|
||||
|
|
|
@ -30,10 +30,6 @@ marionette:
|
|||
by-test-platform:
|
||||
android-em.*: xlarge
|
||||
default: default
|
||||
tier:
|
||||
by-test-platform:
|
||||
android-em.*: 2
|
||||
default: default
|
||||
chunks:
|
||||
by-test-platform:
|
||||
android-em.*: 10
|
||||
|
|
|
@ -72,7 +72,7 @@ The mailing list for geckodriver discussion is
|
|||
tools-marionette@lists.mozilla.org ([subscribe], [archive]).
|
||||
|
||||
There is also an IRC channel to talk about using and developing
|
||||
geckodriver in #ateam on irc.mozilla.org.
|
||||
geckodriver in #interop on irc.mozilla.org.
|
||||
|
||||
[subscribe]: https://lists.mozilla.org/listinfo/tools-marionette
|
||||
[archive]: https://lists.mozilla.org/pipermail/tools-marionette/
|
||||
|
|
|
@ -24,7 +24,7 @@ Accounts, communication
|
|||
2. For a direct communication with us it will be beneficial to setup [IRC].
|
||||
Make sure to also register your nickname as described in the linked document.
|
||||
|
||||
3. Join our #ateam channel, and introduce yourself to the team. :ato,
|
||||
3. Join our #interop channel, and introduce yourself to the team. :ato,
|
||||
:AutomatedTester, :maja_zf, and :whimboo are all familiar with Marionette.
|
||||
We're nice, I promise, but we might not answer right away due to different
|
||||
time zones, time off, etc. So please be patient.
|
||||
|
|
|
@ -62,7 +62,7 @@ Communication
|
|||
The mailing list for Marionette discussion is
|
||||
tools-marionette@lists.mozilla.org (`subscribe`_, `archive`_).
|
||||
|
||||
If you prefer real-time chat, there is often someone in the #ateam IRC
|
||||
If you prefer real-time chat, there is often someone in the #interop IRC
|
||||
channel on irc.mozilla.org. Don’t ask if you may ask a question; just go ahead
|
||||
and ask, and please wait for an answer as we might not be in your timezone.
|
||||
|
||||
|
|
|
@ -149,7 +149,7 @@ def main():
|
|||
(options, args) = parser.parse_args(args=None, values=None)
|
||||
|
||||
if len(args) != 1:
|
||||
parser.error('Path to the environment has to be specified')
|
||||
parser.error('Path to the environment has to be specified')
|
||||
target = args[0]
|
||||
assert(target)
|
||||
|
||||
|
@ -178,8 +178,8 @@ def main():
|
|||
update_configfile(os.path.join(here, 'config', 'config.json.in'),
|
||||
os.path.join(target, 'config.json'),
|
||||
replacements={
|
||||
'__TESTDIR__': testdir.replace('\\','/'),
|
||||
'__EXTENSIONDIR__': extdir.replace('\\','/'),
|
||||
'__TESTDIR__': testdir.replace('\\', '/'),
|
||||
'__EXTENSIONDIR__': extdir.replace('\\', '/'),
|
||||
'__FX_ACCOUNT_USERNAME__': options.username,
|
||||
'__FX_ACCOUNT_PASSWORD__': options.password,
|
||||
'__SYNC_ACCOUNT_USERNAME__': options.sync_username,
|
||||
|
@ -195,5 +195,6 @@ def main():
|
|||
print usage_message.format(TARGET=target,
|
||||
BIN_NAME=bin_name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -17,7 +17,7 @@ deps = ['httplib2 == 0.9.2',
|
|||
'mozprofile ~= 2.1',
|
||||
'mozrunner ~= 7.2',
|
||||
'mozversion == 1.5',
|
||||
]
|
||||
]
|
||||
|
||||
# we only support python 2.6+ right now
|
||||
assert sys.version_info[0] == 2
|
||||
|
@ -29,8 +29,8 @@ setup(name='tps',
|
|||
long_description="""\
|
||||
""",
|
||||
classifiers=['Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 2 :: Only',
|
||||
], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||
'Programming Language :: Python :: 2 :: Only',
|
||||
], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||
keywords='',
|
||||
author='Mozilla Automation and Tools team',
|
||||
author_email='tools@lists.mozilla.org',
|
||||
|
|
|
@ -2,5 +2,6 @@
|
|||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
# flake8: noqa
|
||||
from . firefoxrunner import TPSFirefoxRunner
|
||||
from . testrunner import TPSTestRunner
|
||||
|
|
|
@ -34,11 +34,11 @@ def main():
|
|||
default=False,
|
||||
help='run in debug mode')
|
||||
parser.add_option('--ignore-unused-engines',
|
||||
default=False,
|
||||
action='store_true',
|
||||
dest='ignore_unused_engines',
|
||||
help='If defined, do not load unused engines in individual tests.'
|
||||
' Has no effect for pulse monitor.')
|
||||
default=False,
|
||||
action='store_true',
|
||||
dest='ignore_unused_engines',
|
||||
help='If defined, do not load unused engines in individual tests.'
|
||||
' Has no effect for pulse monitor.')
|
||||
parser.add_option('--logfile',
|
||||
action='store',
|
||||
type='string',
|
||||
|
@ -122,11 +122,12 @@ def main():
|
|||
rlock=rlock,
|
||||
testfile=testfile,
|
||||
stop_on_error=options.stop_on_error,
|
||||
)
|
||||
)
|
||||
TPS.run_tests()
|
||||
|
||||
if TPS.numfailed > 0 or TPS.numpassed == 0:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
import copy
|
||||
import httplib2
|
||||
import os
|
||||
|
||||
|
@ -34,7 +33,7 @@ class TPSFirefoxRunner(object):
|
|||
def download_url(self, url, dest=None):
|
||||
h = httplib2.Http()
|
||||
resp, content = h.request(url, 'GET')
|
||||
if dest == None:
|
||||
if dest is None:
|
||||
dest = os.path.basename(url)
|
||||
|
||||
local = open(dest, 'wb')
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import re
|
||||
import os.path
|
||||
|
||||
|
||||
class TPSTestPhase(object):
|
||||
|
||||
lineRe = re.compile(
|
||||
|
@ -14,7 +15,7 @@ class TPSTestPhase(object):
|
|||
firefoxRunner, logfn, ignore_unused_engines=False):
|
||||
self.phase = phase
|
||||
self.profile = profile
|
||||
self.testname = str(testname) # this might be passed in as unicode
|
||||
self.testname = str(testname) # this might be passed in as unicode
|
||||
self.testpath = testpath
|
||||
self.logfile = logfile
|
||||
self.env = env
|
||||
|
@ -38,7 +39,7 @@ class TPSTestPhase(object):
|
|||
"testing.tps.ignoreUnusedEngines": self.ignore_unused_engines
|
||||
}
|
||||
|
||||
self.profile.set_preferences(prefs);
|
||||
self.profile.set_preferences(prefs)
|
||||
|
||||
self.log('\nLaunching Firefox for phase %s with prefs %s\n' %
|
||||
(self.phase, str(prefs)))
|
||||
|
|
|
@ -4,8 +4,6 @@
|
|||
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import random
|
||||
import re
|
||||
import tempfile
|
||||
import time
|
||||
|
@ -188,7 +186,7 @@ class TPSTestRunner(object):
|
|||
def _zip_add_dir(self, zip, dir, rootDir):
|
||||
try:
|
||||
zip.write(os.path.join(rootDir, dir), dir)
|
||||
except:
|
||||
except Exception:
|
||||
# on some OS's, adding directory entries doesn't seem to work
|
||||
pass
|
||||
for root, dirs, files in os.walk(os.path.join(rootDir, dir)):
|
||||
|
@ -197,10 +195,13 @@ class TPSTestRunner(object):
|
|||
|
||||
def handle_phase_failure(self, profiles):
|
||||
for profile in profiles:
|
||||
self.log('\nDumping sync log for profile %s\n' % profiles[profile].profile)
|
||||
for root, dirs, files in os.walk(os.path.join(profiles[profile].profile, 'weave', 'logs')):
|
||||
self.log('\nDumping sync log for profile %s\n'
|
||||
% profiles[profile].profile)
|
||||
for root, dirs, files in os.walk(
|
||||
os.path.join(profiles[profile].profile, 'weave', 'logs')):
|
||||
for f in files:
|
||||
weavelog = os.path.join(profiles[profile].profile, 'weave', 'logs', f)
|
||||
weavelog = os.path.join(
|
||||
profiles[profile].profile, 'weave', 'logs', f)
|
||||
if os.access(weavelog, os.F_OK):
|
||||
with open(weavelog, 'r') as fh:
|
||||
for line in fh:
|
||||
|
@ -208,9 +209,9 @@ class TPSTestRunner(object):
|
|||
if len(possible_time) == 13 and possible_time.isdigit():
|
||||
time_ms = int(possible_time)
|
||||
formatted = time.strftime('%Y-%m-%d %H:%M:%S',
|
||||
time.localtime(time_ms / 1000))
|
||||
time.localtime(time_ms / 1000))
|
||||
self.log('%s.%03d %s' % (
|
||||
formatted, time_ms % 1000, line[14:] ))
|
||||
formatted, time_ms % 1000, line[14:]))
|
||||
else:
|
||||
self.log(line)
|
||||
|
||||
|
@ -225,7 +226,7 @@ class TPSTestRunner(object):
|
|||
f.close()
|
||||
try:
|
||||
test = json.loads(testcontent)
|
||||
except:
|
||||
except Exception:
|
||||
test = json.loads(testcontent[testcontent.find('{'):testcontent.find('}') + 1])
|
||||
|
||||
self.preferences['tps.seconds_since_epoch'] = int(time.time())
|
||||
|
@ -237,9 +238,9 @@ class TPSTestRunner(object):
|
|||
profilename = test[phase]
|
||||
|
||||
# create the profile if necessary
|
||||
if not profilename in profiles:
|
||||
profiles[profilename] = Profile(preferences = self.preferences.copy(),
|
||||
addons = self.extensions)
|
||||
if profilename not in profiles:
|
||||
profiles[profilename] = Profile(preferences=self.preferences.copy(),
|
||||
addons=self.extensions)
|
||||
|
||||
# create the test phase
|
||||
phaselist.append(TPSTestPhase(
|
||||
|
@ -262,7 +263,7 @@ class TPSTestRunner(object):
|
|||
phase.run()
|
||||
if phase.status != 'PASS':
|
||||
failed = True
|
||||
break;
|
||||
break
|
||||
|
||||
for profilename in profiles:
|
||||
print "### Cleanup Profile ", profilename
|
||||
|
@ -303,12 +304,13 @@ class TPSTestRunner(object):
|
|||
'PASS': lambda x: ('TEST-PASS', ''),
|
||||
'FAIL': lambda x: ('TEST-UNEXPECTED-FAIL', x.rstrip()),
|
||||
'unknown': lambda x: ('TEST-UNEXPECTED-FAIL', 'test did not complete')
|
||||
} [phase.status](phase.errline)
|
||||
logstr = "\n%s | %s%s\n" % (result[0], testname, (' | %s' % result[1] if result[1] else ''))
|
||||
}[phase.status](phase.errline)
|
||||
logstr = "\n%s | %s%s\n" % (result[0], testname, (' | %s' %
|
||||
result[1] if result[1] else ''))
|
||||
|
||||
try:
|
||||
repoinfo = mozversion.get_version(self.binary)
|
||||
except:
|
||||
except Exception:
|
||||
repoinfo = {}
|
||||
apprepo = repoinfo.get('application_repository', '')
|
||||
appchangeset = repoinfo.get('application_changeset', '')
|
||||
|
@ -321,20 +323,20 @@ class TPSTestRunner(object):
|
|||
tmplogfile.close()
|
||||
self.errorlogs[testname] = tmplogfile
|
||||
|
||||
resultdata = ({ 'productversion': { 'version': firefox_version,
|
||||
'buildid': firefox_buildid,
|
||||
'builddate': firefox_buildid[0:8],
|
||||
'product': 'Firefox',
|
||||
'repository': apprepo,
|
||||
'changeset': appchangeset,
|
||||
resultdata = ({'productversion': {'version': firefox_version,
|
||||
'buildid': firefox_buildid,
|
||||
'builddate': firefox_buildid[0:8],
|
||||
'product': 'Firefox',
|
||||
'repository': apprepo,
|
||||
'changeset': appchangeset,
|
||||
},
|
||||
'addonversion': { 'version': sync_version,
|
||||
'product': 'Firefox Sync' },
|
||||
'name': testname,
|
||||
'message': result[1],
|
||||
'state': result[0],
|
||||
'logdata': logdata
|
||||
})
|
||||
'addonversion': {'version': sync_version,
|
||||
'product': 'Firefox Sync'},
|
||||
'name': testname,
|
||||
'message': result[1],
|
||||
'state': result[0],
|
||||
'logdata': logdata
|
||||
})
|
||||
|
||||
self.log(logstr, True)
|
||||
for phase in phaselist:
|
||||
|
@ -346,7 +348,7 @@ class TPSTestRunner(object):
|
|||
self.preferences = self.default_preferences.copy()
|
||||
|
||||
if self.mobile:
|
||||
self.preferences.update({'services.sync.client.type' : 'mobile'})
|
||||
self.preferences.update({'services.sync.client.type': 'mobile'})
|
||||
|
||||
# If we are using legacy Sync, then set a dummy username to force the
|
||||
# correct authentication type. Without this pref set to a value
|
||||
|
@ -388,14 +390,14 @@ class TPSTestRunner(object):
|
|||
# now, run the test group
|
||||
self.run_test_group()
|
||||
|
||||
except:
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
self.numpassed = 0
|
||||
self.numfailed = 1
|
||||
try:
|
||||
self.writeToResultFile(self.postdata,
|
||||
'<pre>%s</pre>' % traceback.format_exc())
|
||||
except:
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
else:
|
||||
try:
|
||||
|
@ -406,12 +408,12 @@ class TPSTestRunner(object):
|
|||
To = self.config['email'].get('passednotificationlist')
|
||||
self.writeToResultFile(self.postdata,
|
||||
sendTo=To)
|
||||
except:
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
try:
|
||||
self.writeToResultFile(self.postdata,
|
||||
'<pre>%s</pre>' % traceback.format_exc())
|
||||
except:
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
# release our lock
|
||||
|
@ -472,16 +474,17 @@ class TPSTestRunner(object):
|
|||
else:
|
||||
self.numfailed += 1
|
||||
if self.stop_on_error:
|
||||
print '\nTest failed with --stop-on-error specified; not running any more tests.\n'
|
||||
print '\nTest failed with --stop-on-error specified; ' \
|
||||
'not running any more tests.\n'
|
||||
break
|
||||
|
||||
self.mozhttpd.stop()
|
||||
|
||||
# generate the postdata we'll use to post the results to the db
|
||||
self.postdata = { 'tests': self.results,
|
||||
'os': '%s %sbit' % (mozinfo.version, mozinfo.bits),
|
||||
'testtype': 'crossweave',
|
||||
'productversion': self.productversion,
|
||||
'addonversion': self.addonversion,
|
||||
'synctype': self.synctype,
|
||||
}
|
||||
self.postdata = {'tests': self.results,
|
||||
'os': '%s %sbit' % (mozinfo.version, mozinfo.bits),
|
||||
'testtype': 'crossweave',
|
||||
'productversion': self.productversion,
|
||||
'addonversion': self.addonversion,
|
||||
'synctype': self.synctype,
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ The mailing list for webdriver discussion is
|
|||
tools-marionette@lists.mozilla.org ([subscribe], [archive]).
|
||||
|
||||
There is also an IRC channel to talk about using and developing
|
||||
webdriver in #ateam on irc.mozilla.org.
|
||||
webdriver in #interop on irc.mozilla.org.
|
||||
|
||||
[subscribe]: https://lists.mozilla.org/listinfo/tools-marionette
|
||||
[archive]: https://lists.mozilla.org/pipermail/tools-marionette/
|
||||
|
|
|
@ -2110,94 +2110,6 @@ sw:
|
|||
- 'main'
|
||||
- 'content'
|
||||
|
||||
# The following section contains the BrowserErrorReporter scalars.
|
||||
browser.errors:
|
||||
collected_count:
|
||||
bug_numbers:
|
||||
- 1444554
|
||||
description: >
|
||||
The count of all browser chrome JS errors that were collected locally.
|
||||
expires: "67"
|
||||
kind: uint
|
||||
notification_emails:
|
||||
- nightly-js-errors@mozilla.com
|
||||
- mkelly@mozilla.com
|
||||
record_in_processes:
|
||||
- 'main'
|
||||
|
||||
collected_with_stack_count:
|
||||
bug_numbers:
|
||||
- 1444554
|
||||
description: >
|
||||
The count of browser chrome JS errors that were collected locally and had
|
||||
a usable stack trace.
|
||||
expires: "67"
|
||||
kind: uint
|
||||
notification_emails:
|
||||
- nightly-js-errors@mozilla.com
|
||||
- mkelly@mozilla.com
|
||||
record_in_processes:
|
||||
- 'main'
|
||||
|
||||
reported_success_count:
|
||||
bug_numbers:
|
||||
- 1444554
|
||||
description: >
|
||||
The count of all browser chrome JS errors that were reported to the
|
||||
remote collection service.
|
||||
expires: "67"
|
||||
kind: uint
|
||||
notification_emails:
|
||||
- nightly-js-errors@mozilla.com
|
||||
- mkelly@mozilla.com
|
||||
record_in_processes:
|
||||
- 'main'
|
||||
|
||||
reported_failure_count:
|
||||
bug_numbers:
|
||||
- 1444554
|
||||
description: >
|
||||
The count of all browser chrome JS errors that we attempted to report to
|
||||
the remote collection service, but failed to.
|
||||
expires: "67"
|
||||
kind: uint
|
||||
notification_emails:
|
||||
- nightly-js-errors@mozilla.com
|
||||
- mkelly@mozilla.com
|
||||
record_in_processes:
|
||||
- 'main'
|
||||
|
||||
sample_rate:
|
||||
bug_numbers:
|
||||
- 1444554
|
||||
description: >
|
||||
The sample rate at which collected errors were reported.
|
||||
expires: "67"
|
||||
kind: string
|
||||
notification_emails:
|
||||
- nightly-js-errors@mozilla.com
|
||||
- mkelly@mozilla.com
|
||||
record_in_processes:
|
||||
- 'main'
|
||||
|
||||
collected_count_by_filename:
|
||||
bug_numbers:
|
||||
- 1444554
|
||||
description: >
|
||||
The count of all browser chrome JS errors that were collected locally,
|
||||
keyed by the filename of the file in which the error occurred. Collected
|
||||
filenames are limited to specific paths under the resource:// and
|
||||
chrome:// protocols; non-matching filenames are reported as "FILTERED".
|
||||
Long filenames are truncated to the first 70 characters.
|
||||
keyed: true
|
||||
expires: "67"
|
||||
kind: uint
|
||||
notification_emails:
|
||||
- nightly-js-errors@mozilla.com
|
||||
- mkelly@mozilla.com
|
||||
record_in_processes:
|
||||
- 'main'
|
||||
|
||||
widget:
|
||||
ime_name_on_windows:
|
||||
bug_numbers:
|
||||
|
|
|
@ -50,6 +50,7 @@ flake8:
|
|||
- testing/remotecppunittests.py
|
||||
- testing/runcppunittests.py
|
||||
- testing/talos/
|
||||
- testing/tps/
|
||||
- testing/xpcshell
|
||||
- toolkit/components/telemetry
|
||||
- toolkit/crashreporter/tools/upload_symbols.py
|
||||
|
|
Загрузка…
Ссылка в новой задаче