diff --git a/dom/interfaces/base/nsIServiceWorkerManager.idl b/dom/interfaces/base/nsIServiceWorkerManager.idl index 3defcb1d23ea..cf578d2789ac 100644 --- a/dom/interfaces/base/nsIServiceWorkerManager.idl +++ b/dom/interfaces/base/nsIServiceWorkerManager.idl @@ -122,6 +122,18 @@ interface nsIServiceWorkerManagerListener : nsISupports [scriptable, builtinclass, uuid(7404c8e8-4d47-4449-8ed1-47d1261d4e33)] interface nsIServiceWorkerManager : nsISupports { + /** + * A testing helper that registers a service worker for testing purpose (e.g. used to test + * a remote worker that has to spawn a new process to be launched). + * This method can only be used when "dom.serviceWorkers.testing.enabled" is true and + * it doesn't support all the registration options (e.g. updateViaCache is set automatically + * to "imports"). + */ + [implicit_jscontext] + Promise registerForTest(in nsIPrincipal aPrincipal, + in AString aScope, + in AString aScriptURL); + /** * Unregister an existing ServiceWorker registration for `aScope`. * It keeps aCallback alive until the operation is concluded. diff --git a/dom/serviceworkers/ServiceWorkerManager.cpp b/dom/serviceworkers/ServiceWorkerManager.cpp index e9debd0901ce..c678173e571d 100644 --- a/dom/serviceworkers/ServiceWorkerManager.cpp +++ b/dom/serviceworkers/ServiceWorkerManager.cpp @@ -939,6 +939,85 @@ class ResolvePromiseRunnable final : public CancelableRunnable { } // namespace +NS_IMETHODIMP +ServiceWorkerManager::RegisterForTest(nsIPrincipal* aPrincipal, + const nsAString& aScopeURL, + const nsAString& aScriptURL, + JSContext* aCx, + mozilla::dom::Promise** aPromise) { + nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx); + if (NS_WARN_IF(!global)) { + return NS_ERROR_FAILURE; + } + + ErrorResult erv; + RefPtr outer = Promise::Create(global, erv); + if (NS_WARN_IF(erv.Failed())) { + return erv.StealNSResult(); + } + + if (!StaticPrefs::dom_serviceWorkers_testing_enabled()) { + outer->MaybeRejectWithAbortError( + "registerForTest only allowed when dom.serviceWorkers.testing.enabled " + "is true"); + return NS_OK; + } + + if (aPrincipal == nullptr) { + outer->MaybeRejectWithAbortError("Missing principal"); + return NS_OK; + } + + if (aScriptURL.IsEmpty()) { + outer->MaybeRejectWithAbortError("Missing script url"); + return NS_OK; + } + + if (aScopeURL.IsEmpty()) { + outer->MaybeRejectWithAbortError("Missing scope url"); + return NS_OK; + } + + // The ClientType isn't really used here, but ClientType::Window + // is the least bad choice since this is happening on the main thread. + Maybe clientInfo = + dom::ClientManager::CreateInfo(ClientType::Window, aPrincipal); + + if (!clientInfo.isSome()) { + outer->MaybeRejectWithUnknownError("Error creating clientInfo"); + return NS_OK; + } + + auto scope = NS_ConvertUTF16toUTF8(aScopeURL); + auto scriptURL = NS_ConvertUTF16toUTF8(aScriptURL); + + auto regPromise = Register(clientInfo.ref(), scope, scriptURL, + dom::ServiceWorkerUpdateViaCache::Imports); + const RefPtr self(this); + const nsCOMPtr principal(aPrincipal); + regPromise->Then( + GetMainThreadSerialEventTarget(), __func__, + [self, outer, principal, + scope](const ServiceWorkerRegistrationDescriptor& regDesc) { + RefPtr registration = + self->GetRegistration(principal, NS_ConvertUTF16toUTF8(scope)); + if (registration) { + outer->MaybeResolve(registration); + } else { + outer->MaybeRejectWithUnknownError( + "Failed to retrieve ServiceWorkerRegistrationInfo"); + } + }, + [outer](const mozilla::CopyableErrorResult& err) { + CopyableErrorResult result(err); + outer->MaybeReject(std::move(result)); + }); + + outer.forget(aPromise); + + return NS_OK; +} + RefPtr ServiceWorkerManager::Register( const ClientInfo& aClientInfo, const nsACString& aScopeURL, const nsACString& aScriptURL, ServiceWorkerUpdateViaCache aUpdateViaCache) { diff --git a/dom/workers/test/xpcshell/test_remoteworker_launch_new_process.js b/dom/workers/test/xpcshell/test_remoteworker_launch_new_process.js new file mode 100644 index 000000000000..1d9f7f7feac3 --- /dev/null +++ b/dom/workers/test/xpcshell/test_remoteworker_launch_new_process.js @@ -0,0 +1,134 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const { TestUtils } = ChromeUtils.import( + "resource://testing-common/TestUtils.jsm" +); + +const { AddonTestUtils } = ChromeUtils.import( + "resource://testing-common/AddonTestUtils.jsm" +); +const { createHttpServer } = AddonTestUtils; + +// Force ServiceWorkerRegistrar to init by calling do_get_profile. +// (This has to be called before AddonTestUtils.init, because it does +// also call do_get_profile internally but it doesn't notify +// profile-after-change). +do_get_profile(true); + +AddonTestUtils.init(this); + +const server = createHttpServer({ + hosts: ["localhost", "example.org"], +}); + +server.registerPathHandler("/sw.js", (request, response) => { + info(`/sw.js is being requested: ${JSON.stringify(request)}`); + response.setHeader("Content-Type", "application/javascript"); + response.write(""); +}); + +add_task(async function setup_prefs() { + equal( + Services.prefs.getBoolPref("browser.tabs.remote.autostart"), + true, + "e10s is expected to be enabled" + ); + + // Enable nsIServiceWorkerManager.registerForTest. + Services.prefs.setBoolPref("dom.serviceWorkers.testing.enabled", true); + + // Configure prefs to configure example.org as a domain to load + // in a privilegedmozilla content child process. + Services.prefs.setBoolPref( + "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", + true + ); + Services.prefs.setCharPref( + "browser.tabs.remote.separatedMozillaDomains", + "example.org" + ); + + registerCleanupFunction(() => { + Services.prefs.clearUserPref("dom.serviceWorkers.testing.enabled"); + Services.prefs.clearUserPref( + "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess" + ); + Services.prefs.clearUserPref("browser.tabs.remote.separatedMozillaDomains"); + }); +}); + +/** + * This test installs a ServiceWorker via test API and verify that the install + * process spawns a new process. (Normally ServiceWorker installation won't + * cause a new content process to be spawned because the call to register must + * be coming from within an existing content process, but the registerForTest + * API allows us to bypass this restriction.) + * + * This models the real-world situation of a push notification being received + * from the network which results in a ServiceWorker being spawned without their + * necessarily being an existing content process to host it (especially under Fission). + */ +add_task(async function launch_remoteworkers_in_new_processes() { + const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + + const ssm = Services.scriptSecurityManager; + + const initialChildCount = Services.ppmm.childCount; + + // A test service worker that should spawn a regular web content child process. + const swRegInfoWeb = await swm.registerForTest( + ssm.createContentPrincipal(Services.io.newURI("http://localhost"), {}), + "http://localhost/scope", + "http://localhost/sw.js" + ); + swRegInfoWeb.QueryInterface(Ci.nsIServiceWorkerRegistrationInfo); + + info( + `web content service worker registered: ${JSON.stringify({ + principal: swRegInfoWeb.principal.URI.spec, + scope: swRegInfoWeb.scope, + })}` + ); + + // A test service worker that should spawn a privilegedmozilla child process. + const swRegInfoPriv = await swm.registerForTest( + ssm.createContentPrincipal(Services.io.newURI("http://example.org"), {}), + "http://example.org/scope", + "http://example.org/sw.js" + ); + swRegInfoPriv.QueryInterface(Ci.nsIServiceWorkerRegistrationInfo); + + info( + `privilegedmozilla service worker registered: ${JSON.stringify({ + principal: swRegInfoPriv.principal.URI.spec, + scope: swRegInfoPriv.scope, + })}` + ); + + info("Wait new process to be launched"); + await TestUtils.waitForCondition(() => { + return Services.ppmm.childCount - initialChildCount >= 2; + }, "wait for a new child processes to be started"); + + // Wait both workers to become active to be sure that. besides spawning + // the new child processes as expected, the two remote worker have been + // able to run successfully (in other word their remote worker data did + // pass successfull the IsRemoteTypeAllowed check in RemoteworkerChild). + info("Wait for webcontent worker to become active"); + await TestUtils.waitForCondition( + () => swRegInfoPriv.activeWorker, + `wait workers for scope ${swRegInfoPriv.scope} to be active` + ); + + info("Wait for privilegedmozille worker to become active"); + await TestUtils.waitForCondition( + () => swRegInfoPriv.activeWorker, + `wait workers for scope ${swRegInfoPriv.scope} to be active` + ); +}); diff --git a/dom/workers/test/xpcshell/xpcshell.ini b/dom/workers/test/xpcshell/xpcshell.ini index 775faf289833..2acc533849c1 100644 --- a/dom/workers/test/xpcshell/xpcshell.ini +++ b/dom/workers/test/xpcshell/xpcshell.ini @@ -8,3 +8,12 @@ support-files = [test_workers.js] [test_fileReader.js] +[test_remoteworker_launch_new_process.js] +# The following firefox-appdir make sure that this xpcshell test will run +# with e10s enabled (which is needed to make sure that the test case is +# going to launch the expected new processes) +firefox-appdir = browser +# Disable plugin loading to make it rr able to record and replay this test. +prefs = + plugin.disable=true +