213 lines
5.8 KiB
JavaScript
213 lines
5.8 KiB
JavaScript
/* 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/. */
|
|
|
|
/**
|
|
* This module contains `HiddenFrame`, a class which creates a windowless browser,
|
|
* and `HiddenBrowserManager` which is a singleton that can be used to manage
|
|
* creating and using multiple hidden frames.
|
|
*/
|
|
|
|
const XUL_PAGE = Services.io.newURI("chrome://global/content/win.xhtml");
|
|
|
|
const gAllHiddenFrames = new Set();
|
|
|
|
// The screen sizes to use for the background browser created by
|
|
// `HiddenBrowserManager`.
|
|
const BACKGROUND_WIDTH = 1024;
|
|
const BACKGROUND_HEIGHT = 768;
|
|
|
|
let cleanupRegistered = false;
|
|
function ensureCleanupRegistered() {
|
|
if (!cleanupRegistered) {
|
|
cleanupRegistered = true;
|
|
Services.obs.addObserver(function () {
|
|
for (let hiddenFrame of gAllHiddenFrames) {
|
|
hiddenFrame.destroy();
|
|
}
|
|
}, "xpcom-shutdown");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A hidden frame class. It takes care of creating a windowless browser and
|
|
* passing the window containing a blank XUL <window> back.
|
|
*/
|
|
export class HiddenFrame {
|
|
#frame = null;
|
|
#browser = null;
|
|
#listener = null;
|
|
#webProgress = null;
|
|
#deferred = null;
|
|
|
|
/**
|
|
* Gets the |contentWindow| of the hidden frame. Creates the frame if needed.
|
|
*
|
|
* @returns {Promise} Returns a promise which is resolved when the hidden frame has finished
|
|
* loading.
|
|
*/
|
|
get() {
|
|
if (!this.#deferred) {
|
|
this.#deferred = Promise.withResolvers();
|
|
this.#create();
|
|
}
|
|
|
|
return this.#deferred.promise;
|
|
}
|
|
|
|
/**
|
|
* Fetch a sync ref to the window inside the frame (needed for the add-on SDK).
|
|
*
|
|
* @returns {DOMWindow}
|
|
*/
|
|
getWindow() {
|
|
this.get();
|
|
return this.#browser.document.ownerGlobal;
|
|
}
|
|
|
|
/**
|
|
* Destroys the browser, freeing resources.
|
|
*/
|
|
destroy() {
|
|
if (this.#browser) {
|
|
if (this.#listener) {
|
|
this.#webProgress.removeProgressListener(this.#listener);
|
|
this.#listener = null;
|
|
this.#webProgress = null;
|
|
}
|
|
this.#frame = null;
|
|
this.#deferred = null;
|
|
|
|
gAllHiddenFrames.delete(this);
|
|
this.#browser.close();
|
|
this.#browser = null;
|
|
}
|
|
}
|
|
|
|
#create() {
|
|
ensureCleanupRegistered();
|
|
let chromeFlags = Ci.nsIWebBrowserChrome.CHROME_REMOTE_WINDOW;
|
|
if (Services.appinfo.fissionAutostart) {
|
|
chromeFlags |= Ci.nsIWebBrowserChrome.CHROME_FISSION_WINDOW;
|
|
}
|
|
this.#browser = Services.appShell.createWindowlessBrowser(
|
|
true,
|
|
chromeFlags
|
|
);
|
|
this.#browser.QueryInterface(Ci.nsIInterfaceRequestor);
|
|
gAllHiddenFrames.add(this);
|
|
this.#webProgress = this.#browser.getInterface(Ci.nsIWebProgress);
|
|
this.#listener = {
|
|
QueryInterface: ChromeUtils.generateQI([
|
|
"nsIWebProgressListener",
|
|
"nsIWebProgressListener2",
|
|
"nsISupportsWeakReference",
|
|
]),
|
|
};
|
|
this.#listener.onStateChange = (wbp, request, stateFlags) => {
|
|
if (!request) {
|
|
return;
|
|
}
|
|
if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
|
|
this.#webProgress.removeProgressListener(this.#listener);
|
|
this.#listener = null;
|
|
this.#webProgress = null;
|
|
// Get the window reference via the document.
|
|
this.#frame = this.#browser.document.ownerGlobal;
|
|
this.#deferred.resolve(this.#frame);
|
|
}
|
|
};
|
|
this.#webProgress.addProgressListener(
|
|
this.#listener,
|
|
Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT
|
|
);
|
|
let docShell = this.#browser.docShell;
|
|
let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
|
|
docShell.createAboutBlankDocumentViewer(systemPrincipal, systemPrincipal);
|
|
let browsingContext = this.#browser.browsingContext;
|
|
browsingContext.useGlobalHistory = false;
|
|
let loadURIOptions = {
|
|
triggeringPrincipal: systemPrincipal,
|
|
};
|
|
this.#browser.loadURI(XUL_PAGE, loadURIOptions);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A manager for hidden browsers. Responsible for creating and destroying a
|
|
* hidden frame to hold them.
|
|
*/
|
|
export const HiddenBrowserManager = new (class HiddenBrowserManager {
|
|
/**
|
|
* The hidden frame if one has been created.
|
|
*
|
|
* @type {HiddenFrame | null}
|
|
*/
|
|
#frame = null;
|
|
/**
|
|
* The number of hidden browser elements currently in use.
|
|
*
|
|
* @type {number}
|
|
*/
|
|
#browsers = 0;
|
|
|
|
/**
|
|
* Creates and returns a new hidden browser.
|
|
*
|
|
* @returns {Browser}
|
|
*/
|
|
async #acquireBrowser() {
|
|
this.#browsers++;
|
|
if (!this.#frame) {
|
|
this.#frame = new HiddenFrame();
|
|
}
|
|
|
|
let frame = await this.#frame.get();
|
|
let doc = frame.document;
|
|
let browser = doc.createXULElement("browser");
|
|
browser.setAttribute("remote", "true");
|
|
browser.setAttribute("type", "content");
|
|
browser.style.width = `${BACKGROUND_WIDTH}px`;
|
|
browser.style.minWidth = `${BACKGROUND_WIDTH}px`;
|
|
browser.style.height = `${BACKGROUND_HEIGHT}px`;
|
|
browser.style.minHeight = `${BACKGROUND_HEIGHT}px`;
|
|
browser.setAttribute("maychangeremoteness", "true");
|
|
doc.documentElement.appendChild(browser);
|
|
|
|
return browser;
|
|
}
|
|
|
|
/**
|
|
* Releases the given hidden browser.
|
|
*
|
|
* @param {Browser} browser
|
|
* The hidden browser element.
|
|
*/
|
|
#releaseBrowser(browser) {
|
|
browser.remove();
|
|
|
|
this.#browsers--;
|
|
if (this.#browsers == 0) {
|
|
this.#frame.destroy();
|
|
this.#frame = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calls a callback function with a new hidden browser.
|
|
* This function will return whatever the callback function returns.
|
|
*
|
|
* @param {Callback} callback
|
|
* The callback function will be called with the browser element and may
|
|
* be asynchronous.
|
|
* @returns {T}
|
|
*/
|
|
async withHiddenBrowser(callback) {
|
|
let browser = await this.#acquireBrowser();
|
|
try {
|
|
return await callback(browser);
|
|
} finally {
|
|
this.#releaseBrowser(browser);
|
|
}
|
|
}
|
|
})();
|