Bug 1569135 Fix --screenshot r=kmag

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

--HG--
rename : browser/components/shell/HeadlessShell.jsm => browser/components/shell/ScreenshotChild.jsm
extra : moz-landing-system : lando
This commit is contained in:
Andrew Swan 2019-08-07 21:33:49 +00:00
parent 724b9427aa
commit 036b82a357
8 changed files with 118 additions and 74 deletions

View file

@ -103,9 +103,6 @@ var whitelist = [
{ file: "resource://gre/res/fonts/mathfontSTIXGeneral.properties" }, { file: "resource://gre/res/fonts/mathfontSTIXGeneral.properties" },
{ file: "resource://gre/res/fonts/mathfontUnicode.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. // The l10n build system can't package string files only for some platforms.
{ {
file: file:

View file

@ -397,7 +397,7 @@ add_task(async function checkAllTheCSS() {
// chrome URI so that it's allowed to load and parse any styles. // chrome URI so that it's allowed to load and parse any styles.
let testFile = getRootDirectory(gTestPath) + "dummy_page.html"; let testFile = getRootDirectory(gTestPath) + "dummy_page.html";
let HiddenFrame = ChromeUtils.import( let HiddenFrame = ChromeUtils.import(
"resource://testing-common/HiddenFrame.jsm", "resource://gre/modules/HiddenFrame.jsm",
{} {}
).HiddenFrame; ).HiddenFrame;
let hiddenFrame = new HiddenFrame(); let hiddenFrame = new HiddenFrame();

View file

@ -5,7 +5,7 @@
/* import-globals-from ../../../../../toolkit/components/extensions/test/mochitest/head_webrequest.js */ /* import-globals-from ../../../../../toolkit/components/extensions/test/mochitest/head_webrequest.js */
loadTestSubscript("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"; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
SimpleTest.requestCompleteLog(); SimpleTest.requestCompleteLog();

View file

@ -4,16 +4,38 @@
"use strict"; "use strict";
var EXPORTED_SYMBOLS = ["HeadlessShell"]; var EXPORTED_SYMBOLS = ["HeadlessShell", "ScreenshotParent"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 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 { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
// Refrences to the progress listeners to keep them from being gc'ed // Refrences to the progress listeners to keep them from being gc'ed
// before they are called. // before they are called.
const progressListeners = new Map(); const progressListeners = new Set();
function loadContentWindow(webNavigation, url, principal) { class ScreenshotParent extends JSWindowActorParent {
takeScreenshot(params) {
return this.sendQuery("TakeScreenshot", params);
}
}
ChromeUtils.registerWindowActor("Screenshot", {
parent: {
moduleURI: "resource:///modules/HeadlessShell.jsm",
},
child: {
moduleURI: "resource:///modules/ScreenshotChild.jsm",
messages: ["TakeScreenshot"],
},
});
function loadContentWindow(browser, url) {
let uri; let uri;
try { try {
uri = Services.io.newURI(url); uri = Services.io.newURI(url);
@ -22,21 +44,20 @@ function loadContentWindow(webNavigation, url, principal) {
Cu.reportError(msg); Cu.reportError(msg);
return Promise.reject(new Error(msg)); return Promise.reject(new Error(msg));
} }
const principal = Services.scriptSecurityManager.getSystemPrincipal();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let loadURIOptions = { let loadURIOptions = {
triggeringPrincipal: principal, triggeringPrincipal: principal,
remoteType: E10SUtils.getRemoteTypeForURI(url, true, false),
}; };
webNavigation.loadURI(uri.spec, loadURIOptions); browser.loadURI(uri.spec, loadURIOptions);
let docShell = webNavigation let { webProgress } = browser;
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell);
let webProgress = docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
let progressListener = { let progressListener = {
onLocationChange(progress, request, location, flags) { onLocationChange(progress, request, location, flags) {
// Ignore inner-frame events // Ignore inner-frame events
if (progress != webProgress) { if (!progress.isTopLevel) {
return; return;
} }
// Ignore events that don't change the document // Ignore events that don't change the document
@ -47,23 +68,17 @@ function loadContentWindow(webNavigation, url, principal) {
if (location.spec == "about:blank" && uri.spec != "about:blank") { if (location.spec == "about:blank" && uri.spec != "about:blank") {
return; return;
} }
let contentWindow = docShell.domWindow;
progressListeners.delete(progressListener); progressListeners.delete(progressListener);
webProgress.removeProgressListener(progressListener); webProgress.removeProgressListener(progressListener);
contentWindow.addEventListener( resolve();
"load",
event => {
resolve(contentWindow);
},
{ once: true }
);
}, },
QueryInterface: ChromeUtils.generateQI([ QueryInterface: ChromeUtils.generateQI([
"nsIWebProgressListener", "nsIWebProgressListener",
"nsISupportsWeakReference", "nsISupportsWeakReference",
]), ]),
}; };
progressListeners.set(progressListener, progressListener); progressListeners.add(progressListener);
webProgress.addProgressListener( webProgress.addProgressListener(
progressListener, progressListener,
Ci.nsIWebProgress.NOTIFY_LOCATION Ci.nsIWebProgress.NOTIFY_LOCATION
@ -79,56 +94,37 @@ async function takeScreenshot(
path, path,
url url
) { ) {
let frame;
try { try {
var windowlessBrowser = Services.appShell.createWindowlessBrowser(false); frame = new HiddenFrame();
// nsIWindowlessBrowser inherits from nsIWebNavigation. let windowlessBrowser = await frame.get();
let contentWindow = await loadContentWindow(
windowlessBrowser, let doc = windowlessBrowser.document;
url, let browser = doc.createXULElement("browser");
Services.scriptSecurityManager.getSystemPrincipal() 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;`
); );
contentWindow.resizeTo(contentWidth, contentHeight); doc.documentElement.appendChild(browser);
let canvas = contentWindow.document.createElementNS( await loadContentWindow(browser, url);
"http://www.w3.org/1999/xhtml",
"html:canvas" let actor = browser.browsingContext.currentWindowGlobal.getActor(
); "Screenshot"
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)"
); );
let blob = await actor.takeScreenshot({
fullWidth,
fullHeight,
});
function getBlob() { let reader = await new Promise(resolve => {
return new Promise(resolve => canvas.toBlob(resolve)); let fr = new FileReader();
} fr.onloadend = () => resolve(fr);
fr.readAsArrayBuffer(blob);
});
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), { await OS.File.writeAtomic(path, new Uint8Array(reader.result), {
flush: true, flush: true,
}); });
@ -136,8 +132,8 @@ async function takeScreenshot(
} catch (e) { } catch (e) {
dump("Failure taking screenshot: " + e + "\n"); dump("Failure taking screenshot: " + e + "\n");
} finally { } finally {
if (windowlessBrowser) { if (frame) {
windowlessBrowser.close(); frame.destroy();
} }
} }
} }

View file

@ -0,0 +1,50 @@
"use strict";
const EXPORTED_SYMBOLS = ["ScreenshotChild"];
class ScreenshotChild extends JSWindowActorChild {
receiveMessage(message) {
if (message.name == "TakeScreenshot") {
return this.takeScreenshot(message.data);
}
return null;
}
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 = 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)"
);
return new Promise(resolve => canvas.toBlob(resolve));
}
}

View file

@ -49,6 +49,7 @@ if SOURCES:
EXTRA_JS_MODULES += [ EXTRA_JS_MODULES += [
'HeadlessShell.jsm', 'HeadlessShell.jsm',
'ScreenshotChild.jsm',
'ShellService.jsm', 'ShellService.jsm',
] ]

View file

@ -4,7 +4,7 @@
"use strict"; "use strict";
var HiddenFrame = ChromeUtils.import( var HiddenFrame = ChromeUtils.import(
"resource://testing-common/HiddenFrame.jsm", "resource://gre/modules/HiddenFrame.jsm",
{} {}
).HiddenFrame; ).HiddenFrame;

View file

@ -147,7 +147,6 @@ BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
MOCHITEST_CHROME_MANIFESTS += ['tests/chrome/chrome.ini'] MOCHITEST_CHROME_MANIFESTS += ['tests/chrome/chrome.ini']
TESTING_JS_MODULES += [ TESTING_JS_MODULES += [
'HiddenFrame.jsm',
'tests/modules/MockDocument.jsm', 'tests/modules/MockDocument.jsm',
'tests/modules/PromiseTestUtils.jsm', 'tests/modules/PromiseTestUtils.jsm',
'tests/modules/Task.jsm', 'tests/modules/Task.jsm',
@ -194,6 +193,7 @@ EXTRA_JS_MODULES += [
'GMPExtractorWorker.js', 'GMPExtractorWorker.js',
'GMPInstallManager.jsm', 'GMPInstallManager.jsm',
'GMPUtils.jsm', 'GMPUtils.jsm',
'HiddenFrame.jsm',
'Http.jsm', 'Http.jsm',
'IgnoreLists.jsm', 'IgnoreLists.jsm',
'IndexedDB.jsm', 'IndexedDB.jsm',