devops: add firefox-stable channel browser (#6173)

This adds a firefox-stable application to build on our bots.
This is basically a rebaselined version of 66541552d0

The firefox base revision is bb9bf7e886
Which is taken from `about://buildconfig` of a stable Firefox version
on Mac as of Apr 9, 2021.

References #5993
This commit is contained in:
Andrey Lushnikov 2021-04-10 00:13:19 -05:00 committed by GitHub
parent bba7ca34c8
commit 17c6406e6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 11056 additions and 3 deletions

View File

@ -188,6 +188,45 @@ elif [[ "$BUILD_FLAVOR" == "firefox-win64" ]]; then
BUILD_BLOB_NAME="firefox-win64.zip"
# ===============================
# FIREFOX-STABLE COMPILATION
# ===============================
elif [[ "$BUILD_FLAVOR" == "firefox-stable-ubuntu-18.04" ]]; then
BROWSER_NAME="firefox-stable"
EXTRA_BUILD_ARGS="--full"
EXPECTED_HOST_OS="Ubuntu"
EXPECTED_HOST_OS_VERSION="18.04"
BUILD_BLOB_NAME="firefox-stable-ubuntu-18.04.zip"
elif [[ "$BUILD_FLAVOR" == "firefox-stable-ubuntu-20.04" ]]; then
BROWSER_NAME="firefox-stable"
EXTRA_BUILD_ARGS="--full"
EXPECTED_HOST_OS="Ubuntu"
EXPECTED_HOST_OS_VERSION="20.04"
BUILD_BLOB_NAME="firefox-stable-ubuntu-20.04.zip"
elif [[ "$BUILD_FLAVOR" == "firefox-stable-mac-10.14" ]]; then
BROWSER_NAME="firefox-stable"
EXTRA_BUILD_ARGS="--full"
EXPECTED_HOST_OS="Darwin"
EXPECTED_HOST_OS_VERSION="10.14"
BUILD_BLOB_NAME="firefox-stable-mac-10.14.zip"
elif [[ "$BUILD_FLAVOR" == "firefox-stable-mac-11.0-arm64" ]]; then
BROWSER_NAME="firefox-stable"
EXTRA_BUILD_ARGS="--full"
EXPECTED_HOST_OS="Darwin"
EXPECTED_HOST_OS_VERSION="11.0"
EXPECTED_ARCH="arm64"
BUILD_BLOB_NAME="firefox-stable-mac-11.0-arm64.zip"
elif [[ "$BUILD_FLAVOR" == "firefox-stable-win32" ]]; then
BROWSER_NAME="firefox-stable"
EXTRA_BUILD_ARGS="--full"
EXPECTED_HOST_OS="MINGW"
BUILD_BLOB_NAME="firefox-stable-win32.zip"
elif [[ "$BUILD_FLAVOR" == "firefox-stable-win64" ]]; then
BROWSER_NAME="firefox-stable"
EXTRA_BUILD_ARGS="--win64 --full"
EXPECTED_HOST_OS="MINGW"
BUILD_BLOB_NAME="firefox-stable-win64.zip"
# ===========================
# WEBKIT COMPILATION
# ===========================

View File

@ -53,6 +53,19 @@ if [[ ("$1" == "firefox") || ("$1" == "firefox/") || ("$1" == "ff") ]]; then
CHECKOUT_PATH="${FF_CHECKOUT_PATH}"
FRIENDLY_CHECKOUT_PATH="<FF_CHECKOUT_PATH>"
fi
elif [[ ("$1" == "firefox-stable") ]]; then
FRIENDLY_CHECKOUT_PATH="//browser_patches/firefox-stable/checkout";
CHECKOUT_PATH="$PWD/firefox-stable/checkout"
EXTRA_FOLDER_PW_PATH="$PWD/firefox-stable/juggler"
EXTRA_FOLDER_CHECKOUT_RELPATH="juggler"
EXPORT_PATH="$PWD/firefox-stable"
BUILD_NUMBER_UPSTREAM_URL="https://raw.githubusercontent.com/microsoft/playwright/master/browser_patches/firefox-stable/BUILD_NUMBER"
source "./firefox-stable/UPSTREAM_CONFIG.sh"
if [[ ! -z "${FF_CHECKOUT_PATH}" ]]; then
echo "WARNING: using checkout path from FF_CHECKOUT_PATH env: ${FF_CHECKOUT_PATH}"
CHECKOUT_PATH="${FF_CHECKOUT_PATH}"
FRIENDLY_CHECKOUT_PATH="<FF_CHECKOUT_PATH>"
fi
elif [[ ("$1" == "webkit") || ("$1" == "webkit/") || ("$1" == "wk") ]]; then
FRIENDLY_CHECKOUT_PATH="//browser_patches/webkit/checkout";
CHECKOUT_PATH="$PWD/webkit/checkout"
@ -129,8 +142,9 @@ NEW_BASE_REVISION=$(git merge-base $REMOTE_BROWSER_UPSTREAM/$BASE_BRANCH $CURREN
NEW_DIFF=$(git diff --diff-algorithm=myers --full-index $NEW_BASE_REVISION $CURRENT_BRANCH -- . ":!${EXTRA_FOLDER_CHECKOUT_RELPATH}")
# Increment BUILD_NUMBER
BUILD_NUMBER=$(curl ${BUILD_NUMBER_UPSTREAM_URL} | head -1)
BUILD_NUMBER=$((BUILD_NUMBER+1))
#BUILD_NUMBER=$(curl ${BUILD_NUMBER_UPSTREAM_URL} | head -1)
#BUILD_NUMBER=$((BUILD_NUMBER+1))
BUILD_NUMBER=1242
echo "REMOTE_URL=\"$REMOTE_URL\"
BASE_BRANCH=\"$BASE_BRANCH\"

View File

@ -0,0 +1 @@
/checkout

View File

@ -0,0 +1,2 @@
1242
Changed: lushnikov@chromium.org Fri 09 Apr 2021 09:56:28 PM PDT

View File

@ -0,0 +1,5 @@
firefox-stable-mac-10.14.zip
firefox-stable-ubuntu-18.04.zip
firefox-stable-ubuntu-20.04.zip
firefox-stable-win32.zip
firefox-stable-win64.zip

View File

@ -0,0 +1,3 @@
REMOTE_URL="https://github.com/mozilla/gecko-dev"
BASE_BRANCH="release"
BASE_REVISION="4068febfd76d9ec557591240d7496be42c27c17f"

View File

@ -0,0 +1,60 @@
#!/bin/bash
set -e
set +x
if [[ ("$1" == "-h") || ("$1" == "--help") ]]; then
echo "usage: $(basename $0) [output-absolute-path]"
echo
echo "Generate distributable .zip archive from ./checkout folder that was previously built."
echo
exit 0
fi
ZIP_PATH=$1
if [[ $ZIP_PATH != /* ]]; then
echo "ERROR: path $ZIP_PATH is not absolute"
exit 1
fi
if [[ $ZIP_PATH != *.zip ]]; then
echo "ERROR: path $ZIP_PATH must have .zip extension"
exit 1
fi
if [[ -f $ZIP_PATH ]]; then
echo "ERROR: path $ZIP_PATH exists; can't do anything."
exit 1
fi
if ! [[ -d $(dirname $ZIP_PATH) ]]; then
echo "ERROR: folder for path $($ZIP_PATH) does not exist."
exit 1
fi
trap "cd $(pwd -P)" EXIT
cd "$(dirname $0)"
SCRIPT_FOLDER="$(pwd -P)"
if [[ ! -z "${FF_CHECKOUT_PATH}" ]]; then
cd "${FF_CHECKOUT_PATH}"
echo "WARNING: checkout path from FF_CHECKOUT_PATH env: ${FF_CHECKOUT_PATH}"
else
cd "checkout"
fi
OBJ_FOLDER="obj-build-playwright"
./mach package
node "${SCRIPT_FOLDER}"/install-preferences.js $PWD/$OBJ_FOLDER/dist/firefox
if ! [[ -d $OBJ_FOLDER/dist/firefox ]]; then
echo "ERROR: cannot find $OBJ_FOLDER/dist/firefox folder in the checkout/. Did you build?"
exit 1;
fi
# Copy the libstdc++ version we linked against.
# TODO(aslushnikov): this won't be needed with official builds.
if [[ "$(uname)" == "Linux" ]]; then
cp /usr/lib/x86_64-linux-gnu/libstdc++.so.6 $OBJ_FOLDER/dist/firefox/libstdc++.so.6
fi
# tar resulting directory and cleanup TMP.
cd $OBJ_FOLDER/dist
zip -r $ZIP_PATH firefox

View File

@ -0,0 +1,126 @@
#!/bin/bash
set -e
set +x
RUST_VERSION="1.49.0"
CBINDGEN_VERSION="0.16.0"
# Certain minimal SDK Version is required by firefox
MACOS_SDK_VERSION="10.12"
# XCode version can be determined from https://en.wikipedia.org/wiki/Xcode
XCODE_VERSION_WITH_REQUIRED_SDK_VERSION="8.3.3"
trap "cd $(pwd -P)" EXIT
cd "$(dirname $0)"
SCRIPT_FOLDER="$(pwd -P)"
if [[ ! -z "${FF_CHECKOUT_PATH}" ]]; then
cd "${FF_CHECKOUT_PATH}"
echo "WARNING: checkout path from FF_CHECKOUT_PATH env: ${FF_CHECKOUT_PATH}"
else
cd "checkout"
fi
rm -rf .mozconfig
if [[ "$(uname)" == "Darwin" ]]; then
if [[ $(uname -m) == "arm64" ]]; then
# Building on Apple Silicon requires XCode12.2 and does not require any extra SDKs.
if ! [[ -d "/Applications/Xcode12.2.app" ]]; then
echo "As of Jan 2021, building Firefox on Apple Silicon requires XCode 12.2"
echo "Make sure there's an /Applications/Xcode12.2.app"
echo "Download XCode from https://developer.apple.com/download/more/"
echo ""
exit 1
fi
export DEVELOPER_DIR=/Applications/Xcode12.2.app/Contents/Developer
else
# Firefox currently does not build on 10.15 out of the box - it requires SDK for 10.12.
# Make sure the SDK is out there.
if ! [[ -d $HOME/SDK-archive/MacOSX${MACOS_SDK_VERSION}.sdk ]]; then
echo "As of Dec 2020, Firefox does not build on Mac without ${MACOS_SDK_VERSION} SDK."
echo "Download XCode ${XCODE_VERSION_WITH_REQUIRED_SDK_VERSION} from https://developer.apple.com/download/more/ and"
echo "extract SDK to $HOME/SDK-archive/MacOSX${MACOS_SDK_VERSION}.sdk"
echo ""
echo "More info: https://firefox-source-docs.mozilla.org/setup/macos_build.html"
exit 1
else
echo "-- configuting .mozconfig with ${MACOS_SDK_VERSION} SDK path"
echo "ac_add_options --with-macos-sdk=$HOME/SDK-archive/MacOSX${MACOS_SDK_VERSION}.sdk/" >> .mozconfig
fi
fi
echo "-- building on Mac"
elif [[ "$(uname)" == "Linux" ]]; then
echo "-- building on Linux"
echo "ac_add_options --disable-av1" >> .mozconfig
elif [[ "$(uname)" == MINGW* ]]; then
echo "ac_add_options --disable-update-agent" >> .mozconfig
echo "ac_add_options --disable-default-browser-agent" >> .mozconfig
DLL_FILE=""
if [[ $1 == "--win64" ]]; then
echo "-- building win64 build on MINGW"
echo "ac_add_options --target=x86_64-pc-mingw32" >> .mozconfig
echo "ac_add_options --host=x86_64-pc-mingw32" >> .mozconfig
DLL_FILE=$("C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -latest -find '**\Redist\MSVC\*\x64\**\vcruntime140.dll')
else
echo "-- building win32 build on MINGW"
DLL_FILE=$("C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -latest -find '**\Redist\MSVC\*\x86\**\vcruntime140.dll')
fi
WIN32_REDIST_DIR=$(dirname "$DLL_FILE")
if ! [[ -d $WIN32_REDIST_DIR ]]; then
echo "ERROR: cannot find MS VS C++ redistributable $WIN32_REDIST_DIR"
exit 1;
fi
else
echo "ERROR: cannot upload on this platform!" 1>&2
exit 1;
fi
OBJ_FOLDER="obj-build-playwright"
echo "mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/${OBJ_FOLDER}" >> .mozconfig
echo "ac_add_options --disable-crashreporter" >> .mozconfig
if [[ $1 == "--full" || $2 == "--full" ]]; then
if [[ "$(uname)" == "Darwin" && "$(uname -m)" == "arm64" ]]; then
./mach artifact toolchain --from-build macosx64-node
rm -rf "$HOME/.mozbuild/node"
mv node "$HOME/.mozbuild/"
elif [[ "$(uname)" == "Darwin" || "$(uname)" == "Linux" ]]; then
SHELL=/bin/sh ./mach --no-interactive bootstrap --application-choice=browser --no-system-changes
fi
if [[ ! -z "${WIN32_REDIST_DIR}" ]]; then
# Having this option in .mozconfig kills incremental compilation.
echo "export WIN32_REDIST_DIR=\"$WIN32_REDIST_DIR\"" >> .mozconfig
fi
fi
if ! [[ -f "$HOME/.mozbuild/_virtualenvs/mach/bin/python" ]]; then
./mach create-mach-environment
fi
if [[ $1 == "--juggler" ]]; then
./mach build faster
else
# TODO: rustup is not in the PATH on Windows
if command -v rustup >/dev/null; then
# We manage Rust version ourselves.
echo "-- Using rust v${RUST_VERSION}"
rustup install "${RUST_VERSION}"
rustup default "${RUST_VERSION}"
fi
# TODO: cargo is not in the PATH on Windows
if command -v cargo >/dev/null; then
echo "-- Using cbindgen v${CBINDGEN_VERSION}"
cargo install cbindgen --version "${CBINDGEN_VERSION}"
fi
./mach build
fi
if [[ "$(uname)" == "Darwin" ]]; then
node "${SCRIPT_FOLDER}"/install-preferences.js $PWD/${OBJ_FOLDER}/dist
else
node "${SCRIPT_FOLDER}"/install-preferences.js $PWD/${OBJ_FOLDER}/dist/bin
fi

View File

@ -0,0 +1,18 @@
#!/bin/bash
set -e
set +x
trap "cd $(pwd -P)" EXIT
if [[ ! -z "${FF_CHECKOUT_PATH}" ]]; then
cd "${FF_CHECKOUT_PATH}"
echo "WARNING: checkout path from FF_CHECKOUT_PATH env: ${FF_CHECKOUT_PATH}"
else
cd "$(dirname $0)"
cd "checkout"
fi
OBJ_FOLDER="obj-build-playwright"
if [[ -d $OBJ_FOLDER ]]; then
rm -rf $OBJ_FOLDER
fi

View File

@ -0,0 +1,96 @@
/**
* Copyright 2018 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const os = require('os');
const fs = require('fs');
const path = require('path');
const util = require('util');
const writeFileAsync = util.promisify(fs.writeFile.bind(fs));
const mkdirAsync = util.promisify(fs.mkdir.bind(fs));
// Install browser preferences after downloading and unpacking
// firefox instances.
// Based on: https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Enterprise_deployment_before_60#Configuration
async function installFirefoxPreferences(distpath) {
let executablePath = '';
if (os.platform() === 'linux')
executablePath = path.join(distpath, 'firefox');
else if (os.platform() === 'darwin')
executablePath = path.join(distpath, 'Nightly.app', 'Contents', 'MacOS', 'firefox');
else if (os.platform() === 'win32')
executablePath = path.join(distpath, 'firefox.exe');
const firefoxFolder = path.dirname(executablePath);
let prefPath = '';
let configPath = '';
if (os.platform() === 'darwin') {
prefPath = path.join(firefoxFolder, '..', 'Resources', 'defaults', 'pref');
configPath = path.join(firefoxFolder, '..', 'Resources');
} else if (os.platform() === 'linux') {
if (!fs.existsSync(path.join(firefoxFolder, 'browser', 'defaults')))
await mkdirAsync(path.join(firefoxFolder, 'browser', 'defaults'));
if (!fs.existsSync(path.join(firefoxFolder, 'browser', 'defaults', 'preferences')))
await mkdirAsync(path.join(firefoxFolder, 'browser', 'defaults', 'preferences'));
prefPath = path.join(firefoxFolder, 'browser', 'defaults', 'preferences');
configPath = firefoxFolder;
} else if (os.platform() === 'win32') {
prefPath = path.join(firefoxFolder, 'defaults', 'pref');
configPath = firefoxFolder;
} else {
throw new Error('Unsupported platform: ' + os.platform());
}
await Promise.all([
copyFile({
from: path.join(__dirname, 'preferences', '00-playwright-prefs.js'),
to: path.join(prefPath, '00-playwright-prefs.js'),
}),
copyFile({
from: path.join(__dirname, 'preferences', 'playwright.cfg'),
to: path.join(configPath, 'playwright.cfg'),
}),
]);
}
function copyFile({from, to}) {
const rd = fs.createReadStream(from);
const wr = fs.createWriteStream(to);
return new Promise(function(resolve, reject) {
rd.on('error', reject);
wr.on('error', reject);
wr.on('finish', resolve);
rd.pipe(wr);
}).catch(function(error) {
rd.destroy();
wr.end();
throw error;
});
}
if (process.argv.length !== 3) {
console.log('ERROR: expected a path to the directory with browser build');
process.exit(1);
return;
}
installFirefoxPreferences(process.argv[2]).catch(error => {
console.error('ERROR: failed to put preferences!');
console.error(error);
process.exit(1);
});

View File

@ -0,0 +1,135 @@
/* 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/. */
const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
class Helper {
addObserver(handler, topic) {
Services.obs.addObserver(handler, topic);
return () => Services.obs.removeObserver(handler, topic);
}
addMessageListener(receiver, eventName, handler) {
receiver.addMessageListener(eventName, handler);
return () => receiver.removeMessageListener(eventName, handler);
}
addEventListener(receiver, eventName, handler) {
receiver.addEventListener(eventName, handler);
return () => receiver.removeEventListener(eventName, handler);
}
awaitEvent(receiver, eventName) {
return new Promise(resolve => {
receiver.addEventListener(eventName, function listener() {
receiver.removeEventListener(eventName, listener);
resolve();
});
});
}
on(receiver, eventName, handler) {
// The toolkit/modules/EventEmitter.jsm dispatches event name as a first argument.
// Fire event listeners without it for convenience.
const handlerWrapper = (_, ...args) => handler(...args);
receiver.on(eventName, handlerWrapper);
return () => receiver.off(eventName, handlerWrapper);
}
addProgressListener(progress, listener, flags) {
progress.addProgressListener(listener, flags);
return () => progress.removeProgressListener(listener);
}
removeListeners(listeners) {
for (const tearDown of listeners)
tearDown.call(null);
listeners.splice(0, listeners.length);
}
generateId() {
const string = uuidGen.generateUUID().toString();
return string.substring(1, string.length - 1);
}
getLoadContext(channel) {
let loadContext = null;
try {
if (channel.notificationCallbacks)
loadContext = channel.notificationCallbacks.getInterface(Ci.nsILoadContext);
} catch (e) {}
try {
if (!loadContext && channel.loadGroup)
loadContext = channel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
} catch (e) { }
return loadContext;
}
getNetworkErrorStatusText(status) {
if (!status)
return null;
for (const key of Object.keys(Cr)) {
if (Cr[key] === status)
return key;
}
// Security module. The following is taken from
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/How_to_check_the_secruity_state_of_an_XMLHTTPRequest_over_SSL
if ((status & 0xff0000) === 0x5a0000) {
// NSS_SEC errors (happen below the base value because of negative vals)
if ((status & 0xffff) < Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE)) {
// The bases are actually negative, so in our positive numeric space, we
// need to subtract the base off our value.
const nssErr = Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE) - (status & 0xffff);
switch (nssErr) {
case 11:
return 'SEC_ERROR_EXPIRED_CERTIFICATE';
case 12:
return 'SEC_ERROR_REVOKED_CERTIFICATE';
case 13:
return 'SEC_ERROR_UNKNOWN_ISSUER';
case 20:
return 'SEC_ERROR_UNTRUSTED_ISSUER';
case 21:
return 'SEC_ERROR_UNTRUSTED_CERT';
case 36:
return 'SEC_ERROR_CA_CERT_INVALID';
case 90:
return 'SEC_ERROR_INADEQUATE_KEY_USAGE';
case 176:
return 'SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED';
default:
return 'SEC_ERROR_UNKNOWN';
}
}
const sslErr = Math.abs(Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE) - (status & 0xffff);
switch (sslErr) {
case 3:
return 'SSL_ERROR_NO_CERTIFICATE';
case 4:
return 'SSL_ERROR_BAD_CERTIFICATE';
case 8:
return 'SSL_ERROR_UNSUPPORTED_CERTIFICATE_TYPE';
case 9:
return 'SSL_ERROR_UNSUPPORTED_VERSION';
case 12:
return 'SSL_ERROR_BAD_CERT_DOMAIN';
default:
return 'SSL_ERROR_UNKNOWN';
}
}
return '<unknown error>';
}
browsingContextToFrameId(browsingContext) {
if (!browsingContext)
return undefined;
return 'frame-' + browsingContext.id;
}
}
var EXPORTED_SYMBOLS = [ "Helper" ];
this.Helper = Helper;

View File

@ -0,0 +1,913 @@
/* 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";
const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
const Cm = Components.manager;
const CC = Components.Constructor;
const helper = new Helper();
const UINT32_MAX = Math.pow(2, 32)-1;
const BinaryInputStream = CC('@mozilla.org/binaryinputstream;1', 'nsIBinaryInputStream', 'setInputStream');
const BinaryOutputStream = CC('@mozilla.org/binaryoutputstream;1', 'nsIBinaryOutputStream', 'setOutputStream');
const StorageStream = CC('@mozilla.org/storagestream;1', 'nsIStorageStream', 'init');
// Cap response storage with 100Mb per tracked tab.
const MAX_RESPONSE_STORAGE_SIZE = 100 * 1024 * 1024;
/**
* This is a nsIChannelEventSink implementation that monitors channel redirects.
*/
const SINK_CLASS_DESCRIPTION = "Juggler NetworkMonitor Channel Event Sink";
const SINK_CLASS_ID = Components.ID("{c2b4c83e-607a-405a-beab-0ef5dbfb7617}");
const SINK_CONTRACT_ID = "@mozilla.org/network/monitor/channeleventsink;1";
const SINK_CATEGORY_NAME = "net-channel-event-sinks";
const pageNetworkSymbol = Symbol('PageNetwork');
class PageNetwork {
static _forPageTarget(target) {
let result = target[pageNetworkSymbol];
if (!result) {
result = new PageNetwork(target);
target[pageNetworkSymbol] = result;
}
return result;
}
constructor(target) {
EventEmitter.decorate(this);
this._target = target;
this._extraHTTPHeaders = null;
this._responseStorage = new ResponseStorage(MAX_RESPONSE_STORAGE_SIZE, MAX_RESPONSE_STORAGE_SIZE / 10);
this._requestInterceptionEnabled = false;
// This is requestId => NetworkRequest map, only contains requests that are
// awaiting interception action (abort, resume, fulfill) over the protocol.
this._interceptedRequests = new Map();
}
setExtraHTTPHeaders(headers) {
this._extraHTTPHeaders = headers;
}
enableRequestInterception() {
this._requestInterceptionEnabled = true;
}
disableRequestInterception() {
this._requestInterceptionEnabled = false;
for (const intercepted of this._interceptedRequests.values())
intercepted.resume();
this._interceptedRequests.clear();
}
resumeInterceptedRequest(requestId, url, method, headers, postData) {
this._takeIntercepted(requestId).resume(url, method, headers, postData);
}
fulfillInterceptedRequest(requestId, status, statusText, headers, base64body) {
this._takeIntercepted(requestId).fulfill(status, statusText, headers, base64body);
}
abortInterceptedRequest(requestId, errorCode) {
this._takeIntercepted(requestId).abort(errorCode);
}
getResponseBody(requestId) {
if (!this._responseStorage)
throw new Error('Responses are not tracked for the given browser');
return this._responseStorage.getBase64EncodedResponse(requestId);
}
_takeIntercepted(requestId) {
const intercepted = this._interceptedRequests.get(requestId);
if (!intercepted)
throw new Error(`Cannot find request "${requestId}"`);
this._interceptedRequests.delete(requestId);
return intercepted;
}
}
class NetworkRequest {
constructor(networkObserver, httpChannel, redirectedFrom) {
this._networkObserver = networkObserver;
this.httpChannel = httpChannel;
this._networkObserver._channelToRequest.set(this.httpChannel, this);
const loadInfo = this.httpChannel.loadInfo;
let browsingContext = loadInfo?.frameBrowsingContext || loadInfo?.browsingContext;
// TODO: Unfortunately, requests from web workers don't have frameBrowsingContext or
// browsingContext.
//
// We fail to attribute them to the original frames on the browser side, but we
// can use load context top frame to attribute them to the top frame at least.
if (!browsingContext) {
const loadContext = helper.getLoadContext(this.httpChannel);
browsingContext = loadContext?.topFrameElement?.browsingContext;
}
this._frameId = helper.browsingContextToFrameId(browsingContext);
this.requestId = httpChannel.channelId + '';
this.navigationId = httpChannel.isMainDocumentChannel ? this.requestId : undefined;
const internalCauseType = this.httpChannel.loadInfo ? this.httpChannel.loadInfo.internalContentPolicyType : Ci.nsIContentPolicy.TYPE_OTHER;
this._redirectedIndex = 0;
const ignoredRedirect = redirectedFrom && !redirectedFrom._sentOnResponse;
if (ignoredRedirect) {
// We just ignore redirect that did not hit the network before being redirected.
// This happens, for example, for automatic http->https redirects.
this.navigationId = redirectedFrom.navigationId;
} else if (redirectedFrom) {
this.redirectedFromId = redirectedFrom.requestId;
this._redirectedIndex = redirectedFrom._redirectedIndex + 1;
this.requestId = this.requestId + '-redirect' + this._redirectedIndex;
this.navigationId = redirectedFrom.navigationId;
// Finish previous request now. Since we inherit the listener, we could in theory
// use onStopRequest, but that will only happen after the last redirect has finished.
redirectedFrom._sendOnRequestFinished();
}
this._maybeInactivePageNetwork = this._findPageNetwork();
this._expectingInterception = false;
this._expectingResumedRequest = undefined; // { method, headers, postData }
this._sentOnResponse = false;
const pageNetwork = this._activePageNetwork();
if (pageNetwork) {
appendExtraHTTPHeaders(httpChannel, pageNetwork._target.browserContext().extraHTTPHeaders);
appendExtraHTTPHeaders(httpChannel, pageNetwork._extraHTTPHeaders);
}
this._responseBodyChunks = [];
httpChannel.QueryInterface(Ci.nsITraceableChannel);
this._originalListener = httpChannel.setNewListener(this);
if (redirectedFrom) {
// Listener is inherited for regular redirects, so we'd like to avoid
// calling into previous NetworkRequest.
this._originalListener = redirectedFrom._originalListener;
}
this._previousCallbacks = httpChannel.notificationCallbacks;
httpChannel.notificationCallbacks = this;
this.QueryInterface = ChromeUtils.generateQI([
Ci.nsIAuthPrompt2,
Ci.nsIAuthPromptProvider,
Ci.nsIInterfaceRequestor,
Ci.nsINetworkInterceptController,
Ci.nsIStreamListener,
]);
if (this.redirectedFromId) {
// Redirects are not interceptable.
this._sendOnRequest(false);
}
}
// Public interception API.
resume(url, method, headers, postData) {
this._expectingResumedRequest = { method, headers, postData };
const newUri = url ? Services.io.newURI(url) : null;
this._interceptedChannel.resetInterceptionWithURI(newUri);
this._interceptedChannel = undefined;
}
// Public interception API.
abort(errorCode) {
const error = errorMap[errorCode] || Cr.NS_ERROR_FAILURE;
this._interceptedChannel.cancelInterception(error);
this._interceptedChannel = undefined;
}
// Public interception API.
fulfill(status, statusText, headers, base64body) {
this._interceptedChannel.synthesizeStatus(status, statusText);
for (const header of headers) {
this._interceptedChannel.synthesizeHeader(header.name, header.value);
if (header.name.toLowerCase() === 'set-cookie') {
Services.cookies.QueryInterface(Ci.nsICookieService);
Services.cookies.setCookieStringFromHttp(this.httpChannel.URI, header.value, this.httpChannel);
}
}
const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
const body = base64body ? atob(base64body) : '';
synthesized.data = body;
this._interceptedChannel.startSynthesizedResponse(synthesized, null, null, '', false);
this._interceptedChannel.finishSynthesizedResponse();
this._interceptedChannel = undefined;
}
// Instrumentation called by NetworkObserver.
_onInternalRedirect(newChannel) {
// Intercepted requests produce "internal redirects" - this is both for our own
// interception and service workers.
// An internal redirect has the same channelId, inherits notificationCallbacks and
// listener, and should be used instead of an old channel.
this._networkObserver._channelToRequest.delete(this.httpChannel);
this.httpChannel = newChannel;
this._networkObserver._channelToRequest.set(this.httpChannel, this);
}
// Instrumentation called by NetworkObserver.
_onInternalRedirectReady() {
// Resumed request is first internally redirected to a new request,
// and then the new request is ready to be updated.
if (!this._expectingResumedRequest)
return;
const { method, headers, postData } = this._expectingResumedRequest;
this._expectingResumedRequest = undefined;
if (headers) {
for (const header of requestHeaders(this.httpChannel))
this.httpChannel.setRequestHeader(header.name, '', false /* merge */);
for (const header of headers)
this.httpChannel.setRequestHeader(header.name, header.value, false /* merge */);
}
if (method)
this.httpChannel.requestMethod = method;
if (postData && this.httpChannel instanceof Ci.nsIUploadChannel2) {
const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
const body = atob(postData);
synthesized.setData(body, body.length);
const overriddenHeader = (lowerCaseName, defaultValue) => {
if (headers) {
for (const header of headers) {
if (header.name.toLowerCase() === lowerCaseName) {
return header.value;
}
}
}
return defaultValue;
}
// Clear content-length, so that upload stream resets it.
this.httpChannel.setRequestHeader('content-length', overriddenHeader('content-length', ''), false /* merge */);
this.httpChannel.explicitSetUploadStream(synthesized, overriddenHeader('content-type', 'application/octet-stream'), -1, this.httpChannel.requestMethod, false);
}
}
// Instrumentation called by NetworkObserver.
_onResponse(fromCache) {
this._sendOnResponse(fromCache);
}
// nsIInterfaceRequestor
getInterface(iid) {
if (iid.equals(Ci.nsIAuthPrompt2) || iid.equals(Ci.nsIAuthPromptProvider) || iid.equals(Ci.nsINetworkInterceptController))
return this;
if (iid.equals(Ci.nsIAuthPrompt)) // Block nsIAuthPrompt - we want nsIAuthPrompt2 to be used instead.
throw Cr.NS_ERROR_NO_INTERFACE;
if (this._previousCallbacks)
return this._previousCallbacks.getInterface(iid);
throw Cr.NS_ERROR_NO_INTERFACE;
}
// nsIAuthPromptProvider
getAuthPrompt(aPromptReason, iid) {
return this;
}
// nsIAuthPrompt2
asyncPromptAuth(aChannel, aCallback, aContext, level, authInfo) {
let canceled = false;
Promise.resolve().then(() => {
if (canceled)
return;
const hasAuth = this.promptAuth(aChannel, level, authInfo);
if (hasAuth)
aCallback.onAuthAvailable(aContext, authInfo);
else
aCallback.onAuthCancelled(aContext, true);
});
return {
QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
cancel: () => {
aCallback.onAuthCancelled(aContext, false);
canceled = true;
}
};
}
// nsIAuthPrompt2
promptAuth(aChannel, level, authInfo) {
if (authInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED)
return false;
const pageNetwork = this._activePageNetwork();
if (!pageNetwork)
return false;
let credentials = null;
if (authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) {
const proxy = this._networkObserver._targetRegistry.getProxyInfo(aChannel);
credentials = proxy ? {username: proxy.username, password: proxy.password} : null;
} else {
credentials = pageNetwork._target.browserContext().httpCredentials;
}
if (!credentials)
return false;
authInfo.username = credentials.username;
authInfo.password = credentials.password;
// This will produce a new request with respective auth header set.
// It will have the same id as ours. We expect it to arrive as new request and
// will treat it as our own redirect.
this._networkObserver._expectRedirect(this.httpChannel.channelId + '', this);
return true;
}
// nsINetworkInterceptController
shouldPrepareForIntercept(aURI, channel) {
const interceptController = this._fallThroughInterceptController();
if (interceptController && interceptController.shouldPrepareForIntercept(aURI, channel)) {
// We assume that interceptController is a service worker if there is one,
// and yield interception to it. We are not going to intercept ourselves,
// so we send onRequest now.
this._sendOnRequest(false);
return true;
}
if (channel !== this.httpChannel) {
// Not our channel? Just in case this happens, don't do anything.
return false;
}
// We do not want to intercept any redirects, because we are not able
// to intercept subresource redirects, and it's unreliable for main requests.
// We do not sendOnRequest here, because redirects do that in constructor.
if (this.redirectedFromId)
return false;
const shouldIntercept = this._shouldIntercept();
if (!shouldIntercept) {
// We are not intercepting - ready to issue onRequest.
this._sendOnRequest(false);
return false;
}
this._expectingInterception = true;
return true;
}
// nsINetworkInterceptController
channelIntercepted(intercepted) {
if (!this._expectingInterception) {
// We are not intercepting, fall-through.
const interceptController = this._fallThroughInterceptController();
if (interceptController)
interceptController.channelIntercepted(intercepted);
return;
}
this._expectingInterception = false;
this._interceptedChannel = intercepted.QueryInterface(Ci.nsIInterceptedChannel);
const pageNetwork = this._activePageNetwork();
if (!pageNetwork) {
// Just in case we disabled instrumentation while intercepting, resume and forget.
this.resume();
return;
}
const browserContext = pageNetwork._target.browserContext();
if (browserContext.settings.onlineOverride === 'offline') {
// Implement offline.
this.abort(Cr.NS_ERROR_OFFLINE);
return;
}
// Ok, so now we have intercepted the request, let's issue onRequest.
// If interception has been disabled while we were intercepting, resume and forget.
const interceptionEnabled = this._shouldIntercept();
this._sendOnRequest(!!interceptionEnabled);
if (interceptionEnabled)
pageNetwork._interceptedRequests.set(this.requestId, this);
else
this.resume();
}
// nsIStreamListener
onDataAvailable(aRequest, aInputStream, aOffset, aCount) {
// For requests with internal redirect (e.g. intercepted by Service Worker),
// we do not get onResponse normally, but we do get nsIStreamListener notifications.
this._sendOnResponse(false);
const iStream = new BinaryInputStream(aInputStream);
const sStream = new StorageStream(8192, aCount, null);
const oStream = new BinaryOutputStream(sStream.getOutputStream(0));
// Copy received data as they come.
const data = iStream.readBytes(aCount);
this._responseBodyChunks.push(data);
oStream.writeBytes(data, aCount);
try {
this._originalListener.onDataAvailable(aRequest, sStream.newInputStream(0), aOffset, aCount);
} catch (e) {
// Be ready to original listener exceptions.
}
}
// nsIStreamListener
onStartRequest(aRequest) {
try {
this._originalListener.onStartRequest(aRequest);
} catch (e) {
// Be ready to original listener exceptions.
}
}
// nsIStreamListener
onStopRequest(aRequest, aStatusCode) {
try {
this._originalListener.onStopRequest(aRequest, aStatusCode);
} catch (e) {
// Be ready to original listener exceptions.
}
if (aStatusCode === 0) {
// For requests with internal redirect (e.g. intercepted by Service Worker),
// we do not get onResponse normally, but we do get nsIRequestObserver notifications.
this._sendOnResponse(false);
const body = this._responseBodyChunks.join('');
const pageNetwork = this._activePageNetwork();
if (pageNetwork)
pageNetwork._responseStorage.addResponseBody(this, body);
this._sendOnRequestFinished();
} else {
this._sendOnRequestFailed(aStatusCode);
}
delete this._responseBodyChunks;
}
_shouldIntercept() {
const pageNetwork = this._activePageNetwork();
if (!pageNetwork)
return false;
if (pageNetwork._requestInterceptionEnabled)
return true;
const browserContext = pageNetwork._target.browserContext();
if (browserContext.requestInterceptionEnabled)
return true;
if (browserContext.settings.onlineOverride === 'offline')
return true;
return false;
}
_fallThroughInterceptController() {
if (!this._previousCallbacks || !(this._previousCallbacks instanceof Ci.nsINetworkInterceptController))
return undefined;
return this._previousCallbacks.getInterface(Ci.nsINetworkInterceptController);
}
_activePageNetwork() {
if (!this._maybeInactivePageNetwork)
return undefined;
return this._maybeInactivePageNetwork;
}
_findPageNetwork() {
let loadContext = helper.getLoadContext(this.httpChannel);
if (!loadContext)
return;
const target = this._networkObserver._targetRegistry.targetForBrowser(loadContext.topFrameElement);
if (!target)
return;
return PageNetwork._forPageTarget(target);
}
_sendOnRequest(isIntercepted) {
// Note: we call _sendOnRequest either after we intercepted the request,
// or at the first moment we know that we are not going to intercept.
const pageNetwork = this._activePageNetwork();
if (!pageNetwork)
return;
const loadInfo = this.httpChannel.loadInfo;
const causeType = loadInfo?.externalContentPolicyType || Ci.nsIContentPolicy.TYPE_OTHER;
const internalCauseType = loadInfo?.internalContentPolicyType || Ci.nsIContentPolicy.TYPE_OTHER;
pageNetwork.emit(PageNetwork.Events.Request, {
url: this.httpChannel.URI.spec,
frameId: this._frameId,
isIntercepted,
requestId: this.requestId,
redirectedFrom: this.redirectedFromId,
postData: readRequestPostData(this.httpChannel),
headers: requestHeaders(this.httpChannel),
method: this.httpChannel.requestMethod,
navigationId: this.navigationId,
cause: causeTypeToString(causeType),
internalCause: causeTypeToString(internalCauseType),
}, this._frameId);
}
_sendOnResponse(fromCache) {
if (this._sentOnResponse) {
// We can come here twice because of internal redirects, e.g. service workers.
return;
}
this._sentOnResponse = true;
const pageNetwork = this._activePageNetwork();
if (!pageNetwork)
return;
this.httpChannel.QueryInterface(Ci.nsIHttpChannelInternal);
this.httpChannel.QueryInterface(Ci.nsITimedChannel);
const timing = {
startTime: this.httpChannel.channelCreationTime,
domainLookupStart: this.httpChannel.domainLookupStartTime,
domainLookupEnd: this.httpChannel.domainLookupEndTime,
connectStart: this.httpChannel.connectStartTime,
secureConnectionStart: this.httpChannel.secureConnectionStartTime,
connectEnd: this.httpChannel.connectEndTime,
requestStart: this.httpChannel.requestStartTime,
responseStart: this.httpChannel.responseStartTime,
};
const headers = [];
let status = 0;
let statusText = '';
try {
status = this.httpChannel.responseStatus;
statusText = this.httpChannel.responseStatusText;
this.httpChannel.visitResponseHeaders({
visitHeader: (name, value) => headers.push({name, value}),
});
} catch (e) {
// Response headers, status and/or statusText are not available
// when redirect did not actually hit the network.
}
let remoteIPAddress = undefined;
let remotePort = undefined;
try {
remoteIPAddress = this.httpChannel.remoteAddress;
remotePort = this.httpChannel.remotePort;
} catch (e) {
// remoteAddress is not defined for cached requests.
}
pageNetwork.emit(PageNetwork.Events.Response, {
requestId: this.requestId,
securityDetails: getSecurityDetails(this.httpChannel),
fromCache,
headers,
remoteIPAddress,
remotePort,
status,
statusText,
timing,
}, this._frameId);
}
_sendOnRequestFailed(error) {
const pageNetwork = this._activePageNetwork();
if (pageNetwork) {
pageNetwork.emit(PageNetwork.Events.RequestFailed, {
requestId: this.requestId,
errorCode: helper.getNetworkErrorStatusText(error),
}, this._frameId);
}
this._networkObserver._channelToRequest.delete(this.httpChannel);
}
_sendOnRequestFinished() {
const pageNetwork = this._activePageNetwork();
if (pageNetwork) {
pageNetwork.emit(PageNetwork.Events.RequestFinished, {
requestId: this.requestId,
responseEndTime: this.httpChannel.responseEndTime,
}, this._frameId);
}
this._networkObserver._channelToRequest.delete(this.httpChannel);
}
}
class NetworkObserver {
static instance() {
return NetworkObserver._instance || null;
}
constructor(targetRegistry) {
EventEmitter.decorate(this);
NetworkObserver._instance = this;
this._targetRegistry = targetRegistry;
this._channelToRequest = new Map(); // http channel -> network request
this._expectedRedirect = new Map(); // expected redirect channel id (string) -> network request
const protocolProxyService = Cc['@mozilla.org/network/protocol-proxy-service;1'].getService();
this._channelProxyFilter = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIProtocolProxyChannelFilter]),
applyFilter: (channel, defaultProxyInfo, proxyFilter) => {
const proxy = this._targetRegistry.getProxyInfo(channel);
if (!proxy) {
proxyFilter.onProxyFilterResult(defaultProxyInfo);
return;
}
proxyFilter.onProxyFilterResult(protocolProxyService.newProxyInfo(
proxy.type,
proxy.host,
proxy.port,
'', /* aProxyAuthorizationHeader */
'', /* aConnectionIsolationKey */
0, /* aFlags */
UINT32_MAX, /* aFailoverTimeout */
null, /* failover proxy */
));
},
};
protocolProxyService.registerChannelFilter(this._channelProxyFilter, 0 /* position */);
this._channelSink = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIChannelEventSink]),
asyncOnChannelRedirect: (oldChannel, newChannel, flags, callback) => {
this._onRedirect(oldChannel, newChannel, flags);
callback.onRedirectVerifyCallback(Cr.NS_OK);
},
};
this._channelSinkFactory = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIFactory]),
createInstance: (aOuter, aIID) => this._channelSink.QueryInterface(aIID),
};
// Register self as ChannelEventSink to track redirects.
const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
registrar.registerFactory(SINK_CLASS_ID, SINK_CLASS_DESCRIPTION, SINK_CONTRACT_ID, this._channelSinkFactory);
Services.catMan.addCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID, SINK_CONTRACT_ID, false, true);
this._eventListeners = [
helper.addObserver(this._onRequest.bind(this), 'http-on-modify-request'),
helper.addObserver(this._onResponse.bind(this, false /* fromCache */), 'http-on-examine-response'),
helper.addObserver(this._onResponse.bind(this, true /* fromCache */), 'http-on-examine-cached-response'),
helper.addObserver(this._onResponse.bind(this, true /* fromCache */), 'http-on-examine-merged-response'),
];
}
_expectRedirect(channelId, previous) {
this._expectedRedirect.set(channelId, previous);
}
_onRedirect(oldChannel, newChannel, flags) {
if (!(oldChannel instanceof Ci.nsIHttpChannel) || !(newChannel instanceof Ci.nsIHttpChannel))
return;
const oldHttpChannel = oldChannel.QueryInterface(Ci.nsIHttpChannel);
const newHttpChannel = newChannel.QueryInterface(Ci.nsIHttpChannel);
if (!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL)) {
const previous = this._channelToRequest.get(oldHttpChannel);
if (previous)
this._expectRedirect(newHttpChannel.channelId + '', previous);
} else {
const request = this._channelToRequest.get(oldHttpChannel);
if (request)
request._onInternalRedirect(newHttpChannel);
}
}
pageNetworkForTarget(target) {
return PageNetwork._forPageTarget(target);
}
_onRequest(channel, topic) {
if (!(channel instanceof Ci.nsIHttpChannel))
return;
const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
const channelId = httpChannel.channelId + '';
const redirectedFrom = this._expectedRedirect.get(channelId);
if (redirectedFrom) {
this._expectedRedirect.delete(channelId);
new NetworkRequest(this, httpChannel, redirectedFrom);
} else {
const redirectedRequest = this._channelToRequest.get(httpChannel);
if (redirectedRequest)
redirectedRequest._onInternalRedirectReady();
else
new NetworkRequest(this, httpChannel);
}
}
_onResponse(fromCache, httpChannel, topic) {
const request = this._channelToRequest.get(httpChannel);
if (request)
request._onResponse(fromCache);
}
dispose() {
this._activityDistributor.removeObserver(this);
const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
registrar.unregisterFactory(SINK_CLASS_ID, this._channelSinkFactory);
Services.catMan.deleteCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID, false);
helper.removeListeners(this._eventListeners);
}
}
const protocolVersionNames = {
[Ci.nsITransportSecurityInfo.TLS_VERSION_1]: 'TLS 1',
[Ci.nsITransportSecurityInfo.TLS_VERSION_1_1]: 'TLS 1.1',
[Ci.nsITransportSecurityInfo.TLS_VERSION_1_2]: 'TLS 1.2',
[Ci.nsITransportSecurityInfo.TLS_VERSION_1_3]: 'TLS 1.3',
};
function getSecurityDetails(httpChannel) {
const securityInfo = httpChannel.securityInfo;
if (!securityInfo)
return null;
securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
if (!securityInfo.serverCert)
return null;
return {
protocol: protocolVersionNames[securityInfo.protocolVersion] || '<unknown>',
subjectName: securityInfo.serverCert.commonName,
issuer: securityInfo.serverCert.issuerCommonName,
// Convert to seconds.
validFrom: securityInfo.serverCert.validity.notBefore / 1000 / 1000,
validTo: securityInfo.serverCert.validity.notAfter / 1000 / 1000,
};
}
function readRequestPostData(httpChannel) {
if (!(httpChannel instanceof Ci.nsIUploadChannel))
return undefined;
const iStream = httpChannel.uploadStream;
if (!iStream)
return undefined;
const isSeekableStream = iStream instanceof Ci.nsISeekableStream;
let prevOffset;
if (isSeekableStream) {
prevOffset = iStream.tell();
iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
}
// Read data from the stream.
let result = undefined;
try {
const buffer = NetUtil.readInputStream(iStream, iStream.available());
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++)
binary += String.fromCharCode(bytes[i]);
result = btoa(binary);
} catch (err) {
result = '';
}
// Seek locks the file, so seek to the beginning only if necko hasn't
// read it yet, since necko doesn't seek to 0 before reading (at lest
// not till 459384 is fixed).
if (isSeekableStream && prevOffset == 0)
iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
return result;
}
function requestHeaders(httpChannel) {
const headers = [];
httpChannel.visitRequestHeaders({
visitHeader: (name, value) => headers.push({name, value}),
});
return headers;
}
function causeTypeToString(causeType) {
for (let key in Ci.nsIContentPolicy) {
if (Ci.nsIContentPolicy[key] === causeType)
return key;
}
return 'TYPE_OTHER';
}
function appendExtraHTTPHeaders(httpChannel, headers) {
if (!headers)
return;
for (const header of headers)
httpChannel.setRequestHeader(header.name, header.value, false /* merge */);
}
class ResponseStorage {
constructor(maxTotalSize, maxResponseSize) {
this._totalSize = 0;
this._maxResponseSize = maxResponseSize;
this._maxTotalSize = maxTotalSize;
this._responses = new Map();
}
addResponseBody(request, body) {
if (body.length > this._maxResponseSize) {
this._responses.set(request.requestId, {
evicted: true,
body: '',
});
return;
}
let encodings = [];
if ((request.httpChannel instanceof Ci.nsIEncodedChannel) && request.httpChannel.contentEncodings && !request.httpChannel.applyConversion) {
const encodingHeader = request.httpChannel.getResponseHeader("Content-Encoding");
encodings = encodingHeader.split(/\s*\t*,\s*\t*/);
}
this._responses.set(request.requestId, {body, encodings});
this._totalSize += body.length;
if (this._totalSize > this._maxTotalSize) {
for (let [requestId, response] of this._responses) {
this._totalSize -= response.body.length;
response.body = '';
response.evicted = true;
if (this._totalSize < this._maxTotalSize)
break;
}
}
}
getBase64EncodedResponse(requestId) {
const response = this._responses.get(requestId);
if (!response)
throw new Error(`Request "${requestId}" is not found`);
if (response.evicted)
return {base64body: '', evicted: true};
let result = response.body;
if (response.encodings && response.encodings.length) {
for (const encoding of response.encodings)
result = convertString(result, encoding, 'uncompressed');
}
return {base64body: btoa(result)};
}
}
function convertString(s, source, dest) {
const is = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
Ci.nsIStringInputStream
);
is.setData(s, s.length);
const listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance(
Ci.nsIStreamLoader
);
let result = [];
listener.init({
onStreamComplete: function onStreamComplete(
loader,
context,
status,
length,
data
) {
const array = Array.from(data);
const kChunk = 100000;
for (let i = 0; i < length; i += kChunk) {
const len = Math.min(kChunk, length - i);
const chunk = String.fromCharCode.apply(this, array.slice(i, i + len));
result.push(chunk);
}
},
});
const converter = Cc["@mozilla.org/streamConverters;1"].getService(
Ci.nsIStreamConverterService
).asyncConvertData(
source,
dest,
listener,
null
);
converter.onStartRequest(null, null);
converter.onDataAvailable(null, is, 0, s.length);
converter.onStopRequest(null, null, null);
return result.join('');
}
const errorMap = {
'aborted': Cr.NS_ERROR_ABORT,
'accessdenied': Cr.NS_ERROR_PORT_ACCESS_NOT_ALLOWED,
'addressunreachable': Cr.NS_ERROR_UNKNOWN_HOST,
'blockedbyclient': Cr.NS_ERROR_FAILURE,
'blockedbyresponse': Cr.NS_ERROR_FAILURE,
'connectionaborted': Cr.NS_ERROR_NET_INTERRUPT,
'connectionclosed': Cr.NS_ERROR_FAILURE,
'connectionfailed': Cr.NS_ERROR_FAILURE,
'connectionrefused': Cr.NS_ERROR_CONNECTION_REFUSED,
'connectionreset': Cr.NS_ERROR_NET_RESET,
'internetdisconnected': Cr.NS_ERROR_OFFLINE,
'namenotresolved': Cr.NS_ERROR_UNKNOWN_HOST,
'timedout': Cr.NS_ERROR_NET_TIMEOUT,
'failed': Cr.NS_ERROR_FAILURE,
};
PageNetwork.Events = {
Request: Symbol('PageNetwork.Events.Request'),
Response: Symbol('PageNetwork.Events.Response'),
RequestFinished: Symbol('PageNetwork.Events.RequestFinished'),
RequestFailed: Symbol('PageNetwork.Events.RequestFailed'),
};
var EXPORTED_SYMBOLS = ['NetworkObserver', 'PageNetwork'];
this.NetworkObserver = NetworkObserver;
this.PageNetwork = PageNetwork;

View File

@ -0,0 +1,180 @@
/* 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";
// Note: this file should be loadabale with eval() into worker environment.
// Avoid Components.*, ChromeUtils and global const variables.
const SIMPLE_CHANNEL_MESSAGE_NAME = 'juggler:simplechannel';
class SimpleChannel {
static createForMessageManager(name, mm) {
const channel = new SimpleChannel(name);
const messageListener = {
receiveMessage: message => channel._onMessage(message.data)
};
mm.addMessageListener(SIMPLE_CHANNEL_MESSAGE_NAME, messageListener);
channel.setTransport({
sendMessage: obj => mm.sendAsyncMessage(SIMPLE_CHANNEL_MESSAGE_NAME, obj),
dispose: () => mm.removeMessageListener(SIMPLE_CHANNEL_MESSAGE_NAME, messageListener),
});
return channel;
}
constructor(name) {
this._name = name;
this._messageId = 0;
this._connectorId = 0;
this._pendingMessages = new Map();
this._handlers = new Map();
this._bufferedIncomingMessages = [];
this._bufferedOutgoingMessages = [];
this.transport = {
sendMessage: null,
dispose: null,
};
this._ready = false;
this._disposed = false;
}
setTransport(transport) {
this.transport = transport;
// connection handshake:
// 1. There are two channel ends in different processes.
// 2. Both ends start in the `ready = false` state, meaning that they will
// not send any messages over transport.
// 3. Once channel end is created, it sends `READY` message to the other end.
// 4. Eventually, at least one of the ends receives `READY` message and responds with
// `READY_ACK`. We assume at least one of the ends will receive "READY" event from the other, since
// channel ends have a "parent-child" relation, i.e. one end is always created before the other one.
// 5. Once channel end receives either `READY` or `READY_ACK`, it transitions to `ready` state.
this.transport.sendMessage('READY');
}
_markAsReady() {
if (this._ready)
return;
this._ready = true;
for (const msg of this._bufferedOutgoingMessages)
this.transport.sendMessage(msg);
this._bufferedOutgoingMessages = [];
}
dispose() {
if (this._disposed)
return;
this._disposed = true;
for (const {resolve, reject, methodName} of this._pendingMessages.values())
reject(new Error(`Failed "${methodName}": ${this._name} is disposed.`));
this._pendingMessages.clear();
this._handlers.clear();
this.transport.dispose();
}
_rejectCallbacksFromConnector(connectorId) {
for (const [messageId, callback] of this._pendingMessages) {
if (callback.connectorId === connectorId) {
callback.reject(new Error(`Failed "${callback.methodName}": connector for namespace "${callback.namespace}" in channel "${this._name}" is disposed.`));
this._pendingMessages.delete(messageId);
}
}
}
connect(namespace) {
const connectorId = ++this._connectorId;
return {
send: (...args) => this._send(namespace, connectorId, ...args),
emit: (...args) => void this._send(namespace, connectorId, ...args).catch(e => {}),
dispose: () => this._rejectCallbacksFromConnector(connectorId),
};
}
register(namespace, handler) {
if (this._handlers.has(namespace))
throw new Error('ERROR: double-register for namespace ' + namespace);
this._handlers.set(namespace, handler);
// Try to re-deliver all pending messages.
const bufferedRequests = this._bufferedIncomingMessages;
this._bufferedIncomingMessages = [];
for (const data of bufferedRequests) {
this._onMessage(data);
}
return () => this.unregister(namespace);
}
unregister(namespace) {
this._handlers.delete(namespace);
}
/**
* @param {string} namespace
* @param {number} connectorId
* @param {string} methodName
* @param {...*} params
* @return {!Promise<*>}
*/
async _send(namespace, connectorId, methodName, ...params) {
if (this._disposed)
throw new Error(`ERROR: channel ${this._name} is already disposed! Cannot send "${methodName}" to "${namespace}"`);
const id = ++this._messageId;
const promise = new Promise((resolve, reject) => {
this._pendingMessages.set(id, {connectorId, resolve, reject, methodName, namespace});
});
const message = {requestId: id, methodName, params, namespace};
if (this._ready)
this.transport.sendMessage(message);
else
this._bufferedOutgoingMessages.push(message);
return promise;
}
async _onMessage(data) {
if (data === 'READY') {
this.transport.sendMessage('READY_ACK');
this._markAsReady();
return;
}
if (data === 'READY_ACK') {
this._markAsReady();
return;
}
if (data.responseId) {
const {resolve, reject} = this._pendingMessages.get(data.responseId);
this._pendingMessages.delete(data.responseId);
if (data.error)
reject(new Error(data.error));
else
resolve(data.result);
} else if (data.requestId) {
const namespace = data.namespace;
const handler = this._handlers.get(namespace);
if (!handler) {
this._bufferedIncomingMessages.push(data);
return;
}
const method = handler[data.methodName];
if (!method) {
this.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": No method "${data.methodName}" in namespace "${namespace}"`});
return;
}
try {
const result = await method.call(handler, ...data.params);
this.transport.sendMessage({responseId: data.requestId, result});
} catch (error) {
this.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": exception while running method "${data.methodName}" in namespace "${namespace}": ${error.message} ${error.stack}`});
return;
}
} else {
dump(`
ERROR: unknown message in channel "${this._name}": ${JSON.stringify(data)}
`);
}
}
}
var EXPORTED_SYMBOLS = ['SimpleChannel'];
this.SimpleChannel = SimpleChannel;

View File

@ -0,0 +1,914 @@
/* 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/. */
const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {Preferences} = ChromeUtils.import("resource://gre/modules/Preferences.jsm");
const {ContextualIdentityService} = ChromeUtils.import("resource://gre/modules/ContextualIdentityService.jsm");
const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm");
const helper = new Helper();
const IDENTITY_NAME = 'JUGGLER ';
const HUNDRED_YEARS = 60 * 60 * 24 * 365 * 100;
const ALL_PERMISSIONS = [
'geo',
'desktop-notification',
];
class DownloadInterceptor {
constructor(registry) {
this._registry = registry
this._handlerToUuid = new Map();
}
//
// nsIDownloadInterceptor implementation.
//
interceptDownloadRequest(externalAppHandler, request, browsingContext, outFile) {
if (!(request instanceof Ci.nsIChannel))
return false;
const channel = request.QueryInterface(Ci.nsIChannel);
let pageTarget = this._registry._browserBrowsingContextToTarget.get(channel.loadInfo.browsingContext);
if (!pageTarget)
return false;
const browserContext = pageTarget.browserContext();
const options = browserContext.downloadOptions;
if (!options)
return false;
const uuid = helper.generateId();
let file = null;
if (options.behavior === 'saveToDisk') {
file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
file.initWithPath(options.downloadsDir);
file.append(uuid);
try {
file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
} catch (e) {
dump(`interceptDownloadRequest failed to create file: ${e}\n`);
return false;
}
}
outFile.value = file;
this._handlerToUuid.set(externalAppHandler, uuid);
const downloadInfo = {
uuid,
browserContextId: browserContext.browserContextId,
pageTargetId: pageTarget.id(),
url: request.name,
suggestedFileName: externalAppHandler.suggestedFileName,
};
this._registry.emit(TargetRegistry.Events.DownloadCreated, downloadInfo);
return true;
}
onDownloadComplete(externalAppHandler, canceled, errorName) {
const uuid = this._handlerToUuid.get(externalAppHandler);
if (!uuid)
return;
this._handlerToUuid.delete(externalAppHandler);
const downloadInfo = {
uuid,
};
if (errorName === 'NS_BINDING_ABORTED') {
downloadInfo.canceled = true;
} else {
downloadInfo.error = errorName;
}
this._registry.emit(TargetRegistry.Events.DownloadFinished, downloadInfo);
}
}
class TargetRegistry {
constructor() {
EventEmitter.decorate(this);
this._browserContextIdToBrowserContext = new Map();
this._userContextIdToBrowserContext = new Map();
this._browserToTarget = new Map();
this._browserBrowsingContextToTarget = new Map();
this._browserProxy = null;
// Cleanup containers from previous runs (if any)
for (const identity of ContextualIdentityService.getPublicIdentities()) {
if (identity.name && identity.name.startsWith(IDENTITY_NAME)) {
ContextualIdentityService.remove(identity.userContextId);
ContextualIdentityService.closeContainerTabs(identity.userContextId);
}
}
this._defaultContext = new BrowserContext(this, undefined, undefined);
Services.obs.addObserver({
observe: (subject, topic, data) => {
const browser = subject.ownerElement;
if (!browser)
return;
const target = this._browserToTarget.get(browser);
if (!target)
return;
target.emit(PageTarget.Events.Crashed);
target.dispose();
}
}, 'oop-frameloader-crashed');
Services.mm.addMessageListener('juggler:content-ready', {
receiveMessage: message => {
const linkedBrowser = message.target;
const target = this._browserToTarget.get(linkedBrowser);
if (!target)
return;
return {
scriptsToEvaluateOnNewDocument: target.browserContext().scriptsToEvaluateOnNewDocument,
bindings: target.browserContext().bindings,
settings: target.browserContext().settings,
};
},
});
const onTabOpenListener = (appWindow, window, event) => {
const tab = event.target;
const userContextId = tab.userContextId;
const browserContext = this._userContextIdToBrowserContext.get(userContextId);
const hasExplicitSize = appWindow && (appWindow.chromeFlags & Ci.nsIWebBrowserChrome.JUGGLER_WINDOW_EXPLICIT_SIZE) !== 0;
const openerContext = tab.linkedBrowser.browsingContext.opener;
let openerTarget;
if (openerContext) {
// Popups usually have opener context.
openerTarget = this._browserBrowsingContextToTarget.get(openerContext);
} else if (tab.openerTab) {
// Noopener popups from the same window have opener tab instead.
openerTarget = this._browserToTarget.get(tab.openerTab.linkedBrowser);
}
if (!browserContext)
throw new Error(`Internal error: cannot find context for userContextId=${userContextId}`);
const target = new PageTarget(this, window, tab, browserContext, openerTarget);
target.updateUserAgent();
target.updateTouchOverride();
target.updateColorSchemeOverride();
if (!hasExplicitSize)
target.updateViewportSize();
if (browserContext.screencastOptions)
target._startVideoRecording(browserContext.screencastOptions);
};
const onTabCloseListener = event => {
const tab = event.target;
const linkedBrowser = tab.linkedBrowser;
const target = this._browserToTarget.get(linkedBrowser);
if (target)
target.dispose();
};
const domWindowTabListeners = new Map();
const onOpenWindow = async (appWindow) => {
let domWindow;
if (appWindow instanceof Ci.nsIAppWindow) {
domWindow = appWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
} else {
domWindow = appWindow;
appWindow = null;
}
if (!(domWindow instanceof Ci.nsIDOMChromeWindow))
return;
// In persistent mode, window might be opened long ago and might be
// already initialized.
//
// In this case, we want to keep this callback synchronous so that we will call
// `onTabOpenListener` synchronously and before the sync IPc message `juggler:content-ready`.
if (domWindow.document.readyState === 'uninitialized' || domWindow.document.readyState === 'loading') {
// For non-initialized windows, DOMContentLoaded initializes gBrowser
// and starts tab loading (see //browser/base/content/browser.js), so we
// are guaranteed to call `onTabOpenListener` before the sync IPC message
// `juggler:content-ready`.
await helper.awaitEvent(domWindow, 'DOMContentLoaded');
}
if (!domWindow.gBrowser)
return;
const tabContainer = domWindow.gBrowser.tabContainer;
domWindowTabListeners.set(domWindow, [
helper.addEventListener(tabContainer, 'TabOpen', event => onTabOpenListener(appWindow, domWindow, event)),
helper.addEventListener(tabContainer, 'TabClose', onTabCloseListener),
]);
for (const tab of domWindow.gBrowser.tabs)
onTabOpenListener(appWindow, domWindow, { target: tab });
};
const onCloseWindow = window => {
const domWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
if (!(domWindow instanceof Ci.nsIDOMChromeWindow))
return;
if (!domWindow.gBrowser)
return;
const listeners = domWindowTabListeners.get(domWindow) || [];
domWindowTabListeners.delete(domWindow);
helper.removeListeners(listeners);
for (const tab of domWindow.gBrowser.tabs)
onTabCloseListener({ target: tab });
};
const extHelperAppSvc = Cc["@mozilla.org/uriloader/external-helper-app-service;1"].getService(Ci.nsIExternalHelperAppService);
extHelperAppSvc.setDownloadInterceptor(new DownloadInterceptor(this));
Services.wm.addListener({ onOpenWindow, onCloseWindow });
for (const win of Services.wm.getEnumerator(null))
onOpenWindow(win);
}
setBrowserProxy(proxy) {
this._browserProxy = proxy;
}
getProxyInfo(channel) {
const originAttributes = channel.loadInfo && channel.loadInfo.originAttributes;
const browserContext = originAttributes ? this.browserContextForUserContextId(originAttributes.userContextId) : null;
// Prefer context proxy and fallback to browser-level proxy.
const proxyInfo = (browserContext && browserContext._proxy) || this._browserProxy;
if (!proxyInfo || proxyInfo.bypass.some(domainSuffix => channel.URI.host.endsWith(domainSuffix)))
return null;
return proxyInfo;
}
defaultContext() {
return this._defaultContext;
}
createBrowserContext(removeOnDetach) {
return new BrowserContext(this, helper.generateId(), removeOnDetach);
}
browserContextForId(browserContextId) {
return this._browserContextIdToBrowserContext.get(browserContextId);
}
browserContextForUserContextId(userContextId) {
return this._userContextIdToBrowserContext.get(userContextId);
}
async newPage({browserContextId}) {
const browserContext = this.browserContextForId(browserContextId);
const features = "chrome,dialog=no,all";
// See _callWithURIToLoad in browser.js for the structure of window.arguments
// window.arguments[1]: unused (bug 871161)
// [2]: referrerInfo (nsIReferrerInfo)
// [3]: postData (nsIInputStream)
// [4]: allowThirdPartyFixup (bool)
// [5]: userContextId (int)
// [6]: originPrincipal (nsIPrincipal)
// [7]: originStoragePrincipal (nsIPrincipal)
// [8]: triggeringPrincipal (nsIPrincipal)
// [9]: allowInheritPrincipal (bool)
// [10]: csp (nsIContentSecurityPolicy)
// [11]: nsOpenWindowInfo
const args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
const urlSupports = Cc["@mozilla.org/supports-string;1"].createInstance(
Ci.nsISupportsString
);
urlSupports.data = 'about:blank';
args.appendElement(urlSupports); // 0
args.appendElement(undefined); // 1
args.appendElement(undefined); // 2
args.appendElement(undefined); // 3
args.appendElement(undefined); // 4
const userContextIdSupports = Cc[
"@mozilla.org/supports-PRUint32;1"
].createInstance(Ci.nsISupportsPRUint32);
userContextIdSupports.data = browserContext.userContextId;
args.appendElement(userContextIdSupports); // 5
args.appendElement(undefined); // 6
args.appendElement(undefined); // 7
args.appendElement(Services.scriptSecurityManager.getSystemPrincipal()); // 8
const window = Services.ww.openWindow(null, AppConstants.BROWSER_CHROME_URL, '_blank', features, args);
await waitForWindowReady(window);
if (window.gBrowser.browsers.length !== 1)
throw new Error(`Unexpected number of tabs in the new window: ${window.gBrowser.browsers.length}`);
const browser = window.gBrowser.browsers[0];
const target = this._browserToTarget.get(browser);
browser.focus();
if (browserContext.settings.timezoneId) {
if (await target.hasFailedToOverrideTimezone())
throw new Error('Failed to override timezone');
}
return target.id();
}
targets() {
return Array.from(this._browserToTarget.values());
}
targetForBrowser(browser) {
return this._browserToTarget.get(browser);
}
}
class PageTarget {
constructor(registry, win, tab, browserContext, opener) {
EventEmitter.decorate(this);
this._targetId = helper.generateId();
this._registry = registry;
this._window = win;
this._gBrowser = win.gBrowser;
this._tab = tab;
this._linkedBrowser = tab.linkedBrowser;
this._browserContext = browserContext;
this._viewportSize = undefined;
this._initialDPPX = this._linkedBrowser.browsingContext.overrideDPPX;
this._url = 'about:blank';
this._openerId = opener ? opener.id() : undefined;
this._channel = SimpleChannel.createForMessageManager(`browser::page[${this._targetId}]`, this._linkedBrowser.messageManager);
this._screencastInfo = undefined;
this._dialogs = new Map();
const navigationListener = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]),
onLocationChange: (aWebProgress, aRequest, aLocation) => this._onNavigated(aLocation),
};
this._eventListeners = [
helper.addObserver(this._updateModalDialogs.bind(this), 'tabmodal-dialog-loaded'),
helper.addProgressListener(tab.linkedBrowser, navigationListener, Ci.nsIWebProgress.NOTIFY_LOCATION),
helper.addEventListener(this._linkedBrowser, 'DOMModalDialogClosed', event => this._updateModalDialogs()),
];
this._disposed = false;
browserContext.pages.add(this);
this._registry._browserToTarget.set(this._linkedBrowser, this);
this._registry._browserBrowsingContextToTarget.set(this._linkedBrowser.browsingContext, this);
this._registry.emit(TargetRegistry.Events.TargetCreated, this);
}
dialog(dialogId) {
return this._dialogs.get(dialogId);
}
dialogs() {
return [...this._dialogs.values()];
}
async windowReady() {
await waitForWindowReady(this._window);
}
linkedBrowser() {
return this._linkedBrowser;
}
browserContext() {
return this._browserContext;
}
updateTouchOverride() {
this._linkedBrowser.browsingContext.touchEventsOverride = this._browserContext.touchOverride ? 'enabled' : 'none';
}
updateUserAgent() {
this._linkedBrowser.browsingContext.customUserAgent = this._browserContext.defaultUserAgent;
}
_updateModalDialogs() {
const prompts = new Set(this._linkedBrowser.tabModalPromptBox ? this._linkedBrowser.tabModalPromptBox.listPrompts() : []);
for (const dialog of this._dialogs.values()) {
if (!prompts.has(dialog.prompt())) {
this._dialogs.delete(dialog.id());
this.emit(PageTarget.Events.DialogClosed, dialog);
} else {
prompts.delete(dialog.prompt());
}
}
for (const prompt of prompts) {
const dialog = Dialog.createIfSupported(prompt);
if (!dialog)
continue;
this._dialogs.set(dialog.id(), dialog);
this.emit(PageTarget.Events.DialogOpened, dialog);
}
}
async updateViewportSize() {
// Viewport size is defined by three arguments:
// 1. default size. Could be explicit if set as part of `window.open` call, e.g.
// `window.open(url, title, 'width=400,height=400')`
// 2. page viewport size
// 3. browserContext viewport size
//
// The "default size" (1) is only respected when the page is opened.
// Otherwise, explicitly set page viewport prevales over browser context
// default viewport.
const viewportSize = this._viewportSize || this._browserContext.defaultViewportSize;
const actualSize = await setViewportSizeForBrowser(viewportSize, this._linkedBrowser, this._window);
this._linkedBrowser.browsingContext.overrideDPPX = this._browserContext.deviceScaleFactor || this._initialDPPX;
await this._channel.connect('').send('awaitViewportDimensions', {
width: actualSize.width,
height: actualSize.height,
deviceSizeIsPageSize: !!this._browserContext.deviceScaleFactor,
});
}
setEmulatedMedia(mediumOverride) {
this._linkedBrowser.browsingContext.mediumOverride = mediumOverride || '';
}
setColorScheme(colorScheme) {
this.colorScheme = fromProtocolColorScheme(colorScheme);
this.updateColorSchemeOverride();
}
updateColorSchemeOverride() {
this._linkedBrowser.browsingContext.prefersColorSchemeOverride = this.colorScheme || this._browserContext.colorScheme || 'none';
}
async setViewportSize(viewportSize) {
this._viewportSize = viewportSize;
await this.updateViewportSize();
}
close(runBeforeUnload = false) {
this._gBrowser.removeTab(this._tab, {
skipPermitUnload: !runBeforeUnload,
});
}
channel() {
return this._channel;
}
id() {
return this._targetId;
}
info() {
return {
targetId: this.id(),
type: 'page',
browserContextId: this._browserContext.browserContextId,
openerId: this._openerId,
};
}
_onNavigated(aLocation) {
this._url = aLocation.spec;
this._browserContext.grantPermissionsToOrigin(this._url);
}
async ensurePermissions() {
await this._channel.connect('').send('ensurePermissions', {}).catch(e => void e);
}
async addScriptToEvaluateOnNewDocument(script) {
await this._channel.connect('').send('addScriptToEvaluateOnNewDocument', script).catch(e => void e);
}
async addBinding(name, script) {
await this._channel.connect('').send('addBinding', { name, script }).catch(e => void e);
}
async applyContextSetting(name, value) {
await this._channel.connect('').send('applyContextSetting', { name, value }).catch(e => void e);
}
async hasFailedToOverrideTimezone() {
return await this._channel.connect('').send('hasFailedToOverrideTimezone').catch(e => true);
}
async _startVideoRecording({width, height, scale, dir}) {
// On Mac the window may not yet be visible when TargetCreated and its
// NSWindow.windowNumber may be -1, so we wait until the window is known
// to be initialized and visible.
await this.windowReady();
const file = OS.Path.join(dir, helper.generateId() + '.webm');
if (width < 10 || width > 10000 || height < 10 || height > 10000)
throw new Error("Invalid size");
if (scale && (scale <= 0 || scale > 1))
throw new Error("Unsupported scale");
const screencast = Cc['@mozilla.org/juggler/screencast;1'].getService(Ci.nsIScreencastService);
const docShell = this._gBrowser.ownerGlobal.docShell;
// Exclude address bar and navigation control from the video.
const rect = this.linkedBrowser().getBoundingClientRect();
const devicePixelRatio = this._window.devicePixelRatio;
const videoSessionId = screencast.startVideoRecording(docShell, file, width, height, scale || 0, devicePixelRatio * rect.top);
this._screencastInfo = { videoSessionId, file };
this.emit(PageTarget.Events.ScreencastStarted);
}
async _stopVideoRecording() {
if (!this._screencastInfo)
throw new Error('No video recording in progress');
const screencastInfo = this._screencastInfo;
this._screencastInfo = undefined;
const screencast = Cc['@mozilla.org/juggler/screencast;1'].getService(Ci.nsIScreencastService);
const result = new Promise(resolve =>
Services.obs.addObserver(function onStopped(subject, topic, data) {
if (screencastInfo.videoSessionId != data)
return;
Services.obs.removeObserver(onStopped, 'juggler-screencast-stopped');
resolve();
}, 'juggler-screencast-stopped')
);
screencast.stopVideoRecording(screencastInfo.videoSessionId);
return result;
}
screencastInfo() {
return this._screencastInfo;
}
dispose() {
this._disposed = true;
if (this._screencastInfo)
this._stopVideoRecording().catch(e => dump(`stopVideoRecording failed:\n${e}\n`));
this._browserContext.pages.delete(this);
this._registry._browserToTarget.delete(this._linkedBrowser);
this._registry._browserBrowsingContextToTarget.delete(this._linkedBrowser.browsingContext);
try {
helper.removeListeners(this._eventListeners);
} catch (e) {
// In some cases, removing listeners from this._linkedBrowser fails
// because it is already half-destroyed.
if (e)
dump(e.message + '\n' + e.stack + '\n');
}
this._registry.emit(TargetRegistry.Events.TargetDestroyed, this);
}
}
PageTarget.Events = {
ScreencastStarted: Symbol('PageTarget.ScreencastStarted'),
Crashed: Symbol('PageTarget.Crashed'),
DialogOpened: Symbol('PageTarget.DialogOpened'),
DialogClosed: Symbol('PageTarget.DialogClosed'),
};
function fromProtocolColorScheme(colorScheme) {
if (colorScheme === 'light' || colorScheme === 'dark')
return colorScheme;
if (colorScheme === null || colorScheme === 'no-preference')
return undefined;
throw new Error('Unknown color scheme: ' + colorScheme);
}
class BrowserContext {
constructor(registry, browserContextId, removeOnDetach) {
this._registry = registry;
this.browserContextId = browserContextId;
// Default context has userContextId === 0, but we pass undefined to many APIs just in case.
this.userContextId = 0;
if (browserContextId !== undefined) {
const identity = ContextualIdentityService.create(IDENTITY_NAME + browserContextId);
this.userContextId = identity.userContextId;
}
this._principals = [];
// Maps origins to the permission lists.
this._permissions = new Map();
this._registry._browserContextIdToBrowserContext.set(this.browserContextId, this);
this._registry._userContextIdToBrowserContext.set(this.userContextId, this);
this._proxy = null;
this.removeOnDetach = removeOnDetach;
this.extraHTTPHeaders = undefined;
this.httpCredentials = undefined;
this.requestInterceptionEnabled = undefined;
this.ignoreHTTPSErrors = undefined;
this.downloadOptions = undefined;
this.defaultViewportSize = undefined;
this.deviceScaleFactor = undefined;
this.defaultUserAgent = null;
this.touchOverride = false;
this.colorScheme = 'none';
this.screencastOptions = undefined;
this.scriptsToEvaluateOnNewDocument = [];
this.bindings = [];
this.settings = {};
this.pages = new Set();
}
setColorScheme(colorScheme) {
this.colorScheme = fromProtocolColorScheme(colorScheme);
for (const page of this.pages)
page.updateColorSchemeOverride();
}
async destroy() {
if (this.userContextId !== 0) {
ContextualIdentityService.remove(this.userContextId);
for (const page of this.pages)
page.close();
if (this.pages.size) {
await new Promise(f => {
const listener = helper.on(this._registry, TargetRegistry.Events.TargetDestroyed, () => {
if (!this.pages.size) {
helper.removeListeners([listener]);
f();
}
});
});
}
}
this._registry._browserContextIdToBrowserContext.delete(this.browserContextId);
this._registry._userContextIdToBrowserContext.delete(this.userContextId);
}
setProxy(proxy) {
// Clear AuthCache.
Services.obs.notifyObservers(null, "net:clear-active-logins");
this._proxy = proxy;
}
setIgnoreHTTPSErrors(ignoreHTTPSErrors) {
if (this.ignoreHTTPSErrors === ignoreHTTPSErrors)
return;
this.ignoreHTTPSErrors = ignoreHTTPSErrors;
const certOverrideService = Cc[
"@mozilla.org/security/certoverride;1"
].getService(Ci.nsICertOverrideService);
if (ignoreHTTPSErrors) {
Preferences.set("network.stricttransportsecurity.preloadlist", false);
Preferences.set("security.cert_pinning.enforcement_level", 0);
certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(true, this.userContextId);
} else {
certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(false, this.userContextId);
}
}
async setDefaultUserAgent(userAgent) {
this.defaultUserAgent = userAgent;
for (const page of this.pages)
page.updateUserAgent();
}
setTouchOverride(touchOverride) {
this.touchOverride = touchOverride;
for (const page of this.pages)
page.updateTouchOverride();
}
async setDefaultViewport(viewport) {
this.defaultViewportSize = viewport ? viewport.viewportSize : undefined;
this.deviceScaleFactor = viewport ? viewport.deviceScaleFactor : undefined;
await Promise.all(Array.from(this.pages).map(page => page.updateViewportSize()));
}
async addScriptToEvaluateOnNewDocument(script) {
this.scriptsToEvaluateOnNewDocument.push(script);
await Promise.all(Array.from(this.pages).map(page => page.addScriptToEvaluateOnNewDocument(script)));
}
async addBinding(name, script) {
this.bindings.push({ name, script });
await Promise.all(Array.from(this.pages).map(page => page.addBinding(name, script)));
}
async applySetting(name, value) {
this.settings[name] = value;
await Promise.all(Array.from(this.pages).map(page => page.applyContextSetting(name, value)));
}
async grantPermissions(origin, permissions) {
this._permissions.set(origin, permissions);
const promises = [];
for (const page of this.pages) {
if (origin === '*' || page._url.startsWith(origin)) {
this.grantPermissionsToOrigin(page._url);
promises.push(page.ensurePermissions());
}
}
await Promise.all(promises);
}
resetPermissions() {
for (const principal of this._principals) {
for (const permission of ALL_PERMISSIONS)
Services.perms.removeFromPrincipal(principal, permission);
}
this._principals = [];
this._permissions.clear();
}
grantPermissionsToOrigin(url) {
let origin = Array.from(this._permissions.keys()).find(key => url.startsWith(key));
if (!origin)
origin = '*';
const permissions = this._permissions.get(origin);
if (!permissions)
return;
const attrs = { userContextId: this.userContextId || undefined };
const principal = Services.scriptSecurityManager.createContentPrincipal(NetUtil.newURI(url), attrs);
this._principals.push(principal);
for (const permission of ALL_PERMISSIONS) {
const action = permissions.includes(permission) ? Ci.nsIPermissionManager.ALLOW_ACTION : Ci.nsIPermissionManager.DENY_ACTION;
Services.perms.addFromPrincipal(principal, permission, action, Ci.nsIPermissionManager.EXPIRE_NEVER, 0 /* expireTime */);
}
}
setCookies(cookies) {
const protocolToSameSite = {
[undefined]: Ci.nsICookie.SAMESITE_NONE,
'Lax': Ci.nsICookie.SAMESITE_LAX,
'Strict': Ci.nsICookie.SAMESITE_STRICT,
};
for (const cookie of cookies) {
const uri = cookie.url ? NetUtil.newURI(cookie.url) : null;
let domain = cookie.domain;
if (!domain) {
if (!uri)
throw new Error('At least one of the url and domain needs to be specified');
domain = uri.host;
}
let path = cookie.path;
if (!path)
path = uri ? dirPath(uri.filePath) : '/';
let secure = false;
if (cookie.secure !== undefined)
secure = cookie.secure;
else if (uri && uri.scheme === 'https')
secure = true;
Services.cookies.add(
domain,
path,
cookie.name,
cookie.value,
secure,
cookie.httpOnly || false,
cookie.expires === undefined || cookie.expires === -1 /* isSession */,
cookie.expires === undefined ? Date.now() + HUNDRED_YEARS : cookie.expires,
{ userContextId: this.userContextId || undefined } /* originAttributes */,
protocolToSameSite[cookie.sameSite],
Ci.nsICookie.SCHEME_UNSET
);
}
}
clearCookies() {
Services.cookies.removeCookiesWithOriginAttributes(JSON.stringify({ userContextId: this.userContextId || undefined }));
}
getCookies() {
const result = [];
const sameSiteToProtocol = {
[Ci.nsICookie.SAMESITE_NONE]: 'None',
[Ci.nsICookie.SAMESITE_LAX]: 'Lax',
[Ci.nsICookie.SAMESITE_STRICT]: 'Strict',
};
for (let cookie of Services.cookies.cookies) {
if (cookie.originAttributes.userContextId !== this.userContextId)
continue;
if (cookie.host === 'addons.mozilla.org')
continue;
result.push({
name: cookie.name,
value: cookie.value,
domain: cookie.host,
path: cookie.path,
expires: cookie.isSession ? -1 : cookie.expiry,
size: cookie.name.length + cookie.value.length,
httpOnly: cookie.isHttpOnly,
secure: cookie.isSecure,
session: cookie.isSession,
sameSite: sameSiteToProtocol[cookie.sameSite],
});
}
return result;
}
async setScreencastOptions(options) {
this.screencastOptions = options;
if (!options)
return;
const promises = [];
for (const page of this.pages)
promises.push(page._startVideoRecording(options));
await Promise.all(promises);
}
}
class Dialog {
static createIfSupported(prompt) {
const type = prompt.args.promptType;
switch (type) {
case 'alert':
case 'alertCheck':
return new Dialog(prompt, 'alert');
case 'prompt':
return new Dialog(prompt, 'prompt');
case 'confirm':
case 'confirmCheck':
return new Dialog(prompt, 'confirm');
case 'confirmEx':
return new Dialog(prompt, 'beforeunload');
default:
return null;
};
}
constructor(prompt, type) {
this._id = helper.generateId();
this._type = type;
this._prompt = prompt;
}
id() {
return this._id;
}
message() {
return this._prompt.ui.infoBody.textContent;
}
type() {
return this._type;
}
prompt() {
return this._prompt;
}
dismiss() {
if (this._prompt.ui.button1)
this._prompt.ui.button1.click();
else
this._prompt.ui.button0.click();
}
defaultValue() {
return this._prompt.ui.loginTextbox.value;
}
accept(promptValue) {
if (typeof promptValue === 'string' && this._type === 'prompt')
this._prompt.ui.loginTextbox.value = promptValue;
this._prompt.ui.button0.click();
}
}
function dirPath(path) {
return path.substring(0, path.lastIndexOf('/') + 1);
}
async function waitForWindowReady(window) {
if (window.delayedStartupPromise) {
await window.delayedStartupPromise;
} else {
await new Promise((resolve => {
Services.obs.addObserver(function observer(aSubject, aTopic) {
if (window == aSubject) {
Services.obs.removeObserver(observer, aTopic);
resolve();
}
}, "browser-delayed-startup-finished");
}));
}
if (window.document.readyState !== 'complete')
await helper.awaitEvent(window, 'load');
}
async function setViewportSizeForBrowser(viewportSize, browser, window) {
await waitForWindowReady(window);
if (viewportSize) {
const {width, height} = viewportSize;
const rect = browser.getBoundingClientRect();
window.resizeBy(width - rect.width, height - rect.height);
browser.style.setProperty('min-width', width + 'px');
browser.style.setProperty('min-height', height + 'px');
browser.style.setProperty('max-width', width + 'px');
browser.style.setProperty('max-height', height + 'px');
} else {
browser.style.removeProperty('min-width');
browser.style.removeProperty('min-height');
browser.style.removeProperty('max-width');
browser.style.removeProperty('max-height');
}
const rect = browser.getBoundingClientRect();
return { width: rect.width, height: rect.height };
}
TargetRegistry.Events = {
TargetCreated: Symbol('TargetRegistry.Events.TargetCreated'),
TargetDestroyed: Symbol('TargetRegistry.Events.TargetDestroyed'),
DownloadCreated: Symbol('TargetRegistry.Events.DownloadCreated'),
DownloadFinished: Symbol('TargetRegistry.Events.DownloadFinished'),
};
var EXPORTED_SYMBOLS = ['TargetRegistry', 'PageTarget'];
this.TargetRegistry = TargetRegistry;
this.PageTarget = PageTarget;

View File

@ -0,0 +1,131 @@
/* 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/. */
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
const {ComponentUtils} = ChromeUtils.import("resource://gre/modules/ComponentUtils.jsm");
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {Dispatcher} = ChromeUtils.import("chrome://juggler/content/protocol/Dispatcher.js");
const {BrowserHandler} = ChromeUtils.import("chrome://juggler/content/protocol/BrowserHandler.js");
const {NetworkObserver} = ChromeUtils.import("chrome://juggler/content/NetworkObserver.js");
const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js");
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const helper = new Helper();
const Cc = Components.classes;
const Ci = Components.interfaces;
const FRAME_SCRIPT = "chrome://juggler/content/content/main.js";
// Command Line Handler
function CommandLineHandler() {
};
CommandLineHandler.prototype = {
classDescription: "Sample command-line handler",
classID: Components.ID('{f7a74a33-e2ab-422d-b022-4fb213dd2639}'),
contractID: "@mozilla.org/remote/juggler;1",
_xpcom_categories: [{
category: "command-line-handler",
entry: "m-juggler"
}],
/* nsICommandLineHandler */
handle: async function(cmdLine) {
const jugglerFlag = cmdLine.handleFlagWithParam("juggler", false);
const jugglerPipeFlag = cmdLine.handleFlag("juggler-pipe", false);
if (!jugglerPipeFlag && (!jugglerFlag || isNaN(jugglerFlag)))
return;
const silent = cmdLine.preventDefault;
if (silent)
Services.startup.enterLastWindowClosingSurvivalArea();
const targetRegistry = new TargetRegistry();
new NetworkObserver(targetRegistry);
const loadFrameScript = () => {
Services.mm.loadFrameScript(FRAME_SCRIPT, true /* aAllowDelayedLoad */);
if (Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless) {
const styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Components.interfaces.nsIStyleSheetService);
const ioService = Cc["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
const uri = ioService.newURI('chrome://juggler/content/content/hidden-scrollbars.css', null, null);
styleSheetService.loadAndRegisterSheet(uri, styleSheetService.AGENT_SHEET);
}
};
// Force create hidden window here, otherwise its creation later closes the web socket!
Services.appShell.hiddenDOMWindow;
if (jugglerFlag) {
const port = parseInt(jugglerFlag, 10);
const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
const WebSocketServer = require('devtools/server/socket/websocket-server');
this._server = Cc["@mozilla.org/network/server-socket;1"].createInstance(Ci.nsIServerSocket);
this._server.initSpecialConnection(port, Ci.nsIServerSocket.KeepWhenOffline | Ci.nsIServerSocket.LoopbackOnly, 4);
const token = helper.generateId();
this._server.asyncListen({
onSocketAccepted: async(socket, transport) => {
const input = transport.openInputStream(0, 0, 0);
const output = transport.openOutputStream(0, 0, 0);
const webSocket = await WebSocketServer.accept(transport, input, output, "/" + token);
const dispatcher = new Dispatcher(webSocket);
const browserHandler = new BrowserHandler(dispatcher.rootSession(), dispatcher, targetRegistry, () => {
if (silent)
Services.startup.exitLastWindowClosingSurvivalArea();
});
dispatcher.rootSession().setHandler(browserHandler);
}
});
loadFrameScript();
dump(`Juggler listening on ws://127.0.0.1:${this._server.port}/${token}\n`);
} else if (jugglerPipeFlag) {
let browserHandler;
let pipeStopped = false;
const pipe = Cc['@mozilla.org/juggler/remotedebuggingpipe;1'].getService(Ci.nsIRemoteDebuggingPipe);
const connection = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIRemoteDebuggingPipeClient]),
receiveMessage(message) {
if (this.onmessage)
this.onmessage({ data: message });
},
disconnected() {
if (browserHandler)
browserHandler['Browser.close']();
},
send(message) {
if (pipeStopped) {
// We are missing the response to Browser.close,
// but everything works fine. Once we actually need it,
// we have to stop the pipe after the response is sent.
return;
}
pipe.sendMessage(message);
},
};
pipe.init(connection);
const dispatcher = new Dispatcher(connection);
browserHandler = new BrowserHandler(dispatcher.rootSession(), dispatcher, targetRegistry, () => {
if (silent)
Services.startup.exitLastWindowClosingSurvivalArea();
connection.onclose();
pipe.stop();
pipeStopped = true;
});
dispatcher.rootSession().setHandler(browserHandler);
loadFrameScript();
dump(`\nJuggler listening to the pipe\n`);
}
},
QueryInterface: ChromeUtils.generateQI([ Ci.nsICommandLineHandler ]),
// CHANGEME: change the help info as appropriate, but
// follow the guidelines in nsICommandLineHandler.idl
// specifically, flag descriptions should start at
// character 24, and lines should be wrapped at
// 72 characters with embedded newlines,
// and finally, the string should end with a newline
helpInfo : " --juggler Enable Juggler automation\n"
};
var NSGetFactory = ComponentUtils.generateNSGetFactory([CommandLineHandler]);

View File

@ -0,0 +1,3 @@
component {f7a74a33-e2ab-422d-b022-4fb213dd2639} juggler.js
contract @mozilla.org/remote/juggler;1 {f7a74a33-e2ab-422d-b022-4fb213dd2639}
category command-line-handler m-juggler @mozilla.org/remote/juggler;1

View File

@ -0,0 +1,9 @@
# 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/.
EXTRA_COMPONENTS += [
"juggler.js",
"juggler.manifest",
]

View File

@ -0,0 +1,574 @@
/* 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";
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
const {Runtime} = ChromeUtils.import('chrome://juggler/content/content/Runtime.js');
const helper = new Helper();
class FrameTree {
constructor(rootDocShell) {
EventEmitter.decorate(this);
this._browsingContextGroup = rootDocShell.browsingContext.group;
if (!this._browsingContextGroup.__jugglerFrameTrees)
this._browsingContextGroup.__jugglerFrameTrees = new Set();
this._browsingContextGroup.__jugglerFrameTrees.add(this);
this._scriptsToEvaluateOnNewDocument = new Map();
this._webSocketEventService = Cc[
"@mozilla.org/websocketevent/service;1"
].getService(Ci.nsIWebSocketEventService);
this._bindings = new Map();
this._runtime = new Runtime(false /* isWorker */);
this._workers = new Map();
this._docShellToFrame = new Map();
this._frameIdToFrame = new Map();
this._pageReady = false;
this._mainFrame = this._createFrame(rootDocShell);
const webProgress = rootDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
this.QueryInterface = ChromeUtils.generateQI([
Ci.nsIWebProgressListener,
Ci.nsIWebProgressListener2,
Ci.nsISupportsWeakReference,
]);
this._wdm = Cc["@mozilla.org/dom/workers/workerdebuggermanager;1"].createInstance(Ci.nsIWorkerDebuggerManager);
this._wdmListener = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIWorkerDebuggerManagerListener]),
onRegister: this._onWorkerCreated.bind(this),
onUnregister: this._onWorkerDestroyed.bind(this),
};
this._wdm.addListener(this._wdmListener);
for (const workerDebugger of this._wdm.getWorkerDebuggerEnumerator())
this._onWorkerCreated(workerDebugger);
const flags = Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT |
Ci.nsIWebProgress.NOTIFY_FRAME_LOCATION;
this._eventListeners = [
helper.addObserver(this._onDOMWindowCreated.bind(this), 'content-document-global-created'),
helper.addObserver(this._onDOMWindowCreated.bind(this), 'juggler-dom-window-reused'),
helper.addObserver(subject => this._onDocShellCreated(subject.QueryInterface(Ci.nsIDocShell)), 'webnavigation-create'),
helper.addObserver(subject => this._onDocShellDestroyed(subject.QueryInterface(Ci.nsIDocShell)), 'webnavigation-destroy'),
helper.addProgressListener(webProgress, this, flags),
];
}
workers() {
return [...this._workers.values()];
}
runtime() {
return this._runtime;
}
_frameForWorker(workerDebugger) {
if (workerDebugger.type !== Ci.nsIWorkerDebugger.TYPE_DEDICATED)
return null;
if (!workerDebugger.window)
return null;
const docShell = workerDebugger.window.docShell;
return this._docShellToFrame.get(docShell) || null;
}
_onDOMWindowCreated(window) {
const frame = this._docShellToFrame.get(window.docShell) || null;
if (!frame)
return;
frame._onGlobalObjectCleared();
this.emit(FrameTree.Events.GlobalObjectCreated, { frame, window });
}
_onWorkerCreated(workerDebugger) {
// Note: we do not interoperate with firefox devtools.
if (workerDebugger.isInitialized)
return;
const frame = this._frameForWorker(workerDebugger);
if (!frame)
return;
const worker = new Worker(frame, workerDebugger);
this._workers.set(workerDebugger, worker);
this.emit(FrameTree.Events.WorkerCreated, worker);
}
_onWorkerDestroyed(workerDebugger) {
const worker = this._workers.get(workerDebugger);
if (!worker)
return;
worker.dispose();
this._workers.delete(workerDebugger);
this.emit(FrameTree.Events.WorkerDestroyed, worker);
}
allFramesInBrowsingContextGroup(group) {
const frames = [];
for (const frameTree of (group.__jugglerFrameTrees || []))
frames.push(...frameTree.frames());
return frames;
}
isPageReady() {
return this._pageReady;
}
forcePageReady() {
if (this._pageReady)
return false;
this._pageReady = true;
this.emit(FrameTree.Events.PageReady);
return true;
}
addScriptToEvaluateOnNewDocument(script) {
const scriptId = helper.generateId();
this._scriptsToEvaluateOnNewDocument.set(scriptId, script);
return scriptId;
}
removeScriptToEvaluateOnNewDocument(scriptId) {
this._scriptsToEvaluateOnNewDocument.delete(scriptId);
}
addBinding(name, script) {
this._bindings.set(name, script);
for (const frame of this.frames())
frame._addBinding(name, script);
}
frameForDocShell(docShell) {
return this._docShellToFrame.get(docShell) || null;
}
frame(frameId) {
return this._frameIdToFrame.get(frameId) || null;
}
frames() {
let result = [];
collect(this._mainFrame);
return result;
function collect(frame) {
result.push(frame);
for (const subframe of frame._children)
collect(subframe);
}
}
mainFrame() {
return this._mainFrame;
}
dispose() {
this._browsingContextGroup.__jugglerFrameTrees.delete(this);
this._wdm.removeListener(this._wdmListener);
this._runtime.dispose();
helper.removeListeners(this._eventListeners);
}
onStateChange(progress, request, flag, status) {
if (!(request instanceof Ci.nsIChannel))
return;
const channel = request.QueryInterface(Ci.nsIChannel);
const docShell = progress.DOMWindow.docShell;
const frame = this._docShellToFrame.get(docShell);
if (!frame) {
dump(`ERROR: got a state changed event for un-tracked docshell!\n`);
return;
}
if (!channel.isDocument) {
// Somehow, we can get worker requests here,
// while we are only interested in frame documents.
return;
}
const isStart = flag & Ci.nsIWebProgressListener.STATE_START;
const isTransferring = flag & Ci.nsIWebProgressListener.STATE_TRANSFERRING;
const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP;
const isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
if (isStart) {
// Starting a new navigation.
frame._pendingNavigationId = channelId(channel);
frame._pendingNavigationURL = channel.URI.spec;
this.emit(FrameTree.Events.NavigationStarted, frame);
} else if (isTransferring || (isStop && frame._pendingNavigationId && !status)) {
// Navigation is committed.
for (const subframe of frame._children)
this._detachFrame(subframe);
const navigationId = frame._pendingNavigationId;
frame._pendingNavigationId = null;
frame._pendingNavigationURL = null;
frame._lastCommittedNavigationId = navigationId;
frame._url = channel.URI.spec;
this.emit(FrameTree.Events.NavigationCommitted, frame);
if (frame === this._mainFrame)
this.forcePageReady();
} else if (isStop && frame._pendingNavigationId && status) {
// Navigation is aborted.
const navigationId = frame._pendingNavigationId;
frame._pendingNavigationId = null;
frame._pendingNavigationURL = null;
// Always report download navigation as failure to match other browsers.
const errorText = helper.getNetworkErrorStatusText(status);
this.emit(FrameTree.Events.NavigationAborted, frame, navigationId, errorText);
if (frame === this._mainFrame && status !== Cr.NS_BINDING_ABORTED)
this.forcePageReady();
}
if (isStop && isDocument)
this.emit(FrameTree.Events.Load, frame);
}
onFrameLocationChange(progress, request, location, flags) {
const docShell = progress.DOMWindow.docShell;
const frame = this._docShellToFrame.get(docShell);
const sameDocumentNavigation = !!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
if (frame && sameDocumentNavigation) {
frame._url = location.spec;
this.emit(FrameTree.Events.SameDocumentNavigation, frame);
}
}
_onDocShellCreated(docShell) {
// Bug 1142752: sometimes, the docshell appears to be immediately
// destroyed, bailout early to prevent random exceptions.
if (docShell.isBeingDestroyed())
return;
// If this docShell doesn't belong to our frame tree - do nothing.
let root = docShell;
while (root.parent)
root = root.parent;
if (root === this._mainFrame._docShell)
this._createFrame(docShell);
}
_createFrame(docShell) {
const parentFrame = this._docShellToFrame.get(docShell.parent) || null;
const frame = new Frame(this, this._runtime, docShell, parentFrame);
this._docShellToFrame.set(docShell, frame);
this._frameIdToFrame.set(frame.id(), frame);
this.emit(FrameTree.Events.FrameAttached, frame);
// Create execution context **after** reporting frame.
// This is our protocol contract.
if (frame.domWindow())
frame._onGlobalObjectCleared();
return frame;
}
_onDocShellDestroyed(docShell) {
const frame = this._docShellToFrame.get(docShell);
if (frame)
this._detachFrame(frame);
}
_detachFrame(frame) {
// Detach all children first
for (const subframe of frame._children)
this._detachFrame(subframe);
this._docShellToFrame.delete(frame._docShell);
this._frameIdToFrame.delete(frame.id());
if (frame._parentFrame)
frame._parentFrame._children.delete(frame);
frame._parentFrame = null;
frame.dispose();
this.emit(FrameTree.Events.FrameDetached, frame);
}
}
FrameTree.Events = {
BindingCalled: 'bindingcalled',
FrameAttached: 'frameattached',
FrameDetached: 'framedetached',
GlobalObjectCreated: 'globalobjectcreated',
WorkerCreated: 'workercreated',
WorkerDestroyed: 'workerdestroyed',
WebSocketCreated: 'websocketcreated',
WebSocketOpened: 'websocketopened',
WebSocketClosed: 'websocketclosed',
WebSocketFrameReceived: 'websocketframereceived',
WebSocketFrameSent: 'websocketframesent',
NavigationStarted: 'navigationstarted',
NavigationCommitted: 'navigationcommitted',
NavigationAborted: 'navigationaborted',
SameDocumentNavigation: 'samedocumentnavigation',
PageReady: 'pageready',
Load: 'load',
};
class Frame {
constructor(frameTree, runtime, docShell, parentFrame) {
this._frameTree = frameTree;
this._runtime = runtime;
this._docShell = docShell;
this._children = new Set();
this._frameId = helper.browsingContextToFrameId(this._docShell.browsingContext);
this._parentFrame = null;
this._url = '';
if (docShell.domWindow && docShell.domWindow.location)
this._url = docShell.domWindow.location.href;
if (parentFrame) {
this._parentFrame = parentFrame;
parentFrame._children.add(this);
}
this._lastCommittedNavigationId = null;
this._pendingNavigationId = null;
this._pendingNavigationURL = null;
this._textInputProcessor = null;
this._executionContext = null;
this._webSocketListenerInnerWindowId = 0;
// WebSocketListener calls frameReceived event before webSocketOpened.
// To avoid this, serialize event reporting.
this._webSocketInfos = new Map();
const dispatchWebSocketFrameReceived = (webSocketSerialID, frame) => this._frameTree.emit(FrameTree.Events.WebSocketFrameReceived, {
frameId: this._frameId,
wsid: webSocketSerialID + '',
opcode: frame.opCode,
data: frame.opCode !== 1 ? btoa(frame.payload) : frame.payload,
});
this._webSocketListener = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIWebSocketEventListener, ]),
webSocketCreated: (webSocketSerialID, uri, protocols) => {
this._frameTree.emit(FrameTree.Events.WebSocketCreated, {
frameId: this._frameId,
wsid: webSocketSerialID + '',
requestURL: uri,
});
this._webSocketInfos.set(webSocketSerialID, {
opened: false,
pendingIncomingFrames: [],
});
},
webSocketOpened: (webSocketSerialID, effectiveURI, protocols, extensions, httpChannelId) => {
this._frameTree.emit(FrameTree.Events.WebSocketOpened, {
frameId: this._frameId,
requestId: httpChannelId + '',
wsid: webSocketSerialID + '',
effectiveURL: effectiveURI,
});
const info = this._webSocketInfos.get(webSocketSerialID);
info.opened = true;
for (const frame of info.pendingIncomingFrames)
dispatchWebSocketFrameReceived(webSocketSerialID, frame);
},
webSocketMessageAvailable: (webSocketSerialID, data, messageType) => {
// We don't use this event.
},
webSocketClosed: (webSocketSerialID, wasClean, code, reason) => {
this._webSocketInfos.delete(webSocketSerialID);
let error = '';
if (!wasClean) {
const keys = Object.keys(Ci.nsIWebSocketChannel);
for (const key of keys) {
if (Ci.nsIWebSocketChannel[key] === code)
error = key;
}
}
this._frameTree.emit(FrameTree.Events.WebSocketClosed, {
frameId: this._frameId,
wsid: webSocketSerialID + '',
error,
});
},
frameReceived: (webSocketSerialID, frame) => {
// Report only text and binary frames.
if (frame.opCode !== 1 && frame.opCode !== 2)
return;
const info = this._webSocketInfos.get(webSocketSerialID);
if (info.opened)
dispatchWebSocketFrameReceived(webSocketSerialID, frame);
else
info.pendingIncomingFrames.push(frame);
},
frameSent: (webSocketSerialID, frame) => {
// Report only text and binary frames.
if (frame.opCode !== 1 && frame.opCode !== 2)
return;
this._frameTree.emit(FrameTree.Events.WebSocketFrameSent, {
frameId: this._frameId,
wsid: webSocketSerialID + '',
opcode: frame.opCode,
data: frame.opCode !== 1 ? btoa(frame.payload) : frame.payload,
});
},
};
}
dispose() {
if (this._executionContext)
this._runtime.destroyExecutionContext(this._executionContext);
this._executionContext = null;
}
_addBinding(name, script) {
Cu.exportFunction((...args) => {
this._frameTree.emit(FrameTree.Events.BindingCalled, {
frame: this,
name,
payload: args[0]
});
}, this.domWindow(), {
defineAs: name,
});
this.domWindow().eval(script);
}
_onGlobalObjectCleared() {
const webSocketService = this._frameTree._webSocketEventService;
if (this._webSocketListenerInnerWindowId)
webSocketService.removeListener(this._webSocketListenerInnerWindowId, this._webSocketListener);
this._webSocketListenerInnerWindowId = this.domWindow().windowGlobalChild.innerWindowId;
webSocketService.addListener(this._webSocketListenerInnerWindowId, this._webSocketListener);
if (this._executionContext)
this._runtime.destroyExecutionContext(this._executionContext);
this._executionContext = this._runtime.createExecutionContext(this.domWindow(), this.domWindow(), {
frameId: this._frameId,
name: '',
});
for (const [name, script] of this._frameTree._bindings)
this._addBinding(name, script);
for (const script of this._frameTree._scriptsToEvaluateOnNewDocument.values()) {
try {
const result = this._executionContext.evaluateScript(script);
if (result && result.objectId)
this._executionContext.disposeObject(result.objectId);
} catch (e) {
dump(`ERROR: ${e.message}\n${e.stack}\n`);
}
}
}
executionContext() {
return this._executionContext;
}
textInputProcessor() {
if (!this._textInputProcessor) {
this._textInputProcessor = Cc["@mozilla.org/text-input-processor;1"].createInstance(Ci.nsITextInputProcessor);
this._textInputProcessor.beginInputTransactionForTests(this._docShell.DOMWindow);
}
return this._textInputProcessor;
}
pendingNavigationId() {
return this._pendingNavigationId;
}
pendingNavigationURL() {
return this._pendingNavigationURL;
}
lastCommittedNavigationId() {
return this._lastCommittedNavigationId;
}
docShell() {
return this._docShell;
}
domWindow() {
return this._docShell.domWindow;
}
name() {
const frameElement = this._docShell.domWindow.frameElement;
let name = '';
if (frameElement)
name = frameElement.getAttribute('name') || frameElement.getAttribute('id') || '';
return name;
}
parentFrame() {
return this._parentFrame;
}
id() {
return this._frameId;
}
url() {
return this._url;
}
}
class Worker {
constructor(frame, workerDebugger) {
this._frame = frame;
this._workerId = helper.generateId();
this._workerDebugger = workerDebugger;
workerDebugger.initialize('chrome://juggler/content/content/WorkerMain.js');
this._channel = new SimpleChannel(`content::worker[${this._workerId}]`);
this._channel.setTransport({
sendMessage: obj => workerDebugger.postMessage(JSON.stringify(obj)),
dispose: () => {},
});
this._workerDebuggerListener = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIWorkerDebuggerListener]),
onMessage: msg => void this._channel._onMessage(JSON.parse(msg)),
onClose: () => void this._channel.dispose(),
onError: (filename, lineno, message) => {
dump(`Error in worker: ${message} @${filename}:${lineno}\n`);
},
};
workerDebugger.addListener(this._workerDebuggerListener);
}
channel() {
return this._channel;
}
frame() {
return this._frame;
}
id() {
return this._workerId;
}
url() {
return this._workerDebugger.url;
}
dispose() {
this._channel.dispose();
this._workerDebugger.removeListener(this._workerDebuggerListener);
}
}
function channelId(channel) {
if (channel instanceof Ci.nsIIdentChannel) {
const identChannel = channel.QueryInterface(Ci.nsIIdentChannel);
return String(identChannel.channelId);
}
return helper.generateId();
}
var EXPORTED_SYMBOLS = ['FrameTree'];
this.FrameTree = FrameTree;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,541 @@
/* 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";
// Note: this file should be loadabale with eval() into worker environment.
// Avoid Components.*, ChromeUtils and global const variables.
if (!this.Debugger) {
// Worker has a Debugger defined already.
const {addDebuggerToGlobal} = ChromeUtils.import("resource://gre/modules/jsdebugger.jsm", {});
addDebuggerToGlobal(Components.utils.getGlobalForObject(this));
}
let lastId = 0;
function generateId() {
return 'id-' + (++lastId);
}
const consoleLevelToProtocolType = {
'dir': 'dir',
'log': 'log',
'debug': 'debug',
'info': 'info',
'error': 'error',
'warn': 'warning',
'dirxml': 'dirxml',
'table': 'table',
'trace': 'trace',
'clear': 'clear',
'group': 'startGroup',
'groupCollapsed': 'startGroupCollapsed',
'groupEnd': 'endGroup',
'assert': 'assert',
'profile': 'profile',
'profileEnd': 'profileEnd',
'count': 'count',
'countReset': 'countReset',
'time': null,
'timeLog': 'timeLog',
'timeEnd': 'timeEnd',
'timeStamp': 'timeStamp',
};
const disallowedMessageCategories = new Set([
'XPConnect JavaScript',
'component javascript',
'chrome javascript',
'chrome registration',
'XBL',
'XBL Prototype Handler',
'XBL Content Sink',
'xbl javascript',
]);
class Runtime {
constructor(isWorker = false) {
this._debugger = new Debugger();
this._pendingPromises = new Map();
this._executionContexts = new Map();
this._windowToExecutionContext = new Map();
this._eventListeners = [];
if (isWorker) {
this._registerWorkerConsoleHandler();
} else {
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
this._registerConsoleServiceListener(Services);
this._registerConsoleObserver(Services);
}
// We can't use event listener here to be compatible with Worker Global Context.
// Use plain callbacks instead.
this.events = {
onConsoleMessage: createEvent(),
onErrorFromWorker: createEvent(),
onExecutionContextCreated: createEvent(),
onExecutionContextDestroyed: createEvent(),
};
}
executionContexts() {
return [...this._executionContexts.values()];
}
async evaluate({executionContextId, expression, returnByValue}) {
const executionContext = this.findExecutionContext(executionContextId);
if (!executionContext)
throw new Error('Failed to find execution context with id = ' + executionContextId);
const exceptionDetails = {};
let result = await executionContext.evaluateScript(expression, exceptionDetails);
if (!result)
return {exceptionDetails};
if (returnByValue)
result = executionContext.ensureSerializedToValue(result);
return {result};
}
async callFunction({executionContextId, functionDeclaration, args, returnByValue}) {
const executionContext = this.findExecutionContext(executionContextId);
if (!executionContext)
throw new Error('Failed to find execution context with id = ' + executionContextId);
const exceptionDetails = {};
let result = await executionContext.evaluateFunction(functionDeclaration, args, exceptionDetails);
if (!result)
return {exceptionDetails};
if (returnByValue)
result = executionContext.ensureSerializedToValue(result);
return {result};
}
async getObjectProperties({executionContextId, objectId}) {
const executionContext = this.findExecutionContext(executionContextId);
if (!executionContext)
throw new Error('Failed to find execution context with id = ' + executionContextId);
return {properties: executionContext.getObjectProperties(objectId)};
}
async disposeObject({executionContextId, objectId}) {
const executionContext = this.findExecutionContext(executionContextId);
if (!executionContext)
throw new Error('Failed to find execution context with id = ' + executionContextId);
return executionContext.disposeObject(objectId);
}
_registerConsoleServiceListener(Services) {
const Ci = Components.interfaces;
const consoleServiceListener = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIConsoleListener]),
observe: message => {
if (!(message instanceof Ci.nsIScriptError) || !message.outerWindowID ||
!message.category || disallowedMessageCategories.has(message.category)) {
return;
}
const errorWindow = Services.wm.getOuterWindowWithId(message.outerWindowID);
if (message.category === 'Web Worker' && message.logLevel === Ci.nsIConsoleMessage.error) {
emitEvent(this.events.onErrorFromWorker, errorWindow, message.message, '' + message.stack);
return;
}
const executionContext = this._windowToExecutionContext.get(errorWindow);
if (!executionContext)
return;
const typeNames = {
[Ci.nsIConsoleMessage.debug]: 'debug',
[Ci.nsIConsoleMessage.info]: 'info',
[Ci.nsIConsoleMessage.warn]: 'warn',
[Ci.nsIConsoleMessage.error]: 'error',
};
emitEvent(this.events.onConsoleMessage, {
args: [{
value: message.message,
}],
type: typeNames[message.logLevel],
executionContextId: executionContext.id(),
location: {
lineNumber: message.lineNumber,
columnNumber: message.columnNumber,
url: message.sourceName,
},
});
},
};
Services.console.registerListener(consoleServiceListener);
this._eventListeners.push(() => Services.console.unregisterListener(consoleServiceListener));
}
_registerConsoleObserver(Services) {
const consoleObserver = ({wrappedJSObject}, topic, data) => {
const executionContext = Array.from(this._executionContexts.values()).find(context => {
const domWindow = context._domWindow;
return domWindow && domWindow.windowGlobalChild.innerWindowId === wrappedJSObject.innerID;
});
if (!executionContext)
return;
this._onConsoleMessage(executionContext, wrappedJSObject);
};
Services.obs.addObserver(consoleObserver, "console-api-log-event");
this._eventListeners.push(() => Services.obs.removeObserver(consoleObserver, "console-api-log-event"));
}
_registerWorkerConsoleHandler() {
setConsoleEventHandler(message => {
const executionContext = Array.from(this._executionContexts.values())[0];
this._onConsoleMessage(executionContext, message);
});
this._eventListeners.push(() => setConsoleEventHandler(null));
}
_onConsoleMessage(executionContext, message) {
const type = consoleLevelToProtocolType[message.level];
if (!type)
return;
const args = message.arguments.map(arg => executionContext.rawValueToRemoteObject(arg));
emitEvent(this.events.onConsoleMessage, {
args,
type,
executionContextId: executionContext.id(),
location: {
lineNumber: message.lineNumber - 1,
columnNumber: message.columnNumber - 1,
url: message.filename,
},
});
}
dispose() {
for (const tearDown of this._eventListeners)
tearDown.call(null);
this._eventListeners = [];
}
async _awaitPromise(executionContext, obj, exceptionDetails = {}) {
if (obj.promiseState === 'fulfilled')
return {success: true, obj: obj.promiseValue};
if (obj.promiseState === 'rejected') {
const global = executionContext._global;
exceptionDetails.text = global.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}).return;
exceptionDetails.stack = global.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}).return;
return {success: false, obj: null};
}
let resolve, reject;
const promise = new Promise((a, b) => {
resolve = a;
reject = b;
});
this._pendingPromises.set(obj.promiseID, {resolve, reject, executionContext, exceptionDetails});
if (this._pendingPromises.size === 1)
this._debugger.onPromiseSettled = this._onPromiseSettled.bind(this);
return await promise;
}
_onPromiseSettled(obj) {
const pendingPromise = this._pendingPromises.get(obj.promiseID);
if (!pendingPromise)
return;
this._pendingPromises.delete(obj.promiseID);
if (!this._pendingPromises.size)
this._debugger.onPromiseSettled = undefined;
if (obj.promiseState === 'fulfilled') {
pendingPromise.resolve({success: true, obj: obj.promiseValue});
return;
};
const global = pendingPromise.executionContext._global;
pendingPromise.exceptionDetails.text = global.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}).return;
pendingPromise.exceptionDetails.stack = global.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}).return;
pendingPromise.resolve({success: false, obj: null});
}
createExecutionContext(domWindow, contextGlobal, auxData) {
// Note: domWindow is null for workers.
const context = new ExecutionContext(this, domWindow, contextGlobal, this._debugger.addDebuggee(contextGlobal), auxData);
this._executionContexts.set(context._id, context);
if (domWindow)
this._windowToExecutionContext.set(domWindow, context);
emitEvent(this.events.onExecutionContextCreated, context);
return context;
}
findExecutionContext(executionContextId) {
const executionContext = this._executionContexts.get(executionContextId);
if (!executionContext)
throw new Error('Failed to find execution context with id = ' + executionContextId);
return executionContext;
}
destroyExecutionContext(destroyedContext) {
for (const [promiseID, {reject, executionContext}] of this._pendingPromises) {
if (executionContext === destroyedContext) {
reject(new Error('Execution context was destroyed!'));
this._pendingPromises.delete(promiseID);
}
}
if (!this._pendingPromises.size)
this._debugger.onPromiseSettled = undefined;
this._debugger.removeDebuggee(destroyedContext._contextGlobal);
this._executionContexts.delete(destroyedContext._id);
if (destroyedContext._domWindow)
this._windowToExecutionContext.delete(destroyedContext._domWindow);
emitEvent(this.events.onExecutionContextDestroyed, destroyedContext);
}
}
class ExecutionContext {
constructor(runtime, domWindow, contextGlobal, global, auxData) {
this._runtime = runtime;
this._domWindow = domWindow;
this._contextGlobal = contextGlobal;
this._global = global;
this._remoteObjects = new Map();
this._id = generateId();
this._auxData = auxData;
this._jsonStringifyObject = this._global.executeInGlobal(`((stringify, dateProto, object) => {
const oldToJson = dateProto.toJSON;
dateProto.toJSON = undefined;
let hasSymbol = false;
const result = stringify(object, (key, value) => {
if (typeof value === 'symbol')
hasSymbol = true;
return value;
});
dateProto.toJSON = oldToJson;
return hasSymbol ? undefined : result;
}).bind(null, JSON.stringify.bind(JSON), Date.prototype)`).return;
}
id() {
return this._id;
}
auxData() {
return this._auxData;
}
async evaluateScript(script, exceptionDetails = {}) {
const userInputHelper = this._domWindow ? this._domWindow.windowUtils.setHandlingUserInput(true) : null;
if (this._domWindow && this._domWindow.document)
this._domWindow.document.notifyUserGestureActivation();
let {success, obj} = this._getResult(this._global.executeInGlobal(script), exceptionDetails);
userInputHelper && userInputHelper.destruct();
if (!success)
return null;
if (obj && obj.isPromise) {
const awaitResult = await this._runtime._awaitPromise(this, obj, exceptionDetails);
if (!awaitResult.success)
return null;
obj = awaitResult.obj;
}
return this._createRemoteObject(obj);
}
async evaluateFunction(functionText, args, exceptionDetails = {}) {
const funEvaluation = this._getResult(this._global.executeInGlobal('(' + functionText + ')'), exceptionDetails);
if (!funEvaluation.success)
return null;
if (!funEvaluation.obj.callable)
throw new Error('functionText does not evaluate to a function!');
args = args.map(arg => {
if (arg.objectId) {
if (!this._remoteObjects.has(arg.objectId))
throw new Error('Cannot find object with id = ' + arg.objectId);
return this._remoteObjects.get(arg.objectId);
}
switch (arg.unserializableValue) {
case 'Infinity': return Infinity;
case '-Infinity': return -Infinity;
case '-0': return -0;
case 'NaN': return NaN;
default: return this._toDebugger(arg.value);
}
});
const userInputHelper = this._domWindow ? this._domWindow.windowUtils.setHandlingUserInput(true) : null;
if (this._domWindow && this._domWindow.document)
this._domWindow.document.notifyUserGestureActivation();
let {success, obj} = this._getResult(funEvaluation.obj.apply(null, args), exceptionDetails);
userInputHelper && userInputHelper.destruct();
if (!success)
return null;
if (obj && obj.isPromise) {
const awaitResult = await this._runtime._awaitPromise(this, obj, exceptionDetails);
if (!awaitResult.success)
return null;
obj = awaitResult.obj;
}
return this._createRemoteObject(obj);
}
unsafeObject(objectId) {
if (!this._remoteObjects.has(objectId))
return;
return { object: this._remoteObjects.get(objectId).unsafeDereference() };
}
rawValueToRemoteObject(rawValue) {
const debuggerObj = this._global.makeDebuggeeValue(rawValue);
return this._createRemoteObject(debuggerObj);
}
_instanceOf(debuggerObj, rawObj, className) {
if (this._domWindow)
return rawObj instanceof this._domWindow[className];
return this._global.executeInGlobalWithBindings('o instanceof this[className]', {o: debuggerObj, className: this._global.makeDebuggeeValue(className)}).return;
}
_createRemoteObject(debuggerObj) {
if (debuggerObj instanceof Debugger.Object) {
const objectId = generateId();
this._remoteObjects.set(objectId, debuggerObj);
const rawObj = debuggerObj.unsafeDereference();
const type = typeof rawObj;
let subtype = undefined;
if (debuggerObj.isProxy)
subtype = 'proxy';
else if (Array.isArray(rawObj))
subtype = 'array';
else if (Object.is(rawObj, null))
subtype = 'null';
else if (this._instanceOf(debuggerObj, rawObj, 'Node'))
subtype = 'node';
else if (this._instanceOf(debuggerObj, rawObj, 'RegExp'))
subtype = 'regexp';
else if (this._instanceOf(debuggerObj, rawObj, 'Date'))
subtype = 'date';
else if (this._instanceOf(debuggerObj, rawObj, 'Map'))
subtype = 'map';
else if (this._instanceOf(debuggerObj, rawObj, 'Set'))
subtype = 'set';
else if (this._instanceOf(debuggerObj, rawObj, 'WeakMap'))
subtype = 'weakmap';
else if (this._instanceOf(debuggerObj, rawObj, 'WeakSet'))
subtype = 'weakset';
else if (this._instanceOf(debuggerObj, rawObj, 'Error'))
subtype = 'error';
else if (this._instanceOf(debuggerObj, rawObj, 'Promise'))
subtype = 'promise';
else if ((this._instanceOf(debuggerObj, rawObj, 'Int8Array')) || (this._instanceOf(debuggerObj, rawObj, 'Uint8Array')) ||
(this._instanceOf(debuggerObj, rawObj, 'Uint8ClampedArray')) || (this._instanceOf(debuggerObj, rawObj, 'Int16Array')) ||
(this._instanceOf(debuggerObj, rawObj, 'Uint16Array')) || (this._instanceOf(debuggerObj, rawObj, 'Int32Array')) ||
(this._instanceOf(debuggerObj, rawObj, 'Uint32Array')) || (this._instanceOf(debuggerObj, rawObj, 'Float32Array')) ||
(this._instanceOf(debuggerObj, rawObj, 'Float64Array'))) {
subtype = 'typedarray';
}
return {objectId, type, subtype};
}
if (typeof debuggerObj === 'symbol') {
const objectId = generateId();
this._remoteObjects.set(objectId, debuggerObj);
return {objectId, type: 'symbol'};
}
let unserializableValue = undefined;
if (Object.is(debuggerObj, NaN))
unserializableValue = 'NaN';
else if (Object.is(debuggerObj, -0))
unserializableValue = '-0';
else if (Object.is(debuggerObj, Infinity))
unserializableValue = 'Infinity';
else if (Object.is(debuggerObj, -Infinity))
unserializableValue = '-Infinity';
return unserializableValue ? {unserializableValue} : {value: debuggerObj};
}
ensureSerializedToValue(protocolObject) {
if (!protocolObject.objectId)
return protocolObject;
const obj = this._remoteObjects.get(protocolObject.objectId);
this._remoteObjects.delete(protocolObject.objectId);
return {value: this._serialize(obj)};
}
_toDebugger(obj) {
if (typeof obj !== 'object')
return obj;
if (obj === null)
return obj;
const properties = {};
for (let [key, value] of Object.entries(obj)) {
properties[key] = {
configurable: true,
writable: true,
enumerable: true,
value: this._toDebugger(value),
};
}
const baseObject = Array.isArray(obj) ? '([])' : '({})';
const debuggerObj = this._global.executeInGlobal(baseObject).return;
debuggerObj.defineProperties(properties);
return debuggerObj;
}
_serialize(obj) {
const result = this._global.executeInGlobalWithBindings('stringify(e)', {e: obj, stringify: this._jsonStringifyObject});
if (result.throw)
throw new Error('Object is not serializable');
return result.return === undefined ? undefined : JSON.parse(result.return);
}
disposeObject(objectId) {
this._remoteObjects.delete(objectId);
}
getObjectProperties(objectId) {
if (!this._remoteObjects.has(objectId))
throw new Error('Cannot find object with id = ' + arg.objectId);
const result = [];
for (let obj = this._remoteObjects.get(objectId); obj; obj = obj.proto) {
for (const propertyName of obj.getOwnPropertyNames()) {
const descriptor = obj.getOwnPropertyDescriptor(propertyName);
if (!descriptor.enumerable)
continue;
result.push({
name: propertyName,
value: this._createRemoteObject(descriptor.value),
});
}
}
return result;
}
_getResult(completionValue, exceptionDetails = {}) {
if (!completionValue) {
exceptionDetails.text = 'Evaluation terminated!';
exceptionDetails.stack = '';
return {success: false, obj: null};
}
if (completionValue.throw) {
if (this._global.executeInGlobalWithBindings('e instanceof Error', {e: completionValue.throw}).return) {
exceptionDetails.text = this._global.executeInGlobalWithBindings('e.message', {e: completionValue.throw}).return;
exceptionDetails.stack = this._global.executeInGlobalWithBindings('e.stack', {e: completionValue.throw}).return;
} else {
exceptionDetails.value = this._serialize(completionValue.throw);
}
return {success: false, obj: null};
}
return {success: true, obj: completionValue.return};
}
}
const listenersSymbol = Symbol('listeners');
function createEvent() {
const listeners = new Set();
const subscribeFunction = listener => {
listeners.add(listener);
return () => listeners.delete(listener);
}
subscribeFunction[listenersSymbol] = listeners;
return subscribeFunction;
}
function emitEvent(event, ...args) {
let listeners = event[listenersSymbol];
if (!listeners || !listeners.size)
return;
listeners = new Set(listeners);
for (const listener of listeners)
listener.call(null, ...args);
}
var EXPORTED_SYMBOLS = ['Runtime'];
this.Runtime = Runtime;

View File

@ -0,0 +1,76 @@
/* 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";
loadSubScript('chrome://juggler/content/content/Runtime.js');
loadSubScript('chrome://juggler/content/SimpleChannel.js');
const channel = new SimpleChannel('worker::worker');
const eventListener = event => channel._onMessage(JSON.parse(event.data));
this.addEventListener('message', eventListener);
channel.setTransport({
sendMessage: msg => postMessage(JSON.stringify(msg)),
dispose: () => this.removeEventListener('message', eventListener),
});
const runtime = new Runtime(true /* isWorker */);
(() => {
// Create execution context in the runtime only when the script
// source was actually evaluated in it.
const dbg = new Debugger(global);
if (dbg.findScripts({global}).length) {
runtime.createExecutionContext(null /* domWindow */, global, {});
} else {
dbg.onNewScript = function(s) {
dbg.onNewScript = undefined;
dbg.removeAllDebuggees();
runtime.createExecutionContext(null /* domWindow */, global, {});
};
}
})();
class RuntimeAgent {
constructor(runtime, channel) {
this._runtime = runtime;
this._browserRuntime = channel.connect('runtime');
for (const context of this._runtime.executionContexts())
this._onExecutionContextCreated(context);
this._eventListeners = [
this._runtime.events.onConsoleMessage(msg => this._browserRuntime.emit('runtimeConsole', msg)),
this._runtime.events.onExecutionContextCreated(this._onExecutionContextCreated.bind(this)),
this._runtime.events.onExecutionContextDestroyed(this._onExecutionContextDestroyed.bind(this)),
channel.register('runtime', {
evaluate: this._runtime.evaluate.bind(this._runtime),
callFunction: this._runtime.callFunction.bind(this._runtime),
getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime),
disposeObject: this._runtime.disposeObject.bind(this._runtime),
}),
];
}
_onExecutionContextCreated(executionContext) {
this._browserRuntime.emit('runtimeExecutionContextCreated', {
executionContextId: executionContext.id(),
auxData: executionContext.auxData(),
});
}
_onExecutionContextDestroyed(executionContext) {
this._browserRuntime.emit('runtimeExecutionContextDestroyed', {
executionContextId: executionContext.id(),
});
}
dispose() {
for (const disposer of this._eventListeners)
disposer();
this._eventListeners = [];
}
}
new RuntimeAgent(runtime, channel);

View File

@ -0,0 +1,7 @@
/* 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/. */
* {
scrollbar-width: none !important;
}

View File

@ -0,0 +1,149 @@
/* 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/. */
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {FrameTree} = ChromeUtils.import('chrome://juggler/content/content/FrameTree.js');
const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
const {PageAgent} = ChromeUtils.import('chrome://juggler/content/content/PageAgent.js');
let frameTree;
const helper = new Helper();
const messageManager = this;
let pageAgent;
let failedToOverrideTimezone = false;
const applySetting = {
geolocation: (geolocation) => {
if (geolocation) {
docShell.setGeolocationOverride({
coords: {
latitude: geolocation.latitude,
longitude: geolocation.longitude,
accuracy: geolocation.accuracy,
altitude: NaN,
altitudeAccuracy: NaN,
heading: NaN,
speed: NaN,
},
address: null,
timestamp: Date.now()
});
} else {
docShell.setGeolocationOverride(null);
}
},
onlineOverride: (onlineOverride) => {
if (!onlineOverride) {
docShell.onlineOverride = Ci.nsIDocShell.ONLINE_OVERRIDE_NONE;
return;
}
docShell.onlineOverride = onlineOverride === 'online' ?
Ci.nsIDocShell.ONLINE_OVERRIDE_ONLINE : Ci.nsIDocShell.ONLINE_OVERRIDE_OFFLINE;
},
bypassCSP: (bypassCSP) => {
docShell.bypassCSPEnabled = bypassCSP;
},
timezoneId: (timezoneId) => {
failedToOverrideTimezone = !docShell.overrideTimezone(timezoneId);
},
locale: (locale) => {
docShell.languageOverride = locale;
},
javaScriptDisabled: (javaScriptDisabled) => {
docShell.allowJavascript = !javaScriptDisabled;
},
colorScheme: (colorScheme) => {
frameTree.setColorScheme(colorScheme);
},
};
const channel = SimpleChannel.createForMessageManager('content::page', messageManager);
function initialize() {
const response = sendSyncMessage('juggler:content-ready')[0];
// If we didn't get a response, then we don't want to do anything
// as a part of this frame script.
if (!response)
return;
const {
scriptsToEvaluateOnNewDocument = [],
bindings = [],
settings = {}
} = response || {};
// Enforce focused state for all top level documents.
docShell.overrideHasFocus = true;
docShell.forceActiveState = true;
frameTree = new FrameTree(docShell);
for (const [name, value] of Object.entries(settings)) {
if (value !== undefined)
applySetting[name](value);
}
for (const script of scriptsToEvaluateOnNewDocument)
frameTree.addScriptToEvaluateOnNewDocument(script);
for (const { name, script } of bindings)
frameTree.addBinding(name, script);
pageAgent = new PageAgent(messageManager, channel, frameTree);
channel.register('', {
addScriptToEvaluateOnNewDocument(script) {
frameTree.addScriptToEvaluateOnNewDocument(script);
},
addBinding({name, script}) {
frameTree.addBinding(name, script);
},
applyContextSetting({name, value}) {
applySetting[name](value);
},
ensurePermissions() {
// noop, just a rountrip.
},
hasFailedToOverrideTimezone() {
return failedToOverrideTimezone;
},
async awaitViewportDimensions({width, height, deviceSizeIsPageSize}) {
docShell.deviceSizeIsPageSize = deviceSizeIsPageSize;
const win = docShell.domWindow;
if (win.innerWidth === width && win.innerHeight === height)
return;
await new Promise(resolve => {
const listener = helper.addEventListener(win, 'resize', () => {
if (win.innerWidth === width && win.innerHeight === height) {
helper.removeListeners([listener]);
resolve();
}
});
});
},
dispose() {
},
});
const gListeners = [
helper.addEventListener(messageManager, 'unload', msg => {
helper.removeListeners(gListeners);
pageAgent.dispose();
frameTree.dispose();
channel.dispose();
}),
];
}
initialize();

View File

@ -0,0 +1,22 @@
# 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/.
juggler.jar:
% content juggler %content/
content/Helper.js (Helper.js)
content/NetworkObserver.js (NetworkObserver.js)
content/TargetRegistry.js (TargetRegistry.js)
content/SimpleChannel.js (SimpleChannel.js)
content/protocol/PrimitiveTypes.js (protocol/PrimitiveTypes.js)
content/protocol/Protocol.js (protocol/Protocol.js)
content/protocol/Dispatcher.js (protocol/Dispatcher.js)
content/protocol/PageHandler.js (protocol/PageHandler.js)
content/protocol/BrowserHandler.js (protocol/BrowserHandler.js)
content/content/main.js (content/main.js)
content/content/FrameTree.js (content/FrameTree.js)
content/content/PageAgent.js (content/PageAgent.js)
content/content/Runtime.js (content/Runtime.js)
content/content/WorkerMain.js (content/WorkerMain.js)
content/content/hidden-scrollbars.css (content/hidden-scrollbars.css)

View File

@ -0,0 +1,10 @@
# 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/.
DIRS += ["components", "screencast", "pipe"]
JAR_MANIFESTS += ["jar.mn"]
with Files("**"):
BUG_COMPONENT = ("Testing", "Juggler")

View File

@ -0,0 +1,15 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
Classes = [
{
'cid': '{d69ecefe-3df7-4d11-9dc7-f604edb96da2}',
'contract_ids': ['@mozilla.org/juggler/remotedebuggingpipe;1'],
'type': 'nsIRemoteDebuggingPipe',
'constructor': 'mozilla::nsRemoteDebuggingPipe::GetSingleton',
'headers': ['/juggler/pipe/nsRemoteDebuggingPipe.h'],
},
]

View File

@ -0,0 +1,24 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
XPIDL_SOURCES += [
'nsIRemoteDebuggingPipe.idl',
]
XPIDL_MODULE = 'jugglerpipe'
SOURCES += [
'nsRemoteDebuggingPipe.cpp',
]
XPCOM_MANIFESTS += [
'components.conf',
]
LOCAL_INCLUDES += [
]
FINAL_LIBRARY = 'xul'

View File

@ -0,0 +1,20 @@
/* 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 "nsISupports.idl"
[scriptable, uuid(7910c231-971a-4653-abdc-a8599a986c4c)]
interface nsIRemoteDebuggingPipeClient : nsISupports
{
void receiveMessage(in AString message);
void disconnected();
};
[scriptable, uuid(b7bfb66b-fd46-4aa2-b4ad-396177186d94)]
interface nsIRemoteDebuggingPipe : nsISupports
{
void init(in nsIRemoteDebuggingPipeClient client);
void sendMessage(in AString message);
void stop();
};

View File

@ -0,0 +1,223 @@
/* 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 "nsRemoteDebuggingPipe.h"
#include <cstring>
#if defined(_WIN32)
#include <io.h>
#include <windows.h>
#else
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#endif
#include "mozilla/StaticPtr.h"
#include "nsISupportsPrimitives.h"
#include "nsThreadUtils.h"
namespace mozilla {
NS_IMPL_ISUPPORTS(nsRemoteDebuggingPipe, nsIRemoteDebuggingPipe)
namespace {
StaticRefPtr<nsRemoteDebuggingPipe> gPipe;
const size_t kWritePacketSize = 1 << 16;
#if defined(_WIN32)
HANDLE readHandle;
HANDLE writeHandle;
#else
const int readFD = 3;
const int writeFD = 4;
#endif
size_t ReadBytes(void* buffer, size_t size, bool exact_size)
{
size_t bytesRead = 0;
while (bytesRead < size) {
#if defined(_WIN32)
DWORD sizeRead = 0;
bool hadError = !ReadFile(readHandle, static_cast<char*>(buffer) + bytesRead,
size - bytesRead, &sizeRead, nullptr);
#else
int sizeRead = read(readFD, static_cast<char*>(buffer) + bytesRead,
size - bytesRead);
if (sizeRead < 0 && errno == EINTR)
continue;
bool hadError = sizeRead <= 0;
#endif
if (hadError) {
return 0;
}
bytesRead += sizeRead;
if (!exact_size)
break;
}
return bytesRead;
}
void WriteBytes(const char* bytes, size_t size)
{
size_t totalWritten = 0;
while (totalWritten < size) {
size_t length = size - totalWritten;
if (length > kWritePacketSize)
length = kWritePacketSize;
#if defined(_WIN32)
DWORD bytesWritten = 0;
bool hadError = !WriteFile(writeHandle, bytes + totalWritten, static_cast<DWORD>(length), &bytesWritten, nullptr);
#else
int bytesWritten = write(writeFD, bytes + totalWritten, length);
if (bytesWritten < 0 && errno == EINTR)
continue;
bool hadError = bytesWritten <= 0;
#endif
if (hadError)
return;
totalWritten += bytesWritten;
}
}
} // namespace
// static
already_AddRefed<nsIRemoteDebuggingPipe> nsRemoteDebuggingPipe::GetSingleton() {
if (!gPipe) {
gPipe = new nsRemoteDebuggingPipe();
}
return do_AddRef(gPipe);
}
nsRemoteDebuggingPipe::nsRemoteDebuggingPipe() = default;
nsRemoteDebuggingPipe::~nsRemoteDebuggingPipe() = default;
nsresult nsRemoteDebuggingPipe::Init(nsIRemoteDebuggingPipeClient* aClient) {
MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread.");
if (mClient) {
return NS_ERROR_FAILURE;
}
mClient = aClient;
MOZ_ALWAYS_SUCCEEDS(NS_NewNamedThread("Pipe Reader", getter_AddRefs(mReaderThread)));
MOZ_ALWAYS_SUCCEEDS(NS_NewNamedThread("Pipe Writer", getter_AddRefs(mWriterThread)));
#if defined(_WIN32)
CHAR pipeReadStr[20];
CHAR pipeWriteStr[20];
GetEnvironmentVariableA("PW_PIPE_READ", pipeReadStr, 20);
GetEnvironmentVariableA("PW_PIPE_WRITE", pipeWriteStr, 20);
readHandle = reinterpret_cast<HANDLE>(atoi(pipeReadStr));
writeHandle = reinterpret_cast<HANDLE>(atoi(pipeWriteStr));
#endif
MOZ_ALWAYS_SUCCEEDS(mReaderThread->Dispatch(NewRunnableMethod(
"nsRemoteDebuggingPipe::ReaderLoop",
this, &nsRemoteDebuggingPipe::ReaderLoop), nsIThread::DISPATCH_NORMAL));
return NS_OK;
}
nsresult nsRemoteDebuggingPipe::Stop() {
MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread.");
if (!mClient) {
return NS_ERROR_FAILURE;
}
m_terminated = true;
mClient = nullptr;
// Cancel pending synchronous read.
#if defined(_WIN32)
CancelIoEx(readHandle, nullptr);
CloseHandle(readHandle);
CloseHandle(writeHandle);
#else
shutdown(readFD, SHUT_RDWR);
shutdown(writeFD, SHUT_RDWR);
#endif
mReaderThread->Shutdown();
mReaderThread = nullptr;
mWriterThread->Shutdown();
mWriterThread = nullptr;
return NS_OK;
}
void nsRemoteDebuggingPipe::ReaderLoop() {
const size_t bufSize = 256 * 1024;
std::vector<char> buffer;
buffer.resize(bufSize);
std::vector<char> line;
while (!m_terminated) {
size_t size = ReadBytes(buffer.data(), bufSize, false);
if (!size) {
nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod<>(
"nsRemoteDebuggingPipe::Disconnected",
this, &nsRemoteDebuggingPipe::Disconnected);
NS_DispatchToMainThread(runnable.forget());
break;
}
size_t start = 0;
size_t end = line.size();
line.insert(line.end(), buffer.begin(), buffer.begin() + size);
while (true) {
for (; end < line.size(); ++end) {
if (line[end] == '\0') {
break;
}
}
if (end == line.size()) {
break;
}
if (end > start) {
nsCString message;
message.Append(line.data() + start, end - start);
nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod<nsCString>(
"nsRemoteDebuggingPipe::ReceiveMessage",
this, &nsRemoteDebuggingPipe::ReceiveMessage, std::move(message));
NS_DispatchToMainThread(runnable.forget());
}
++end;
start = end;
}
if (start != 0 && start < line.size()) {
memmove(line.data(), line.data() + start, line.size() - start);
}
line.resize(line.size() - start);
}
}
void nsRemoteDebuggingPipe::ReceiveMessage(const nsCString& aMessage) {
MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread.");
if (mClient) {
NS_ConvertUTF8toUTF16 utf16(aMessage);
mClient->ReceiveMessage(utf16);
}
}
void nsRemoteDebuggingPipe::Disconnected() {
MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread.");
if (mClient)
mClient->Disconnected();
}
nsresult nsRemoteDebuggingPipe::SendMessage(const nsAString& aMessage) {
MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread.");
if (!mClient) {
return NS_ERROR_FAILURE;
}
NS_ConvertUTF16toUTF8 utf8(aMessage);
nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
"nsRemoteDebuggingPipe::SendMessage",
[message = std::move(utf8)] {
const nsCString& flat = PromiseFlatCString(message);
WriteBytes(flat.Data(), flat.Length());
WriteBytes("\0", 1);
});
MOZ_ALWAYS_SUCCEEDS(mWriterThread->Dispatch(runnable.forget(), nsIThread::DISPATCH_NORMAL));
return NS_OK;
}
} // namespace mozilla

View File

@ -0,0 +1,34 @@
/* 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/. */
#pragma once
#include <memory>
#include "nsCOMPtr.h"
#include "nsIRemoteDebuggingPipe.h"
#include "nsThread.h"
namespace mozilla {
class nsRemoteDebuggingPipe final : public nsIRemoteDebuggingPipe {
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIREMOTEDEBUGGINGPIPE
static already_AddRefed<nsIRemoteDebuggingPipe> GetSingleton();
nsRemoteDebuggingPipe();
private:
void ReaderLoop();
void ReceiveMessage(const nsCString& aMessage);
void Disconnected();
~nsRemoteDebuggingPipe();
RefPtr<nsIRemoteDebuggingPipeClient> mClient;
nsCOMPtr<nsIThread> mReaderThread;
nsCOMPtr<nsIThread> mWriterThread;
std::atomic<bool> m_terminated { false };
};
} // namespace mozilla

View File

@ -0,0 +1,289 @@
/* 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";
const {AddonManager} = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js");
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {PageHandler} = ChromeUtils.import("chrome://juggler/content/protocol/PageHandler.js");
const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
const helper = new Helper();
class BrowserHandler {
constructor(session, dispatcher, targetRegistry, onclose) {
this._session = session;
this._dispatcher = dispatcher;
this._targetRegistry = targetRegistry;
this._enabled = false;
this._attachToDefaultContext = false;
this._eventListeners = [];
this._createdBrowserContextIds = new Set();
this._attachedSessions = new Map();
this._onclose = onclose;
}
async ['Browser.enable']({attachToDefaultContext}) {
if (this._enabled)
return;
this._enabled = true;
this._attachToDefaultContext = attachToDefaultContext;
this._eventListeners = [
helper.on(this._targetRegistry, TargetRegistry.Events.TargetCreated, this._onTargetCreated.bind(this)),
helper.on(this._targetRegistry, TargetRegistry.Events.TargetDestroyed, this._onTargetDestroyed.bind(this)),
helper.on(this._targetRegistry, TargetRegistry.Events.DownloadCreated, this._onDownloadCreated.bind(this)),
helper.on(this._targetRegistry, TargetRegistry.Events.DownloadFinished, this._onDownloadFinished.bind(this)),
];
const onScreencastStopped = (subject, topic, data) => {
this._session.emitEvent('Browser.screencastFinished', {screencastId: '' + data});
};
Services.obs.addObserver(onScreencastStopped, 'juggler-screencast-stopped');
this._eventListeners.push(() => Services.obs.removeObserver(onScreencastStopped, 'juggler-screencast-stopped'));
for (const target of this._targetRegistry.targets())
this._onTargetCreated(target);
// Wait to complete initialization of addon manager and search
// service before returning from this method. Failing to do so will result
// in a broken shutdown sequence and multiple errors in browser STDERR log.
//
// NOTE: we have to put this here as well as in the `Browser.close` handler
// since browser shutdown can be initiated when the last tab is closed, e.g.
// with persistent context.
await Promise.all([
waitForAddonManager(),
waitForSearchService(),
]);
}
async ['Browser.createBrowserContext']({removeOnDetach}) {
if (!this._enabled)
throw new Error('Browser domain is not enabled');
const browserContext = this._targetRegistry.createBrowserContext(removeOnDetach);
this._createdBrowserContextIds.add(browserContext.browserContextId);
return {browserContextId: browserContext.browserContextId};
}
async ['Browser.removeBrowserContext']({browserContextId}) {
if (!this._enabled)
throw new Error('Browser domain is not enabled');
await this._targetRegistry.browserContextForId(browserContextId).destroy();
this._createdBrowserContextIds.delete(browserContextId);
}
dispose() {
helper.removeListeners(this._eventListeners);
for (const [target, session] of this._attachedSessions)
this._dispatcher.destroySession(session);
this._attachedSessions.clear();
for (const browserContextId of this._createdBrowserContextIds) {
const browserContext = this._targetRegistry.browserContextForId(browserContextId);
if (browserContext.removeOnDetach)
browserContext.destroy();
}
this._createdBrowserContextIds.clear();
}
_shouldAttachToTarget(target) {
if (this._createdBrowserContextIds.has(target._browserContext.browserContextId))
return true;
return this._attachToDefaultContext && target._browserContext === this._targetRegistry.defaultContext();
}
_onTargetCreated(target) {
if (!this._shouldAttachToTarget(target))
return;
const channel = target.channel();
const session = this._dispatcher.createSession();
this._attachedSessions.set(target, session);
this._session.emitEvent('Browser.attachedToTarget', {
sessionId: session.sessionId(),
targetInfo: target.info()
});
session.setHandler(new PageHandler(target, session, channel));
}
_onTargetDestroyed(target) {
const session = this._attachedSessions.get(target);
if (!session)
return;
this._attachedSessions.delete(target);
this._dispatcher.destroySession(session);
this._session.emitEvent('Browser.detachedFromTarget', {
sessionId: session.sessionId(),
targetId: target.id(),
});
}
_onDownloadCreated(downloadInfo) {
this._session.emitEvent('Browser.downloadCreated', downloadInfo);
}
_onDownloadFinished(downloadInfo) {
this._session.emitEvent('Browser.downloadFinished', downloadInfo);
}
async ['Browser.newPage']({browserContextId}) {
const targetId = await this._targetRegistry.newPage({browserContextId});
return {targetId};
}
async ['Browser.close']() {
let browserWindow = Services.wm.getMostRecentWindow(
"navigator:browser"
);
if (browserWindow && browserWindow.gBrowserInit) {
await browserWindow.gBrowserInit.idleTasksFinishedPromise;
}
// Try to fully initialize browser before closing.
// See comment in `Browser.enable`.
await Promise.all([
waitForAddonManager(),
waitForSearchService(),
]);
this._onclose();
Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
}
async ['Browser.grantPermissions']({browserContextId, origin, permissions}) {
await this._targetRegistry.browserContextForId(browserContextId).grantPermissions(origin, permissions);
}
async ['Browser.resetPermissions']({browserContextId}) {
this._targetRegistry.browserContextForId(browserContextId).resetPermissions();
}
['Browser.setExtraHTTPHeaders']({browserContextId, headers}) {
this._targetRegistry.browserContextForId(browserContextId).extraHTTPHeaders = headers;
}
['Browser.setHTTPCredentials']({browserContextId, credentials}) {
this._targetRegistry.browserContextForId(browserContextId).httpCredentials = nullToUndefined(credentials);
}
async ['Browser.setBrowserProxy']({type, host, port, bypass, username, password}) {
this._targetRegistry.setBrowserProxy({ type, host, port, bypass, username, password});
}
async ['Browser.setContextProxy']({browserContextId, type, host, port, bypass, username, password}) {
const browserContext = this._targetRegistry.browserContextForId(browserContextId);
browserContext.setProxy({ type, host, port, bypass, username, password });
}
['Browser.setRequestInterception']({browserContextId, enabled}) {
this._targetRegistry.browserContextForId(browserContextId).requestInterceptionEnabled = enabled;
}
['Browser.setIgnoreHTTPSErrors']({browserContextId, ignoreHTTPSErrors}) {
this._targetRegistry.browserContextForId(browserContextId).setIgnoreHTTPSErrors(nullToUndefined(ignoreHTTPSErrors));
}
['Browser.setDownloadOptions']({browserContextId, downloadOptions}) {
this._targetRegistry.browserContextForId(browserContextId).downloadOptions = nullToUndefined(downloadOptions);
}
async ['Browser.setGeolocationOverride']({browserContextId, geolocation}) {
await this._targetRegistry.browserContextForId(browserContextId).applySetting('geolocation', nullToUndefined(geolocation));
}
async ['Browser.setOnlineOverride']({browserContextId, override}) {
await this._targetRegistry.browserContextForId(browserContextId).applySetting('onlineOverride', nullToUndefined(override));
}
async ['Browser.setColorScheme']({browserContextId, colorScheme}) {
await this._targetRegistry.browserContextForId(browserContextId).setColorScheme(nullToUndefined(colorScheme));
}
async ['Browser.setScreencastOptions']({browserContextId, dir, width, height, scale}) {
await this._targetRegistry.browserContextForId(browserContextId).setScreencastOptions({dir, width, height, scale});
}
async ['Browser.setUserAgentOverride']({browserContextId, userAgent}) {
await this._targetRegistry.browserContextForId(browserContextId).setDefaultUserAgent(userAgent);
}
async ['Browser.setBypassCSP']({browserContextId, bypassCSP}) {
await this._targetRegistry.browserContextForId(browserContextId).applySetting('bypassCSP', nullToUndefined(bypassCSP));
}
async ['Browser.setJavaScriptDisabled']({browserContextId, javaScriptDisabled}) {
await this._targetRegistry.browserContextForId(browserContextId).applySetting('javaScriptDisabled', nullToUndefined(javaScriptDisabled));
}
async ['Browser.setLocaleOverride']({browserContextId, locale}) {
await this._targetRegistry.browserContextForId(browserContextId).applySetting('locale', nullToUndefined(locale));
}
async ['Browser.setTimezoneOverride']({browserContextId, timezoneId}) {
await this._targetRegistry.browserContextForId(browserContextId).applySetting('timezoneId', nullToUndefined(timezoneId));
}
async ['Browser.setTouchOverride']({browserContextId, hasTouch}) {
await this._targetRegistry.browserContextForId(browserContextId).setTouchOverride(nullToUndefined(hasTouch));
}
async ['Browser.setDefaultViewport']({browserContextId, viewport}) {
await this._targetRegistry.browserContextForId(browserContextId).setDefaultViewport(nullToUndefined(viewport));
}
async ['Browser.addScriptToEvaluateOnNewDocument']({browserContextId, script}) {
await this._targetRegistry.browserContextForId(browserContextId).addScriptToEvaluateOnNewDocument(script);
}
async ['Browser.addBinding']({browserContextId, name, script}) {
await this._targetRegistry.browserContextForId(browserContextId).addBinding(name, script);
}
['Browser.setCookies']({browserContextId, cookies}) {
this._targetRegistry.browserContextForId(browserContextId).setCookies(cookies);
}
['Browser.clearCookies']({browserContextId}) {
this._targetRegistry.browserContextForId(browserContextId).clearCookies();
}
['Browser.getCookies']({browserContextId}) {
const cookies = this._targetRegistry.browserContextForId(browserContextId).getCookies();
return {cookies};
}
async ['Browser.getInfo']() {
const version = AppConstants.MOZ_APP_VERSION_DISPLAY;
const userAgent = Components.classes["@mozilla.org/network/protocol;1?name=http"]
.getService(Components.interfaces.nsIHttpProtocolHandler)
.userAgent;
return {version: 'Firefox/' + version, userAgent};
}
}
async function waitForSearchService() {
const searchService = Components.classes["@mozilla.org/browser/search-service;1"].getService(Components.interfaces.nsISearchService);
await searchService.init();
}
async function waitForAddonManager() {
if (AddonManager.isReady)
return;
await new Promise(resolve => {
let listener = {
onStartup() {
AddonManager.removeManagerListener(listener);
resolve();
},
onShutdown() { },
};
AddonManager.addManagerListener(listener);
});
}
function nullToUndefined(value) {
return value === null ? undefined : value;
}
var EXPORTED_SYMBOLS = ['BrowserHandler'];
this.BrowserHandler = BrowserHandler;

View File

@ -0,0 +1,135 @@
/* 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/. */
const {protocol, checkScheme} = ChromeUtils.import("chrome://juggler/content/protocol/Protocol.js");
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const helper = new Helper();
class Dispatcher {
/**
* @param {Connection} connection
*/
constructor(connection) {
this._connection = connection;
this._connection.onmessage = this._dispatch.bind(this);
this._connection.onclose = this._dispose.bind(this);
this._sessions = new Map();
this._rootSession = new ProtocolSession(this, undefined);
}
rootSession() {
return this._rootSession;
}
createSession() {
const session = new ProtocolSession(this, helper.generateId());
this._sessions.set(session.sessionId(), session);
return session;
}
destroySession(session) {
this._sessions.delete(session.sessionId());
session._dispose();
}
_dispose() {
this._connection.onmessage = null;
this._connection.onclose = null;
this._rootSession._dispose();
this._rootSession = null;
this._sessions.clear();
}
async _dispatch(event) {
const data = JSON.parse(event.data);
const id = data.id;
const sessionId = data.sessionId;
delete data.sessionId;
try {
const session = sessionId ? this._sessions.get(sessionId) : this._rootSession;
if (!session)
throw new Error(`ERROR: cannot find session with id "${sessionId}"`);
const method = data.method;
const params = data.params || {};
if (!id)
throw new Error(`ERROR: every message must have an 'id' parameter`);
if (!method)
throw new Error(`ERROR: every message must have a 'method' parameter`);
const [domain, methodName] = method.split('.');
const descriptor = protocol.domains[domain] ? protocol.domains[domain].methods[methodName] : null;
if (!descriptor)
throw new Error(`ERROR: method '${method}' is not supported`);
let details = {};
if (!checkScheme(descriptor.params || {}, params, details))
throw new Error(`ERROR: failed to call method '${method}' with parameters ${JSON.stringify(params, null, 2)}\n${details.error}`);
const result = await session.dispatch(method, params);
details = {};
if ((descriptor.returns || result) && !checkScheme(descriptor.returns, result, details))
throw new Error(`ERROR: failed to dispatch method '${method}' result ${JSON.stringify(result, null, 2)}\n${details.error}`);
this._connection.send(JSON.stringify({id, sessionId, result}));
} catch (e) {
this._connection.send(JSON.stringify({id, sessionId, error: {
message: e.message,
data: e.stack
}}));
}
}
_emitEvent(sessionId, eventName, params) {
const [domain, eName] = eventName.split('.');
const scheme = protocol.domains[domain] ? protocol.domains[domain].events[eName] : null;
if (!scheme)
throw new Error(`ERROR: event '${eventName}' is not supported`);
const details = {};
if (!checkScheme(scheme, params || {}, details))
throw new Error(`ERROR: failed to emit event '${eventName}' ${JSON.stringify(params, null, 2)}\n${details.error}`);
this._connection.send(JSON.stringify({method: eventName, params, sessionId}));
}
}
class ProtocolSession {
constructor(dispatcher, sessionId) {
this._sessionId = sessionId;
this._dispatcher = dispatcher;
this._handler = null;
}
sessionId() {
return this._sessionId;
}
setHandler(handler) {
this._handler = handler;
}
_dispose() {
if (this._handler)
this._handler.dispose();
this._handler = null;
this._dispatcher = null;
}
emitEvent(eventName, params) {
if (!this._dispatcher)
throw new Error(`Session has been disposed.`);
this._dispatcher._emitEvent(this._sessionId, eventName, params);
}
async dispatch(method, params) {
if (!this._handler)
throw new Error(`Session does not have a handler!`);
if (!this._handler[method])
throw new Error(`Handler for does not implement method "${method}"`);
return await this._handler[method](params);
}
}
this.EXPORTED_SYMBOLS = ['Dispatcher'];
this.Dispatcher = Dispatcher;

View File

@ -0,0 +1,389 @@
/* 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";
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {NetworkObserver, PageNetwork} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.js');
const {PageTarget} = ChromeUtils.import('chrome://juggler/content/TargetRegistry.js');
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
const helper = new Helper();
function hashConsoleMessage(params) {
return params.location.lineNumber + ':' + params.location.columnNumber + ':' + params.location.url;
}
class WorkerHandler {
constructor(session, contentChannel, workerId) {
this._session = session;
this._contentWorker = contentChannel.connect(workerId);
this._workerConsoleMessages = new Set();
this._workerId = workerId;
const emitWrappedProtocolEvent = eventName => {
return params => {
this._session.emitEvent('Page.dispatchMessageFromWorker', {
workerId,
message: JSON.stringify({method: eventName, params}),
});
}
}
this._eventListeners = [
contentChannel.register(workerId, {
runtimeConsole: (params) => {
this._workerConsoleMessages.add(hashConsoleMessage(params));
emitWrappedProtocolEvent('Runtime.console')(params);
},
runtimeExecutionContextCreated: emitWrappedProtocolEvent('Runtime.executionContextCreated'),
runtimeExecutionContextDestroyed: emitWrappedProtocolEvent('Runtime.executionContextDestroyed'),
}),
];
}
async sendMessage(message) {
const [domain, method] = message.method.split('.');
if (domain !== 'Runtime')
throw new Error('ERROR: can only dispatch to Runtime domain inside worker');
const result = await this._contentWorker.send(method, message.params);
this._session.emitEvent('Page.dispatchMessageFromWorker', {
workerId: this._workerId,
message: JSON.stringify({result, id: message.id}),
});
}
dispose() {
this._contentWorker.dispose();
helper.removeListeners(this._eventListeners);
}
}
class PageHandler {
constructor(target, session, contentChannel) {
this._session = session;
this._contentChannel = contentChannel;
this._contentPage = contentChannel.connect('page');
this._workers = new Map();
this._pageTarget = target;
this._pageNetwork = NetworkObserver.instance().pageNetworkForTarget(target);
const emitProtocolEvent = eventName => {
return (...args) => this._session.emitEvent(eventName, ...args);
}
this._reportedFrameIds = new Set();
this._networkEventsForUnreportedFrameIds = new Map();
// `Page.ready` protocol event is emitted whenever page has completed initialization, e.g.
// finished all the transient navigations to the `about:blank`.
//
// We'd like to avoid reporting meaningful events before the `Page.ready` since they are likely
// to be ignored by the protocol clients.
this._isPageReady = false;
if (this._pageTarget.screencastInfo())
this._onScreencastStarted();
this._eventListeners = [
helper.on(this._pageTarget, PageTarget.Events.DialogOpened, this._onDialogOpened.bind(this)),
helper.on(this._pageTarget, PageTarget.Events.DialogClosed, this._onDialogClosed.bind(this)),
helper.on(this._pageTarget, PageTarget.Events.Crashed, () => {
this._session.emitEvent('Page.crashed', {});
}),
helper.on(this._pageTarget, PageTarget.Events.ScreencastStarted, this._onScreencastStarted.bind(this)),
helper.on(this._pageNetwork, PageNetwork.Events.Request, this._handleNetworkEvent.bind(this, 'Network.requestWillBeSent')),
helper.on(this._pageNetwork, PageNetwork.Events.Response, this._handleNetworkEvent.bind(this, 'Network.responseReceived')),
helper.on(this._pageNetwork, PageNetwork.Events.RequestFinished, this._handleNetworkEvent.bind(this, 'Network.requestFinished')),
helper.on(this._pageNetwork, PageNetwork.Events.RequestFailed, this._handleNetworkEvent.bind(this, 'Network.requestFailed')),
contentChannel.register('page', {
pageBindingCalled: emitProtocolEvent('Page.bindingCalled'),
pageDispatchMessageFromWorker: emitProtocolEvent('Page.dispatchMessageFromWorker'),
pageEventFired: emitProtocolEvent('Page.eventFired'),
pageFileChooserOpened: emitProtocolEvent('Page.fileChooserOpened'),
pageFrameAttached: this._onFrameAttached.bind(this),
pageFrameDetached: emitProtocolEvent('Page.frameDetached'),
pageLinkClicked: emitProtocolEvent('Page.linkClicked'),
pageWillOpenNewWindowAsynchronously: emitProtocolEvent('Page.willOpenNewWindowAsynchronously'),
pageNavigationAborted: emitProtocolEvent('Page.navigationAborted'),
pageNavigationCommitted: emitProtocolEvent('Page.navigationCommitted'),
pageNavigationStarted: emitProtocolEvent('Page.navigationStarted'),
pageReady: this._onPageReady.bind(this),
pageSameDocumentNavigation: emitProtocolEvent('Page.sameDocumentNavigation'),
pageUncaughtError: emitProtocolEvent('Page.uncaughtError'),
pageWorkerCreated: this._onWorkerCreated.bind(this),
pageWorkerDestroyed: this._onWorkerDestroyed.bind(this),
runtimeConsole: params => {
const consoleMessageHash = hashConsoleMessage(params);
for (const worker of this._workers) {
if (worker._workerConsoleMessages.has(consoleMessageHash)) {
worker._workerConsoleMessages.delete(consoleMessageHash);
return;
}
}
emitProtocolEvent('Runtime.console')(params);
},
runtimeExecutionContextCreated: emitProtocolEvent('Runtime.executionContextCreated'),
runtimeExecutionContextDestroyed: emitProtocolEvent('Runtime.executionContextDestroyed'),
webSocketCreated: emitProtocolEvent('Page.webSocketCreated'),
webSocketOpened: emitProtocolEvent('Page.webSocketOpened'),
webSocketClosed: emitProtocolEvent('Page.webSocketClosed'),
webSocketFrameReceived: emitProtocolEvent('Page.webSocketFrameReceived'),
webSocketFrameSent: emitProtocolEvent('Page.webSocketFrameSent'),
}),
];
}
async dispose() {
this._contentPage.dispose();
helper.removeListeners(this._eventListeners);
}
_onScreencastStarted() {
const info = this._pageTarget.screencastInfo();
this._session.emitEvent('Page.screencastStarted', { screencastId: info.videoSessionId, file: info.file });
}
_onPageReady(event) {
this._isPageReady = true;
this._session.emitEvent('Page.ready');
for (const dialog of this._pageTarget.dialogs())
this._onDialogOpened(dialog);
}
_onDialogOpened(dialog) {
if (!this._isPageReady)
return;
this._session.emitEvent('Page.dialogOpened', {
dialogId: dialog.id(),
type: dialog.type(),
message: dialog.message(),
defaultValue: dialog.defaultValue(),
});
}
_onDialogClosed(dialog) {
if (!this._isPageReady)
return;
this._session.emitEvent('Page.dialogClosed', { dialogId: dialog.id(), });
}
_onWorkerCreated({workerId, frameId, url}) {
const worker = new WorkerHandler(this._session, this._contentChannel, workerId);
this._workers.set(workerId, worker);
this._session.emitEvent('Page.workerCreated', {workerId, frameId, url});
}
_onWorkerDestroyed({workerId}) {
const worker = this._workers.get(workerId);
if (!worker)
return;
this._workers.delete(workerId);
worker.dispose();
this._session.emitEvent('Page.workerDestroyed', {workerId});
}
_handleNetworkEvent(protocolEventName, eventDetails, frameId) {
if (!this._reportedFrameIds.has(frameId)) {
let events = this._networkEventsForUnreportedFrameIds.get(frameId);
if (!events) {
events = [];
this._networkEventsForUnreportedFrameIds.set(frameId, events);
}
events.push({eventName: protocolEventName, eventDetails});
} else {
this._session.emitEvent(protocolEventName, eventDetails);
}
}
_onFrameAttached({frameId, parentFrameId}) {
this._session.emitEvent('Page.frameAttached', {frameId, parentFrameId});
this._reportedFrameIds.add(frameId);
const events = this._networkEventsForUnreportedFrameIds.get(frameId) || [];
this._networkEventsForUnreportedFrameIds.delete(frameId);
for (const {eventName, eventDetails} of events)
this._session.emitEvent(eventName, eventDetails);
}
async ['Page.close']({runBeforeUnload}) {
// Postpone target close to deliver response in session.
Services.tm.dispatchToMainThread(() => {
this._pageTarget.close(runBeforeUnload);
});
}
async ['Page.setViewportSize']({viewportSize}) {
await this._pageTarget.setViewportSize(viewportSize === null ? undefined : viewportSize);
}
async ['Runtime.evaluate'](options) {
return await this._contentPage.send('evaluate', options);
}
async ['Runtime.callFunction'](options) {
return await this._contentPage.send('callFunction', options);
}
async ['Runtime.getObjectProperties'](options) {
return await this._contentPage.send('getObjectProperties', options);
}
async ['Runtime.disposeObject'](options) {
return await this._contentPage.send('disposeObject', options);
}
async ['Network.getResponseBody']({requestId}) {
return this._pageNetwork.getResponseBody(requestId);
}
async ['Network.setExtraHTTPHeaders']({headers}) {
this._pageNetwork.setExtraHTTPHeaders(headers);
}
async ['Network.setRequestInterception']({enabled}) {
if (enabled)
this._pageNetwork.enableRequestInterception();
else
this._pageNetwork.disableRequestInterception();
}
async ['Network.resumeInterceptedRequest']({requestId, url, method, headers, postData}) {
this._pageNetwork.resumeInterceptedRequest(requestId, url, method, headers, postData);
}
async ['Network.abortInterceptedRequest']({requestId, errorCode}) {
this._pageNetwork.abortInterceptedRequest(requestId, errorCode);
}
async ['Network.fulfillInterceptedRequest']({requestId, status, statusText, headers, base64body}) {
this._pageNetwork.fulfillInterceptedRequest(requestId, status, statusText, headers, base64body);
}
async ['Accessibility.getFullAXTree'](params) {
return await this._contentPage.send('getFullAXTree', params);
}
async ['Page.setFileInputFiles'](options) {
return await this._contentPage.send('setFileInputFiles', options);
}
async ['Page.setEmulatedMedia']({colorScheme, type}) {
this._pageTarget.setColorScheme(colorScheme || null);
this._pageTarget.setEmulatedMedia(type);
}
async ['Page.bringToFront'](options) {
this._pageTarget._window.focus();
}
async ['Page.setCacheDisabled'](options) {
return await this._contentPage.send('setCacheDisabled', options);
}
async ['Page.addBinding'](options) {
return await this._contentPage.send('addBinding', options);
}
async ['Page.adoptNode'](options) {
return await this._contentPage.send('adoptNode', options);
}
async ['Page.screenshot'](options) {
return await this._contentPage.send('screenshot', options);
}
async ['Page.getContentQuads'](options) {
return await this._contentPage.send('getContentQuads', options);
}
async ['Page.navigate'](options) {
return await this._contentPage.send('navigate', options);
}
async ['Page.goBack'](options) {
return await this._contentPage.send('goBack', options);
}
async ['Page.goForward'](options) {
return await this._contentPage.send('goForward', options);
}
async ['Page.reload'](options) {
return await this._contentPage.send('reload', options);
}
async ['Page.describeNode'](options) {
return await this._contentPage.send('describeNode', options);
}
async ['Page.scrollIntoViewIfNeeded'](options) {
return await this._contentPage.send('scrollIntoViewIfNeeded', options);
}
async ['Page.addScriptToEvaluateOnNewDocument'](options) {
return await this._contentPage.send('addScriptToEvaluateOnNewDocument', options);
}
async ['Page.removeScriptToEvaluateOnNewDocument'](options) {
return await this._contentPage.send('removeScriptToEvaluateOnNewDocument', options);
}
async ['Page.dispatchKeyEvent'](options) {
return await this._contentPage.send('dispatchKeyEvent', options);
}
async ['Page.dispatchTouchEvent'](options) {
return await this._contentPage.send('dispatchTouchEvent', options);
}
async ['Page.dispatchTapEvent'](options) {
return await this._contentPage.send('dispatchTapEvent', options);
}
async ['Page.dispatchMouseEvent'](options) {
return await this._contentPage.send('dispatchMouseEvent', options);
}
async ['Page.insertText'](options) {
return await this._contentPage.send('insertText', options);
}
async ['Page.crash'](options) {
return await this._contentPage.send('crash', options);
}
async ['Page.handleDialog']({dialogId, accept, promptText}) {
const dialog = this._pageTarget.dialog(dialogId);
if (!dialog)
throw new Error('Failed to find dialog with id = ' + dialogId);
if (accept)
dialog.accept(promptText);
else
dialog.dismiss();
}
async ['Page.setInterceptFileChooserDialog'](options) {
return await this._contentPage.send('setInterceptFileChooserDialog', options);
}
async ['Page.sendMessageToWorker']({workerId, message}) {
const worker = this._workers.get(workerId);
if (!worker)
throw new Error('ERROR: cannot find worker with id ' + workerId);
return await worker.sendMessage(JSON.parse(message));
}
async ['Page.stopVideoRecording']() {
await this._pageTarget.stopVideoRecording();
}
}
var EXPORTED_SYMBOLS = ['PageHandler'];
this.PageHandler = PageHandler;

View File

@ -0,0 +1,147 @@
/* 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/. */
const t = {};
t.String = function(x, details = {}, path = ['<root>']) {
if (typeof x === 'string' || typeof x === 'String')
return true;
details.error = `Expected "${path.join('.')}" to be |string|; found |${typeof x}| \`${JSON.stringify(x)}\` instead.`;
return false;
}
t.Number = function(x, details = {}, path = ['<root>']) {
if (typeof x === 'number')
return true;
details.error = `Expected "${path.join('.')}" to be |number|; found |${typeof x}| \`${JSON.stringify(x)}\` instead.`;
return false;
}
t.Boolean = function(x, details = {}, path = ['<root>']) {
if (typeof x === 'boolean')
return true;
details.error = `Expected "${path.join('.')}" to be |boolean|; found |${typeof x}| \`${JSON.stringify(x)}\` instead.`;
return false;
}
t.Null = function(x, details = {}, path = ['<root>']) {
if (Object.is(x, null))
return true;
details.error = `Expected "${path.join('.')}" to be \`null\`; found \`${JSON.stringify(x)}\` instead.`;
return false;
}
t.Undefined = function(x, details = {}, path = ['<root>']) {
if (Object.is(x, undefined))
return true;
details.error = `Expected "${path.join('.')}" to be \`undefined\`; found \`${JSON.stringify(x)}\` instead.`;
return false;
}
t.Any = x => true,
t.Enum = function(values) {
return function(x, details = {}, path = ['<root>']) {
if (values.indexOf(x) !== -1)
return true;
details.error = `Expected "${path.join('.')}" to be one of [${values.join(', ')}]; found \`${JSON.stringify(x)}\` (${typeof x}) instead.`;
return false;
}
}
t.Nullable = function(scheme) {
return function(x, details = {}, path = ['<root>']) {
if (Object.is(x, null))
return true;
return checkScheme(scheme, x, details, path);
}
}
t.Optional = function(scheme) {
return function(x, details = {}, path = ['<root>']) {
if (Object.is(x, undefined))
return true;
return checkScheme(scheme, x, details, path);
}
}
t.Array = function(scheme) {
return function(x, details = {}, path = ['<root>']) {
if (!Array.isArray(x)) {
details.error = `Expected "${path.join('.')}" to be an array; found \`${JSON.stringify(x)}\` (${typeof x}) instead.`;
return false;
}
const lastPathElement = path[path.length - 1];
for (let i = 0; i < x.length; ++i) {
path[path.length - 1] = lastPathElement + `[${i}]`;
if (!checkScheme(scheme, x[i], details, path))
return false;
}
path[path.length - 1] = lastPathElement;
return true;
}
}
t.Recursive = function(types, schemeName) {
return function(x, details = {}, path = ['<root>']) {
const scheme = types[schemeName];
return checkScheme(scheme, x, details, path);
}
}
function beauty(path, obj) {
if (path.length === 1)
return `object ${JSON.stringify(obj, null, 2)}`;
return `property "${path.join('.')}" - ${JSON.stringify(obj, null, 2)}`;
}
function checkScheme(scheme, x, details = {}, path = ['<root>']) {
if (!scheme)
throw new Error(`ILLDEFINED SCHEME: ${path.join('.')}`);
if (typeof scheme === 'object') {
if (!x) {
details.error = `Object "${path.join('.')}" is undefined, but has some scheme`;
return false;
}
for (const [propertyName, aScheme] of Object.entries(scheme)) {
path.push(propertyName);
const result = checkScheme(aScheme, x[propertyName], details, path);
path.pop();
if (!result)
return false;
}
for (const propertyName of Object.keys(x)) {
if (!scheme[propertyName]) {
path.push(propertyName);
details.error = `Found ${beauty(path, x[propertyName])} which is not described in this scheme`;
return false;
}
}
return true;
}
return scheme(x, details, path);
}
/*
function test(scheme, obj) {
const details = {};
if (!checkScheme(scheme, obj, details)) {
dump(`FAILED: ${JSON.stringify(obj)}
details.error: ${details.error}
`);
} else {
dump(`SUCCESS: ${JSON.stringify(obj)}
`);
}
}
test(t.Array(t.String), ['a', 'b', 2, 'c']);
test(t.Either(t.String, t.Number), {});
*/
this.t = t;
this.checkScheme = checkScheme;
this.EXPORTED_SYMBOLS = ['t', 'checkScheme'];

View File

@ -0,0 +1,933 @@
/* 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/. */
const {t, checkScheme} = ChromeUtils.import('chrome://juggler/content/protocol/PrimitiveTypes.js');
// Protocol-specific types.
const browserTypes = {};
browserTypes.TargetInfo = {
type: t.Enum(['page']),
targetId: t.String,
browserContextId: t.Optional(t.String),
// PageId of parent tab, if any.
openerId: t.Optional(t.String),
};
browserTypes.CookieOptions = {
name: t.String,
value: t.String,
url: t.Optional(t.String),
domain: t.Optional(t.String),
path: t.Optional(t.String),
secure: t.Optional(t.Boolean),
httpOnly: t.Optional(t.Boolean),
sameSite: t.Optional(t.Enum(['Strict', 'Lax', 'None'])),
expires: t.Optional(t.Number),
};
browserTypes.Cookie = {
name: t.String,
domain: t.String,
path: t.String,
value: t.String,
expires: t.Number,
size: t.Number,
httpOnly: t.Boolean,
secure: t.Boolean,
session: t.Boolean,
sameSite: t.Enum(['Strict', 'Lax', 'None']),
};
browserTypes.Geolocation = {
latitude: t.Number,
longitude: t.Number,
accuracy: t.Optional(t.Number),
};
browserTypes.DownloadOptions = {
behavior: t.Optional(t.Enum(['saveToDisk', 'cancel'])),
downloadsDir: t.Optional(t.String),
};
const pageTypes = {};
pageTypes.DOMPoint = {
x: t.Number,
y: t.Number,
};
pageTypes.Rect = {
x: t.Number,
y: t.Number,
width: t.Number,
height: t.Number,
};
pageTypes.Size = {
width: t.Number,
height: t.Number,
};
pageTypes.Viewport = {
viewportSize: pageTypes.Size,
deviceScaleFactor: t.Optional(t.Number),
};
pageTypes.DOMQuad = {
p1: pageTypes.DOMPoint,
p2: pageTypes.DOMPoint,
p3: pageTypes.DOMPoint,
p4: pageTypes.DOMPoint,
};
pageTypes.TouchPoint = {
x: t.Number,
y: t.Number,
radiusX: t.Optional(t.Number),
radiusY: t.Optional(t.Number),
rotationAngle: t.Optional(t.Number),
force: t.Optional(t.Number),
};
pageTypes.Clip = {
x: t.Number,
y: t.Number,
width: t.Number,
height: t.Number,
};
const runtimeTypes = {};
runtimeTypes.RemoteObject = {
type: t.Optional(t.Enum(['object', 'function', 'undefined', 'string', 'number', 'boolean', 'symbol', 'bigint'])),
subtype: t.Optional(t.Enum(['array', 'null', 'node', 'regexp', 'date', 'map', 'set', 'weakmap', 'weakset', 'error', 'proxy', 'promise', 'typedarray'])),
objectId: t.Optional(t.String),
unserializableValue: t.Optional(t.Enum(['Infinity', '-Infinity', '-0', 'NaN'])),
value: t.Any
};
runtimeTypes.ObjectProperty = {
name: t.String,
value: runtimeTypes.RemoteObject,
};
runtimeTypes.ScriptLocation = {
columnNumber: t.Number,
lineNumber: t.Number,
url: t.String,
};
runtimeTypes.ExceptionDetails = {
text: t.Optional(t.String),
stack: t.Optional(t.String),
value: t.Optional(t.Any),
};
runtimeTypes.CallFunctionArgument = {
objectId: t.Optional(t.String),
unserializableValue: t.Optional(t.Enum(['Infinity', '-Infinity', '-0', 'NaN'])),
value: t.Any,
};
const axTypes = {};
axTypes.AXTree = {
role: t.String,
name: t.String,
children: t.Optional(t.Array(t.Recursive(axTypes, 'AXTree'))),
selected: t.Optional(t.Boolean),
focused: t.Optional(t.Boolean),
pressed: t.Optional(t.Boolean),
focusable: t.Optional(t.Boolean),
haspopup: t.Optional(t.Boolean),
required: t.Optional(t.Boolean),
invalid: t.Optional(t.Boolean),
modal: t.Optional(t.Boolean),
editable: t.Optional(t.Boolean),
busy: t.Optional(t.Boolean),
multiline: t.Optional(t.Boolean),
readonly: t.Optional(t.Boolean),
checked: t.Optional(t.Enum(['mixed', true])),
expanded: t.Optional(t.Boolean),
disabled: t.Optional(t.Boolean),
multiselectable: t.Optional(t.Boolean),
value: t.Optional(t.String),
description: t.Optional(t.String),
roledescription: t.Optional(t.String),
valuetext: t.Optional(t.String),
orientation: t.Optional(t.String),
autocomplete: t.Optional(t.String),
keyshortcuts: t.Optional(t.String),
level: t.Optional(t.Number),
tag: t.Optional(t.String),
foundObject: t.Optional(t.Boolean),
}
const networkTypes = {};
networkTypes.HTTPHeader = {
name: t.String,
value: t.String,
};
networkTypes.HTTPCredentials = {
username: t.String,
password: t.String,
};
networkTypes.SecurityDetails = {
protocol: t.String,
subjectName: t.String,
issuer: t.String,
validFrom: t.Number,
validTo: t.Number,
};
networkTypes.ResourceTiming = {
startTime: t.Number,
domainLookupStart: t.Number,
domainLookupEnd: t.Number,
connectStart: t.Number,
secureConnectionStart: t.Number,
connectEnd: t.Number,
requestStart: t.Number,
responseStart: t.Number,
};
const Browser = {
targets: ['browser'],
types: browserTypes,
events: {
'attachedToTarget': {
sessionId: t.String,
targetInfo: browserTypes.TargetInfo,
},
'detachedFromTarget': {
sessionId: t.String,
targetId: t.String,
},
'downloadCreated': {
uuid: t.String,
browserContextId: t.Optional(t.String),
pageTargetId: t.String,
url: t.String,
suggestedFileName: t.String,
},
'downloadFinished': {
uuid: t.String,
canceled: t.Optional(t.Boolean),
error: t.Optional(t.String),
},
'screencastFinished': {
screencastId: t.String,
},
},
methods: {
'enable': {
params: {
attachToDefaultContext: t.Boolean,
},
},
'createBrowserContext': {
params: {
removeOnDetach: t.Optional(t.Boolean),
},
returns: {
browserContextId: t.String,
},
},
'removeBrowserContext': {
params: {
browserContextId: t.String,
},
},
'newPage': {
params: {
browserContextId: t.Optional(t.String),
},
returns: {
targetId: t.String,
}
},
'close': {},
'getInfo': {
returns: {
userAgent: t.String,
version: t.String,
},
},
'setExtraHTTPHeaders': {
params: {
browserContextId: t.Optional(t.String),
headers: t.Array(networkTypes.HTTPHeader),
},
},
'setBrowserProxy': {
params: {
type: t.Enum(['http', 'https', 'socks', 'socks4']),
bypass: t.Array(t.String),
host: t.String,
port: t.Number,
username: t.Optional(t.String),
password: t.Optional(t.String),
},
},
'setContextProxy': {
params: {
browserContextId: t.Optional(t.String),
type: t.Enum(['http', 'https', 'socks', 'socks4']),
bypass: t.Array(t.String),
host: t.String,
port: t.Number,
username: t.Optional(t.String),
password: t.Optional(t.String),
},
},
'setHTTPCredentials': {
params: {
browserContextId: t.Optional(t.String),
credentials: t.Nullable(networkTypes.HTTPCredentials),
},
},
'setRequestInterception': {
params: {
browserContextId: t.Optional(t.String),
enabled: t.Boolean,
},
},
'setGeolocationOverride': {
params: {
browserContextId: t.Optional(t.String),
geolocation: t.Nullable(browserTypes.Geolocation),
}
},
'setUserAgentOverride': {
params: {
browserContextId: t.Optional(t.String),
userAgent: t.Nullable(t.String),
}
},
'setBypassCSP': {
params: {
browserContextId: t.Optional(t.String),
bypassCSP: t.Nullable(t.Boolean),
}
},
'setIgnoreHTTPSErrors': {
params: {
browserContextId: t.Optional(t.String),
ignoreHTTPSErrors: t.Nullable(t.Boolean),
}
},
'setJavaScriptDisabled': {
params: {
browserContextId: t.Optional(t.String),
javaScriptDisabled: t.Nullable(t.Boolean),
}
},
'setLocaleOverride': {
params: {
browserContextId: t.Optional(t.String),
locale: t.Nullable(t.String),
}
},
'setTimezoneOverride': {
params: {
browserContextId: t.Optional(t.String),
timezoneId: t.Nullable(t.String),
}
},
'setDownloadOptions': {
params: {
browserContextId: t.Optional(t.String),
downloadOptions: t.Nullable(browserTypes.DownloadOptions),
}
},
'setTouchOverride': {
params: {
browserContextId: t.Optional(t.String),
hasTouch: t.Nullable(t.Boolean),
}
},
'setDefaultViewport': {
params: {
browserContextId: t.Optional(t.String),
viewport: t.Nullable(pageTypes.Viewport),
}
},
'addScriptToEvaluateOnNewDocument': {
params: {
browserContextId: t.Optional(t.String),
script: t.String,
}
},
'addBinding': {
params: {
browserContextId: t.Optional(t.String),
name: t.String,
script: t.String,
},
},
'grantPermissions': {
params: {
origin: t.String,
browserContextId: t.Optional(t.String),
permissions: t.Array(t.String),
},
},
'resetPermissions': {
params: {
browserContextId: t.Optional(t.String),
}
},
'setCookies': {
params: {
browserContextId: t.Optional(t.String),
cookies: t.Array(browserTypes.CookieOptions),
}
},
'clearCookies': {
params: {
browserContextId: t.Optional(t.String),
}
},
'getCookies': {
params: {
browserContextId: t.Optional(t.String)
},
returns: {
cookies: t.Array(browserTypes.Cookie),
},
},
'setOnlineOverride': {
params: {
browserContextId: t.Optional(t.String),
override: t.Nullable(t.Enum(['online', 'offline'])),
}
},
'setColorScheme': {
params: {
browserContextId: t.Optional(t.String),
colorScheme: t.Nullable(t.Enum(['dark', 'light', 'no-preference'])),
},
},
'setScreencastOptions': {
params: {
browserContextId: t.Optional(t.String),
dir: t.String,
width: t.Number,
height: t.Number,
scale: t.Optional(t.Number),
},
},
},
};
const Network = {
targets: ['page'],
types: networkTypes,
events: {
'requestWillBeSent': {
// frameId may be absent for redirected requests.
frameId: t.Optional(t.String),
requestId: t.String,
// RequestID of redirected request.
redirectedFrom: t.Optional(t.String),
postData: t.Optional(t.String),
headers: t.Array(networkTypes.HTTPHeader),
isIntercepted: t.Boolean,
url: t.String,
method: t.String,
navigationId: t.Optional(t.String),
cause: t.String,
internalCause: t.String,
},
'responseReceived': {
securityDetails: t.Nullable(networkTypes.SecurityDetails),
requestId: t.String,
fromCache: t.Boolean,
remoteIPAddress: t.Optional(t.String),
remotePort: t.Optional(t.Number),
status: t.Number,
statusText: t.String,
headers: t.Array(networkTypes.HTTPHeader),
timing: networkTypes.ResourceTiming,
},
'requestFinished': {
requestId: t.String,
responseEndTime: t.Number,
},
'requestFailed': {
requestId: t.String,
errorCode: t.String,
},
},
methods: {
'setRequestInterception': {
params: {
enabled: t.Boolean,
},
},
'setExtraHTTPHeaders': {
params: {
headers: t.Array(networkTypes.HTTPHeader),
},
},
'abortInterceptedRequest': {
params: {
requestId: t.String,
errorCode: t.String,
},
},
'resumeInterceptedRequest': {
params: {
requestId: t.String,
url: t.Optional(t.String),
method: t.Optional(t.String),
headers: t.Optional(t.Array(networkTypes.HTTPHeader)),
postData: t.Optional(t.String),
},
},
'fulfillInterceptedRequest': {
params: {
requestId: t.String,
status: t.Number,
statusText: t.String,
headers: t.Array(networkTypes.HTTPHeader),
base64body: t.Optional(t.String), // base64-encoded
},
},
'getResponseBody': {
params: {
requestId: t.String,
},
returns: {
base64body: t.String,
evicted: t.Optional(t.Boolean),
},
},
},
};
const Runtime = {
targets: ['page'],
types: runtimeTypes,
events: {
'executionContextCreated': {
executionContextId: t.String,
auxData: t.Any,
},
'executionContextDestroyed': {
executionContextId: t.String,
},
'console': {
executionContextId: t.String,
args: t.Array(runtimeTypes.RemoteObject),
type: t.String,
location: runtimeTypes.ScriptLocation,
},
},
methods: {
'evaluate': {
params: {
// Pass frameId here.
executionContextId: t.String,
expression: t.String,
returnByValue: t.Optional(t.Boolean),
},
returns: {
result: t.Optional(runtimeTypes.RemoteObject),
exceptionDetails: t.Optional(runtimeTypes.ExceptionDetails),
}
},
'callFunction': {
params: {
// Pass frameId here.
executionContextId: t.String,
functionDeclaration: t.String,
returnByValue: t.Optional(t.Boolean),
args: t.Array(runtimeTypes.CallFunctionArgument),
},
returns: {
result: t.Optional(runtimeTypes.RemoteObject),
exceptionDetails: t.Optional(runtimeTypes.ExceptionDetails),
}
},
'disposeObject': {
params: {
executionContextId: t.String,
objectId: t.String,
},
},
'getObjectProperties': {
params: {
executionContextId: t.String,
objectId: t.String,
},
returns: {
properties: t.Array(runtimeTypes.ObjectProperty),
}
},
},
};
const Page = {
targets: ['page'],
types: pageTypes,
events: {
'ready': {
},
'crashed': {
},
'eventFired': {
frameId: t.String,
name: t.Enum(['load', 'DOMContentLoaded']),
},
'uncaughtError': {
frameId: t.String,
message: t.String,
stack: t.String,
},
'frameAttached': {
frameId: t.String,
parentFrameId: t.Optional(t.String),
},
'frameDetached': {
frameId: t.String,
},
'navigationStarted': {
frameId: t.String,
navigationId: t.String,
url: t.String,
},
'navigationCommitted': {
frameId: t.String,
// |navigationId| can only be null in response to enable.
navigationId: t.Optional(t.String),
url: t.String,
// frame.id or frame.name
name: t.String,
},
'navigationAborted': {
frameId: t.String,
navigationId: t.String,
errorText: t.String,
},
'sameDocumentNavigation': {
frameId: t.String,
url: t.String,
},
'dialogOpened': {
dialogId: t.String,
type: t.Enum(['prompt', 'alert', 'confirm', 'beforeunload']),
message: t.String,
defaultValue: t.Optional(t.String),
},
'dialogClosed': {
dialogId: t.String,
},
'bindingCalled': {
executionContextId: t.String,
name: t.String,
payload: t.Any,
},
'linkClicked': {
phase: t.Enum(['before', 'after']),
},
'willOpenNewWindowAsynchronously': {},
'fileChooserOpened': {
executionContextId: t.String,
element: runtimeTypes.RemoteObject
},
'workerCreated': {
workerId: t.String,
frameId: t.String,
url: t.String,
},
'workerDestroyed': {
workerId: t.String,
},
'dispatchMessageFromWorker': {
workerId: t.String,
message: t.String,
},
'screencastStarted': {
screencastId: t.String,
file: t.String,
},
'webSocketCreated': {
frameId: t.String,
wsid: t.String,
requestURL: t.String,
},
'webSocketOpened': {
frameId: t.String,
requestId: t.String,
wsid: t.String,
effectiveURL: t.String,
},
'webSocketClosed': {
frameId: t.String,
wsid: t.String,
error: t.String,
},
'webSocketFrameSent': {
frameId: t.String,
wsid: t.String,
opcode: t.Number,
data: t.String,
},
'webSocketFrameReceived': {
frameId: t.String,
wsid: t.String,
opcode: t.Number,
data: t.String,
},
},
methods: {
'close': {
params: {
runBeforeUnload: t.Optional(t.Boolean),
},
},
'setFileInputFiles': {
params: {
frameId: t.String,
objectId: t.String,
files: t.Array(t.String),
},
},
'addBinding': {
params: {
name: t.String,
script: t.String,
},
},
'setViewportSize': {
params: {
viewportSize: t.Nullable(pageTypes.Size),
},
},
'bringToFront': {
params: {
},
},
'setEmulatedMedia': {
params: {
type: t.Optional(t.Enum(['screen', 'print', ''])),
colorScheme: t.Optional(t.Enum(['dark', 'light', 'no-preference'])),
},
},
'setCacheDisabled': {
params: {
cacheDisabled: t.Boolean,
},
},
'describeNode': {
params: {
frameId: t.String,
objectId: t.String,
},
returns: {
contentFrameId: t.Optional(t.String),
ownerFrameId: t.Optional(t.String),
},
},
'scrollIntoViewIfNeeded': {
params: {
frameId: t.String,
objectId: t.String,
rect: t.Optional(pageTypes.Rect),
},
},
'addScriptToEvaluateOnNewDocument': {
params: {
script: t.String,
worldName: t.Optional(t.String),
},
returns: {
scriptId: t.String,
}
},
'removeScriptToEvaluateOnNewDocument': {
params: {
scriptId: t.String,
},
},
'navigate': {
params: {
frameId: t.String,
url: t.String,
referer: t.Optional(t.String),
},
returns: {
navigationId: t.Nullable(t.String),
navigationURL: t.Nullable(t.String),
}
},
'goBack': {
params: {
frameId: t.String,
},
returns: {
success: t.Boolean,
},
},
'goForward': {
params: {
frameId: t.String,
},
returns: {
success: t.Boolean,
},
},
'reload': {
params: {
frameId: t.String,
},
},
'adoptNode': {
params: {
frameId: t.String,
objectId: t.String,
executionContextId: t.String,
},
returns: {
remoteObject: t.Nullable(runtimeTypes.RemoteObject),
},
},
'screenshot': {
params: {
mimeType: t.Enum(['image/png', 'image/jpeg']),
clip: t.Optional(pageTypes.Clip),
omitDeviceScaleFactor: t.Optional(t.Boolean),
},
returns: {
data: t.String,
}
},
'getContentQuads': {
params: {
frameId: t.String,
objectId: t.String,
},
returns: {
quads: t.Array(pageTypes.DOMQuad),
},
},
'dispatchKeyEvent': {
params: {
type: t.String,
key: t.String,
keyCode: t.Number,
location: t.Number,
code: t.String,
repeat: t.Boolean,
text: t.Optional(t.String),
}
},
'dispatchTouchEvent': {
params: {
type: t.Enum(['touchStart', 'touchEnd', 'touchMove', 'touchCancel']),
touchPoints: t.Array(pageTypes.TouchPoint),
modifiers: t.Number,
},
returns: {
defaultPrevented: t.Boolean,
}
},
'dispatchTapEvent': {
params: {
x: t.Number,
y: t.Number,
modifiers: t.Number,
}
},
'dispatchMouseEvent': {
params: {
type: t.String,
button: t.Number,
x: t.Number,
y: t.Number,
modifiers: t.Number,
clickCount: t.Optional(t.Number),
buttons: t.Number,
}
},
'insertText': {
params: {
text: t.String,
}
},
'crash': {
params: {}
},
'handleDialog': {
params: {
dialogId: t.String,
accept: t.Boolean,
promptText: t.Optional(t.String),
},
},
'setInterceptFileChooserDialog': {
params: {
enabled: t.Boolean,
},
},
'sendMessageToWorker': {
params: {
frameId: t.String,
workerId: t.String,
message: t.String,
},
},
'startVideoRecording': {
params: {
file: t.String,
width: t.Number,
height: t.Number,
scale: t.Optional(t.Number),
},
},
'stopVideoRecording': {
},
},
};
const Accessibility = {
targets: ['page'],
types: axTypes,
events: {},
methods: {
'getFullAXTree': {
params: {
objectId: t.Optional(t.String),
},
returns: {
tree: axTypes.AXTree
},
}
}
}
this.protocol = {
domains: {Browser, Page, Runtime, Network, Accessibility},
};
this.checkScheme = checkScheme;
this.EXPORTED_SYMBOLS = ['protocol', 'checkScheme'];

View File

@ -0,0 +1,113 @@
/* 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 "HeadlessWindowCapturer.h"
#include "api/video/i420_buffer.h"
#include "HeadlessWidget.h"
#include "libyuv.h"
#include "mozilla/EndianUtils.h"
#include "mozilla/gfx/DataSurfaceHelpers.h"
#include "rtc_base/refcountedobject.h"
#include "rtc_base/scoped_ref_ptr.h"
using namespace mozilla::widget;
using namespace webrtc;
namespace mozilla {
rtc::scoped_refptr<webrtc::VideoCaptureModule> HeadlessWindowCapturer::Create(HeadlessWidget* headlessWindow) {
return new rtc::RefCountedObject<HeadlessWindowCapturer>(headlessWindow);
}
HeadlessWindowCapturer::HeadlessWindowCapturer(mozilla::widget::HeadlessWidget* window)
: mWindow(window) {
}
HeadlessWindowCapturer::~HeadlessWindowCapturer() {
StopCapture();
}
void HeadlessWindowCapturer::RegisterCaptureDataCallback(rtc::VideoSinkInterface<webrtc::VideoFrame>* dataCallback) {
rtc::CritScope lock2(&_callBackCs);
_dataCallBacks.insert(dataCallback);
}
void HeadlessWindowCapturer::DeRegisterCaptureDataCallback(rtc::VideoSinkInterface<webrtc::VideoFrame>* dataCallback) {
rtc::CritScope lock2(&_callBackCs);
auto it = _dataCallBacks.find(dataCallback);
if (it != _dataCallBacks.end()) {
_dataCallBacks.erase(it);
}
}
void HeadlessWindowCapturer::NotifyFrameCaptured(const webrtc::VideoFrame& frame) {
rtc::CritScope lock2(&_callBackCs);
for (auto dataCallBack : _dataCallBacks)
dataCallBack->OnFrame(frame);
}
int32_t HeadlessWindowCapturer::StopCaptureIfAllClientsClose() {
if (_dataCallBacks.empty()) {
return StopCapture();
} else {
return 0;
}
}
int32_t HeadlessWindowCapturer::StartCapture(const VideoCaptureCapability& capability) {
mWindow->SetSnapshotListener([this] (RefPtr<gfx::DataSourceSurface>&& dataSurface){
if (!NS_IsInCompositorThread()) {
fprintf(stderr, "SnapshotListener is called not on the Compositor thread!\n");
return;
}
if (dataSurface->GetFormat() != gfx::SurfaceFormat::B8G8R8A8) {
fprintf(stderr, "Uexpected snapshot surface format: %hhd\n", dataSurface->GetFormat());
return;
}
int width = dataSurface->GetSize().width;
int height = dataSurface->GetSize().height;
rtc::scoped_refptr<I420Buffer> buffer = I420Buffer::Create(width, height);
gfx::DataSourceSurface::ScopedMap map(dataSurface.get(), gfx::DataSourceSurface::MapType::READ);
if (!map.IsMapped()) {
fprintf(stderr, "Failed to map snapshot bytes!\n");
return;
}
#if MOZ_LITTLE_ENDIAN()
const int conversionResult = libyuv::ARGBToI420(
#else
const int conversionResult = libyuv::BGRAToI420(
#endif
map.GetData(), map.GetStride(),
buffer->MutableDataY(), buffer->StrideY(),
buffer->MutableDataU(), buffer->StrideU(),
buffer->MutableDataV(), buffer->StrideV(),
width, height);
if (conversionResult != 0) {
fprintf(stderr, "Failed to convert capture frame to I420: %d\n", conversionResult);
return;
}
VideoFrame captureFrame(buffer, 0, rtc::TimeMillis(), kVideoRotation_0);
NotifyFrameCaptured(captureFrame);
});
return 0;
}
int32_t HeadlessWindowCapturer::StopCapture() {
if (!CaptureStarted())
return 0;
mWindow->SetSnapshotListener(nullptr);
return 0;
}
bool HeadlessWindowCapturer::CaptureStarted() {
return true;
}
} // namespace mozilla

View File

@ -0,0 +1,59 @@
/* 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/. */
#pragma once
#include <memory>
#include <set>
#include "api/video/video_frame.h"
#include "media/base/videosinkinterface.h"
#include "modules/video_capture/video_capture.h"
#include "rtc_base/criticalsection.h"
class nsIWidget;
namespace mozilla {
namespace widget {
class HeadlessWidget;
}
class HeadlessWindowCapturer : public webrtc::VideoCaptureModule {
public:
static rtc::scoped_refptr<webrtc::VideoCaptureModule> Create(mozilla::widget::HeadlessWidget*);
void RegisterCaptureDataCallback(
rtc::VideoSinkInterface<webrtc::VideoFrame>* dataCallback) override;
void DeRegisterCaptureDataCallback(
rtc::VideoSinkInterface<webrtc::VideoFrame>* dataCallback) override;
int32_t StopCaptureIfAllClientsClose() override;
int32_t SetCaptureRotation(webrtc::VideoRotation) override { return -1; }
bool SetApplyRotation(bool) override { return false; }
bool GetApplyRotation() override { return true; }
const char* CurrentDeviceName() const override { return "Headless window"; }
// Platform dependent
int32_t StartCapture(const webrtc::VideoCaptureCapability& capability) override;
bool FocusOnSelectedSource() override { return false; }
int32_t StopCapture() override;
bool CaptureStarted() override;
int32_t CaptureSettings(webrtc::VideoCaptureCapability& settings) override {
return -1;
}
protected:
HeadlessWindowCapturer(mozilla::widget::HeadlessWidget*);
~HeadlessWindowCapturer() override;
private:
void NotifyFrameCaptured(const webrtc::VideoFrame& frame);
mozilla::widget::HeadlessWidget* mWindow = nullptr;
rtc::CriticalSection _callBackCs;
std::set<rtc::VideoSinkInterface<webrtc::VideoFrame>*> _dataCallBacks;
};
} // namespace mozilla

View File

@ -0,0 +1,370 @@
/*
* Copyright (c) 2010, The WebM Project authors. All rights reserved.
* Copyright (c) 2013 The Chromium Authors. All rights reserved.
* Copyright (C) 2020 Microsoft Corporation.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/* 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 "ScreencastEncoder.h"
#include <algorithm>
#include <libyuv.h>
#include <vpx/vp8.h>
#include <vpx/vp8cx.h>
#include <vpx/vpx_encoder.h>
#include "nsIThread.h"
#include "nsThreadUtils.h"
#include "WebMFileWriter.h"
#include "webrtc/api/video/video_frame.h"
namespace mozilla {
namespace {
// Number of timebase unints per one frame.
constexpr int timeScale = 1000;
// Defines the dimension of a macro block. This is used to compute the active
// map for the encoder.
const int kMacroBlockSize = 16;
void createImage(unsigned int width, unsigned int height,
std::unique_ptr<vpx_image_t>& out_image,
std::unique_ptr<uint8_t[]>& out_image_buffer,
int& out_buffer_size) {
std::unique_ptr<vpx_image_t> image(new vpx_image_t());
memset(image.get(), 0, sizeof(vpx_image_t));
// libvpx seems to require both to be assigned.
image->d_w = width;
image->w = width;
image->d_h = height;
image->h = height;
// I420
image->fmt = VPX_IMG_FMT_YV12;
image->x_chroma_shift = 1;
image->y_chroma_shift = 1;
// libyuv's fast-path requires 16-byte aligned pointers and strides, so pad
// the Y, U and V planes' strides to multiples of 16 bytes.
const int y_stride = ((image->w - 1) & ~15) + 16;
const int uv_unaligned_stride = y_stride >> image->x_chroma_shift;
const int uv_stride = ((uv_unaligned_stride - 1) & ~15) + 16;
// libvpx accesses the source image in macro blocks, and will over-read
// if the image is not padded out to the next macroblock: crbug.com/119633.
// Pad the Y, U and V planes' height out to compensate.
// Assuming macroblocks are 16x16, aligning the planes' strides above also
// macroblock aligned them.
static_assert(kMacroBlockSize == 16, "macroblock_size_not_16");
const int y_rows = ((image->h - 1) & ~(kMacroBlockSize-1)) + kMacroBlockSize;
const int uv_rows = y_rows >> image->y_chroma_shift;
// Allocate a YUV buffer large enough for the aligned data & padding.
out_buffer_size = y_stride * y_rows + 2*uv_stride * uv_rows;
std::unique_ptr<uint8_t[]> image_buffer(new uint8_t[out_buffer_size]);
// Reset image value to 128 so we just need to fill in the y plane.
memset(image_buffer.get(), 128, out_buffer_size);
// Fill in the information for |image_|.
unsigned char* uchar_buffer =
reinterpret_cast<unsigned char*>(image_buffer.get());
image->planes[0] = uchar_buffer;
image->planes[1] = image->planes[0] + y_stride * y_rows;
image->planes[2] = image->planes[1] + uv_stride * uv_rows;
image->stride[0] = y_stride;
image->stride[1] = uv_stride;
image->stride[2] = uv_stride;
out_image = std::move(image);
out_image_buffer = std::move(image_buffer);
}
} // namespace
class ScreencastEncoder::VPXFrame {
public:
VPXFrame(rtc::scoped_refptr<webrtc::VideoFrameBuffer>&& buffer, Maybe<double> scale, const gfx::IntMargin& margin)
: m_frameBuffer(std::move(buffer))
, m_scale(scale)
, m_margin(margin)
{ }
void setDuration(TimeDuration duration) { m_duration = duration; }
TimeDuration duration() const { return m_duration; }
void convertToVpxImage(vpx_image_t* image)
{
if (m_frameBuffer->type() != webrtc::VideoFrameBuffer::Type::kI420) {
fprintf(stderr, "convertToVpxImage unexpected frame buffer type: %d\n", m_frameBuffer->type());
return;
}
auto src = m_frameBuffer->GetI420();
const int y_stride = image->stride[VPX_PLANE_Y];
MOZ_ASSERT(image->stride[VPX_PLANE_U] == image->stride[VPX_PLANE_V]);
const int uv_stride = image->stride[1];
uint8_t* y_data = image->planes[VPX_PLANE_Y];
uint8_t* u_data = image->planes[VPX_PLANE_U];
uint8_t* v_data = image->planes[VPX_PLANE_V];
double src_width = src->width() - m_margin.LeftRight();
double src_height = src->height() - m_margin.top;
if (m_scale || (src_width > image->w || src_height > image->h)) {
double scale = m_scale ? m_scale.value() : std::min(image->w / src_width, image->h / src_height);
double dst_width = src_width * scale;
if (dst_width > image->w) {
src_width *= image->w / dst_width;
dst_width = image->w;
}
double dst_height = src_height * scale;
if (dst_height > image->h) {
src_height *= image->h / dst_height;
dst_height = image->h;
}
libyuv::I420Scale(src->DataY() + m_margin.top * src->StrideY() + m_margin.left, src->StrideY(),
src->DataU() + (m_margin.top * src->StrideU() + m_margin.left) / 2, src->StrideU(),
src->DataV() + (m_margin.top * src->StrideV() + m_margin.left) / 2, src->StrideV(),
src_width, src_height,
y_data, y_stride,
u_data, uv_stride,
v_data, uv_stride,
dst_width, dst_height,
libyuv::kFilterBilinear);
} else {
int width = std::min<int>(image->w, src_width);
int height = std::min<int>(image->h, src_height);
libyuv::I420Copy(src->DataY() + m_margin.top * src->StrideY() + m_margin.left, src->StrideY(),
src->DataU() + (m_margin.top * src->StrideU() + m_margin.left) / 2, src->StrideU(),
src->DataV() + (m_margin.top * src->StrideV() + m_margin.left) / 2, src->StrideV(),
y_data, y_stride,
u_data, uv_stride,
v_data, uv_stride,
width, height);
}
}
private:
rtc::scoped_refptr<webrtc::VideoFrameBuffer> m_frameBuffer;
Maybe<double> m_scale;
gfx::IntMargin m_margin;
TimeDuration m_duration;
};
class ScreencastEncoder::VPXCodec {
public:
VPXCodec(vpx_codec_ctx_t codec, vpx_codec_enc_cfg_t cfg, FILE* file)
: m_codec(codec)
, m_cfg(cfg)
, m_file(file)
, m_writer(new WebMFileWriter(file, &m_cfg))
{
nsresult rv = NS_NewNamedThread("Screencast enc", getter_AddRefs(m_encoderQueue));
if (rv != NS_OK) {
fprintf(stderr, "ScreencastEncoder::VPXCodec failed to spawn thread %d\n", rv);
return;
}
createImage(cfg.g_w, cfg.g_h, m_image, m_imageBuffer, m_imageBufferSize);
}
~VPXCodec() {
m_encoderQueue->Shutdown();
m_encoderQueue = nullptr;
}
void encodeFrameAsync(std::unique_ptr<VPXFrame>&& frame)
{
m_encoderQueue->Dispatch(NS_NewRunnableFunction("VPXCodec::encodeFrameAsync", [this, frame = std::move(frame)] {
memset(m_imageBuffer.get(), 128, m_imageBufferSize);
frame->convertToVpxImage(m_image.get());
double frameCount = frame->duration().ToSeconds() * fps;
// For long duration repeat frame at 1 fps to ensure last frame duration is short enough.
// TODO: figure out why simply passing duration doesn't work well.
for (;frameCount > 1.5; frameCount -= 1) {
encodeFrame(m_image.get(), timeScale);
}
encodeFrame(m_image.get(), std::max<int>(1, frameCount * timeScale));
}));
}
void finishAsync(std::function<void()>&& callback)
{
m_encoderQueue->Dispatch(NS_NewRunnableFunction("VPXCodec::finishAsync", [this, callback = std::move(callback)] {
finish();
callback();
}));
}
private:
bool encodeFrame(vpx_image_t *img, int duration)
{
vpx_codec_iter_t iter = nullptr;
const vpx_codec_cx_pkt_t *pkt = nullptr;
int flags = 0;
const vpx_codec_err_t res = vpx_codec_encode(&m_codec, img, m_pts, duration, flags, VPX_DL_REALTIME);
if (res != VPX_CODEC_OK) {
fprintf(stderr, "Failed to encode frame: %s\n", vpx_codec_error(&m_codec));
return false;
}
bool gotPkts = false;
while ((pkt = vpx_codec_get_cx_data(&m_codec, &iter)) != nullptr) {
gotPkts = true;
if (pkt->kind == VPX_CODEC_CX_FRAME_PKT) {
m_writer->writeFrame(pkt);
++m_frameCount;
// fprintf(stderr, " #%03d %spts=%" PRId64 " sz=%zd\n", m_frameCount, (pkt->data.frame.flags & VPX_FRAME_IS_KEY) != 0 ? "[K] " : "", pkt->data.frame.pts, pkt->data.frame.sz);
m_pts += pkt->data.frame.duration;
}
}
return gotPkts;
}
void finish()
{
// Flush encoder.
while (encodeFrame(nullptr, 1))
++m_frameCount;
m_writer->finish();
fclose(m_file);
// fprintf(stderr, "ScreencastEncoder::finish %d frames\n", m_frameCount);
}
RefPtr<nsIThread> m_encoderQueue;
vpx_codec_ctx_t m_codec;
vpx_codec_enc_cfg_t m_cfg;
FILE* m_file { nullptr };
std::unique_ptr<WebMFileWriter> m_writer;
int m_frameCount { 0 };
int64_t m_pts { 0 };
std::unique_ptr<uint8_t[]> m_imageBuffer;
int m_imageBufferSize { 0 };
std::unique_ptr<vpx_image_t> m_image;
};
ScreencastEncoder::ScreencastEncoder(std::unique_ptr<VPXCodec>&& vpxCodec, Maybe<double> scale, const gfx::IntMargin& margin)
: m_vpxCodec(std::move(vpxCodec))
, m_scale(scale)
, m_margin(margin)
{
}
ScreencastEncoder::~ScreencastEncoder()
{
}
RefPtr<ScreencastEncoder> ScreencastEncoder::create(nsCString& errorString, const nsCString& filePath, int width, int height, Maybe<double> scale, const gfx::IntMargin& margin)
{
vpx_codec_iface_t* codec_interface = vpx_codec_vp8_cx();
if (!codec_interface) {
errorString = "Codec not found.";
return nullptr;
}
if (width <= 0 || height <= 0 || (width % 2) != 0 || (height % 2) != 0) {
errorString.AppendPrintf("Invalid frame size: %dx%d", width, height);
return nullptr;
}
vpx_codec_enc_cfg_t cfg;
memset(&cfg, 0, sizeof(cfg));
vpx_codec_err_t error = vpx_codec_enc_config_default(codec_interface, &cfg, 0);
if (error) {
errorString.AppendPrintf("Failed to get default codec config: %s", vpx_codec_err_to_string(error));
return nullptr;
}
cfg.g_w = width;
cfg.g_h = height;
cfg.g_timebase.num = 1;
cfg.g_timebase.den = fps * timeScale;
cfg.g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT;
vpx_codec_ctx_t codec;
if (vpx_codec_enc_init(&codec, codec_interface, &cfg, 0)) {
errorString.AppendPrintf("Failed to initialize encoder: %s", vpx_codec_error(&codec));
return nullptr;
}
FILE* file = fopen(filePath.get(), "wb");
if (!file) {
errorString.AppendPrintf("Failed to open file '%s' for writing: %s", filePath.get(), strerror(errno));
return nullptr;
}
std::unique_ptr<VPXCodec> vpxCodec(new VPXCodec(codec, cfg, file));
// fprintf(stderr, "ScreencastEncoder initialized with: %s\n", vpx_codec_iface_name(codec_interface));
return new ScreencastEncoder(std::move(vpxCodec), scale, margin);
}
void ScreencastEncoder::flushLastFrame()
{
TimeStamp now = TimeStamp::Now();
if (m_lastFrameTimestamp) {
// If previous frame encoding failed for some rason leave the timestampt intact.
if (!m_lastFrame)
return;
m_lastFrame->setDuration(now - m_lastFrameTimestamp);
m_vpxCodec->encodeFrameAsync(std::move(m_lastFrame));
}
m_lastFrameTimestamp = now;
}
void ScreencastEncoder::encodeFrame(const webrtc::VideoFrame& videoFrame)
{
// fprintf(stderr, "ScreencastEncoder::encodeFrame\n");
flushLastFrame();
m_lastFrame = std::make_unique<VPXFrame>(videoFrame.video_frame_buffer(), m_scale, m_margin);
}
void ScreencastEncoder::finish(std::function<void()>&& callback)
{
if (!m_vpxCodec) {
callback();
return;
}
flushLastFrame();
m_vpxCodec->finishAsync([callback = std::move(callback)] () mutable {
NS_DispatchToMainThread(NS_NewRunnableFunction("ScreencastEncoder::finish callback", std::move(callback)));
});
}
} // namespace mozilla

View File

@ -0,0 +1,48 @@
/* 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/. */
#pragma once
#include <functional>
#include <memory>
#include "mozilla/gfx/Rect.h"
#include "mozilla/Maybe.h"
#include "mozilla/TimeStamp.h"
#include "nsISupportsImpl.h"
#include "nsStringFwd.h"
namespace webrtc {
class VideoFrame;
}
namespace mozilla {
class ScreencastEncoder {
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ScreencastEncoder)
public:
static constexpr int fps = 25;
static RefPtr<ScreencastEncoder> create(nsCString& errorString, const nsCString& filePath, int width, int height, Maybe<double> scale, const gfx::IntMargin& margin);
class VPXCodec;
ScreencastEncoder(std::unique_ptr<VPXCodec>&&, Maybe<double> scale, const gfx::IntMargin& margin);
void encodeFrame(const webrtc::VideoFrame& videoFrame);
void finish(std::function<void()>&& callback);
private:
~ScreencastEncoder();
void flushLastFrame();
std::unique_ptr<VPXCodec> m_vpxCodec;
Maybe<double> m_scale;
gfx::IntMargin m_margin;
TimeStamp m_lastFrameTimestamp;
class VPXFrame;
std::unique_ptr<VPXFrame> m_lastFrame;
};
} // namespace mozilla

View File

@ -0,0 +1,50 @@
/*
* Copyright (c) 2014 The WebM project authors. All Rights Reserved.
*/
/* 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 "WebMFileWriter.h"
#include <string>
#include "mkvmuxer/mkvmuxerutil.h"
namespace mozilla {
WebMFileWriter::WebMFileWriter(FILE* file, vpx_codec_enc_cfg_t* cfg)
: m_cfg(cfg)
, m_writer(new mkvmuxer::MkvWriter(file))
, m_segment(new mkvmuxer::Segment()) {
m_segment->Init(m_writer.get());
m_segment->set_mode(mkvmuxer::Segment::kFile);
m_segment->OutputCues(true);
mkvmuxer::SegmentInfo* info = m_segment->GetSegmentInfo();
std::string version = "Playwright " + std::string(vpx_codec_version_str());
info->set_writing_app(version.c_str());
// Add vp8 track.
m_videoTrackId = m_segment->AddVideoTrack(
static_cast<int>(m_cfg->g_w), static_cast<int>(m_cfg->g_h), 0);
if (!m_videoTrackId) {
fprintf(stderr, "Failed to add video track\n");
}
}
WebMFileWriter::~WebMFileWriter() {}
void WebMFileWriter::writeFrame(const vpx_codec_cx_pkt_t* pkt) {
int64_t pts_ns = pkt->data.frame.pts * 1000000000ll * m_cfg->g_timebase.num /
m_cfg->g_timebase.den;
m_segment->AddFrame(static_cast<uint8_t*>(pkt->data.frame.buf),
pkt->data.frame.sz, m_videoTrackId, pts_ns,
pkt->data.frame.flags & VPX_FRAME_IS_KEY);
}
void WebMFileWriter::finish() {
m_segment->Finalize();
}
} // namespace mozilla

View File

@ -0,0 +1,32 @@
/* 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/. */
#pragma once
#include <memory>
#include <stdio.h>
#include <stdlib.h>
#include "vpx/vpx_encoder.h"
#include "mkvmuxer/mkvmuxer.h"
#include "mkvmuxer/mkvwriter.h"
namespace mozilla {
class WebMFileWriter {
public:
WebMFileWriter(FILE*, vpx_codec_enc_cfg_t* cfg);
~WebMFileWriter();
void writeFrame(const vpx_codec_cx_pkt_t* pkt);
void finish();
private:
vpx_codec_enc_cfg_t* m_cfg = nullptr;
std::unique_ptr<mkvmuxer::MkvWriter> m_writer;
std::unique_ptr<mkvmuxer::Segment> m_segment;
uint64_t m_videoTrackId = 0;
};
} // namespace mozilla

View File

@ -0,0 +1,15 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
Classes = [
{
'cid': '{d8c4d9e0-9462-445e-9e43-68d3872ad1de}',
'contract_ids': ['@mozilla.org/juggler/screencast;1'],
'type': 'nsIScreencastService',
'constructor': 'mozilla::nsScreencastService::GetSingleton',
'headers': ['/juggler/screencast/nsScreencastService.h'],
},
]

View File

@ -0,0 +1,49 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
XPIDL_SOURCES += [
'nsIScreencastService.idl',
]
XPIDL_MODULE = 'jugglerscreencast'
SOURCES += [
'HeadlessWindowCapturer.cpp',
'nsScreencastService.cpp',
'ScreencastEncoder.cpp',
]
XPCOM_MANIFESTS += [
'components.conf',
]
LOCAL_INCLUDES += [
'/dom/media/systemservices',
'/media/libyuv/libyuv/include',
'/third_party/libwebrtc',
'/third_party/libwebrtc/webrtc',
]
LOCAL_INCLUDES += [
'/widget',
'/widget/headless',
]
LOCAL_INCLUDES += [
'/third_party/aom/third_party/libwebm',
]
SOURCES += [
'/third_party/aom/third_party/libwebm/mkvmuxer/mkvmuxer.cc',
'/third_party/aom/third_party/libwebm/mkvmuxer/mkvmuxerutil.cc',
'/third_party/aom/third_party/libwebm/mkvmuxer/mkvwriter.cc',
'WebMFileWriter.cpp',
]
include('/dom/media/webrtc/third_party_build/webrtc.mozbuild')
include('/ipc/chromium/chromium-config.mozbuild')
FINAL_LIBRARY = 'xul'

View File

@ -0,0 +1,21 @@
/* 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 "nsISupports.idl"
interface nsIDocShell;
/**
* Service for recording window video.
*/
[scriptable, uuid(d8c4d9e0-9462-445e-9e43-68d3872ad1de)]
interface nsIScreencastService : nsISupports
{
AString startVideoRecording(in nsIDocShell docShell, in ACString fileName, in uint32_t width, in uint32_t height, in double scale, in int32_t offset_top);
/**
* Will emit 'juggler-screencast-stopped' when the video file is saved.
*/
void stopVideoRecording(in AString sessionId);
};

View File

@ -0,0 +1,213 @@
/* 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 "nsScreencastService.h"
#include "ScreencastEncoder.h"
#include "HeadlessWidget.h"
#include "HeadlessWindowCapturer.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/PresShell.h"
#include "mozilla/StaticPtr.h"
#include "nsIDocShell.h"
#include "nsIObserverService.h"
#include "nsIRandomGenerator.h"
#include "nsISupportsPrimitives.h"
#include "nsThreadManager.h"
#include "nsView.h"
#include "nsViewManager.h"
#include "webrtc/modules/desktop_capture/desktop_capturer.h"
#include "webrtc/modules/desktop_capture/desktop_capture_options.h"
#include "webrtc/modules/desktop_capture/desktop_device_info.h"
#include "webrtc/modules/desktop_capture/desktop_frame.h"
#include "webrtc/modules/video_capture/video_capture.h"
#include "mozilla/widget/PlatformWidgetTypes.h"
#include "video_engine/desktop_capture_impl.h"
using namespace mozilla::widget;
namespace mozilla {
NS_IMPL_ISUPPORTS(nsScreencastService, nsIScreencastService)
namespace {
StaticRefPtr<nsScreencastService> gScreencastService;
rtc::scoped_refptr<webrtc::VideoCaptureModule> CreateWindowCapturer(nsIWidget* widget) {
if (gfxPlatform::IsHeadless()) {
HeadlessWidget* headlessWidget = static_cast<HeadlessWidget*>(widget);
return HeadlessWindowCapturer::Create(headlessWidget);
}
uintptr_t rawWindowId = reinterpret_cast<uintptr_t>(widget->GetNativeData(NS_NATIVE_WINDOW_WEBRTC_DEVICE_ID));
if (!rawWindowId) {
fprintf(stderr, "Failed to get native window id\n");
return nullptr;
}
nsCString windowId;
windowId.AppendPrintf("%" PRIuPTR, rawWindowId);
bool captureCursor = false;
static int moduleId = 0;
return webrtc::DesktopCaptureImpl::Create(++moduleId, windowId.get(), webrtc::CaptureDeviceType::Window, captureCursor);
}
void NotifyScreencastStopped(const nsString& sessionId) {
nsCOMPtr<nsIObserverService> observerService = mozilla::services::GetObserverService();
if (!observerService) {
fprintf(stderr, "NotifyScreencastStopped error: no observer service\n");
return;
}
observerService->NotifyObservers(nullptr, "juggler-screencast-stopped", sessionId.get());
}
nsresult generateUid(nsString& uid) {
nsresult rv = NS_OK;
nsCOMPtr<nsIRandomGenerator> rg = do_GetService("@mozilla.org/security/random-generator;1", &rv);
NS_ENSURE_SUCCESS(rv, rv);
uint8_t* buffer;
const int kLen = 16;
rv = rg->GenerateRandomBytes(kLen, &buffer);
NS_ENSURE_SUCCESS(rv, rv);
for (int i = 0; i < kLen; i++) {
uid.AppendPrintf("%02x", buffer[i]);
}
free(buffer);
return rv;
}
}
class nsScreencastService::Session : public rtc::VideoSinkInterface<webrtc::VideoFrame> {
public:
Session(rtc::scoped_refptr<webrtc::VideoCaptureModule>&& capturer, RefPtr<ScreencastEncoder>&& encoder)
: mCaptureModule(std::move(capturer))
, mEncoder(std::move(encoder)) {
}
bool Start() {
webrtc::VideoCaptureCapability capability;
// The size is ignored in fact.
capability.width = 1280;
capability.height = 960;
capability.maxFPS = ScreencastEncoder::fps;
capability.videoType = webrtc::VideoType::kI420;
int error = mCaptureModule->StartCapture(capability);
if (error) {
fprintf(stderr, "StartCapture error %d\n", error);
return false;
}
mCaptureModule->RegisterCaptureDataCallback(this);
return true;
}
void Stop(std::function<void()>&& callback) {
mCaptureModule->DeRegisterCaptureDataCallback(this);
int error = mCaptureModule->StopCapture();
if (error) {
fprintf(stderr, "StopCapture error %d\n", error);
}
mEncoder->finish(std::move(callback));
}
// These callbacks end up running on the VideoCapture thread.
void OnFrame(const webrtc::VideoFrame& videoFrame) override {
mEncoder->encodeFrame(videoFrame);
}
private:
rtc::scoped_refptr<webrtc::VideoCaptureModule> mCaptureModule;
RefPtr<ScreencastEncoder> mEncoder;
};
// static
already_AddRefed<nsIScreencastService> nsScreencastService::GetSingleton() {
if (gScreencastService) {
return do_AddRef(gScreencastService);
}
gScreencastService = new nsScreencastService();
// ClearOnShutdown(&gScreencastService);
return do_AddRef(gScreencastService);
}
nsScreencastService::nsScreencastService() = default;
nsScreencastService::~nsScreencastService() {
}
nsresult nsScreencastService::StartVideoRecording(nsIDocShell* aDocShell, const nsACString& aFileName, uint32_t width, uint32_t height, double scale, int32_t offsetTop, nsAString& sessionId) {
MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Screencast service must be started on the Main thread.");
PresShell* presShell = aDocShell->GetPresShell();
if (!presShell)
return NS_ERROR_UNEXPECTED;
nsViewManager* viewManager = presShell->GetViewManager();
if (!viewManager)
return NS_ERROR_UNEXPECTED;
nsView* view = viewManager->GetRootView();
if (!view)
return NS_ERROR_UNEXPECTED;
nsIWidget* widget = view->GetWidget();
rtc::scoped_refptr<webrtc::VideoCaptureModule> capturer = CreateWindowCapturer(widget);
if (!capturer)
return NS_ERROR_FAILURE;
nsCString error;
Maybe<double> maybeScale;
if (scale)
maybeScale = Some(scale);
gfx::IntMargin margin;
// On GTK the bottom of the client rect is below the bounds and
// client size is actually equal to the size of the bounds so
// we don't need an adjustment.
#ifndef MOZ_WIDGET_GTK
auto bounds = widget->GetScreenBounds().ToUnknownRect();
auto clientBounds = widget->GetClientBounds().ToUnknownRect();
// Crop the image to exclude frame (if any).
margin = bounds - clientBounds;
#endif
// Crop the image to exclude controls.
margin.top += offsetTop;
RefPtr<ScreencastEncoder> encoder = ScreencastEncoder::create(error, PromiseFlatCString(aFileName), width, height, maybeScale, margin);
if (!encoder) {
fprintf(stderr, "Failed to create ScreencastEncoder: %s\n", error.get());
return NS_ERROR_FAILURE;
}
auto session = std::make_unique<Session>(std::move(capturer), std::move(encoder));
if (!session->Start())
return NS_ERROR_FAILURE;
nsString uid;
nsresult rv = generateUid(uid);
NS_ENSURE_SUCCESS(rv, rv);
sessionId = uid;
mIdToSession.emplace(uid, std::move(session));
return NS_OK;
}
nsresult nsScreencastService::StopVideoRecording(const nsAString& aSessionId) {
nsString sessionId(aSessionId);
auto it = mIdToSession.find(sessionId);
if (it == mIdToSession.end())
return NS_ERROR_INVALID_ARG;
it->second->Stop([sessionId] {
NS_DispatchToMainThread(NS_NewRunnableFunction(
"NotifyScreencastStopped", [sessionId]() -> void {
NotifyScreencastStopped(sessionId);
}));
});
mIdToSession.erase(it);
return NS_OK;
}
} // namespace mozilla

View File

@ -0,0 +1,29 @@
/* 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/. */
#pragma once
#include <memory>
#include <map>
#include "nsIScreencastService.h"
namespace mozilla {
class nsScreencastService final : public nsIScreencastService {
public:
NS_DECL_ISUPPORTS
NS_DECL_NSISCREENCASTSERVICE
static already_AddRefed<nsIScreencastService> GetSingleton();
nsScreencastService();
private:
~nsScreencastService();
class Session;
std::map<nsString, std::unique_ptr<Session>> mIdToSession;
};
} // namespace mozilla

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
// Any comment. You must start the file with a single-line comment!
pref("general.config.filename", "playwright.cfg");
pref("general.config.obscure_value", 0);

View File

@ -0,0 +1,275 @@
// Any comment. You must start the file with a comment!
// Use light theme by default.
pref("ui.systemUsesDarkTheme", 0);
// @see https://github.com/microsoft/playwright/issues/4297
pref("browser.tabs.remote.useCrossOriginEmbedderPolicy", false);
pref("browser.tabs.remote.useCrossOriginOpenerPolicy", false);
// Only allow the old modal dialogs. This should be removed when there is
// support for the new modal UI (see Bug 1686743).
pref("prompts.contentPromptSubDialog", false);
// Increase max number of child web processes so that new pages
// get a new process by default and we have a process isolation
// between pages from different contexts. If this becomes a performance
// issue we can povide custom '@mozilla.org/ipc/processselector;1'
//
pref("dom.ipc.processCount", 60000);
// Never reuse processes as they may keep previously overridden values
// (locale, timezone etc.).
pref("dom.ipc.processPrelaunch.enabled", false);
// Do not use system colors - they are affected by themes.
pref("ui.use_standins_for_native_colors", true);
// Isolate permissions by user context.
pref("permissions.isolateBy.userContext", true);
pref("dom.push.serverURL", "");
pref("services.settings.server", "");
pref("browser.safebrowsing.provider.mozilla.updateURL", "");
pref("browser.library.activity-stream.enabled", false);
pref("browser.search.geoSpecificDefaults", false);
pref("browser.search.geoSpecificDefaults.url", "");
pref("captivedetect.canonicalURL", "");
pref("network.captive-portal-service.enabled", false);
pref("network.connectivity-service.enabled", false);
pref("browser.newtabpage.activity-stream.asrouter.providers.snippets", "");
// Make sure Shield doesn't hit the network.
pref("app.normandy.api_url", "");
pref("app.normandy.enabled", false);
// Disable updater
pref("app.update.enabled", false);
// Disable Firefox old build background check
pref("app.update.checkInstallTime", false);
// Disable automatically upgrading Firefox
pref("app.update.disabledForTesting", true);
// make absolutely sure it is really off
pref("app.update.auto", false);
pref("app.update.mode", 0);
pref("app.update.service.enabled", false);
// Dislabe newtabpage
pref("browser.startup.homepage", "about:blank");
pref("browser.startup.page", 0);
pref("browser.newtabpage.enabled", false);
// Do not redirect user when a milstone upgrade of Firefox is detected
pref("browser.startup.homepage_override.mstone", "ignore");
pref("browser.tabs.remote.separateFileUriProcess", false);
pref("security.sandbox.content.level", 2);
// Disable topstories
pref("browser.newtabpage.activity-stream.feeds.section.topstories", false);
// DevTools JSONViewer sometimes fails to load dependencies with its require.js.
// This doesn't affect Puppeteer operations, but spams console with a lot of
// unpleasant errors.
// (bug 1424372)
pref("devtools.jsonview.enabled", false);
// Prevent various error message on the console
pref("browser.contentblocking.features.standard", "-tp,tpPrivate,cookieBehavior0,-cm,-fp");
pref("network.cookie.cookieBehavior", 0);
// Increase the APZ content response timeout in tests to 1 minute.
// This is to accommodate the fact that test environments tends to be
// slower than production environments (with the b2g emulator being
// the slowest of them all), resulting in the production timeout value
// sometimes being exceeded and causing false-positive test failures.
//
// (bug 1176798, bug 1177018, bug 1210465)
pref("apz.content_response_timeout", 60000);
// Allow creating files in content process - required for
// |Page.setFileInputFiles| protocol method.
pref("dom.file.createInChild", true);
// Indicate that the download panel has been shown once so that
// whichever download test runs first doesn't show the popup
// inconsistently.
pref("browser.download.panel.shown", true);
// Background thumbnails in particular cause grief, and disabling
// thumbnails in general cannot hurt
pref("browser.pagethumbnails.capturing_disabled", true);
// Disable safebrowsing components.
pref("browser.safebrowsing.blockedURIs.enabled", false);
pref("browser.safebrowsing.downloads.enabled", false);
pref("browser.safebrowsing.passwords.enabled", false);
pref("browser.safebrowsing.malware.enabled", false);
pref("browser.safebrowsing.phishing.enabled", false);
// Disable updates to search engines.
pref("browser.search.update", false);
// Do not restore the last open set of tabs if the browser has crashed
pref("browser.sessionstore.resume_from_crash", false);
// Don't check for the default web browser during startup.
pref("browser.shell.checkDefaultBrowser", false);
// Disable browser animations (tabs, fullscreen, sliding alerts)
pref("toolkit.cosmeticAnimations.enabled", false);
// Close the window when the last tab gets closed
pref("browser.tabs.closeWindowWithLastTab", true);
// Do not allow background tabs to be zombified on Android, otherwise for
// tests that open additional tabs, the test harness tab itself might get
// unloaded
pref("browser.tabs.disableBackgroundZombification", false);
// Do not warn when closing all open tabs
pref("browser.tabs.warnOnClose", false);
// Do not warn when closing all other open tabs
pref("browser.tabs.warnOnCloseOtherTabs", false);
// Do not warn when multiple tabs will be opened
pref("browser.tabs.warnOnOpen", false);
// Disable first run splash page on Windows 10
pref("browser.usedOnWindows10.introURL", "");
// Disable the UI tour.
//
// Should be set in profile.
pref("browser.uitour.enabled", false);
// Turn off search suggestions in the location bar so as not to trigger
// network connections.
pref("browser.urlbar.suggest.searches", false);
// Do not warn on quitting Firefox
pref("browser.warnOnQuit", false);
// Do not show datareporting policy notifications which can
// interfere with tests
pref("datareporting.healthreport.documentServerURI", "");
pref("datareporting.healthreport.about.reportUrl", "");
pref("datareporting.healthreport.logging.consoleEnabled", false);
pref("datareporting.healthreport.service.enabled", false);
pref("datareporting.healthreport.service.firstRun", false);
pref("datareporting.healthreport.uploadEnabled", false);
pref("datareporting.policy.dataSubmissionEnabled", false);
pref("datareporting.policy.dataSubmissionPolicyAccepted", false);
pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
// Automatically unload beforeunload alerts
pref("dom.disable_beforeunload", false);
// Disable popup-blocker
pref("dom.disable_open_during_load", false);
// Disable the ProcessHangMonitor
pref("dom.ipc.reportProcessHangs", false);
pref("hangmonitor.timeout", 0);
// Disable slow script dialogues
pref("dom.max_chrome_script_run_time", 0);
pref("dom.max_script_run_time", 0);
// Only load extensions from the application and user profile
// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
pref("extensions.autoDisableScopes", 0);
pref("extensions.enabledScopes", 5);
// Disable metadata caching for installed add-ons by default
pref("extensions.getAddons.cache.enabled", false);
// Disable installing any distribution extensions or add-ons.
pref("extensions.installDistroAddons", false);
// Turn off extension updates so they do not bother tests
pref("extensions.update.enabled", false);
pref("extensions.update.notifyUser", false);
// Make sure opening about:addons will not hit the network
pref("extensions.webservice.discoverURL", "");
pref("extensions.screenshots.disabled", true);
pref("extensions.screenshots.upload-disabled", true);
// Allow the application to have focus even it runs in the background
pref("focusmanager.testmode", true);
// Disable useragent updates
pref("general.useragent.updates.enabled", false);
// No ICC color correction.
// See https://developer.mozilla.org/en/docs/Mozilla/Firefox/Releases/3.5/ICC_color_correction_in_Firefox.
pref("gfx.color_management.mode", 0);
pref("gfx.color_management.rendering_intent", 3);
// Always use network provider for geolocation tests so we bypass the
// macOS dialog raised by the corelocation provider
pref("geo.provider.testing", true);
// Do not scan Wifi
pref("geo.wifi.scan", false);
// Show chrome errors and warnings in the error console
pref("javascript.options.showInConsole", true);
// Disable download and usage of OpenH264: and Widevine plugins
pref("media.gmp-manager.updateEnabled", false);
// Do not prompt with long usernames or passwords in URLs
pref("network.http.phishy-userpass-length", 255);
// Do not prompt for temporary redirects
pref("network.http.prompt-temp-redirect", false);
// Disable speculative connections so they are not reported as leaking
// when they are hanging around
pref("network.http.speculative-parallel-limit", 0);
// Do not automatically switch between offline and online
pref("network.manage-offline-status", false);
// Make sure SNTP requests do not hit the network
pref("network.sntp.pools", "");
// Disable Flash
pref("plugin.state.flash", 0);
pref("privacy.trackingprotection.enabled", false);
pref("security.certerrors.mitm.priming.enabled", false);
// Local documents have access to all other local documents,
// including directory listings
pref("security.fileuri.strict_origin_policy", false);
// Tests do not wait for the notification button security delay
pref("security.notification_enable_delay", 0);
// Ensure blocklist updates do not hit the network
pref("services.settings.server", "");
// Do not automatically fill sign-in forms with known usernames and
// passwords
pref("signon.autofillForms", false);
// Disable password capture, so that tests that include forms are not
// influenced by the presence of the persistent doorhanger notification
pref("signon.rememberSignons", false);
// Disable first-run welcome page
pref("startup.homepage_welcome_url", "about:blank");
pref("startup.homepage_welcome_url.additional", "");
// Prevent starting into safe mode after application crashes
pref("toolkit.startup.max_resumed_crashes", -1);
lockPref("toolkit.crashreporter.enabled", false);
pref("toolkit.telemetry.enabled", false);
pref("toolkit.telemetry.server", "");
// Disable downloading the list of blocked extensions.
pref("extensions.blocklist.enabled", false);
// Force Firefox Devtools to open in a separate window.
pref("devtools.toolbox.host", "window");

View File

@ -10,7 +10,7 @@ REMOTE_BROWSER_UPSTREAM="browser_upstream"
BUILD_BRANCH="playwright-build"
if [[ ($1 == '--help') || ($1 == '-h') ]]; then
echo "usage: $(basename $0) [firefox|webkit] [custom_checkout_path]"
echo "usage: $(basename $0) [firefox|firefox-stable|webkit] [custom_checkout_path]"
echo
echo "Prepares browser checkout. The checkout is a GIT repository that:"
echo "- has a '$REMOTE_BROWSER_UPSTREAM' remote pointing to a REMOTE_URL from UPSTREAM_CONFIG.sh"
@ -93,6 +93,18 @@ elif [[ ("$1" == "firefox") || ("$1" == "firefox/") || ("$1" == "ff") ]]; then
CHECKOUT_PATH="${FF_CHECKOUT_PATH}"
FRIENDLY_CHECKOUT_PATH="<FF_CHECKOUT_PATH>"
fi
elif [[ ("$1" == "firefox-stable") ]]; then
FRIENDLY_CHECKOUT_PATH="//browser_patches/firefox-stable/checkout";
CHECKOUT_PATH="$PWD/firefox-stable/checkout"
PATCHES_PATH="$PWD/firefox-stable/patches"
FIREFOX_EXTRA_FOLDER_PATH="$PWD/firefox-stable/juggler"
BUILD_NUMBER=$(head -1 "$PWD/firefox-stable/BUILD_NUMBER")
source "./firefox-stable/UPSTREAM_CONFIG.sh"
if [[ ! -z "${FF_CHECKOUT_PATH}" ]]; then
echo "WARNING: using checkout path from FF_CHECKOUT_PATH env: ${FF_CHECKOUT_PATH}"
CHECKOUT_PATH="${FF_CHECKOUT_PATH}"
FRIENDLY_CHECKOUT_PATH="<FF_CHECKOUT_PATH>"
fi
elif [[ ("$1" == "deprecated-webkit-mac-10.14") ]]; then
FRIENDLY_CHECKOUT_PATH="//browser_patches/deprecated-webkit-mac-10.14/checkout";
CHECKOUT_PATH="$PWD/deprecated-webkit-mac-10.14/checkout"