Bug 1068412: Forward dynamically registered resource URI mappings to content processes. r=billm
--- a/chrome/RegistryMessageUtils.h
+++ b/chrome/RegistryMessageUtils.h
@@ -38,16 +38,22 @@ struct ChromePackage
flags == rhs.flags;
}
};
struct ResourceMapping
{
nsCString resource;
SerializedURI resolvedURI;
+
+ bool operator ==(const ResourceMapping& rhs) const
+ {
+ return resource.Equals(rhs.resource) &&
+ resolvedURI == rhs.resolvedURI;
+ }
};
struct OverrideMapping
{
SerializedURI originalURI;
SerializedURI overrideURI;
bool operator==(const OverrideMapping& rhs) const
--- a/chrome/nsChromeRegistryChrome.cpp
+++ b/chrome/nsChromeRegistryChrome.cpp
@@ -448,27 +448,31 @@ nsChromeRegistryChrome::SendRegisteredCh
InfallibleTArray<ResourceMapping> resources;
InfallibleTArray<OverrideMapping> overrides;
EnumerationArgs args = {
packages, mSelectedLocale, mSelectedSkin
};
mPackagesHash.EnumerateRead(CollectPackages, &args);
- nsCOMPtr<nsIIOService> io (do_GetIOService());
- NS_ENSURE_TRUE_VOID(io);
+ // If we were passed a parent then a new child process has been created and
+ // has requested all of the chrome so send it the resources too. Otherwise
+ // resource mappings are sent by the resource protocol handler dynamically.
+ if (aParent) {
+ nsCOMPtr<nsIIOService> io (do_GetIOService());
+ NS_ENSURE_TRUE_VOID(io);
- nsCOMPtr<nsIProtocolHandler> ph;
- nsresult rv = io->GetProtocolHandler("resource", getter_AddRefs(ph));
- NS_ENSURE_SUCCESS_VOID(rv);
+ nsCOMPtr<nsIProtocolHandler> ph;
+ nsresult rv = io->GetProtocolHandler("resource", getter_AddRefs(ph));
+ NS_ENSURE_SUCCESS_VOID(rv);
- //FIXME: Some substitutions are set up lazily and might not exist yet
- nsCOMPtr<nsIResProtocolHandler> irph (do_QueryInterface(ph));
- nsResProtocolHandler* rph = static_cast<nsResProtocolHandler*>(irph.get());
- rph->CollectSubstitutions(resources);
+ nsCOMPtr<nsIResProtocolHandler> irph (do_QueryInterface(ph));
+ nsResProtocolHandler* rph = static_cast<nsResProtocolHandler*>(irph.get());
+ rph->CollectSubstitutions(resources);
+ }
mOverrideTable.EnumerateRead(&EnumerateOverride, &overrides);
if (aParent) {
bool success = aParent->SendRegisterChrome(packages, resources, overrides,
mSelectedLocale, false);
NS_ENSURE_TRUE_VOID(success);
} else {
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -1569,16 +1569,20 @@ ContentChild::RecvRegisterChromeItem(con
case ChromeRegistryItem::TChromePackage:
chromeRegistry->RegisterPackage(item.get_ChromePackage());
break;
case ChromeRegistryItem::TOverrideMapping:
chromeRegistry->RegisterOverride(item.get_OverrideMapping());
break;
+ case ChromeRegistryItem::TResourceMapping:
+ chromeRegistry->RegisterResource(item.get_ResourceMapping());
+ break;
+
default:
MOZ_ASSERT(false, "bad chrome item");
return false;
}
return true;
}
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -66,16 +66,17 @@ using mozilla::dom::NativeThreadId from
using mozilla::dom::quota::PersistenceType from "mozilla/dom/quota/PersistenceType.h";
using mozilla::hal::ProcessPriority from "mozilla/HalTypes.h";
using gfxIntSize from "nsSize.h";
union ChromeRegistryItem
{
ChromePackage;
OverrideMapping;
+ ResourceMapping;
};
namespace mozilla {
namespace dom {
struct FontListEntry {
nsString familyName;
nsString faceName;
--- a/netwerk/protocol/res/nsResProtocolHandler.cpp
+++ b/netwerk/protocol/res/nsResProtocolHandler.cpp
@@ -1,24 +1,29 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* 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/. */
#include "mozilla/chrome/RegistryMessageUtils.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/unused.h"
#include "nsResProtocolHandler.h"
#include "nsIIOService.h"
#include "nsIFile.h"
#include "nsNetUtil.h"
#include "nsURLHelper.h"
#include "nsEscape.h"
#include "mozilla/Omnijar.h"
+using mozilla::dom::ContentParent;
+using mozilla::unused;
+
static NS_DEFINE_CID(kResURLCID, NS_RESURL_CID);
static nsResProtocolHandler *gResHandler = nullptr;
#if defined(PR_LOGGING)
//
// Log module for Resource Protocol logging...
//
@@ -299,44 +304,72 @@ nsResProtocolHandler::AllowPort(int32_t
*_retval = false;
return NS_OK;
}
//----------------------------------------------------------------------------
// nsResProtocolHandler::nsIResProtocolHandler
//----------------------------------------------------------------------------
+static void
+SendResourceSubstitution(const nsACString& root, nsIURI* baseURI)
+{
+ if (GeckoProcessType_Content == XRE_GetProcessType()) {
+ return;
+ }
+
+ ResourceMapping resourceMapping;
+ resourceMapping.resource = root;
+ if (baseURI) {
+ baseURI->GetSpec(resourceMapping.resolvedURI.spec);
+ baseURI->GetOriginCharset(resourceMapping.resolvedURI.charset);
+ }
+
+ nsTArray<ContentParent*> parents;
+ ContentParent::GetAll(parents);
+ if (!parents.Length()) {
+ return;
+ }
+
+ for (uint32_t i = 0; i < parents.Length(); i++) {
+ unused << parents[i]->SendRegisterChromeItem(resourceMapping);
+ }
+}
+
NS_IMETHODIMP
nsResProtocolHandler::SetSubstitution(const nsACString& root, nsIURI *baseURI)
{
if (!baseURI) {
mSubstitutions.Remove(root);
+ SendResourceSubstitution(root, baseURI);
return NS_OK;
}
// If baseURI isn't a resource URI, we can set the substitution immediately.
nsAutoCString scheme;
nsresult rv = baseURI->GetScheme(scheme);
NS_ENSURE_SUCCESS(rv, rv);
if (!scheme.EqualsLiteral("resource")) {
mSubstitutions.Put(root, baseURI);
+ SendResourceSubstitution(root, baseURI);
return NS_OK;
}
// baseURI is a resource URI, let's resolve it first.
nsAutoCString newBase;
rv = ResolveURI(baseURI, newBase);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIURI> newBaseURI;
rv = mIOService->NewURI(newBase, nullptr, nullptr,
getter_AddRefs(newBaseURI));
NS_ENSURE_SUCCESS(rv, rv);
mSubstitutions.Put(root, newBaseURI);
+ SendResourceSubstitution(root, newBaseURI);
return NS_OK;
}
NS_IMETHODIMP
nsResProtocolHandler::GetSubstitution(const nsACString& root, nsIURI **result)
{
NS_ENSURE_ARG_POINTER(result);
--- a/netwerk/test/browser/browser.ini
+++ b/netwerk/test/browser/browser.ini
@@ -1,3 +1,6 @@
[DEFAULT]
+support-files =
+ dummy.html
[browser_NetUtil.js]
+[browser_child_resource.js]
new file mode 100644
--- /dev/null
+++ b/netwerk/test/browser/browser_child_resource.js
@@ -0,0 +1,256 @@
+/*
+Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+// This must be loaded in the remote process for this test to be useful
+const TEST_URL = "http://example.com/browser/netwerk/test/browser/dummy.html";
+
+const expectedRemote = gMultiProcessBrowser ? "true" : "";
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+const resProtocol = Cc["@mozilla.org/network/protocol;1?name=resource"]
+ .getService(Ci.nsIResProtocolHandler);
+
+function getMinidumpDirectory() {
+ var dir = Services.dirsvc.get('ProfD', Ci.nsIFile);
+ dir.append("minidumps");
+ return dir;
+}
+
+// This observer is needed so we can clean up all evidence of the crash so
+// the testrunner thinks things are peachy.
+let CrashObserver = {
+ observe: function(subject, topic, data) {
+ is(topic, 'ipc:content-shutdown', 'Received correct observer topic.');
+ ok(subject instanceof Ci.nsIPropertyBag2,
+ 'Subject implements nsIPropertyBag2.');
+ // we might see this called as the process terminates due to previous tests.
+ // We are only looking for "abnormal" exits...
+ if (!subject.hasKey("abnormal")) {
+ info("This is a normal termination and isn't the one we are looking for...");
+ return;
+ }
+
+ var dumpID;
+ if ('nsICrashReporter' in Ci) {
+ dumpID = subject.getPropertyAsAString('dumpID');
+ ok(dumpID, "dumpID is present and not an empty string");
+ }
+
+ if (dumpID) {
+ var minidumpDirectory = getMinidumpDirectory();
+ let file = minidumpDirectory.clone();
+ file.append(dumpID + '.dmp');
+ file.remove(true);
+ file = minidumpDirectory.clone();
+ file.append(dumpID + '.extra');
+ file.remove(true);
+ }
+ }
+}
+Services.obs.addObserver(CrashObserver, 'ipc:content-shutdown', false);
+
+registerCleanupFunction(() => {
+ Services.obs.removeObserver(CrashObserver, 'ipc:content-shutdown');
+});
+
+function frameScript() {
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ let resProtocol = Components.classes["@mozilla.org/network/protocol;1?name=resource"]
+ .getService(Components.interfaces.nsIResProtocolHandler);
+
+ addMessageListener("Test:ResolveURI", function({ data: uri }) {
+ uri = Services.io.newURI(uri, null, null);
+ try {
+ let resolved = resProtocol.resolveURI(uri);
+ sendAsyncMessage("Test:ResolvedURI", resolved);
+ }
+ catch (e) {
+ sendAsyncMessage("Test:ResolvedURI", null);
+ }
+ });
+
+ addMessageListener("Test:Crash", function() {
+ dump("Crashing\n");
+ privateNoteIntentionalCrash();
+ Components.utils.import("resource://gre/modules/ctypes.jsm");
+ let zero = new ctypes.intptr_t(8);
+ let badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));
+ badptr.contents
+ });
+}
+
+function waitForEvent(obj, name, capturing, chromeEvent) {
+ info("Waiting for " + name);
+ return new Promise((resolve) => {
+ function listener(event) {
+ info("Saw " + name);
+ obj.removeEventListener(name, listener, capturing, chromeEvent);
+ resolve(event);
+ }
+
+ obj.addEventListener(name, listener, capturing, chromeEvent);
+ });
+}
+
+function resolveURI(uri) {
+ uri = Services.io.newURI(uri, null, null);
+ try {
+ return resProtocol.resolveURI(uri);
+ }
+ catch (e) {
+ return null;
+ }
+}
+
+function remoteResolveURI(uri) {
+ return new Promise((resolve) => {
+ let manager = gBrowser.selectedBrowser.messageManager;
+
+ function listener({ data: resolved }) {
+ manager.removeMessageListener("Test:ResolvedURI", listener);
+ resolve(resolved);
+ }
+
+ manager.addMessageListener("Test:ResolvedURI", listener);
+ manager.sendAsyncMessage("Test:ResolveURI", uri);
+ });
+}
+
+let loadTestTab = Task.async(function*() {
+ gBrowser.selectedTab = gBrowser.addTab(TEST_URL);
+ let browser = gBrowser.selectedBrowser;
+ yield waitForEvent(browser, "load", true);
+ browser.messageManager.loadFrameScript("data:,(" + frameScript.toString() + ")();", true);
+ return browser;
+});
+
+// Restarts the child process by crashing it then reloading the tab
+let restart = Task.async(function*() {
+ let browser = gBrowser.selectedBrowser;
+ // If the tab isn't remote this would crash the main process so skip it
+ if (browser.getAttribute("remote") != "true")
+ return browser;
+
+ browser.messageManager.sendAsyncMessage("Test:Crash");
+ yield waitForEvent(browser, "AboutTabCrashedLoad", false, true);
+
+ browser.reload();
+
+ yield waitForEvent(browser, "load", true);
+ is(browser.getAttribute("remote"), expectedRemote, "Browser should be in the right process");
+ browser.messageManager.loadFrameScript("data:,(" + frameScript.toString() + ")();", true);
+ return browser;
+});
+
+// Sanity check that this test is going to be useful
+add_task(function*() {
+ let browser = yield loadTestTab();
+
+ // This must be loaded in the remote process for this test to be useful
+ is(browser.getAttribute("remote"), expectedRemote, "Browser should be in the right process");
+
+ let local = resolveURI("resource://gre/modules/Services.jsm");
+ let remote = yield remoteResolveURI("resource://gre/modules/Services.jsm");
+ is(local, remote, "Services.jsm should resolve in both processes");
+
+ gBrowser.removeCurrentTab();
+});
+
+// Add a mapping, update it then remove it
+add_task(function*() {
+ let browser = yield loadTestTab();
+
+ info("Set");
+ resProtocol.setSubstitution("testing", Services.io.newURI("chrome://global/content", null, null));
+ let local = resolveURI("resource://testing/test.js");
+ let remote = yield remoteResolveURI("resource://testing/test.js");
+ is(local, "chrome://global/content/test.js", "Should resolve in main process");
+ is(remote, "chrome://global/content/test.js", "Should resolve in child process");
+
+ info("Change");
+ resProtocol.setSubstitution("testing", Services.io.newURI("chrome://global/skin", null, null));
+ local = resolveURI("resource://testing/test.js");
+ remote = yield remoteResolveURI("resource://testing/test.js");
+ is(local, "chrome://global/skin/test.js", "Should resolve in main process");
+ is(remote, "chrome://global/skin/test.js", "Should resolve in child process");
+
+ info("Clear");
+ resProtocol.setSubstitution("testing", null);
+ local = resolveURI("resource://testing/test.js");
+ remote = yield remoteResolveURI("resource://testing/test.js");
+ is(local, null, "Shouldn't resolve in main process");
+ is(remote, null, "Shouldn't resolve in child process");
+
+ gBrowser.removeCurrentTab();
+});
+
+// Add a mapping, restart the child process then check it is still there
+add_task(function*() {
+ let browser = yield loadTestTab();
+
+ info("Set");
+ resProtocol.setSubstitution("testing", Services.io.newURI("chrome://global/content", null, null));
+ let local = resolveURI("resource://testing/test.js");
+ let remote = yield remoteResolveURI("resource://testing/test.js");
+ is(local, "chrome://global/content/test.js", "Should resolve in main process");
+ is(remote, "chrome://global/content/test.js", "Should resolve in child process");
+
+ yield restart();
+
+ local = resolveURI("resource://testing/test.js");
+ remote = yield remoteResolveURI("resource://testing/test.js");
+ is(local, "chrome://global/content/test.js", "Should resolve in main process");
+ is(remote, "chrome://global/content/test.js", "Should resolve in child process");
+
+ info("Change");
+ resProtocol.setSubstitution("testing", Services.io.newURI("chrome://global/skin", null, null));
+
+ yield restart();
+
+ local = resolveURI("resource://testing/test.js");
+ remote = yield remoteResolveURI("resource://testing/test.js");
+ is(local, "chrome://global/skin/test.js", "Should resolve in main process");
+ is(remote, "chrome://global/skin/test.js", "Should resolve in child process");
+
+ info("Clear");
+ resProtocol.setSubstitution("testing", null);
+
+ yield restart();
+
+ local = resolveURI("resource://testing/test.js");
+ remote = yield remoteResolveURI("resource://testing/test.js");
+ is(local, null, "Shouldn't resolve in main process");
+ is(remote, null, "Shouldn't resolve in child process");
+
+ gBrowser.removeCurrentTab();
+});
+
+// Adding a mapping to a resource URI should work
+add_task(function*() {
+ let browser = yield loadTestTab();
+
+ info("Set");
+ resProtocol.setSubstitution("testing", Services.io.newURI("chrome://global/content", null, null));
+ resProtocol.setSubstitution("testing2", Services.io.newURI("resource://testing", null, null));
+ let local = resolveURI("resource://testing2/test.js");
+ let remote = yield remoteResolveURI("resource://testing2/test.js");
+ is(local, "chrome://global/content/test.js", "Should resolve in main process");
+ is(remote, "chrome://global/content/test.js", "Should resolve in child process");
+
+ info("Clear");
+ resProtocol.setSubstitution("testing", null);
+ local = resolveURI("resource://testing2/test.js");
+ remote = yield remoteResolveURI("resource://testing2/test.js");
+ is(local, "chrome://global/content/test.js", "Should resolve in main process");
+ is(remote, "chrome://global/content/test.js", "Should resolve in child process");
+
+ resProtocol.setSubstitution("testing2", null);
+ local = resolveURI("resource://testing2/test.js");
+ remote = yield remoteResolveURI("resource://testing2/test.js");
+ is(local, null, "Shouldn't resolve in main process");
+ is(remote, null, "Shouldn't resolve in child process");
+
+ gBrowser.removeCurrentTab();
+});
new file mode 100644
--- /dev/null
+++ b/netwerk/test/browser/dummy.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+
+<html>
+<body>
+ <p>Dummy Page</p>
+</body>
+</html>