Bug 1569135 Fix --screenshot r=kmag a=RyanVM
authorAndrew Swan <aswan@mozilla.com>
Wed, 07 Aug 2019 21:33:49 +0000
changeset 545058 f80d1353670b6519a5621349c9e51aa1efb566ad
parent 545057 b0454be04d9e416c9f63c65cf1d465a698b5d4c5
child 545059 0291822afd2da38c59d12ff0fee97fb5ae34432f
push id2131
push userffxbld-merge
push dateMon, 26 Aug 2019 18:30:20 +0000
treeherdermozilla-release@b19ffb3ca153 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag, RyanVM
bugs1569135
milestone69.0
Bug 1569135 Fix --screenshot r=kmag a=RyanVM This patch ressurects HiddenFrame.jsm and uses it when handling the --screenshot command line argument to load the requested page in a content process. The actual logic for grabbing the image is also ported to a JSWindowActor. The test for this feature remains suboptimal as described in the bug. Differential Revision: https://phabricator.services.mozilla.com/D40148
browser/base/content/test/static/browser_all_files_referenced.js
browser/base/content/test/static/browser_parsable_css.js
browser/components/extensions/test/browser/browser_ext_webRequest.js
browser/components/shell/HeadlessShell.jsm
browser/components/shell/ScreenshotChild.jsm
browser/components/shell/moz.build
browser/components/uitour/test/browser_no_tabs.js
toolkit/modules/moz.build
--- a/browser/base/content/test/static/browser_all_files_referenced.js
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -98,19 +98,16 @@ var whitelist = [
 
   // browser/extensions/pdfjs/content/web/viewer.js
   { file: "resource://pdf.js/build/pdf.worker.js" },
 
   // layout/mathml/nsMathMLChar.cpp
   { file: "resource://gre/res/fonts/mathfontSTIXGeneral.properties" },
   { file: "resource://gre/res/fonts/mathfontUnicode.properties" },
 
-  // Needed by HiddenFrame.jsm, but can't be packaged test-only
-  { file: "chrome://global/content/win.xul" },
-
   // The l10n build system can't package string files only for some platforms.
   {
     file:
       "resource://gre/chrome/en-US/locale/en-US/global-platform/mac/accessible.properties",
     platforms: ["linux", "win"],
   },
   {
     file:
--- a/browser/base/content/test/static/browser_parsable_css.js
+++ b/browser/base/content/test/static/browser_parsable_css.js
@@ -392,17 +392,17 @@ add_task(async function checkAllTheCSS()
   // test infrastructure because it runs against jarfiles there, and
   // our zipreader APIs are all sync)
   let uris = await generateURIsFromDirTree(appDir, [".css", ".manifest"]);
 
   // Create a clean iframe to load all the files into. This needs to live at a
   // chrome URI so that it's allowed to load and parse any styles.
   let testFile = getRootDirectory(gTestPath) + "dummy_page.html";
   let HiddenFrame = ChromeUtils.import(
-    "resource://testing-common/HiddenFrame.jsm",
+    "resource://gre/modules/HiddenFrame.jsm",
     {}
   ).HiddenFrame;
   let hiddenFrame = new HiddenFrame();
   let win = await hiddenFrame.get();
   let iframe = win.document.createElementNS(
     "http://www.w3.org/1999/xhtml",
     "html:iframe"
   );
--- a/browser/components/extensions/test/browser/browser_ext_webRequest.js
+++ b/browser/components/extensions/test/browser/browser_ext_webRequest.js
@@ -1,16 +1,16 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 /* import-globals-from ../../../../../toolkit/components/extensions/test/mochitest/head_webrequest.js */
 loadTestSubscript("head_webrequest.js");
 
-ChromeUtils.import("resource://testing-common/HiddenFrame.jsm", this);
+ChromeUtils.import("resource://gre/modules/HiddenFrame.jsm", this);
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 SimpleTest.requestCompleteLog();
 
 function createHiddenBrowser(url) {
   let frame = new HiddenFrame();
   return new Promise(resolve =>
     frame.get().then(subframe => {
--- a/browser/components/shell/HeadlessShell.jsm
+++ b/browser/components/shell/HeadlessShell.jsm
@@ -1,148 +1,144 @@
 /* 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/. */
 
 "use strict";
 
-var EXPORTED_SYMBOLS = ["HeadlessShell"];
+var EXPORTED_SYMBOLS = ["HeadlessShell", "ScreenshotParent"];
 
+const { E10SUtils } = ChromeUtils.import(
+  "resource://gre/modules/E10SUtils.jsm"
+);
+const { HiddenFrame } = ChromeUtils.import(
+  "resource://gre/modules/HiddenFrame.jsm"
+);
+const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
 
 // Refrences to the progress listeners to keep them from being gc'ed
 // before they are called.
-const progressListeners = new Map();
+const progressListeners = new Set();
+
+class ScreenshotParent extends JSWindowActorParent {
+  takeScreenshot(params) {
+    return this.sendQuery("TakeScreenshot", params);
+  }
+}
 
-function loadContentWindow(webNavigation, url, principal) {
+ChromeUtils.registerWindowActor("Screenshot", {
+  parent: {
+    moduleURI: "resource:///modules/HeadlessShell.jsm",
+  },
+  child: {
+    moduleURI: "resource:///modules/ScreenshotChild.jsm",
+    messages: ["TakeScreenshot"],
+  },
+});
+
+function loadContentWindow(browser, url) {
   let uri;
   try {
     uri = Services.io.newURI(url);
   } catch (e) {
     let msg = `Invalid URL passed to loadContentWindow(): ${url}`;
     Cu.reportError(msg);
     return Promise.reject(new Error(msg));
   }
+
+  const principal = Services.scriptSecurityManager.getSystemPrincipal();
   return new Promise((resolve, reject) => {
     let loadURIOptions = {
       triggeringPrincipal: principal,
+      remoteType: E10SUtils.getRemoteTypeForURI(url, true, false),
     };
-    webNavigation.loadURI(uri.spec, loadURIOptions);
-    let docShell = webNavigation
-      .QueryInterface(Ci.nsIInterfaceRequestor)
-      .getInterface(Ci.nsIDocShell);
-    let webProgress = docShell
-      .QueryInterface(Ci.nsIInterfaceRequestor)
-      .getInterface(Ci.nsIWebProgress);
+    browser.loadURI(uri.spec, loadURIOptions);
+    let { webProgress } = browser;
+
     let progressListener = {
       onLocationChange(progress, request, location, flags) {
         // Ignore inner-frame events
-        if (progress != webProgress) {
+        if (!progress.isTopLevel) {
           return;
         }
         // Ignore events that don't change the document
         if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
           return;
         }
         // Ignore the initial about:blank, unless about:blank is requested
         if (location.spec == "about:blank" && uri.spec != "about:blank") {
           return;
         }
-        let contentWindow = docShell.domWindow;
+
         progressListeners.delete(progressListener);
         webProgress.removeProgressListener(progressListener);
-        contentWindow.addEventListener(
-          "load",
-          event => {
-            resolve(contentWindow);
-          },
-          { once: true }
-        );
+        resolve();
       },
       QueryInterface: ChromeUtils.generateQI([
         "nsIWebProgressListener",
         "nsISupportsWeakReference",
       ]),
     };
-    progressListeners.set(progressListener, progressListener);
+    progressListeners.add(progressListener);
     webProgress.addProgressListener(
       progressListener,
       Ci.nsIWebProgress.NOTIFY_LOCATION
     );
   });
 }
 
 async function takeScreenshot(
   fullWidth,
   fullHeight,
   contentWidth,
   contentHeight,
   path,
   url
 ) {
+  let frame;
   try {
-    var windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
-    // nsIWindowlessBrowser inherits from nsIWebNavigation.
-    let contentWindow = await loadContentWindow(
-      windowlessBrowser,
-      url,
-      Services.scriptSecurityManager.getSystemPrincipal()
-    );
-    contentWindow.resizeTo(contentWidth, contentHeight);
+    frame = new HiddenFrame();
+    let windowlessBrowser = await frame.get();
 
-    let canvas = contentWindow.document.createElementNS(
-      "http://www.w3.org/1999/xhtml",
-      "html:canvas"
+    let doc = windowlessBrowser.document;
+    let browser = doc.createXULElement("browser");
+    browser.setAttribute("remote", "true");
+    browser.setAttribute("type", "content");
+    browser.setAttribute(
+      "style",
+      `width: ${contentWidth}px; min-width: ${contentWidth}px; height: ${contentHeight}px; min-height: ${contentHeight}px;`
     );
-    let context = canvas.getContext("2d");
-    let width = fullWidth
-      ? contentWindow.innerWidth +
-        contentWindow.scrollMaxX -
-        contentWindow.scrollMinX
-      : contentWindow.innerWidth;
-    let height = fullHeight
-      ? contentWindow.innerHeight +
-        contentWindow.scrollMaxY -
-        contentWindow.scrollMinY
-      : contentWindow.innerHeight;
-    canvas.width = width;
-    canvas.height = height;
-    context.drawWindow(
-      contentWindow,
-      0,
-      0,
-      width,
-      height,
-      "rgb(255, 255, 255)"
+    doc.documentElement.appendChild(browser);
+
+    await loadContentWindow(browser, url);
+
+    let actor = browser.browsingContext.currentWindowGlobal.getActor(
+      "Screenshot"
     );
-
-    function getBlob() {
-      return new Promise(resolve => canvas.toBlob(resolve));
-    }
+    let blob = await actor.takeScreenshot({
+      fullWidth,
+      fullHeight,
+    });
 
-    function readBlob(blob) {
-      return new Promise(resolve => {
-        let reader = new FileReader();
-        reader.onloadend = () => resolve(reader);
-        reader.readAsArrayBuffer(blob);
-      });
-    }
+    let reader = await new Promise(resolve => {
+      let fr = new FileReader();
+      fr.onloadend = () => resolve(fr);
+      fr.readAsArrayBuffer(blob);
+    });
 
-    let blob = await getBlob();
-    let reader = await readBlob(blob);
     await OS.File.writeAtomic(path, new Uint8Array(reader.result), {
       flush: true,
     });
     dump("Screenshot saved to: " + path + "\n");
   } catch (e) {
     dump("Failure taking screenshot: " + e + "\n");
   } finally {
-    if (windowlessBrowser) {
-      windowlessBrowser.close();
+    if (frame) {
+      frame.destroy();
     }
   }
 }
 
 let HeadlessShell = {
   async handleCmdLineArgs(cmdLine, URLlist) {
     try {
       // Don't quit even though we don't create a window
copy from browser/components/shell/HeadlessShell.jsm
copy to browser/components/shell/ScreenshotChild.jsm
--- a/browser/components/shell/HeadlessShell.jsm
+++ b/browser/components/shell/ScreenshotChild.jsm
@@ -1,225 +1,50 @@
-/* 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/. */
-
 "use strict";
 
-var EXPORTED_SYMBOLS = ["HeadlessShell"];
-
-const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
-
-// Refrences to the progress listeners to keep them from being gc'ed
-// before they are called.
-const progressListeners = new Map();
+const EXPORTED_SYMBOLS = ["ScreenshotChild"];
 
-function loadContentWindow(webNavigation, url, principal) {
-  let uri;
-  try {
-    uri = Services.io.newURI(url);
-  } catch (e) {
-    let msg = `Invalid URL passed to loadContentWindow(): ${url}`;
-    Cu.reportError(msg);
-    return Promise.reject(new Error(msg));
+class ScreenshotChild extends JSWindowActorChild {
+  receiveMessage(message) {
+    if (message.name == "TakeScreenshot") {
+      return this.takeScreenshot(message.data);
+    }
+    return null;
   }
-  return new Promise((resolve, reject) => {
-    let loadURIOptions = {
-      triggeringPrincipal: principal,
-    };
-    webNavigation.loadURI(uri.spec, loadURIOptions);
-    let docShell = webNavigation
-      .QueryInterface(Ci.nsIInterfaceRequestor)
-      .getInterface(Ci.nsIDocShell);
-    let webProgress = docShell
-      .QueryInterface(Ci.nsIInterfaceRequestor)
-      .getInterface(Ci.nsIWebProgress);
-    let progressListener = {
-      onLocationChange(progress, request, location, flags) {
-        // Ignore inner-frame events
-        if (progress != webProgress) {
-          return;
-        }
-        // Ignore events that don't change the document
-        if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
-          return;
-        }
-        // Ignore the initial about:blank, unless about:blank is requested
-        if (location.spec == "about:blank" && uri.spec != "about:blank") {
-          return;
-        }
-        let contentWindow = docShell.domWindow;
-        progressListeners.delete(progressListener);
-        webProgress.removeProgressListener(progressListener);
-        contentWindow.addEventListener(
-          "load",
-          event => {
-            resolve(contentWindow);
-          },
-          { once: true }
-        );
-      },
-      QueryInterface: ChromeUtils.generateQI([
-        "nsIWebProgressListener",
-        "nsISupportsWeakReference",
-      ]),
-    };
-    progressListeners.set(progressListener, progressListener);
-    webProgress.addProgressListener(
-      progressListener,
-      Ci.nsIWebProgress.NOTIFY_LOCATION
-    );
-  });
-}
 
-async function takeScreenshot(
-  fullWidth,
-  fullHeight,
-  contentWidth,
-  contentHeight,
-  path,
-  url
-) {
-  try {
-    var windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
-    // nsIWindowlessBrowser inherits from nsIWebNavigation.
-    let contentWindow = await loadContentWindow(
-      windowlessBrowser,
-      url,
-      Services.scriptSecurityManager.getSystemPrincipal()
-    );
-    contentWindow.resizeTo(contentWidth, contentHeight);
+  async takeScreenshot(params) {
+    if (this.document.readyState != "complete") {
+      await new Promise(resolve =>
+        this.contentWindow.addEventListener("load", resolve, { once: true })
+      );
+    }
+
+    let { fullWidth, fullHeight } = params;
+    let { contentWindow } = this;
 
     let canvas = contentWindow.document.createElementNS(
       "http://www.w3.org/1999/xhtml",
       "html:canvas"
     );
     let context = canvas.getContext("2d");
-    let width = fullWidth
-      ? contentWindow.innerWidth +
-        contentWindow.scrollMaxX -
-        contentWindow.scrollMinX
-      : contentWindow.innerWidth;
-    let height = fullHeight
-      ? contentWindow.innerHeight +
-        contentWindow.scrollMaxY -
-        contentWindow.scrollMinY
-      : contentWindow.innerHeight;
+    let width = contentWindow.innerWidth;
+    let height = contentWindow.innerHeight;
+    if (fullWidth) {
+      width += contentWindow.scrollMaxX - contentWindow.scrollMinX;
+    }
+    if (fullHeight) {
+      height += contentWindow.scrollMaxY - contentWindow.scrollMinY;
+    }
+
     canvas.width = width;
     canvas.height = height;
     context.drawWindow(
       contentWindow,
       0,
       0,
       width,
       height,
       "rgb(255, 255, 255)"
     );
 
-    function getBlob() {
-      return new Promise(resolve => canvas.toBlob(resolve));
-    }
-
-    function readBlob(blob) {
-      return new Promise(resolve => {
-        let reader = new FileReader();
-        reader.onloadend = () => resolve(reader);
-        reader.readAsArrayBuffer(blob);
-      });
-    }
-
-    let blob = await getBlob();
-    let reader = await readBlob(blob);
-    await OS.File.writeAtomic(path, new Uint8Array(reader.result), {
-      flush: true,
-    });
-    dump("Screenshot saved to: " + path + "\n");
-  } catch (e) {
-    dump("Failure taking screenshot: " + e + "\n");
-  } finally {
-    if (windowlessBrowser) {
-      windowlessBrowser.close();
-    }
+    return new Promise(resolve => canvas.toBlob(resolve));
   }
 }
-
-let HeadlessShell = {
-  async handleCmdLineArgs(cmdLine, URLlist) {
-    try {
-      // Don't quit even though we don't create a window
-      Services.startup.enterLastWindowClosingSurvivalArea();
-
-      // Default options
-      let fullWidth = true;
-      let fullHeight = true;
-      // Most common screen resolution of Firefox users
-      let contentWidth = 1366;
-      let contentHeight = 768;
-
-      // Parse `window-size`
-      try {
-        var dimensionsStr = cmdLine.handleFlagWithParam("window-size", true);
-      } catch (e) {
-        dump("expected format: --window-size width[,height]\n");
-        return;
-      }
-      if (dimensionsStr) {
-        let success;
-        let dimensions = dimensionsStr.split(",", 2);
-        if (dimensions.length == 1) {
-          success = dimensions[0] > 0;
-          if (success) {
-            fullWidth = false;
-            fullHeight = true;
-            contentWidth = dimensions[0];
-          }
-        } else {
-          success = dimensions[0] > 0 && dimensions[1] > 0;
-          if (success) {
-            fullWidth = false;
-            fullHeight = false;
-            contentWidth = dimensions[0];
-            contentHeight = dimensions[1];
-          }
-        }
-
-        if (!success) {
-          dump("expected format: --window-size width[,height]\n");
-          return;
-        }
-      }
-
-      // Only command line argument left should be `screenshot`
-      // There could still be URLs however
-      try {
-        var path = cmdLine.handleFlagWithParam("screenshot", true);
-        if (!cmdLine.length && !URLlist.length) {
-          URLlist.push(path); // Assume the user wanted to specify a URL
-          path = OS.Path.join(cmdLine.workingDirectory.path, "screenshot.png");
-        }
-      } catch (e) {
-        path = OS.Path.join(cmdLine.workingDirectory.path, "screenshot.png");
-        cmdLine.handleFlag("screenshot", true); // Remove `screenshot`
-      }
-
-      for (let i = 0; i < cmdLine.length; ++i) {
-        URLlist.push(cmdLine.getArgument(i)); // Assume that all remaining arguments are URLs
-      }
-
-      if (URLlist.length == 1) {
-        await takeScreenshot(
-          fullWidth,
-          fullHeight,
-          contentWidth,
-          contentHeight,
-          path,
-          URLlist[0]
-        );
-      } else {
-        dump("expected exactly one URL when using `screenshot`\n");
-      }
-    } finally {
-      Services.startup.exitLastWindowClosingSurvivalArea();
-      Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
-    }
-  },
-};
--- a/browser/components/shell/moz.build
+++ b/browser/components/shell/moz.build
@@ -44,16 +44,17 @@ elif CONFIG['OS_ARCH'] == 'WINNT':
 
 XPIDL_MODULE = 'shellservice'
 
 if SOURCES:
     FINAL_LIBRARY = 'browsercomps'
 
 EXTRA_JS_MODULES += [
     'HeadlessShell.jsm',
+    'ScreenshotChild.jsm',
     'ShellService.jsm',
 ]
 
 for var in ('MOZ_APP_NAME', 'MOZ_APP_VERSION'):
     DEFINES[var] = '"%s"' % CONFIG[var]
 
 CXXFLAGS += CONFIG['TK_CFLAGS']
 
--- a/browser/components/uitour/test/browser_no_tabs.js
+++ b/browser/components/uitour/test/browser_no_tabs.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 var HiddenFrame = ChromeUtils.import(
-  "resource://testing-common/HiddenFrame.jsm",
+  "resource://gre/modules/HiddenFrame.jsm",
   {}
 ).HiddenFrame;
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 /**
  * Create a frame in the |hiddenDOMWindow| to host a |browser|, then load the URL in the
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -142,17 +142,16 @@ with Files('WindowsRegistry.jsm'):
     BUG_COMPONENT = ('Toolkit', 'General')
 
 
 XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
 BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
 MOCHITEST_CHROME_MANIFESTS += ['tests/chrome/chrome.ini']
 
 TESTING_JS_MODULES += [
-    'HiddenFrame.jsm',
     'tests/modules/MockDocument.jsm',
     'tests/modules/PromiseTestUtils.jsm',
     'tests/modules/Task.jsm',
     'tests/xpcshell/TestIntegration.jsm',
 ]
 
 SPHINX_TREES['toolkit_modules'] = 'docs'
 
@@ -189,16 +188,17 @@ EXTRA_JS_MODULES += [
     'FinderHighlighter.jsm',
     'FinderIterator.jsm',
     'FinderParent.jsm',
     'FormLikeFactory.jsm',
     'Geometry.jsm',
     'GMPExtractorWorker.js',
     'GMPInstallManager.jsm',
     'GMPUtils.jsm',
+    'HiddenFrame.jsm',
     'Http.jsm',
     'IndexedDB.jsm',
     'InlineSpellChecker.jsm',
     'InlineSpellCheckerContent.jsm',
     'Integration.jsm',
     'JSONFile.jsm',
     'Log.jsm',
     'NewTabUtils.jsm',