mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-04 17:35:48 +03:00
devops: add firefox-stable channel browser (#6173)
This adds a firefox-stable application to build on our bots. This is basically a rebaselined version of66541552d0
The firefox base revision isbb9bf7e886
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:
parent
bba7ca34c8
commit
17c6406e6c
@ -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
|
||||
# ===========================
|
||||
|
@ -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\"
|
||||
|
1
browser_patches/firefox-stable/.gitignore
vendored
Normal file
1
browser_patches/firefox-stable/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/checkout
|
2
browser_patches/firefox-stable/BUILD_NUMBER
Normal file
2
browser_patches/firefox-stable/BUILD_NUMBER
Normal file
@ -0,0 +1,2 @@
|
||||
1242
|
||||
Changed: lushnikov@chromium.org Fri 09 Apr 2021 09:56:28 PM PDT
|
5
browser_patches/firefox-stable/EXPECTED_BUILDS
Normal file
5
browser_patches/firefox-stable/EXPECTED_BUILDS
Normal 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
|
3
browser_patches/firefox-stable/UPSTREAM_CONFIG.sh
Normal file
3
browser_patches/firefox-stable/UPSTREAM_CONFIG.sh
Normal file
@ -0,0 +1,3 @@
|
||||
REMOTE_URL="https://github.com/mozilla/gecko-dev"
|
||||
BASE_BRANCH="release"
|
||||
BASE_REVISION="4068febfd76d9ec557591240d7496be42c27c17f"
|
60
browser_patches/firefox-stable/archive.sh
Executable file
60
browser_patches/firefox-stable/archive.sh
Executable 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
|
126
browser_patches/firefox-stable/build.sh
Executable file
126
browser_patches/firefox-stable/build.sh
Executable 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
|
||||
|
18
browser_patches/firefox-stable/clean.sh
Executable file
18
browser_patches/firefox-stable/clean.sh
Executable 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
|
||||
|
96
browser_patches/firefox-stable/install-preferences.js
Normal file
96
browser_patches/firefox-stable/install-preferences.js
Normal 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);
|
||||
});
|
135
browser_patches/firefox-stable/juggler/Helper.js
Normal file
135
browser_patches/firefox-stable/juggler/Helper.js
Normal 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;
|
||||
|
913
browser_patches/firefox-stable/juggler/NetworkObserver.js
Normal file
913
browser_patches/firefox-stable/juggler/NetworkObserver.js
Normal 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;
|
180
browser_patches/firefox-stable/juggler/SimpleChannel.js
Normal file
180
browser_patches/firefox-stable/juggler/SimpleChannel.js
Normal 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;
|
914
browser_patches/firefox-stable/juggler/TargetRegistry.js
Normal file
914
browser_patches/firefox-stable/juggler/TargetRegistry.js
Normal 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;
|
131
browser_patches/firefox-stable/juggler/components/juggler.js
Normal file
131
browser_patches/firefox-stable/juggler/components/juggler.js
Normal 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]);
|
@ -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
|
@ -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",
|
||||
]
|
||||
|
574
browser_patches/firefox-stable/juggler/content/FrameTree.js
Normal file
574
browser_patches/firefox-stable/juggler/content/FrameTree.js
Normal 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;
|
||||
|
1028
browser_patches/firefox-stable/juggler/content/PageAgent.js
Normal file
1028
browser_patches/firefox-stable/juggler/content/PageAgent.js
Normal file
File diff suppressed because it is too large
Load Diff
541
browser_patches/firefox-stable/juggler/content/Runtime.js
Normal file
541
browser_patches/firefox-stable/juggler/content/Runtime.js
Normal 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;
|
76
browser_patches/firefox-stable/juggler/content/WorkerMain.js
Normal file
76
browser_patches/firefox-stable/juggler/content/WorkerMain.js
Normal 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);
|
||||
|
@ -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;
|
||||
}
|
149
browser_patches/firefox-stable/juggler/content/main.js
Normal file
149
browser_patches/firefox-stable/juggler/content/main.js
Normal 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();
|
22
browser_patches/firefox-stable/juggler/jar.mn
Normal file
22
browser_patches/firefox-stable/juggler/jar.mn
Normal 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)
|
||||
|
10
browser_patches/firefox-stable/juggler/moz.build
Normal file
10
browser_patches/firefox-stable/juggler/moz.build
Normal 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")
|
||||
|
15
browser_patches/firefox-stable/juggler/pipe/components.conf
Normal file
15
browser_patches/firefox-stable/juggler/pipe/components.conf
Normal 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'],
|
||||
},
|
||||
]
|
24
browser_patches/firefox-stable/juggler/pipe/moz.build
Normal file
24
browser_patches/firefox-stable/juggler/pipe/moz.build
Normal 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'
|
@ -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();
|
||||
};
|
@ -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
|
@ -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
|
@ -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;
|
135
browser_patches/firefox-stable/juggler/protocol/Dispatcher.js
Normal file
135
browser_patches/firefox-stable/juggler/protocol/Dispatcher.js
Normal 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;
|
||||
|
389
browser_patches/firefox-stable/juggler/protocol/PageHandler.js
Normal file
389
browser_patches/firefox-stable/juggler/protocol/PageHandler.js
Normal 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;
|
@ -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'];
|
933
browser_patches/firefox-stable/juggler/protocol/Protocol.js
Normal file
933
browser_patches/firefox-stable/juggler/protocol/Protocol.js
Normal 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'];
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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'],
|
||||
},
|
||||
]
|
49
browser_patches/firefox-stable/juggler/screencast/moz.build
Normal file
49
browser_patches/firefox-stable/juggler/screencast/moz.build
Normal 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'
|
@ -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);
|
||||
};
|
@ -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
|
@ -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
|
2499
browser_patches/firefox-stable/patches/bootstrap.diff
Normal file
2499
browser_patches/firefox-stable/patches/bootstrap.diff
Normal file
File diff suppressed because it is too large
Load Diff
@ -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);
|
275
browser_patches/firefox-stable/preferences/playwright.cfg
Normal file
275
browser_patches/firefox-stable/preferences/playwright.cfg
Normal 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");
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user