Merge mozilla-central to mozilla-inbound. CLOSED TREE

This commit is contained in:
Dorel Luca 2019-01-16 00:25:16 +02:00
Родитель 6d9401a9e6 70ae3e33d1
Коммит bb4780fb18
49 изменённых файлов: 1133 добавлений и 1955 удалений

Просмотреть файл

@ -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

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 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. Dont 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