зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1759300 - Cancel suspend on idle if the extension EventPage has StreamFilter instances still active. r=mixedpuppy
Differential Revision: https://phabricator.services.mozilla.com/D145257
This commit is contained in:
Родитель
4e6096c73d
Коммит
6035e48afd
|
@ -2084,6 +2084,8 @@ const PROXIED_EVENTS = new Set([
|
|||
"test-harness-message",
|
||||
"add-permissions",
|
||||
"remove-permissions",
|
||||
"background-script-suspend",
|
||||
"background-script-suspend-canceled",
|
||||
"background-script-suspend-ignored",
|
||||
]);
|
||||
|
||||
|
@ -2413,6 +2415,18 @@ class Extension extends ExtensionData {
|
|||
return frameLoader || ExtensionParent.DebugUtils.getFrameLoader(this.id);
|
||||
}
|
||||
|
||||
get backgroundContext() {
|
||||
for (let view of this.views) {
|
||||
if (
|
||||
view.viewType === "background" ||
|
||||
view.viewType === "background_worker"
|
||||
) {
|
||||
return view;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
on(hook, f) {
|
||||
return this.emitter.on(hook, f);
|
||||
}
|
||||
|
|
|
@ -557,6 +557,12 @@ class BrowserExtensionContent extends EventEmitter {
|
|||
super.emit(event, ...args);
|
||||
}
|
||||
|
||||
// TODO(Bug 1768471): consider folding this back into emit if we will change it to
|
||||
// return a value as EventEmitter and Extension emit methods do.
|
||||
emitLocalWithResult(event, ...args) {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
|
||||
receiveMessage({ name, data }) {
|
||||
if (name === this.MESSAGE_EMIT_EVENT) {
|
||||
super.emit(data.event, ...data.args);
|
||||
|
@ -777,7 +783,7 @@ class ChildAPIManager {
|
|||
"AddListener",
|
||||
"RemoveListener",
|
||||
],
|
||||
recv: ["CallResult", "RunListener"],
|
||||
recv: ["CallResult", "RunListener", "StreamFilterSuspendCancel"],
|
||||
});
|
||||
|
||||
this.conduit.sendCreateProxyContext({
|
||||
|
@ -854,6 +860,20 @@ class ChildAPIManager {
|
|||
}
|
||||
}
|
||||
|
||||
async recvStreamFilterSuspendCancel() {
|
||||
const promise = this.context.extension.emitLocalWithResult(
|
||||
"internal:stream-filter-suspend-cancel"
|
||||
);
|
||||
// if all listeners throws emitLocalWithResult returns undefined.
|
||||
if (!promise) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return promise.then(results =>
|
||||
results.some(hasActiveStreamFilter => hasActiveStreamFilter === true)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a function in the parent process and ignores its return value.
|
||||
*
|
||||
|
|
|
@ -59,6 +59,8 @@ function getConsole() {
|
|||
|
||||
XPCOMUtils.defineLazyGetter(this, "console", getConsole);
|
||||
|
||||
const BACKGROUND_SCRIPTS_VIEW_TYPES = ["background", "background_worker"];
|
||||
|
||||
var ExtensionCommon;
|
||||
|
||||
// Run a function and report exceptions.
|
||||
|
@ -547,6 +549,10 @@ class BaseContext {
|
|||
return this.extension.privateBrowsingAllowed;
|
||||
}
|
||||
|
||||
get isBackgroundContext() {
|
||||
return BACKGROUND_SCRIPTS_VIEW_TYPES.includes(this.viewType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the extension context is using the WebIDL bindings for the
|
||||
* WebExtensions APIs.
|
||||
|
|
|
@ -465,6 +465,7 @@ class ProxyContextParent extends BaseContext {
|
|||
constructor(envType, extension, params, xulBrowser, principal) {
|
||||
super(envType, extension);
|
||||
|
||||
this.childId = params.childId;
|
||||
this.uri = Services.io.newURI(params.url);
|
||||
|
||||
this.incognito = params.incognito;
|
||||
|
@ -814,7 +815,7 @@ ParentAPIManager = {
|
|||
"RemoveListener",
|
||||
],
|
||||
send: ["CallResult"],
|
||||
query: ["RunListener"],
|
||||
query: ["RunListener", "StreamFilterSuspendCancel"],
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -853,6 +854,10 @@ ParentAPIManager = {
|
|||
}
|
||||
},
|
||||
|
||||
queryStreamFilterSuspendCancel(childId) {
|
||||
return this.conduit.queryStreamFilterSuspendCancel(childId);
|
||||
},
|
||||
|
||||
recvCreateProxyContext(data, { actor, sender }) {
|
||||
let { envType, extensionId, childId, principal } = data;
|
||||
let target = actor.browsingContext?.top.embedderElement;
|
||||
|
|
|
@ -6,7 +6,55 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
var { ExtensionError } = ExtensionUtils;
|
||||
|
||||
this.webRequest = class extends ExtensionAPI {
|
||||
STREAM_FILTER_INACTIVE_STATUSES = ["closed", "disconnected", "failed"];
|
||||
|
||||
hasActiveStreamFilter(filtersWeakSet) {
|
||||
const iter = ChromeUtils.nondeterministicGetWeakSetKeys(filtersWeakSet);
|
||||
for (let filter of iter) {
|
||||
if (!this.STREAM_FILTER_INACTIVE_STATUSES.includes(filter.status)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
watchStreamFilterSuspendCancel({
|
||||
context,
|
||||
filters,
|
||||
onSuspend,
|
||||
onSuspendCanceled,
|
||||
}) {
|
||||
if (
|
||||
!context.isBackgroundContext ||
|
||||
context.extension.persistentBackground !== false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { extension } = context;
|
||||
const cancelSuspendOnActiveStreamFilter = () =>
|
||||
this.hasActiveStreamFilter(filters);
|
||||
context.callOnClose({
|
||||
close() {
|
||||
extension.off(
|
||||
"internal:stream-filter-suspend-cancel",
|
||||
cancelSuspendOnActiveStreamFilter
|
||||
);
|
||||
extension.off("background-script-suspend", onSuspend);
|
||||
extension.off("background-script-suspend-canceled", onSuspend);
|
||||
},
|
||||
});
|
||||
extension.on(
|
||||
"internal:stream-filter-suspend-cancel",
|
||||
cancelSuspendOnActiveStreamFilter
|
||||
);
|
||||
extension.on("background-script-suspend", onSuspend);
|
||||
extension.on("background-script-suspend-canceled", onSuspendCanceled);
|
||||
}
|
||||
|
||||
getAPI(context) {
|
||||
let filters = new WeakSet();
|
||||
|
||||
|
@ -24,9 +72,22 @@ this.webRequest = class extends ExtensionAPI {
|
|||
},
|
||||
});
|
||||
|
||||
let isSuspending = false;
|
||||
this.watchStreamFilterSuspendCancel({
|
||||
context,
|
||||
filters,
|
||||
onSuspend: () => (isSuspending = true),
|
||||
onSuspendCanceled: () => (isSuspending = false),
|
||||
});
|
||||
|
||||
return {
|
||||
webRequest: {
|
||||
filterResponseData(requestId) {
|
||||
if (isSuspending) {
|
||||
throw new ExtensionError(
|
||||
"filterResponseData method calls forbidden while background extension global is suspending"
|
||||
);
|
||||
}
|
||||
requestId = parseInt(requestId, 10);
|
||||
|
||||
let streamFilter = context.cloneScope.StreamFilter.create(
|
||||
|
|
|
@ -423,6 +423,22 @@ this.backgroundPage = class extends ExtensionAPI {
|
|||
return;
|
||||
}
|
||||
|
||||
const childId = extension.backgroundContext?.childId;
|
||||
if (childId !== undefined) {
|
||||
// Ask to the background page context in the child process to check if there are
|
||||
// StreamFilter instances active (e.g. ones with status "transferringdata" or "suspended",
|
||||
// see StreamFilterStatus enum defined in StreamFilter.webidl).
|
||||
// TODO(Bug 1748533): consider additional changes to prevent a StreamFilter that never gets to an
|
||||
// inactive state from preventing an even page from being ever suspended.
|
||||
const hasActiveStreamFilter = await ExtensionParent.ParentAPIManager.queryStreamFilterSuspendCancel(
|
||||
extension.backgroundContext.childId
|
||||
);
|
||||
if (hasActiveStreamFilter) {
|
||||
extension.emit("background-script-reset-idle");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
extension.backgroundState = BACKGROUND_STATE.SUSPENDING;
|
||||
this.clearIdleTimer();
|
||||
// call runtime.onSuspend
|
||||
|
|
|
@ -0,0 +1,305 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
AddonTestUtils.init(this);
|
||||
AddonTestUtils.createAppInfo(
|
||||
"xpcshell@tests.mozilla.org",
|
||||
"XPCShell",
|
||||
"42",
|
||||
"42"
|
||||
);
|
||||
|
||||
const server = createHttpServer({ hosts: ["example.com"] });
|
||||
|
||||
server.registerPathHandler("/pending_request", (request, response) => {
|
||||
response.processAsync();
|
||||
response.setHeader("Content-Length", "10000", false);
|
||||
response.write("somedata\n");
|
||||
let intervalID = setInterval(() => response.write("continue\n"), 50);
|
||||
|
||||
registerCleanupFunction(() => {
|
||||
try {
|
||||
clearInterval(intervalID);
|
||||
response.finish();
|
||||
} catch (e) {
|
||||
// This will throw, but we don't care at this point.
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.registerPathHandler("/completed_request", (request, response) => {
|
||||
response.write("somedata\n");
|
||||
});
|
||||
|
||||
add_setup(async () => {
|
||||
await AddonTestUtils.promiseStartupManager();
|
||||
});
|
||||
|
||||
async function test_idletimeout_on_streamfilter({
|
||||
manifest_version,
|
||||
expectResetIdle,
|
||||
expectStreamFilterStop,
|
||||
requestUrlPath,
|
||||
}) {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
background: `(${async function(urlPath) {
|
||||
browser.webRequest.onBeforeRequest.addListener(
|
||||
request => {
|
||||
browser.test.log(`webRequest request intercepted: ${request.url}`);
|
||||
const filter = browser.webRequest.filterResponseData(
|
||||
request.requestId
|
||||
);
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
const encoder = new TextEncoder();
|
||||
filter.onstart = () => {
|
||||
browser.test.sendMessage("streamfilter:started");
|
||||
};
|
||||
filter.ondata = event => {
|
||||
let str = decoder.decode(event.data, { stream: true });
|
||||
filter.write(encoder.encode(str));
|
||||
};
|
||||
filter.onstop = () => {
|
||||
filter.close();
|
||||
browser.test.sendMessage("streamfilter:stopped");
|
||||
};
|
||||
},
|
||||
{
|
||||
urls: [`http://example.com/${urlPath}`],
|
||||
},
|
||||
["blocking"]
|
||||
);
|
||||
browser.test.sendMessage("bg:ready");
|
||||
}})("${requestUrlPath}")`,
|
||||
|
||||
useAddonManager: "temporary",
|
||||
manifest: {
|
||||
manifest_version,
|
||||
background: manifest_version >= 3 ? {} : { persistent: false },
|
||||
granted_host_permissions: manifest_version >= 3,
|
||||
permissions: ["webRequest", "webRequestBlocking"],
|
||||
// host_permissions are merged with permissions on a MV2 test extension.
|
||||
host_permissions: ["http://example.com/*"],
|
||||
},
|
||||
});
|
||||
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("bg:ready");
|
||||
const { contextId } = extension.extension.backgroundContext;
|
||||
notEqual(contextId, undefined, "Got a contextId for the background context");
|
||||
|
||||
info("Trigger a webRequest");
|
||||
ExtensionTestUtils.fetch(
|
||||
"http://example.com/",
|
||||
`http://example.com/${requestUrlPath}`
|
||||
);
|
||||
|
||||
info("Wait for the stream filter to be started");
|
||||
await extension.awaitMessage("streamfilter:started");
|
||||
|
||||
if (expectStreamFilterStop) {
|
||||
await extension.awaitMessage("streamfilter:stopped");
|
||||
}
|
||||
|
||||
info("Terminate the background script (simulated idle timeout)");
|
||||
|
||||
if (expectResetIdle) {
|
||||
const promiseResetIdle = promiseExtensionEvent(
|
||||
extension,
|
||||
"background-script-reset-idle"
|
||||
);
|
||||
await extension.terminateBackground();
|
||||
info("Wait for 'background-script-reset-idle' event to be emitted");
|
||||
await promiseResetIdle;
|
||||
equal(
|
||||
extension.extension.backgroundContext.contextId,
|
||||
contextId,
|
||||
"Initial background context is still available as expected"
|
||||
);
|
||||
} else {
|
||||
const { Management } = ChromeUtils.import(
|
||||
"resource://gre/modules/Extension.jsm"
|
||||
);
|
||||
const promiseProxyContextUnloaded = new Promise(resolve => {
|
||||
function listener(evt, context) {
|
||||
if (context.extension.id === extension.id) {
|
||||
Management.off("proxy-context-unload", listener);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
Management.on("proxy-context-unload", listener);
|
||||
});
|
||||
await extension.terminateBackground();
|
||||
await promiseProxyContextUnloaded;
|
||||
equal(
|
||||
extension.extension.backgroundContext,
|
||||
undefined,
|
||||
"Initial background context should have been terminated as expected"
|
||||
);
|
||||
}
|
||||
|
||||
await extension.unload();
|
||||
}
|
||||
|
||||
add_task(
|
||||
{
|
||||
pref_set: [["extensions.eventPages.enabled", true]],
|
||||
},
|
||||
async function test_idletimeout_on_active_streamfilter_mv2_eventpage() {
|
||||
await test_idletimeout_on_streamfilter({
|
||||
manifest_version: 2,
|
||||
requestUrlPath: "pending_request",
|
||||
expectStreamFilterStop: false,
|
||||
expectResetIdle: true,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
add_task(
|
||||
{
|
||||
pref_set: [["extensions.manifestV3.enabled", true]],
|
||||
},
|
||||
async function test_idletimeout_on_active_streamfilter_mv3() {
|
||||
await test_idletimeout_on_streamfilter({
|
||||
manifest_version: 3,
|
||||
requestUrlPath: "pending_request",
|
||||
expectStreamFilterStop: false,
|
||||
expectResetIdle: true,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
add_task(
|
||||
{
|
||||
pref_set: [["extensions.eventPages.enabled", true]],
|
||||
},
|
||||
async function test_idletimeout_on_inactive_streamfilter_mv2_eventpage() {
|
||||
await test_idletimeout_on_streamfilter({
|
||||
manifest_version: 2,
|
||||
requestUrlPath: "completed_request",
|
||||
expectStreamFilterStop: true,
|
||||
expectResetIdle: false,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
add_task(
|
||||
{
|
||||
pref_set: [["extensions.manifestV3.enabled", true]],
|
||||
},
|
||||
async function test_idletimeout_on_inactive_streamfilter_mv3() {
|
||||
await test_idletimeout_on_streamfilter({
|
||||
manifest_version: 3,
|
||||
requestUrlPath: "completed_request",
|
||||
expectStreamFilterStop: true,
|
||||
expectResetIdle: false,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
async function test_create_new_streamfilter_while_suspending({
|
||||
manifest_version,
|
||||
}) {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
async background() {
|
||||
let interceptedRequestId;
|
||||
let resolvePendingWebRequest;
|
||||
|
||||
browser.runtime.onSuspend.addListener(async () => {
|
||||
await browser.test.assertThrows(
|
||||
() => browser.webRequest.filterResponseData(interceptedRequestId),
|
||||
/forbidden while background extension global is suspending/,
|
||||
"Got the expected exception raised from filterResponseData calls while suspending"
|
||||
);
|
||||
browser.test.sendMessage("suspend-listener");
|
||||
});
|
||||
|
||||
browser.runtime.onSuspendCanceled.addListener(async () => {
|
||||
// Once onSuspendCanceled is emitted, filterResponseData
|
||||
// is expected to don't throw.
|
||||
const filter = browser.webRequest.filterResponseData(
|
||||
interceptedRequestId
|
||||
);
|
||||
resolvePendingWebRequest();
|
||||
filter.onstop = () => {
|
||||
filter.disconnect();
|
||||
browser.test.sendMessage("suspend-canceled-listener");
|
||||
};
|
||||
});
|
||||
|
||||
browser.webRequest.onBeforeRequest.addListener(
|
||||
request => {
|
||||
browser.test.log(`webRequest request intercepted: ${request.url}`);
|
||||
interceptedRequestId = request.requestId;
|
||||
return new Promise(resolve => {
|
||||
resolvePendingWebRequest = resolve;
|
||||
browser.test.sendMessage("webrequest-listener:done");
|
||||
});
|
||||
},
|
||||
{
|
||||
urls: [`http://example.com/completed_request`],
|
||||
},
|
||||
["blocking"]
|
||||
);
|
||||
browser.test.sendMessage("bg:ready");
|
||||
},
|
||||
|
||||
useAddonManager: "temporary",
|
||||
manifest: {
|
||||
manifest_version,
|
||||
background: manifest_version >= 3 ? {} : { persistent: false },
|
||||
granted_host_permissions: manifest_version >= 3,
|
||||
permissions: ["webRequest", "webRequestBlocking"],
|
||||
// host_permissions are merged with permissions on a MV2 test extension.
|
||||
host_permissions: ["http://example.com/*"],
|
||||
},
|
||||
});
|
||||
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("bg:ready");
|
||||
const { contextId } = extension.extension.backgroundContext;
|
||||
notEqual(contextId, undefined, "Got a contextId for the background context");
|
||||
|
||||
info("Trigger a webRequest");
|
||||
ExtensionTestUtils.fetch(
|
||||
"http://example.com/",
|
||||
`http://example.com/completed_request`
|
||||
);
|
||||
|
||||
info("Wait for the web request to be intercepted and suspended");
|
||||
await extension.awaitMessage("webrequest-listener:done");
|
||||
|
||||
info("Terminate the background script (simulated idle timeout)");
|
||||
|
||||
extension.terminateBackground();
|
||||
await extension.awaitMessage("suspend-listener");
|
||||
|
||||
info("Simulated idle timeout canceled");
|
||||
extension.extension.emit("background-script-reset-idle");
|
||||
await extension.awaitMessage("suspend-canceled-listener");
|
||||
|
||||
await extension.unload();
|
||||
}
|
||||
|
||||
add_task(
|
||||
{
|
||||
pref_set: [["extensions.eventPages.enabled", true]],
|
||||
},
|
||||
async function test_error_creating_new_streamfilter_while_suspending_mv2_eventpage() {
|
||||
await test_create_new_streamfilter_while_suspending({
|
||||
manifest_version: 2,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
add_task(
|
||||
{
|
||||
pref_set: [["extensions.manifestV3.enabled", true]],
|
||||
},
|
||||
async function test_error_creating_new_streamfilter_while_suspending_mv3() {
|
||||
await test_create_new_streamfilter_while_suspending({
|
||||
manifest_version: 3,
|
||||
});
|
||||
}
|
||||
);
|
|
@ -1,6 +1,7 @@
|
|||
# Similar to xpcshell-common.ini, except tests here only run
|
||||
# when e10s is enabled (with or without out-of-process extensions).
|
||||
|
||||
[test_ext_webRequest_eventPage_StreamFilter.js]
|
||||
[test_ext_webRequest_filterResponseData.js]
|
||||
# tsan failure is for test_filter_301 timing out, bug 1674773
|
||||
skip-if =
|
||||
|
|
Загрузка…
Ссылка в новой задаче