Bug 1068412: Forward dynamically registered resource URI mappings to content processes. r=billm
authorDave Townsend <dtownsend@oxymoronical.com>
Tue, 07 Oct 2014 10:29:40 -0700
changeset 209200 729529607a3a85d2979616e9299998c6ec747868
parent 209199 4fa74ef900c52830e0990d20aa4a91b0b8043353
child 209201 c1cef1625a48b4c5354041d3b451ae85d2648905
push idunknown
push userunknown
push dateunknown
reviewersbillm
bugs1068412
milestone35.0a1
Bug 1068412: Forward dynamically registered resource URI mappings to content processes. r=billm
chrome/RegistryMessageUtils.h
chrome/nsChromeRegistryChrome.cpp
dom/ipc/ContentChild.cpp
dom/ipc/PContent.ipdl
netwerk/protocol/res/nsResProtocolHandler.cpp
netwerk/test/browser/browser.ini
netwerk/test/browser/browser_child_resource.js
netwerk/test/browser/dummy.html
--- 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>