chore: remove Juggler from Puppeteer repository (#3954)

This patch removes Juggler from Puppeteer repository. Instead,
Juggler development is hosted now at
https://github.com/puppeteer/juggler
This commit is contained in:
Andrey Lushnikov 2019-02-08 13:28:43 -08:00 committed by GitHub
parent 72c00474e7
commit 07b43ff29a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 3 additions and 4282 deletions

View File

@ -1,8 +0,0 @@
#### What's this?
This is a **highly experimental** Puppeteer API to drive Firefox browser.
Beware, alligators live here.
- `/juggler` - firefox-puppeteer backend
- `/puppeteer-firefox` - puppeteer API for Firefox

View File

@ -1,38 +0,0 @@
task:
timeout_in: 120m
env:
CIRRUS_WORKING_DIR: /usr/local/src
SOURCE: /usr/local/src/
GS_AUTH: ENCRYPTED[c4b5b0404f5bfdc1b663a1eb5b70f3187b5d470d02eec3265b06b8e0d30226781523630931c1da6db06714c0d359f71f]
PATH: /root/.cargo/bin:$PATH:$SOURCE/gcloud/google-cloud-sdk/bin
SHELL: /bin/bash
container:
dockerfile: Dockerfile
# image: ubuntu
cpu: 8
memory: 24
name: linux
# install_deps_script: apt-get update && apt-get install -y wget python clang llvm git curl
install_gcloud_script: ./scripts/install_gcloud.sh
check_gcloud_script:
- echo "REVISION: $(git rev-parse HEAD)"
- gsutil cp FIREFOX_SHA gs://juggler-builds/$(git rev-parse HEAD)/
clone_firefox_script: ./scripts/fetch_firefox.sh
apply_patches_script:
- cd $SOURCE/firefox
- git config --global user.name "cirrus-ci-builder"
- git config --global user.email "aslushnikov@gmail.com"
- git am ../patches/*
- ln -s $PWD/../juggler testing/juggler
bootstrap_firefox_script:
- cd $SOURCE/firefox
- ./mach bootstrap --application-choice=browser --no-interactive
build_firefox_script:
- cd $SOURCE/firefox
- ./mach build
package_firefox_script:
- cd $SOURCE/firefox
- ./mach package
upload_build_to_gcloud_script:
- bash $SOURCE/scripts/upload_linux.sh

View File

@ -1 +0,0 @@
firefox/

View File

@ -1,27 +0,0 @@
FROM ubuntu:trusty
MAINTAINER Andrey Lushnikov <aslushnikov@gmail.com>
ENV SHELL=/bin/bash
# Install generic deps
RUN apt-get update -y && apt-get install -y wget python clang llvm git curl
# Install gcc7 (mach requires 6.1+)
RUN apt-get update -y && \
apt-get upgrade -y && \
apt-get dist-upgrade -y && \
apt-get install build-essential software-properties-common -y && \
add-apt-repository ppa:ubuntu-toolchain-r/test -y && \
apt-get update -y && \
apt-get install gcc-7 g++-7 -y && \
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-7 60 --slave /usr/bin/g++ g++ /usr/bin/g++-7 && \
update-alternatives --config gcc
# Install llvm 3.9.0 (mach requires 3.9.0+)
RUN echo "deb http://apt.llvm.org/trusty/ llvm-toolchain-trusty-3.9 main" >> /etc/apt/sources.list && \
echo "deb-src http://apt.llvm.org/trusty/ llvm-toolchain-trusty-3.9 main" >> /etc/apt/sources.list && \
apt-get install clang-3.9 lldb-3.9 -y
# Install python 3.6 (mach requires 3.5+)
RUN add-apt-repository ppa:deadsnakes/ppa -y && \
apt-get update -y && apt-get install python3.6 -y

View File

@ -1 +0,0 @@
120450a2c56c25e7c410a909192b5b9ad7b0dff2

View File

@ -1,119 +0,0 @@
# Juggler
> Juggler - Firefox Automation Protocol for implementing the Puppeteer API.
## Protocol
See [`//src/Protocol.js`](https://github.com/GoogleChrome/puppeteer/blob/master/experimental/juggler/src/Protocol.js).
## Building FF with Juggler
1. Clone Juggler repository
```bash
git clone https://github.com/aslushnikov/juggler
cd juggler
```
2. Checkout [pinned Firefox revision](https://github.com/aslushnikov/juggler/blob/master/FIREFOX_SHA) from mozilla [github mirror](https://github.com/mozilla/gecko-dev) into `//firefox` folder.
```bash
SOURCE=$PWD bash scripts/fetch_firefox.sh
```
3. Apply juggler patches to Firefox source code
```bash
cd firefox
git am ../patches/*
```
4. Add Juggler to Firefox. NOTE: On Linux, symlinks work. On OSX, files have to be copied.
```bash
# LINUX:
ln -s $PWD/../src $PWD/testing/juggler
# OSX:
cp -r $PWD/../src $PWD/testing/juggler
```
5. Bootstrap host environment for Firefox build and compile firefox locally
```bash
# OPTIONAL - bootstrap host environment.
./mach bootstrap --application-choice=browser --no-interactive
# Compile browser
./mach build
```
### Troubleshooting when building FF on Mac
#### Black screen after FF Build
As of Jan. 2019 there is a known [bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1493330) that will cause an entirely black screen when running the nightly build of firefox built with the **MacOSX SDK version 10.14.**
The easiest fix right now is downgrading your MacOSX SDK.
To do so:
1) Go to [this repo](https://github.com/phracker/MacOSX-SDKs) and install any **SDK version < 10.14** (e.g. 10.13 works fine)
2) In the `juggler/firefox` folder:
```bash
echo "ac_add_options --with-macos-sdk=path/to/sdk" >> .mozconfig
# your SDK might be located at
# /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/
```
3) run `./mach build` again
#### Missing headers in /usr/include
On MacOS 10.14 (Mojave) you might run into issues when building FF.
The error is related to [a change in the xcode-select installation](https://bugzilla.mozilla.org/show_bug.cgi?id=1487552)
To workaround this issue you can simply run:
```bash
# Write missing headers to /usr/include
sudo installer -pkg /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg -target /
```
## Running Firefox with Juggler
Juggle adds a `-juggler` CLI flag that accepts a port to expose a remote protocol on.
Pass `0` to pick a random port - Juggler will print its port to STDOUT.
```
./mach run -- -juggler 0
```
## Uploading builds to Google Storage
Firefox builds with Juggler support are uploaded to gs://juggler-builds/ bucket.
Project maintainers can upload builds.
To upload a build, do the following:
1. Install [gcloud](https://cloud.google.com/sdk/install) if you haven't yet.
2. Authenticate in the cloud and select project
```bash
gcloud auth login
gcloud config set project juggler-builds
```
3. Make sure **firefox is compiled**; after that, package a build for a redistribution:
```bash
cd firefox
./mach package
```
4. Archive build and copy to the gbucket
We want to ship `*.zip` archives so that it's easy to decompress them on the node-side.
- Linux: `./scripts/upload_linux.sh`
- Mac: `./scripts/upload_mac.sh`

View File

@ -1,155 +0,0 @@
From 5082f80b83290be204cd80124d292d1c563d2d13 Mon Sep 17 00:00:00 2001
From: Andrey Lushnikov <lushnikov@chromium.org>
Date: Thu, 24 Jan 2019 11:13:22 -0500
Subject: [PATCH] Introduce nsIWebProgressListener2::onFrameLocationChange
The event is fired when subframes commit navigation.
Juggler uses this event to track same-document iframe navigations.
---
docshell/base/nsDocShell.cpp | 1 +
.../statusfilter/nsBrowserStatusFilter.cpp | 8 +++++++
uriloader/base/nsDocLoader.cpp | 18 +++++++++++++++
uriloader/base/nsDocLoader.h | 5 ++++
uriloader/base/nsIWebProgress.idl | 7 +++++-
uriloader/base/nsIWebProgressListener2.idl | 23 +++++++++++++++++++
6 files changed, 61 insertions(+), 1 deletion(-)
diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp
index ef2e46b33a..31471e3465 100644
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -1198,6 +1198,7 @@ bool nsDocShell::SetCurrentURI(nsIURI* aURI, nsIRequest* aRequest,
isSubFrame = mLSHE->GetIsSubFrame();
}
+ FireOnFrameLocationChange(this, aRequest, aURI, aLocationFlags);
if (!isSubFrame && !isRoot) {
/*
* We don't want to send OnLocationChange notifications when
diff --git a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp
index 61fcfef258..264f9c1e61 100644
--- a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp
+++ b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp
@@ -170,6 +170,14 @@ nsBrowserStatusFilter::OnStateChange(nsIWebProgress *aWebProgress,
return NS_OK;
}
+NS_IMETHODIMP
+nsBrowserStatusFilter::OnFrameLocationChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest,
+ nsIURI *aLocation,
+ uint32_t aFlags) {
+ return NS_OK;
+}
+
NS_IMETHODIMP
nsBrowserStatusFilter::OnProgressChange(nsIWebProgress *aWebProgress,
nsIRequest *aRequest,
diff --git a/uriloader/base/nsDocLoader.cpp b/uriloader/base/nsDocLoader.cpp
index a3bc24e603..67b3d3eaeb 100644
--- a/uriloader/base/nsDocLoader.cpp
+++ b/uriloader/base/nsDocLoader.cpp
@@ -1252,6 +1252,24 @@ void nsDocLoader::FireOnLocationChange(nsIWebProgress* aWebProgress,
}
}
+void nsDocLoader::FireOnFrameLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ nsIURI *aUri,
+ uint32_t aFlags) {
+ NOTIFY_LISTENERS(nsIWebProgress::NOTIFY_FRAME_LOCATION,
+ nsCOMPtr<nsIWebProgressListener2> listener2 =
+ do_QueryReferent(info.mWeakListener);
+ if (!listener2)
+ continue;
+ listener2->OnFrameLocationChange(aWebProgress, aRequest, aUri, aFlags);
+ );
+
+ // Pass the notification up to the parent...
+ if (mParent) {
+ mParent->FireOnFrameLocationChange(aWebProgress, aRequest, aUri, aFlags);
+ }
+}
+
void nsDocLoader::FireOnStatusChange(nsIWebProgress* aWebProgress,
nsIRequest* aRequest, nsresult aStatus,
const char16_t* aMessage) {
diff --git a/uriloader/base/nsDocLoader.h b/uriloader/base/nsDocLoader.h
index 45f0d3d88e..7848878b70 100644
--- a/uriloader/base/nsDocLoader.h
+++ b/uriloader/base/nsDocLoader.h
@@ -153,6 +153,11 @@ class nsDocLoader : public nsIDocumentLoader,
void FireOnLocationChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
nsIURI* aUri, uint32_t aFlags);
+ void FireOnFrameLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ nsIURI *aUri,
+ uint32_t aFlags);
+
MOZ_MUST_USE bool RefreshAttempted(nsIWebProgress* aWebProgress, nsIURI* aURI,
int32_t aDelay, bool aSameURI);
diff --git a/uriloader/base/nsIWebProgress.idl b/uriloader/base/nsIWebProgress.idl
index 0549f32e1e..3078e35d7a 100644
--- a/uriloader/base/nsIWebProgress.idl
+++ b/uriloader/base/nsIWebProgress.idl
@@ -84,17 +84,22 @@ interface nsIWebProgress : nsISupports
* NOTIFY_REFRESH
* Receive onRefreshAttempted events.
* This is defined on nsIWebProgressListener2.
+ *
+ * NOTIFY_FRAME_LOCATION
+ * Receive onFrameLocationChange events.
+ * This is defined on nsIWebProgressListener2.
*/
const unsigned long NOTIFY_PROGRESS = 0x00000010;
const unsigned long NOTIFY_STATUS = 0x00000020;
const unsigned long NOTIFY_SECURITY = 0x00000040;
const unsigned long NOTIFY_LOCATION = 0x00000080;
const unsigned long NOTIFY_REFRESH = 0x00000100;
+ const unsigned long NOTIFY_FRAME_LOCATION = 0x00000200;
/**
* This flag enables all notifications.
*/
- const unsigned long NOTIFY_ALL = 0x000001ff;
+ const unsigned long NOTIFY_ALL = 0x000002ff;
/**
* Registers a listener to receive web progress events.
diff --git a/uriloader/base/nsIWebProgressListener2.idl b/uriloader/base/nsIWebProgressListener2.idl
index 87701f8d2c..ae1aa85c01 100644
--- a/uriloader/base/nsIWebProgressListener2.idl
+++ b/uriloader/base/nsIWebProgressListener2.idl
@@ -66,4 +66,27 @@ interface nsIWebProgressListener2 : nsIWebProgressListener {
in nsIURI aRefreshURI,
in long aMillis,
in boolean aSameURI);
+
+ /**
+ * Called when the location of the window or its subframes changes. This is not
+ * when a load is requested, but rather once it is verified that the load is
+ * going to occur in the given window. For instance, a load that starts in a
+ * window might send progress and status messages for the new site, but it
+ * will not send the onLocationChange until we are sure that we are loading
+ * this new page here.
+ *
+ * @param aWebProgress
+ * The nsIWebProgress instance that fired the notification.
+ * @param aRequest
+ * The associated nsIRequest. This may be null in some cases.
+ * @param aLocation
+ * The URI of the location that is being loaded.
+ * @param aFlags
+ * This is a value which explains the situation or the reason why
+ * the location has changed.
+ */
+ void onFrameLocationChange(in nsIWebProgress aWebProgress,
+ in nsIRequest aRequest,
+ in nsIURI aLocation,
+ [optional] in unsigned long aFlags);
};
--
2.19.0.605.g01d371f741-goog

View File

@ -1,24 +0,0 @@
From c6f975dbc28b902cc271f79dedc42073ab1bde7d Mon Sep 17 00:00:00 2001
From: Andrey Lushnikov <lushnikov@chromium.org>
Date: Tue, 27 Nov 2018 13:39:00 -0800
Subject: [PATCH 2/3] Add Juggler to gecko build system
---
toolkit/toolkit.mozbuild | 1 +
1 file changed, 1 insertion(+)
diff --git a/toolkit/toolkit.mozbuild b/toolkit/toolkit.mozbuild
index 4a0e5f172..b8abc1e72 100644
--- a/toolkit/toolkit.mozbuild
+++ b/toolkit/toolkit.mozbuild
@@ -163,6 +163,7 @@ if CONFIG['ENABLE_MARIONETTE']:
DIRS += [
'/testing/firefox-ui',
'/testing/marionette',
+ '/testing/juggler',
]
if CONFIG['ENABLE_GECKODRIVER'] and not CONFIG['MOZ_TSAN']:
--
2.19.0.605.g01d371f741-goog

View File

@ -1,43 +0,0 @@
From 1449495af094fbc5e1bb351f8387c3a341977763 Mon Sep 17 00:00:00 2001
From: Andrey Lushnikov <lushnikov@chromium.org>
Date: Thu, 29 Nov 2018 11:40:32 -0800
Subject: [PATCH 3/3] Add Juggler to mozilla packaging script
---
browser/installer/allowed-dupes.mn | 6 ++++++
browser/installer/package-manifest.in | 5 +++++
2 files changed, 11 insertions(+)
diff --git a/browser/installer/allowed-dupes.mn b/browser/installer/allowed-dupes.mn
index 5685a30d9..32ba241b8 100644
--- a/browser/installer/allowed-dupes.mn
+++ b/browser/installer/allowed-dupes.mn
@@ -154,3 +154,9 @@ browser/defaults/settings/main/example.json
# Bug 1463748 - Fork and pref-off the new error pages
browser/chrome/browser/content/browser/aboutNetError-new.xhtml
browser/chrome/browser/content/browser/aboutNetError.xhtml
+
+# Juggler/marionette files
+chrome/juggler/content/content/floating-scrollbars.css
+browser/chrome/devtools/skin/floating-scrollbars-responsive-design.css
+chrome/juggler/content/server/stream-utils.js
+chrome/marionette/content/stream-utils.js
diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in
index 5b828784a..a5d9f9741 100644
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -338,6 +338,11 @@
@RESPATH@/defaults/pref/marionette.js
#endif
+@RESPATH@/chrome/juggler@JAREXT@
+@RESPATH@/chrome/juggler.manifest
+@RESPATH@/components/juggler.manifest
+@RESPATH@/components/juggler.js
+
@RESPATH@/components/nsAsyncShutdown.manifest
@RESPATH@/components/nsAsyncShutdown.js
--
2.19.0.605.g01d371f741-goog

View File

@ -1,18 +0,0 @@
set -e
set -x
if [ -d $SOURCE/firefox ]; then
echo ERROR! Directory "${SOURCE}/firefox" exists. Remove it and re-run this script.
exit 1;
fi
mkdir -p $SOURCE/firefox
cd $SOURCE/firefox
git init
git remote add origin https://github.com/mozilla/gecko-dev.git
git fetch --depth 50 origin release
git reset --hard $(cat $SOURCE/FIREFOX_SHA)
if [[ $? == 0 ]]; then
echo SUCCESS
else
echo FAILED TO CHECKOUT PINNED REVISION
fi

View File

@ -1,9 +0,0 @@
# auth
echo $GS_AUTH > $SOURCE/gsauth
# install gcloud sdk
curl https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz > /tmp/google-cloud-sdk.tar.gz
mkdir -p $SOURCE/gcloud \
&& tar -C $SOURCE/gcloud -xvf /tmp/google-cloud-sdk.tar.gz \
&& CLOUDSDK_CORE_DISABLE_PROMPTS=1 $SOURCE/gcloud/google-cloud-sdk/install.sh
gcloud auth activate-service-account --key-file=$SOURCE/gsauth
gcloud config set project juggler-builds

View File

@ -1,13 +0,0 @@
set -e
if [ -e ./FIREFOX_SHA ]; then
echo Checking Juggler root - OK
else
echo Please run this script from the Juggler root
exit 1;
fi
cd firefox/obj-x86_64-pc-linux-gnu/dist/
zip -r firefox-linux.zip firefox
mv firefox-linux.zip ../../../
cd -
gsutil mv firefox-linux.zip gs://juggler-builds/$(git rev-parse HEAD)/

View File

@ -1,13 +0,0 @@
set -e
if [ -e ./FIREFOX_SHA ]; then
echo Checking Juggler root - OK
else
echo Please run this script from the Juggler root
exit 1;
fi
cd firefox/obj-x86_64-apple-darwin17.7.0/dist/
zip -r firefox-mac.zip firefox
mv firefox-mac.zip ../../../
cd -
gsutil mv firefox-mac.zip gs://juggler-builds/$(git rev-parse HEAD)/

View File

@ -1,149 +0,0 @@
"use strict";
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {PageHandler} = ChromeUtils.import("chrome://juggler/content/PageHandler.jsm");
const {InsecureSweepingOverride} = ChromeUtils.import("chrome://juggler/content/InsecureSweepingOverride.js");
class BrowserHandler {
constructor(session) {
this._session = session;
this._mainWindowPromise = waitForBrowserWindow();
this._pageHandlers = new Map();
this._tabsToPageHandlers = new Map();
this._initializePages();
this._sweepingOverride = null;
}
async setIgnoreHTTPSErrors({enabled}) {
if (!enabled && this._sweepingOverride) {
this._sweepingOverride.unregister();
this._sweepingOverride = null;
Services.prefs.setBoolPref('security.mixed_content.block_active_content', true);
} else if (enabled && !this._sweepingOverride) {
this._sweepingOverride = new InsecureSweepingOverride();
this._sweepingOverride.register();
Services.prefs.setBoolPref('security.mixed_content.block_active_content', false);
}
}
async getInfo() {
const win = await this._mainWindowPromise;
const version = Components.classes["@mozilla.org/xre/app-info;1"]
.getService(Components.interfaces.nsIXULAppInfo)
.version;
const userAgent = Components.classes["@mozilla.org/network/protocol;1?name=http"]
.getService(Components.interfaces.nsIHttpProtocolHandler)
.userAgent;
return {version: 'Firefox/' + version, userAgent};
}
async _initializePages() {
const win = await this._mainWindowPromise;
const tabs = win.gBrowser.tabs;
for (const tab of win.gBrowser.tabs)
this._ensurePageHandler(tab);
win.gBrowser.tabContainer.addEventListener('TabOpen', event => {
this._ensurePageHandler(event.target);
});
win.gBrowser.tabContainer.addEventListener('TabClose', event => {
this._removePageHandlerForTab(event.target);
});
}
pageForId(pageId) {
return this._pageHandlers.get(pageId) || null;
}
_ensurePageHandler(tab) {
if (this._tabsToPageHandlers.has(tab))
return this._tabsToPageHandlers.get(tab);
const pageHandler = new PageHandler(this._session, tab);
this._pageHandlers.set(pageHandler.id(), pageHandler);
this._tabsToPageHandlers.set(tab, pageHandler);
this._session.emitEvent('Browser.tabOpened', {
url: pageHandler.url(),
pageId: pageHandler.id()
});
return pageHandler;
}
_removePageHandlerForTab(tab) {
const pageHandler = this._tabsToPageHandlers.get(tab);
this._tabsToPageHandlers.delete(tab);
this._pageHandlers.delete(pageHandler.id());
pageHandler.dispose();
this._session.emitEvent('Browser.tabClosed', {pageId: pageHandler.id()});
}
async newPage() {
const win = await this._mainWindowPromise;
const tab = win.gBrowser.addTab('about:blank', {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
win.gBrowser.selectedTab = tab;
// Await navigation to about:blank
await new Promise(resolve => {
const wpl = {
onLocationChange: function(aWebProgress, aRequest, aLocation) {
tab.linkedBrowser.removeProgressListener(wpl);
resolve();
},
QueryInterface: ChromeUtils.generateQI([
Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference,
]),
};
tab.linkedBrowser.addProgressListener(wpl);
});
const pageHandler = this._ensurePageHandler(tab);
return {pageId: pageHandler.id()};
}
async closePage({pageId}) {
const win = await this._mainWindowPromise;
const pageHandler = this._pageHandlers.get(pageId);
await win.gBrowser.removeTab(pageHandler.tab());
}
}
/**
* @return {!Promise<Ci.nsIDOMChromeWindow>}
*/
async function waitForBrowserWindow() {
const windowsIt = Services.wm.getEnumerator('navigator:browser');
if (windowsIt.hasMoreElements())
return waitForWindowLoaded(windowsIt.getNext().QueryInterface(Ci.nsIDOMChromeWindow));
let fulfill;
let promise = new Promise(x => fulfill = x);
const listener = {
onOpenWindow: window => {
if (window instanceof Ci.nsIDOMChromeWindow) {
Services.wm.removeListener(listener);
fulfill(waitForWindowLoaded(window));
}
},
onCloseWindow: () => {}
};
Services.wm.addListener(listener);
return promise;
/**
* @param {!Ci.nsIDOMChromeWindow} window
* @return {!Promise<Ci.nsIDOMChromeWindow>}
*/
function waitForWindowLoaded(window) {
if (window.document.readyState === 'complete')
return window;
return new Promise(fulfill => {
window.addEventListener('load', function listener() {
window.removeEventListener('load', listener);
fulfill(window);
});
});
}
}
var EXPORTED_SYMBOLS = ['BrowserHandler'];
this.BrowserHandler = BrowserHandler;

View File

@ -1,72 +0,0 @@
const {BrowserHandler} = ChromeUtils.import("chrome://juggler/content/BrowserHandler.jsm");
const {protocol, checkScheme} = ChromeUtils.import("chrome://juggler/content/Protocol.js");
class ChromeSession {
constructor(connection) {
this._connection = connection;
this._connection.onmessage = this._dispatch.bind(this);
this._browserHandler = new BrowserHandler(this);
}
emitEvent(eventName, params) {
const scheme = protocol.events[eventName];
if (!scheme)
throw new Error(`ERROR: event '${eventName}' is not supported`);
const details = {};
if (!checkScheme(scheme, params || {}, details))
throw new Error(`ERROR: event '${eventName}' is called with ${details.errorType} parameter '${details.propertyName}': ${details.propertyValue}`);
this._connection.send({method: eventName, params});
}
async _dispatch(data) {
const id = data.id;
try {
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 descriptor = protocol.methods[method];
if (!descriptor)
throw new Error(`ERROR: method '${method}' is not supported`);
let details = {};
if (!checkScheme(descriptor.params || {}, params, details))
throw new Error(`ERROR: method '${method}' is called with ${details.errorType} parameter '${details.propertyName}': ${details.propertyValue}`);
const result = await this._innerDispatch(method, params);
details = {};
if ((descriptor.returns || result) && !checkScheme(descriptor.returns, result, details))
throw new Error(`ERROR: method '${method}' returned ${details.errorType} parameter '${details.propertyName}': ${details.propertyValue}`);
this._connection.send({id, result});
} catch (e) {
this._connection.send({id, error: {
message: e.message,
data: e.stack
}});
}
}
async _innerDispatch(method, params) {
const [domainName, methodName] = method.split('.');
if (domainName === 'Browser')
return await this._browserHandler[methodName](params);
if (domainName === 'Page') {
if (!params.pageId)
throw new Error('Parameter "pageId" must be present for Page.* methods');
const pageHandler = this._browserHandler.pageForId(params.pageId);
if (!pageHandler)
throw new Error('Failed to find page for id = ' + pageId);
return await pageHandler[methodName](params);
}
throw new Error(`INTERNAL ERROR: failed to dispatch '${method}'`);
}
}
this.EXPORTED_SYMBOLS = ['ChromeSession'];
this.ChromeSession = ChromeSession;

View File

@ -1,46 +0,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);
}
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() {
return uuidGen.generateUUID().toString();
}
}
var EXPORTED_SYMBOLS = [ "Helper" ];
this.Helper = Helper;

View File

@ -1,78 +0,0 @@
/* 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";
ChromeUtils.import("resource://gre/modules/Preferences.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
const registrar =
Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
const sss = Cc["@mozilla.org/ssservice;1"]
.getService(Ci.nsISiteSecurityService);
const CERT_PINNING_ENFORCEMENT_PREF = "security.cert_pinning.enforcement_level";
const CID = Components.ID("{4b67cce0-a51c-11e6-9598-0800200c9a66}");
const CONTRACT_ID = "@mozilla.org/security/certoverride;1";
const DESC = "All-encompassing cert service that matches on a bitflag";
const HSTS_PRELOAD_LIST_PREF = "network.stricttransportsecurity.preloadlist";
const Error = {
Untrusted: 1,
Mismatch: 2,
Time: 4,
};
/**
* Certificate override service that acts in an all-inclusive manner
* on TLS certificates.
*
* @throws {Components.Exception}
* If there are any problems registering the service.
*/
function InsecureSweepingOverride() {
// This needs to be an old-style class with a function constructor
// and prototype assignment because... XPCOM. Any attempt at
// modernisation will be met with cryptic error messages which will
// make your life miserable.
let service = function() {};
service.prototype = {
hasMatchingOverride(
aHostName, aPort, aCert, aOverrideBits, aIsTemporary) {
aIsTemporary.value = false;
aOverrideBits.value = Error.Untrusted | Error.Mismatch | Error.Time;
return true;
},
QueryInterface: ChromeUtils.generateQI([Ci.nsICertOverrideService]),
};
let factory = XPCOMUtils.generateSingletonFactory(service);
return {
register() {
// make it possible to register certificate overrides for domains
// that use HSTS or HPKP
Preferences.set(HSTS_PRELOAD_LIST_PREF, false);
Preferences.set(CERT_PINNING_ENFORCEMENT_PREF, 0);
registrar.registerFactory(CID, DESC, CONTRACT_ID, factory);
},
unregister() {
registrar.unregisterFactory(CID, factory);
Preferences.reset(HSTS_PRELOAD_LIST_PREF);
Preferences.reset(CERT_PINNING_ENFORCEMENT_PREF);
// clear collected HSTS and HPKP state
// through the site security service
sss.clearAll();
sss.clearPreloads();
},
};
}
this.EXPORTED_SYMBOLS = ["InsecureSweepingOverride"];
this.InsecureSweepingOverride = InsecureSweepingOverride;

View File

@ -1,337 +0,0 @@
"use strict";
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.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 FRAME_SCRIPT = "chrome://juggler/content/content/ContentSession.js";
const helper = new Helper();
class PageHandler {
constructor(chromeSession, tab) {
this._pageId = helper.generateId();
this._chromeSession = chromeSession;
this._tab = tab;
this._browser = tab.linkedBrowser;
this._enabled = false;
this.QueryInterface = ChromeUtils.generateQI([
Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference,
]);
this._browser.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
this._dialogs = new Map();
// First navigation always happens to about:blank - do not report it.
this._skipNextNavigation = true;
}
async setViewport({viewport}) {
if (viewport) {
const {width, height} = viewport;
this._browser.style.setProperty('min-width', width + 'px');
this._browser.style.setProperty('min-height', height + 'px');
this._browser.style.setProperty('max-width', width + 'px');
this._browser.style.setProperty('max-height', height + 'px');
} else {
this._browser.style.removeProperty('min-width');
this._browser.style.removeProperty('min-height');
this._browser.style.removeProperty('max-width');
this._browser.style.removeProperty('max-height');
}
const dimensions = this._browser.getBoundingClientRect();
await Promise.all([
this._contentSession.send('setViewport', {
deviceScaleFactor: viewport ? viewport.deviceScaleFactor : 0,
isMobile: viewport && viewport.isMobile,
hasTouch: viewport && viewport.hasTouch,
}),
this._contentSession.send('awaitViewportDimensions', {
width: dimensions.width,
height: dimensions.height
}),
]);
}
_initializeDialogEvents() {
this._browser.addEventListener('DOMWillOpenModalDialog', async (event) => {
// wait for the dialog to be actually added to DOM.
await Promise.resolve();
this._updateModalDialogs();
});
this._browser.addEventListener('DOMModalDialogClosed', (event) => {
this._updateModalDialogs();
});
this._updateModalDialogs();
}
_updateModalDialogs() {
const elements = new Set(this._browser.parentNode.getElementsByTagNameNS(XUL_NS, "tabmodalprompt"));
for (const dialog of this._dialogs.values()) {
if (!elements.has(dialog.element())) {
this._dialogs.delete(dialog.id());
this._chromeSession.emitEvent('Page.dialogClosed', {
pageId: this._pageId,
dialogId: dialog.id(),
});
} else {
elements.delete(dialog.element());
}
}
for (const element of elements) {
const dialog = Dialog.createIfSupported(element);
if (!dialog)
continue;
this._dialogs.set(dialog.id(), dialog);
this._chromeSession.emitEvent('Page.dialogOpened', {
pageId: this._pageId,
dialogId: dialog.id(),
type: dialog.type(),
message: dialog.message(),
defaultValue: dialog.defaultValue(),
});
}
}
onLocationChange(aWebProgress, aRequest, aLocation) {
if (this._skipNextNavigation) {
this._skipNextNavigation = false;
return;
}
this._chromeSession.emitEvent('Browser.tabNavigated', {
pageId: this._pageId,
url: aLocation.spec
});
}
url() {
return this._browser.currentURI.spec;
}
tab() {
return this._tab;
}
id() {
return this._pageId;
}
async enable() {
if (this._enabled)
return;
this._enabled = true;
this._initializeDialogEvents();
this._contentSession = new ContentSession(this._chromeSession, this._browser, this._pageId);
await this._contentSession.send('enable');
}
async screenshot(options) {
return await this._contentSession.send('screenshot', options);
}
async getBoundingBox(options) {
return await this._contentSession.send('getBoundingBox', options);
}
async getContentQuads(options) {
return await this._contentSession.send('getContentQuads', options);
}
/**
* @param {{frameId: string, url: string}} options
*/
async navigate(options) {
return await this._contentSession.send('navigate', options);
}
/**
* @param {{frameId: string, url: string}} options
*/
async goBack(options) {
return await this._contentSession.send('goBack', options);
}
/**
* @param {{frameId: string, url: string}} options
*/
async goForward(options) {
return await this._contentSession.send('goForward', options);
}
/**
* @param {{frameId: string, url: string}} options
*/
async reload(options) {
return await this._contentSession.send('reload', options);
}
/**
* @param {{functionText: String, frameId: String}} options
* @return {!Promise<*>}
*/
async evaluate(options) {
return await this._contentSession.send('evaluate', options);
}
async getObjectProperties(options) {
return await this._contentSession.send('getObjectProperties', options);
}
async addScriptToEvaluateOnNewDocument(options) {
return await this._contentSession.send('addScriptToEvaluateOnNewDocument', options);
}
async removeScriptToEvaluateOnNewDocument(options) {
return await this._contentSession.send('removeScriptToEvaluateOnNewDocument', options);
}
async disposeObject(options) {
return await this._contentSession.send('disposeObject', options);
}
async dispatchKeyEvent(options) {
return await this._contentSession.send('dispatchKeyEvent', options);
}
async dispatchMouseEvent(options) {
return await this._contentSession.send('dispatchMouseEvent', options);
}
async insertText(options) {
return await this._contentSession.send('insertText', options);
}
async handleDialog({dialogId, accept, promptText}) {
const dialog = this._dialogs.get(dialogId);
if (!dialog)
throw new Error('Failed to find dialog with id = ' + dialogId);
if (accept)
dialog.accept(promptText);
else
dialog.dismiss();
}
dispose() {
this._browser.removeProgressListener(this);
if (this._contentSession) {
this._contentSession.dispose();
this._contentSession = null;
}
}
}
class ContentSession {
constructor(chromeSession, browser, pageId) {
this._chromeSession = chromeSession;
this._browser = browser;
this._pageId = pageId;
this._messageId = 0;
this._pendingMessages = new Map();
this._sessionId = helper.generateId();
this._browser.messageManager.sendAsyncMessage('juggler:create-content-session', this._sessionId);
this._eventListeners = [
helper.addMessageListener(this._browser.messageManager, this._sessionId, {
receiveMessage: message => this._onMessage(message)
}),
];
}
dispose() {
helper.removeListeners(this._eventListeners);
for (const {resolve, reject} of this._pendingMessages.values())
reject(new Error('Page closed.'));
this._pendingMessages.clear();
}
/**
* @param {string} methodName
* @param {*} params
* @return {!Promise<*>}
*/
send(methodName, params) {
const id = ++this._messageId;
const promise = new Promise((resolve, reject) => {
this._pendingMessages.set(id, {resolve, reject});
});
this._browser.messageManager.sendAsyncMessage(this._sessionId, {id, methodName, params});
return promise;
}
_onMessage({data}) {
if (data.id) {
let id = data.id;
const {resolve, reject} = this._pendingMessages.get(data.id);
this._pendingMessages.delete(data.id);
if (data.error)
reject(new Error(data.error));
else
resolve(data.result);
} else {
const {
eventName,
params = {}
} = data;
params.pageId = this._pageId;
this._chromeSession.emitEvent(eventName, params);
}
}
}
class Dialog {
static createIfSupported(element) {
const type = element.Dialog.args.promptType;
switch (type) {
case 'alert':
case 'prompt':
case 'confirm':
return new Dialog(element, type);
case 'confirmEx':
return new Dialog(element, 'beforeunload');
default:
return null;
};
}
constructor(element, type) {
this._id = helper.generateId();
this._type = type;
this._element = element;
}
id() {
return this._id;
}
message() {
return this._element.ui.infoBody.textContent;
}
type() {
return this._type;
}
element() {
return this._element;
}
dismiss() {
if (this._element.ui.button1)
this._element.ui.button1.click();
else
this._element.ui.button0.click();
}
defaultValue() {
return this._element.ui.loginTextbox.value;
}
accept(promptValue) {
if (typeof promptValue === 'string' && this._type === 'prompt')
this._element.ui.loginTextbox.value = promptValue;
this._element.ui.button0.click();
}
}
var EXPORTED_SYMBOLS = ['PageHandler'];
this.PageHandler = PageHandler;

View File

@ -1,371 +0,0 @@
const t = {
String: x => typeof x === 'string' || typeof x === 'String',
Number: x => typeof x === 'number',
Boolean: x => typeof x === 'boolean',
Null: x => Object.is(x, null),
Enum: values => x => values.indexOf(x) !== -1,
Undefined: x => Object.is(x, undefined),
Or: (...schemes) => x => schemes.some(scheme => checkScheme(scheme, x)),
Either: (...schemes) => x => schemes.map(scheme => checkScheme(scheme, x)).reduce((acc, x) => acc + (x ? 1 : 0)) === 1,
Array: scheme => x => Array.isArray(x) && x.every(element => checkScheme(scheme, element)),
Nullable: scheme => x => Object.is(x, null) || checkScheme(scheme, x),
Optional: scheme => x => Object.is(x, undefined) || checkScheme(scheme, x),
Any: x => true,
}
const RemoteObject = t.Either(
{
type: 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.String,
},
{
unserializableValue: t.Enum(['Infinity', '-Infinity', '-0', 'NaN']),
},
{
value: t.Any
},
);
const DOMPoint = {
x: t.Number,
y: t.Number,
};
const DOMQuad = {
p1: DOMPoint,
p2: DOMPoint,
p3: DOMPoint,
p4: DOMPoint
};
const protocol = {
methods: {
'Browser.getInfo': {
returns: {
userAgent: t.String,
version: t.String,
},
},
'Browser.setIgnoreHTTPSErrors': {
params: {
enabled: t.Boolean,
},
},
'Browser.newPage': {
returns: {
pageId: t.String,
}
},
'Browser.closePage': {
params: {
pageId: t.String,
},
},
'Page.enable': {
params: {
pageId: t.String,
},
},
'Page.setViewport': {
params: {
pageId: t.String,
viewport: t.Nullable({
width: t.Number,
height: t.Number,
deviceScaleFactor: t.Number,
isMobile: t.Boolean,
hasTouch: t.Boolean,
isLandscape: t.Boolean,
}),
},
},
'Page.evaluate': {
params: t.Either({
pageId: t.String,
frameId: t.String,
functionText: t.String,
returnByValue: t.Optional(t.Boolean),
args: t.Array(t.Either(
{ objectId: t.String },
{ unserializableValue: t.Enum(['Infinity', '-Infinity', '-0', 'NaN']) },
{ value: t.Any },
)),
}, {
pageId: t.String,
frameId: t.String,
script: t.String,
returnByValue: t.Optional(t.Boolean),
}),
returns: {
result: t.Optional(RemoteObject),
exceptionDetails: t.Optional({
text: t.Optional(t.String),
stack: t.Optional(t.String),
value: t.Optional(t.Any),
}),
}
},
'Page.addScriptToEvaluateOnNewDocument': {
params: {
pageId: t.String,
script: t.String,
},
returns: {
scriptId: t.String,
}
},
'Page.removeScriptToEvaluateOnNewDocument': {
params: {
pageId: t.String,
scriptId: t.String,
},
},
'Page.disposeObject': {
params: {
pageId: t.String,
frameId: t.String,
objectId: t.String,
},
},
'Page.getObjectProperties': {
params: {
pageId: t.String,
frameId: t.String,
objectId: t.String,
},
returns: {
properties: t.Array({
name: t.String,
value: RemoteObject,
}),
}
},
'Page.navigate': {
params: {
pageId: t.String,
frameId: t.String,
url: t.String,
},
returns: {
navigationId: t.Nullable(t.String),
navigationURL: t.Nullable(t.String),
}
},
'Page.goBack': {
params: {
pageId: t.String,
frameId: t.String,
},
returns: {
navigationId: t.Nullable(t.String),
navigationURL: t.Nullable(t.String),
}
},
'Page.goForward': {
params: {
pageId: t.String,
frameId: t.String,
},
returns: {
navigationId: t.Nullable(t.String),
navigationURL: t.Nullable(t.String),
}
},
'Page.reload': {
params: {
pageId: t.String,
frameId: t.String,
},
returns: {
navigationId: t.String,
navigationURL: t.String,
}
},
'Page.getBoundingBox': {
params: {
pageId: t.String,
frameId: t.String,
objectId: t.String,
},
returns: t.Nullable({
x: t.Number,
y: t.Number,
width: t.Number,
height: t.Number,
}),
},
'Page.screenshot': {
params: {
pageId: t.String,
mimeType: t.Enum(['image/png', 'image/jpeg']),
fullPage: t.Optional(t.Boolean),
clip: t.Optional({
x: t.Number,
y: t.Number,
width: t.Number,
height: t.Number,
})
},
returns: {
data: t.String,
}
},
'Page.getContentQuads': {
params: {
pageId: t.String,
frameId: t.String,
objectId: t.String,
},
returns: {
quads: t.Array(DOMQuad),
},
},
'Page.dispatchKeyEvent': {
params: {
pageId: t.String,
type: t.String,
key: t.String,
keyCode: t.Number,
location: t.Number,
code: t.String,
repeat: t.Boolean,
}
},
'Page.dispatchMouseEvent': {
params: {
pageId: t.String,
type: t.String,
button: t.Number,
x: t.Number,
y: t.Number,
modifiers: t.Number,
clickCount: t.Optional(t.Number),
buttons: t.Number,
}
},
'Page.insertText': {
params: {
pageId: t.String,
text: t.String,
}
},
'Page.handleDialog': {
params: {
pageId: t.String,
dialogId: t.String,
accept: t.Boolean,
promptText: t.Optional(t.String),
},
},
},
events: {
'Browser.tabOpened': {
pageId: t.String,
url: t.String,
},
'Browser.tabClosed': { pageId: t.String, },
'Browser.tabNavigated': {
pageId: t.String,
url: t.String
},
'Page.eventFired': {
pageId: t.String,
frameId: t.String,
name: t.Enum(['load', 'DOMContentLoaded']),
},
'Page.uncaughtError': {
pageId: t.String,
frameId: t.String,
message: t.String,
stack: t.String,
},
'Page.frameAttached': {
pageId: t.String,
frameId: t.String,
parentFrameId: t.Optional(t.String),
},
'Page.frameDetached': {
pageId: t.String,
frameId: t.String,
},
'Page.navigationStarted': {
pageId: t.String,
frameId: t.String,
navigationId: t.String,
url: t.String,
},
'Page.navigationCommitted': {
pageId: t.String,
frameId: t.String,
navigationId: t.String,
url: t.String,
// frame.id or frame.name
name: t.String,
},
'Page.navigationAborted': {
pageId: t.String,
frameId: t.String,
navigationId: t.String,
errorText: t.String,
},
'Page.sameDocumentNavigation': {
pageId: t.String,
frameId: t.String,
url: t.String,
},
'Page.consoleAPICalled': {
pageId: t.String,
frameId: t.String,
args: t.Array(RemoteObject),
type: t.String,
},
'Page.dialogOpened': {
pageId: t.String,
dialogId: t.String,
type: t.Enum(['prompt', 'alert', 'confirm', 'beforeunload']),
message: t.String,
defaultValue: t.Optional(t.String),
},
'Page.dialogClosed': {
pageId: t.String,
dialogId: t.String,
},
},
}
function checkScheme(scheme, x, details = {}, path = []) {
if (typeof scheme === 'object') {
for (const [propertyName, check] of Object.entries(scheme)) {
path.push(propertyName);
const result = checkScheme(check, x[propertyName], details, path);
path.pop();
if (!result)
return false;
}
for (const propertyName of Object.keys(x)) {
if (!scheme[propertyName]) {
path.push(propertyName);
details.propertyName = path.join('.');
details.propertyValue = x[propertyName];
details.errorType = 'extra';
return false;
}
}
return true;
}
const result = scheme(x);
if (!result) {
details.propertyName = path.join('.');
details.propertyValue = x;
details.errorType = 'unsupported';
}
return result;
}
this.protocol = protocol;
this.checkScheme = checkScheme;
this.EXPORTED_SYMBOLS = ['protocol', 'checkScheme'];

View File

@ -1,63 +0,0 @@
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {TCPListener} = ChromeUtils.import("chrome://juggler/content/server/server.js");
const {ChromeSession} = ChromeUtils.import("chrome://juggler/content/ChromeSession.js");
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const FRAME_SCRIPT = "chrome://juggler/content/content/main.js";
// Command Line Handler
function CommandLineHandler() {
this._port = 0;
};
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);
if (!jugglerFlag || isNaN(jugglerFlag))
return;
this._port = parseInt(jugglerFlag, 10);
Services.obs.addObserver(this, 'sessionstore-windows-restored');
},
observe: function(subject, topic) {
Services.obs.removeObserver(this, 'sessionstore-windows-restored');
this._server = new TCPListener();
this._sessions = new Map();
this._server.onconnectioncreated = connection => {
this._sessions.set(connection, new ChromeSession(connection));
}
this._server.onconnectionclosed = connection => {
this._sessions.delete(connection);
}
const runningPort = this._server.start(this._port);
Services.mm.loadFrameScript(FRAME_SCRIPT, true /* aAllowDelayedLoad */);
dump('Juggler listening on ' + runningPort + '\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 = XPCOMUtils.generateNSGetFactory([CommandLineHandler]);

View File

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

View File

@ -1,9 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
EXTRA_COMPONENTS += [
"juggler.js",
"juggler.manifest",
]

View File

@ -1,53 +0,0 @@
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {RuntimeAgent} = ChromeUtils.import('chrome://juggler/content/content/RuntimeAgent.js');
const {PageAgent} = ChromeUtils.import('chrome://juggler/content/content/PageAgent.js');
const helper = new Helper();
class ContentSession {
/**
* @param {string} sessionId
* @param {!ContentFrameMessageManager} messageManager
* @param {!FrameTree} frameTree
*/
constructor(sessionId, messageManager, frameTree, scrollbarManager) {
this._sessionId = sessionId;
this._runtimeAgent = new RuntimeAgent();
this._messageManager = messageManager;
this._pageAgent = new PageAgent(this, this._runtimeAgent, frameTree, scrollbarManager);
this._eventListeners = [
helper.addMessageListener(messageManager, this._sessionId, this._onMessage.bind(this)),
];
}
emitEvent(eventName, params) {
this._messageManager.sendAsyncMessage(this._sessionId, {eventName, params});
}
mm() {
return this._messageManager;
}
async _onMessage(msg) {
const id = msg.data.id;
try {
const handler = this._pageAgent[msg.data.methodName];
if (!handler)
throw new Error('unknown method: "' + msg.data.methodName + '"');
const result = await handler.call(this._pageAgent, msg.data.params);
this._messageManager.sendAsyncMessage(this._sessionId, {id, result});
} catch (e) {
this._messageManager.sendAsyncMessage(this._sessionId, {id, error: e.message + '\n' + e.stack});
}
}
dispose() {
helper.removeListeners(this._eventListeners);
this._pageAgent.dispose();
this._runtimeAgent.dispose();
}
}
var EXPORTED_SYMBOLS = ['ContentSession'];
this.ContentSession = ContentSession;

View File

@ -1,287 +0,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 {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
const helper = new Helper();
class FrameTree {
constructor(rootDocShell) {
EventEmitter.decorate(this);
this._docShellToFrame = new Map();
this._frameIdToFrame = new Map();
this._mainFrame = this._createFrame(rootDocShell);
const webProgress = rootDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
this.QueryInterface = ChromeUtils.generateQI([
Ci.nsIWebProgressListener,
Ci.nsIWebProgressListener2,
Ci.nsISupportsWeakReference,
]);
const flags = Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT |
Ci.nsIWebProgress.NOTIFY_FRAME_LOCATION;
this._eventListeners = [
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),
];
}
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() {
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;
}
const isStart = flag & Ci.nsIWebProgressListener.STATE_START;
const isTransferring = flag & Ci.nsIWebProgressListener.STATE_TRANSFERRING;
const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP;
if (isStart) {
// Starting a new navigation.
frame._pendingNavigationId = helper.generateId();
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);
} else if (isStop && frame._pendingNavigationId && status) {
// Navigation is aborted.
const navigationId = frame._pendingNavigationId;
frame._pendingNavigationId = null;
frame._pendingNavigationURL = null;
this.emit(FrameTree.Events.NavigationAborted, frame, navigationId, getErrorStatusText(status));
}
}
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, docShell, parentFrame);
this._docShellToFrame.set(docShell, frame);
this._frameIdToFrame.set(frame.id(), frame);
this.emit(FrameTree.Events.FrameAttached, frame);
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;
this.emit(FrameTree.Events.FrameDetached, frame);
}
}
FrameTree.Events = {
FrameAttached: 'frameattached',
FrameDetached: 'framedetached',
NavigationStarted: 'navigationstarted',
NavigationCommitted: 'navigationcommitted',
NavigationAborted: 'navigationaborted',
SameDocumentNavigation: 'samedocumentnavigation',
};
class Frame {
constructor(frameTree, docShell, parentFrame) {
this._frameTree = frameTree;
this._docShell = docShell;
this._children = new Set();
this._frameId = helper.generateId();
this._parentFrame = null;
this._url = '';
if (parentFrame) {
this._parentFrame = parentFrame;
parentFrame._children.add(this);
}
this._lastCommittedNavigationId = null;
this._pendingNavigationId = null;
this._pendingNavigationURL = null;
this._textInputProcessor = null;
}
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;
}
}
function getErrorStatusText(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>';
}
var EXPORTED_SYMBOLS = ['FrameTree'];
this.FrameTree = FrameTree;

View File

@ -1,460 +0,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 {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
const helper = new Helper();
class PageAgent {
constructor(session, runtimeAgent, frameTree, scrollbarManager) {
this._session = session;
this._runtime = runtimeAgent;
this._frameTree = frameTree;
this._scrollbarManager = scrollbarManager;
this._frameToExecutionContext = new Map();
this._scriptsToEvaluateOnNewDocument = new Map();
this._eventListeners = [];
this._enabled = false;
const docShell = frameTree.mainFrame().docShell();
this._initialDPPX = docShell.contentViewer.overrideDPPX;
this._customScrollbars = null;
}
async awaitViewportDimensions({width, height}) {
const win = this._frameTree.mainFrame().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();
}
});
});
}
async setViewport({deviceScaleFactor, isMobile, hasTouch}) {
const docShell = this._frameTree.mainFrame().docShell();
docShell.contentViewer.overrideDPPX = deviceScaleFactor || this._initialDPPX;
docShell.deviceSizeIsPageSize = isMobile;
docShell.touchEventsOverride = hasTouch ? Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED : Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_NONE;
this._scrollbarManager.setFloatingScrollbars(isMobile);
}
addScriptToEvaluateOnNewDocument({script}) {
const scriptId = helper.generateId();
this._scriptsToEvaluateOnNewDocument.set(scriptId, script);
return {scriptId};
}
removeScriptToEvaluateOnNewDocument({scriptId}) {
this._scriptsToEvaluateOnNewDocument.delete(scriptId);
}
enable() {
if (this._enabled)
return;
this._enabled = true;
this._eventListeners = [
helper.addObserver(this._consoleAPICalled.bind(this), "console-api-log-event"),
helper.addEventListener(this._session.mm(), 'DOMContentLoaded', this._onDOMContentLoaded.bind(this)),
helper.addEventListener(this._session.mm(), 'pageshow', this._onLoad.bind(this)),
helper.addEventListener(this._session.mm(), 'DOMWindowCreated', this._onDOMWindowCreated.bind(this)),
helper.addEventListener(this._session.mm(), 'error', this._onError.bind(this)),
helper.on(this._frameTree, 'frameattached', this._onFrameAttached.bind(this)),
helper.on(this._frameTree, 'framedetached', this._onFrameDetached.bind(this)),
helper.on(this._frameTree, 'navigationstarted', this._onNavigationStarted.bind(this)),
helper.on(this._frameTree, 'navigationcommitted', this._onNavigationCommitted.bind(this)),
helper.on(this._frameTree, 'navigationaborted', this._onNavigationAborted.bind(this)),
helper.on(this._frameTree, 'samedocumentnavigation', this._onSameDocumentNavigation.bind(this)),
];
// Dispatch frameAttached events for all initial frames
for (const frame of this._frameTree.frames()) {
this._onFrameAttached(frame);
if (frame.url())
this._onNavigationCommitted(frame);
if (frame.pendingNavigationId())
this._onNavigationStarted(frame);
}
}
_onDOMContentLoaded(event) {
const docShell = event.target.ownerGlobal.docShell;
const frame = this._frameTree.frameForDocShell(docShell);
if (!frame)
return;
this._session.emitEvent('Page.eventFired', {
frameId: frame.id(),
name: 'DOMContentLoaded',
});
}
_onError(errorEvent) {
const docShell = errorEvent.target.ownerGlobal.docShell;
const frame = this._frameTree.frameForDocShell(docShell);
if (!frame)
return;
this._session.emitEvent('Page.uncaughtError', {
frameId: frame.id(),
message: errorEvent.message,
stack: errorEvent.error.stack
});
}
_onLoad(event) {
const docShell = event.target.ownerGlobal.docShell;
const frame = this._frameTree.frameForDocShell(docShell);
if (!frame)
return;
this._session.emitEvent('Page.eventFired', {
frameId: frame.id(),
name: 'load'
});
}
_onNavigationStarted(frame) {
this._session.emitEvent('Page.navigationStarted', {
frameId: frame.id(),
navigationId: frame.pendingNavigationId(),
url: frame.pendingNavigationURL(),
});
}
_onNavigationAborted(frame, navigationId, errorText) {
this._session.emitEvent('Page.navigationAborted', {
frameId: frame.id(),
navigationId,
errorText,
});
}
_onSameDocumentNavigation(frame) {
this._session.emitEvent('Page.sameDocumentNavigation', {
frameId: frame.id(),
url: frame.url(),
});
}
_onNavigationCommitted(frame) {
const context = this._frameToExecutionContext.get(frame);
if (context) {
this._runtime.destroyExecutionContext(context);
this._frameToExecutionContext.delete(frame);
}
this._session.emitEvent('Page.navigationCommitted', {
frameId: frame.id(),
navigationId: frame.lastCommittedNavigationId(),
url: frame.url(),
name: frame.name(),
});
}
_onDOMWindowCreated(event) {
if (!this._scriptsToEvaluateOnNewDocument.size)
return;
const docShell = event.target.ownerGlobal.docShell;
const frame = this._frameTree.frameForDocShell(docShell);
if (!frame)
return;
const executionContext = this._ensureExecutionContext(frame);
for (const script of this._scriptsToEvaluateOnNewDocument.values()) {
try {
let result = executionContext.evaluateScript(script);
if (result && result.objectId)
executionContext.disposeObject(result.objectId);
} catch (e) {
}
}
}
_onFrameAttached(frame) {
this._session.emitEvent('Page.frameAttached', {
frameId: frame.id(),
parentFrameId: frame.parentFrame() ? frame.parentFrame().id() : undefined,
});
}
_onFrameDetached(frame) {
this._session.emitEvent('Page.frameDetached', {
frameId: frame.id(),
});
}
_ensureExecutionContext(frame) {
let executionContext = this._frameToExecutionContext.get(frame);
if (!executionContext) {
executionContext = this._runtime.createExecutionContext(frame.domWindow());
this._frameToExecutionContext.set(frame, executionContext);
}
return executionContext;
}
dispose() {
helper.removeListeners(this._eventListeners);
}
_consoleAPICalled({wrappedJSObject}, topic, data) {
const levelToType = {
'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 type = levelToType[wrappedJSObject.level];
if (!type) return;
let messageFrame = null;
for (const frame of this._frameTree.frames()) {
const domWindow = frame.domWindow();
if (domWindow && domWindow.windowUtils.currentInnerWindowID === wrappedJSObject.innerID) {
messageFrame = frame;
break;
}
}
if (!messageFrame)
return;
const executionContext = this._ensureExecutionContext(messageFrame);
const args = wrappedJSObject.arguments.map(arg => executionContext.rawValueToRemoteObject(arg));
this._session.emitEvent('Page.consoleAPICalled', {args, type, frameId: messageFrame.id()});
}
async navigate({frameId, url}) {
try {
const uri = NetUtil.newURI(url);
} catch (e) {
throw new Error(`Invalid url: "${url}"`);
}
const frame = this._frameTree.frame(frameId);
const docShell = frame.docShell();
docShell.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null /* referrer */, null /* postData */, null /* headers */);
return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()};
}
async reload({frameId, url}) {
const frame = this._frameTree.frame(frameId);
const docShell = frame.docShell();
docShell.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()};
}
async goBack({frameId, url}) {
const frame = this._frameTree.frame(frameId);
const docShell = frame.docShell();
if (!docShell.canGoBack)
return {navigationId: null, navigationURL: null};
docShell.goBack();
return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()};
}
async goForward({frameId, url}) {
const frame = this._frameTree.frame(frameId);
const docShell = frame.docShell();
if (!docShell.canGoForward)
return {navigationId: null, navigationURL: null};
docShell.goForward();
return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()};
}
async disposeObject({frameId, objectId}) {
const frame = this._frameTree.frame(frameId);
if (!frame)
throw new Error('Failed to find frame with id = ' + frameId);
const executionContext = this._ensureExecutionContext(frame);
return executionContext.disposeObject(objectId);
}
getContentQuads({objectId, frameId}) {
const frame = this._frameTree.frame(frameId);
if (!frame)
throw new Error('Failed to find frame with id = ' + frameId);
const executionContext = this._ensureExecutionContext(frame);
const unsafeObject = executionContext.unsafeObject(objectId);
if (!unsafeObject.getBoxQuads)
throw new Error('RemoteObject is not a node');
const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document}).map(quad => {
return {
p1: {x: quad.p1.x, y: quad.p1.y},
p2: {x: quad.p2.x, y: quad.p2.y},
p3: {x: quad.p3.x, y: quad.p3.y},
p4: {x: quad.p4.x, y: quad.p4.y},
};
});
return {quads};
}
async getBoundingBox({frameId, objectId}) {
const frame = this._frameTree.frame(frameId);
if (!frame)
throw new Error('Failed to find frame with id = ' + frameId);
const executionContext = this._ensureExecutionContext(frame);
const unsafeObject = executionContext.unsafeObject(objectId);
if (!unsafeObject.getBoxQuads)
throw new Error('RemoteObject is not a node');
const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document});
if (!quads.length)
return null;
let x1 = Infinity;
let y1 = Infinity;
let x2 = -Infinity;
let y2 = -Infinity;
for (const quad of quads) {
const boundingBox = quad.getBounds();
x1 = Math.min(boundingBox.x, x1);
y1 = Math.min(boundingBox.y, y1);
x2 = Math.max(boundingBox.x + boundingBox.width, x2);
y2 = Math.max(boundingBox.y + boundingBox.height, y2);
}
return {x: x1 + frame.domWindow().scrollX, y: y1 + frame.domWindow().scrollY, width: x2 - x1, height: y2 - y1};
}
async evaluate({frameId, functionText, args, script, returnByValue}) {
const frame = this._frameTree.frame(frameId);
if (!frame)
throw new Error('Failed to find frame with id = ' + frameId);
const executionContext = this._ensureExecutionContext(frame);
const exceptionDetails = {};
let result = null;
if (script)
result = await executionContext.evaluateScript(script, exceptionDetails);
else
result = await executionContext.evaluateFunction(functionText, args, exceptionDetails);
if (!result)
return {exceptionDetails};
let isNode = undefined;
if (returnByValue)
result = executionContext.ensureSerializedToValue(result);
return {result};
}
async getObjectProperties({frameId, objectId}) {
const frame = this._frameTree.frame(frameId);
if (!frame)
throw new Error('Failed to find frame with id = ' + frameId);
const executionContext = this._ensureExecutionContext(frame);
return {properties: executionContext.getObjectProperties(objectId)};
}
async screenshot({mimeType, fullPage, frameId, objectId, clip}) {
const content = this._session.mm().content;
if (clip) {
const data = takeScreenshot(content, clip.x, clip.y, clip.width, clip.height, mimeType);
return {data};
}
if (fullPage) {
const rect = content.document.documentElement.getBoundingClientRect();
const width = content.innerWidth + content.scrollMaxX - content.scrollMinX;
const height = content.innerHeight + content.scrollMaxY - content.scrollMinY;
const data = takeScreenshot(content, 0, 0, width, height, mimeType);
return {data};
}
const data = takeScreenshot(content, content.scrollX, content.scrollY, content.innerWidth, content.innerHeight, mimeType);
return {data};
}
async dispatchKeyEvent({type, keyCode, code, key, repeat, location}) {
const frame = this._frameTree.mainFrame();
const tip = frame.textInputProcessor();
let keyEvent = new (frame.domWindow().KeyboardEvent)("", {
key,
code,
location,
repeat,
keyCode
});
const flags = 0;
if (type === 'keydown')
tip.keydown(keyEvent, flags);
else if (type === 'keyup')
tip.keyup(keyEvent, flags);
else
throw new Error(`Unknown type ${type}`);
}
async dispatchMouseEvent({type, x, y, button, clickCount, modifiers, buttons}) {
const frame = this._frameTree.mainFrame();
frame.domWindow().windowUtils.sendMouseEvent(
type,
x,
y,
button,
clickCount,
modifiers,
false /*aIgnoreRootScrollFrame*/,
undefined /*pressure*/,
undefined /*inputSource*/,
undefined /*isDOMEventSynthesized*/,
undefined /*isWidgetEventSynthesized*/,
buttons);
if (type === 'mousedown' && button === 2) {
frame.domWindow().windowUtils.sendMouseEvent(
'contextmenu',
x,
y,
button,
clickCount,
modifiers,
false /*aIgnoreRootScrollFrame*/,
undefined /*pressure*/,
undefined /*inputSource*/,
undefined /*isDOMEventSynthesized*/,
undefined /*isWidgetEventSynthesized*/,
buttons);
}
}
async insertText({text}) {
const frame = this._frameTree.mainFrame();
frame.textInputProcessor().commitCompositionWith(text);
}
}
function takeScreenshot(win, left, top, width, height, mimeType) {
const MAX_SKIA_DIMENSIONS = 32767;
const scale = win.devicePixelRatio;
const canvasWidth = width * scale;
const canvasHeight = height * scale;
if (canvasWidth > MAX_SKIA_DIMENSIONS || canvasHeight > MAX_SKIA_DIMENSIONS)
throw new Error('Cannot take screenshot larger than ' + MAX_SKIA_DIMENSIONS);
const canvas = win.document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
let ctx = canvas.getContext('2d');
ctx.scale(scale, scale);
ctx.drawWindow(win, left, top, width, height, 'rgb(255,255,255)', ctx.DRAWWINDOW_DRAW_CARET);
const dataURL = canvas.toDataURL(mimeType);
return dataURL.substring(dataURL.indexOf(',') + 1);
};
var EXPORTED_SYMBOLS = ['PageAgent'];
this.PageAgent = PageAgent;

View File

@ -1,275 +0,0 @@
"use strict";
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {addDebuggerToGlobal} = ChromeUtils.import("resource://gre/modules/jsdebugger.jsm", {});
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
addDebuggerToGlobal(Cu.getGlobalForObject(this));
const helper = new Helper();
class RuntimeAgent {
constructor() {
this._debugger = new Debugger();
this._pendingPromises = new Map();
}
dispose() {}
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) {
return new ExecutionContext(this, domWindow, this._debugger.addDebuggee(domWindow));
}
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._domWindow);
}
}
class ExecutionContext {
constructor(runtime, DOMWindow, global) {
this._runtime = runtime;
this._domWindow = DOMWindow;
this._global = global;
this._remoteObjects = new Map();
}
async evaluateScript(script, exceptionDetails = {}) {
const userInputHelper = this._domWindow.windowUtils.setHandlingUserInput(true);
let {success, obj} = this._getResult(this._global.executeInGlobal(script), exceptionDetails);
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.windowUtils.setHandlingUserInput(true);
let {success, obj} = this._getResult(funEvaluation.obj.apply(null, args), exceptionDetails);
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))
throw new Error('Cannot find object with id = ' + objectId);
return this._remoteObjects.get(objectId).unsafeDereference();
}
rawValueToRemoteObject(rawValue) {
const debuggerObj = this._global.makeDebuggeeValue(rawValue);
return this._createRemoteObject(debuggerObj);
}
_createRemoteObject(debuggerObj) {
if (debuggerObj instanceof Debugger.Object) {
const objectId = helper.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 (rawObj instanceof this._domWindow.Node)
subtype = 'node';
else if (rawObj instanceof this._domWindow.RegExp)
subtype = 'regexp';
else if (rawObj instanceof this._domWindow.Date)
subtype = 'date';
else if (rawObj instanceof this._domWindow.Map)
subtype = 'map';
else if (rawObj instanceof this._domWindow.Set)
subtype = 'set';
else if (rawObj instanceof this._domWindow.WeakMap)
subtype = 'weakmap';
else if (rawObj instanceof this._domWindow.WeakSet)
subtype = 'weakset';
else if (rawObj instanceof this._domWindow.Error)
subtype = 'error';
else if (rawObj instanceof this._domWindow.Promise)
subtype = 'promise';
else if ((rawObj instanceof this._domWindow.Int8Array) || (rawObj instanceof this._domWindow.Uint8Array) ||
(rawObj instanceof this._domWindow.Uint8ClampedArray) || (rawObj instanceof this._domWindow.Int16Array) ||
(rawObj instanceof this._domWindow.Uint16Array) || (rawObj instanceof this._domWindow.Int32Array) ||
(rawObj instanceof this._domWindow.Uint32Array) || (rawObj instanceof this._domWindow.Float32Array) ||
(rawObj instanceof this._domWindow.Float64Array)) {
subtype = 'typedarray';
}
const isNode = debuggerObj.unsafeDereference() instanceof this._domWindow.Node;
return {objectId, type, subtype};
}
if (typeof debuggerObj === 'symbol') {
const objectId = helper.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;
const properties = {};
for (let [key, value] of Object.entries(obj)) {
properties[key] = {
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('JSON.stringify(e)', {e: obj});
if (result.throw)
throw new Error('Object is not serializable');
return 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};
}
}
var EXPORTED_SYMBOLS = ['RuntimeAgent'];
this.RuntimeAgent = RuntimeAgent;

View File

@ -1,63 +0,0 @@
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
const Cc = Components.classes;
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const HIDDEN_SCROLLBARS = Services.io.newURI('chrome://juggler/content/content/hidden-scrollbars.css');
const FLOATING_SCROLLBARS = Services.io.newURI('chrome://juggler/content/content/floating-scrollbars.css');
const isHeadless = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless;
const helper = new Helper();
class ScrollbarManager {
constructor(mm, docShell) {
this._docShell = docShell;
this._customScrollbars = null;
if (isHeadless)
this._setCustomScrollbars(HIDDEN_SCROLLBARS);
this._eventListeners = [
helper.addEventListener(mm, 'DOMWindowCreated', this._onDOMWindowCreated.bind(this)),
];
}
setFloatingScrollbars(enabled) {
if (this._customScrollbars === HIDDEN_SCROLLBARS)
return;
this._setCustomScrollbars(enabled ? FLOATING_SCROLLBARS : null);
}
_setCustomScrollbars(customScrollbars) {
if (this._customScrollbars === customScrollbars)
return;
const windowUtils = this._docShell.domWindow.windowUtils;
if (this._customScrollbars)
windowUtils.removeSheet(this._customScrollbars, windowUtils.AGENT_SHEET);
this._customScrollbars = customScrollbars;
if (this._customScrollbars)
windowUtils.loadSheet(this._customScrollbars, windowUtils.AGENT_SHEET);
}
dispose() {
this._setCustomScrollbars(null);
helper.removeListeners(this._eventListeners);
}
_onDOMWindowCreated(event) {
const docShell = event.target.ownerGlobal.docShell;
if (docShell === this._docShell)
return;
const windowUtils = docShell.domWindow.windowUtils;
if (this._customScrollbars) {
windowUtils.loadSheet(this._customScrollbars, windowUtils.AGENT_SHEET);
}
}
}
var EXPORTED_SYMBOLS = ['ScrollbarManager'];
this.ScrollbarManager = ScrollbarManager;

View File

@ -1,47 +0,0 @@
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
@namespace html url("http://www.w3.org/1999/xhtml");
/* Restrict all styles to `*|*:not(html|select) > scrollbar` so that scrollbars
inside a <select> are excluded (including them hides the select arrow on
Windows). We want to include both the root scrollbars for the document as
well as any overflow: scroll elements within the page, while excluding
<select>. */
*|*:not(html|select) > scrollbar {
-moz-appearance: none !important;
position: relative;
background-color: transparent;
background-image: none;
z-index: 2147483647;
padding: 2px;
border: none;
}
/* Scrollbar code will reset the margin to the correct side depending on
where layout actually puts the scrollbar */
*|*:not(html|select) > scrollbar[orient="vertical"] {
margin-left: -10px;
min-width: 10px;
max-width: 10px;
}
*|*:not(html|select) > scrollbar[orient="horizontal"] {
margin-top: -10px;
min-height: 10px;
max-height: 10px;
}
*|*:not(html|select) > scrollbar slider {
-moz-appearance: none !important;
}
*|*:not(html|select) > scrollbar thumb {
-moz-appearance: none !important;
background-color: rgba(0,0,0,0.2);
border-width: 0px !important;
border-radius: 3px !important;
}
*|*:not(html|select) > scrollbar scrollbarbutton,
*|*:not(html|select) > scrollbar gripper {
display: none;
}

View File

@ -1,13 +0,0 @@
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
@namespace html url("http://www.w3.org/1999/xhtml");
/* Restrict all styles to `*|*:not(html|select) > scrollbar` so that scrollbars
inside a <select> are excluded (including them hides the select arrow on
Windows). We want to include both the root scrollbars for the document as
well as any overflow: scroll elements within the page, while excluding
<select>. */
*|*:not(html|select) > scrollbar {
-moz-appearance: none !important;
display: none;
}

View File

@ -1,27 +0,0 @@
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {ContentSession} = ChromeUtils.import('chrome://juggler/content/content/ContentSession.js');
const {FrameTree} = ChromeUtils.import('chrome://juggler/content/content/FrameTree.js');
const {ScrollbarManager} = ChromeUtils.import('chrome://juggler/content/content/ScrollbarManager.js');
const sessions = new Map();
const frameTree = new FrameTree(docShell);
const scrollbarManager = new ScrollbarManager(this, docShell);
const helper = new Helper();
const gListeners = [
helper.addMessageListener(this, 'juggler:create-content-session', msg => {
const sessionId = msg.data;
sessions.set(sessionId, new ContentSession(sessionId, this, frameTree, scrollbarManager));
}),
helper.addEventListener(this, 'unload', msg => {
helper.removeListeners(gListeners);
for (const session of sessions.values())
session.dispose();
sessions.clear();
scrollbarManager.dispose();
frameTree.dispose();
}),
];

View File

@ -1,25 +0,0 @@
# 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/ChromeSession.js (ChromeSession.js)
content/InsecureSweepingOverride.js (InsecureSweepingOverride.js)
content/Protocol.js (Protocol.js)
content/Helper.js (Helper.js)
content/PageHandler.jsm (PageHandler.jsm)
content/BrowserHandler.jsm (BrowserHandler.jsm)
content/content/main.js (content/main.js)
content/content/ContentSession.js (content/ContentSession.js)
content/content/FrameTree.js (content/FrameTree.js)
content/content/PageAgent.js (content/PageAgent.js)
content/content/RuntimeAgent.js (content/RuntimeAgent.js)
content/content/ScrollbarManager.js (content/ScrollbarManager.js)
content/content/floating-scrollbars.css (content/floating-scrollbars.css)
content/content/hidden-scrollbars.css (content/hidden-scrollbars.css)
content/server/server.js (server/server.js)
content/server/transport.js (server/transport.js)
content/server/stream-utils.js (server/stream-utils.js)
content/server/packets.js (server/packets.js)

View File

@ -1,15 +0,0 @@
# 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"]
JAR_MANIFESTS += ["jar.mn"]
#JS_PREFERENCE_FILES += ["prefs/marionette.js"]
#MARIONETTE_UNIT_MANIFESTS += ["harness/marionette_harness/tests/unit/unit-tests.ini"]
#XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"]
with Files("**"):
BUG_COMPONENT = ("Testing", "Juggler")

View File

@ -1,407 +0,0 @@
/* 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";
/**
* Packets contain read / write functionality for the different packet types
* supported by the debugging protocol, so that a transport can focus on
* delivery and queue management without worrying too much about the specific
* packet types.
*
* They are intended to be "one use only", so a new packet should be
* instantiated for each incoming or outgoing packet.
*
* A complete Packet type should expose at least the following:
* * read(stream, scriptableStream)
* Called when the input stream has data to read
* * write(stream)
* Called when the output stream is ready to write
* * get done()
* Returns true once the packet is done being read / written
* * destroy()
* Called to clean up at the end of use
*/
const {StreamUtils} =
ChromeUtils.import("chrome://juggler/content/server/stream-utils.js", {});
const unicodeConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
unicodeConverter.charset = "UTF-8";
const defer = function() {
let deferred = {
promise: new Promise((resolve, reject) => {
deferred.resolve = resolve;
deferred.reject = reject;
}),
};
return deferred;
};
this.EXPORTED_SYMBOLS = ["RawPacket", "Packet", "JSONPacket", "BulkPacket"];
// The transport's previous check ensured the header length did not
// exceed 20 characters. Here, we opt for the somewhat smaller, but still
// large limit of 1 TiB.
const PACKET_LENGTH_MAX = Math.pow(2, 40);
/**
* A generic Packet processing object (extended by two subtypes below).
*
* @class
*/
function Packet(transport) {
this._transport = transport;
this._length = 0;
}
/**
* Attempt to initialize a new Packet based on the incoming packet header
* we've received so far. We try each of the types in succession, trying
* JSON packets first since they are much more common.
*
* @param {string} header
* Packet header string to attempt parsing.
* @param {DebuggerTransport} transport
* Transport instance that will own the packet.
*
* @return {Packet}
* Parsed packet of the matching type, or null if no types matched.
*/
Packet.fromHeader = function(header, transport) {
return JSONPacket.fromHeader(header, transport) ||
BulkPacket.fromHeader(header, transport);
};
Packet.prototype = {
get length() {
return this._length;
},
set length(length) {
if (length > PACKET_LENGTH_MAX) {
throw new Error("Packet length " + length +
" exceeds the max length of " + PACKET_LENGTH_MAX);
}
this._length = length;
},
destroy() {
this._transport = null;
},
};
/**
* With a JSON packet (the typical packet type sent via the transport),
* data is transferred as a JSON packet serialized into a string,
* with the string length prepended to the packet, followed by a colon
* ([length]:[packet]). The contents of the JSON packet are specified in
* the Remote Debugging Protocol specification.
*
* @param {DebuggerTransport} transport
* Transport instance that will own the packet.
*/
function JSONPacket(transport) {
Packet.call(this, transport);
this._data = "";
this._done = false;
}
/**
* Attempt to initialize a new JSONPacket based on the incoming packet
* header we've received so far.
*
* @param {string} header
* Packet header string to attempt parsing.
* @param {DebuggerTransport} transport
* Transport instance that will own the packet.
*
* @return {JSONPacket}
* Parsed packet, or null if it's not a match.
*/
JSONPacket.fromHeader = function(header, transport) {
let match = this.HEADER_PATTERN.exec(header);
if (!match) {
return null;
}
let packet = new JSONPacket(transport);
packet.length = +match[1];
return packet;
};
JSONPacket.HEADER_PATTERN = /^(\d+):$/;
JSONPacket.prototype = Object.create(Packet.prototype);
Object.defineProperty(JSONPacket.prototype, "object", {
/**
* Gets the object (not the serialized string) being read or written.
*/
get() {
return this._object;
},
/**
* Sets the object to be sent when write() is called.
*/
set(object) {
this._object = object;
let data = JSON.stringify(object);
this._data = unicodeConverter.ConvertFromUnicode(data);
this.length = this._data.length;
},
});
JSONPacket.prototype.read = function(stream, scriptableStream) {
// Read in more packet data.
this._readData(stream, scriptableStream);
if (!this.done) {
// Don't have a complete packet yet.
return;
}
let json = this._data;
try {
json = unicodeConverter.ConvertToUnicode(json);
this._object = JSON.parse(json);
} catch (e) {
let msg = "Error parsing incoming packet: " + json + " (" + e +
" - " + e.stack + ")";
console.error(msg);
dump(msg + "\n");
return;
}
this._transport._onJSONObjectReady(this._object);
};
JSONPacket.prototype._readData = function(stream, scriptableStream) {
let bytesToRead = Math.min(
this.length - this._data.length,
stream.available());
this._data += scriptableStream.readBytes(bytesToRead);
this._done = this._data.length === this.length;
};
JSONPacket.prototype.write = function(stream) {
if (this._outgoing === undefined) {
// Format the serialized packet to a buffer
this._outgoing = this.length + ":" + this._data;
}
let written = stream.write(this._outgoing, this._outgoing.length);
this._outgoing = this._outgoing.slice(written);
this._done = !this._outgoing.length;
};
Object.defineProperty(JSONPacket.prototype, "done", {
get() {
return this._done;
},
});
JSONPacket.prototype.toString = function() {
return JSON.stringify(this._object, null, 2);
};
/**
* With a bulk packet, data is transferred by temporarily handing over
* the transport's input or output stream to the application layer for
* writing data directly. This can be much faster for large data sets,
* and avoids various stages of copies and data duplication inherent in
* the JSON packet type. The bulk packet looks like:
*
* bulk [actor] [type] [length]:[data]
*
* The interpretation of the data portion depends on the kind of actor and
* the packet's type. See the Remote Debugging Protocol Stream Transport
* spec for more details.
*
* @param {DebuggerTransport} transport
* Transport instance that will own the packet.
*/
function BulkPacket(transport) {
Packet.call(this, transport);
this._done = false;
this._readyForWriting = defer();
}
/**
* Attempt to initialize a new BulkPacket based on the incoming packet
* header we've received so far.
*
* @param {string} header
* Packet header string to attempt parsing.
* @param {DebuggerTransport} transport
* Transport instance that will own the packet.
*
* @return {BulkPacket}
* Parsed packet, or null if it's not a match.
*/
BulkPacket.fromHeader = function(header, transport) {
let match = this.HEADER_PATTERN.exec(header);
if (!match) {
return null;
}
let packet = new BulkPacket(transport);
packet.header = {
actor: match[1],
type: match[2],
length: +match[3],
};
return packet;
};
BulkPacket.HEADER_PATTERN = /^bulk ([^: ]+) ([^: ]+) (\d+):$/;
BulkPacket.prototype = Object.create(Packet.prototype);
BulkPacket.prototype.read = function(stream) {
// Temporarily pause monitoring of the input stream
this._transport.pauseIncoming();
let deferred = defer();
this._transport._onBulkReadReady({
actor: this.actor,
type: this.type,
length: this.length,
copyTo: (output) => {
let copying = StreamUtils.copyStream(stream, output, this.length);
deferred.resolve(copying);
return copying;
},
stream,
done: deferred,
});
// Await the result of reading from the stream
deferred.promise.then(() => {
this._done = true;
this._transport.resumeIncoming();
}, this._transport.close);
// Ensure this is only done once
this.read = () => {
throw new Error("Tried to read() a BulkPacket's stream multiple times.");
};
};
BulkPacket.prototype.write = function(stream) {
if (this._outgoingHeader === undefined) {
// Format the serialized packet header to a buffer
this._outgoingHeader = "bulk " + this.actor + " " + this.type + " " +
this.length + ":";
}
// Write the header, or whatever's left of it to write.
if (this._outgoingHeader.length) {
let written = stream.write(this._outgoingHeader,
this._outgoingHeader.length);
this._outgoingHeader = this._outgoingHeader.slice(written);
return;
}
// Temporarily pause the monitoring of the output stream
this._transport.pauseOutgoing();
let deferred = defer();
this._readyForWriting.resolve({
copyFrom: (input) => {
let copying = StreamUtils.copyStream(input, stream, this.length);
deferred.resolve(copying);
return copying;
},
stream,
done: deferred,
});
// Await the result of writing to the stream
deferred.promise.then(() => {
this._done = true;
this._transport.resumeOutgoing();
}, this._transport.close);
// Ensure this is only done once
this.write = () => {
throw new Error("Tried to write() a BulkPacket's stream multiple times.");
};
};
Object.defineProperty(BulkPacket.prototype, "streamReadyForWriting", {
get() {
return this._readyForWriting.promise;
},
});
Object.defineProperty(BulkPacket.prototype, "header", {
get() {
return {
actor: this.actor,
type: this.type,
length: this.length,
};
},
set(header) {
this.actor = header.actor;
this.type = header.type;
this.length = header.length;
},
});
Object.defineProperty(BulkPacket.prototype, "done", {
get() {
return this._done;
},
});
BulkPacket.prototype.toString = function() {
return "Bulk: " + JSON.stringify(this.header, null, 2);
};
/**
* RawPacket is used to test the transport's error handling of malformed
* packets, by writing data directly onto the stream.
* @param transport DebuggerTransport
* The transport instance that will own the packet.
* @param data string
* The raw string to send out onto the stream.
*/
function RawPacket(transport, data) {
Packet.call(this, transport);
this._data = data;
this.length = data.length;
this._done = false;
}
RawPacket.prototype = Object.create(Packet.prototype);
RawPacket.prototype.read = function() {
// this has not yet been needed for testing
throw new Error("Not implemented");
};
RawPacket.prototype.write = function(stream) {
let written = stream.write(this._data, this._data.length);
this._data = this._data.slice(written);
this._done = !this._data.length;
};
Object.defineProperty(RawPacket.prototype, "done", {
get() {
return this._done;
},
});

View File

@ -1,97 +0,0 @@
/* 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 CC = Components.Constructor;
const ServerSocket = CC(
"@mozilla.org/network/server-socket;1",
"nsIServerSocket",
"initSpecialConnection");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
const {DebuggerTransport} = ChromeUtils.import("chrome://juggler/content/server/transport.js", {});
const {KeepWhenOffline, LoopbackOnly} = Ci.nsIServerSocket;
this.EXPORTED_SYMBOLS = [
"TCPConnection",
"TCPListener",
];
class TCPListener {
constructor() {
this._socket = null;
this._nextConnID = 0;
this.onconnectioncreated = null;
this.onconnectionclosed = null;
}
start(port) {
if (this._socket)
return;
try {
const flags = KeepWhenOffline | LoopbackOnly;
const backlog = 1;
this._socket = new ServerSocket(port, flags, backlog);
} catch (e) {
throw new Error(`Could not bind to port ${port} (${e.name})`);
}
this._socket.asyncListen(this);
return this._socket.port;
}
stop() {
if (!this._socket)
return;
// Note that closing the server socket will not close currently active
// connections.
this._socket.close();
this._socket = null;
}
onSocketAccepted(serverSocket, clientSocket) {
const input = clientSocket.openInputStream(0, 0, 0);
const output = clientSocket.openOutputStream(0, 0, 0);
const transport = new DebuggerTransport(input, output);
const connection = new TCPConnection(this._nextConnID++, transport, () => {
if (this.onconnectionclosed)
this.onconnectionclosed.call(null, connection);
});
transport.ready();
if (this.onconnectioncreated)
this.onconnectioncreated.call(null, connection);
}
}
this.TCPListener = TCPListener;
class TCPConnection {
constructor(id, transport, closeCallback) {
this._id = id;
this._transport = transport;
// transport hooks are TCPConnection#onPacket
// and TCPConnection#onClosed
this._transport.hooks = this;
this._closeCallback = closeCallback;
this.onmessage = null;
}
send(msg) {
this._transport.send(msg);
}
onClosed() {
this._closeCallback.call(null);
}
async onPacket(data) {
if (this.onmessage)
this.onmessage.call(null, data);
}
}
this.TCPConnection = TCPConnection;

View File

@ -1,247 +0,0 @@
/* 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 CC = Components.Constructor;
ChromeUtils.import("resource://gre/modules/EventEmitter.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
const IOUtil = Cc["@mozilla.org/io-util;1"].getService(Ci.nsIIOUtil);
const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1",
"nsIScriptableInputStream", "init");
this.EXPORTED_SYMBOLS = ["StreamUtils"];
const BUFFER_SIZE = 0x8000;
/**
* This helper function (and its companion object) are used by bulk
* senders and receivers to read and write data in and out of other streams.
* Functions that make use of this tool are passed to callers when it is
* time to read or write bulk data. It is highly recommended to use these
* copier functions instead of the stream directly because the copier
* enforces the agreed upon length. Since bulk mode reuses an existing
* stream, the sender and receiver must write and read exactly the agreed
* upon amount of data, or else the entire transport will be left in a
* invalid state. Additionally, other methods of stream copying (such as
* NetUtil.asyncCopy) close the streams involved, which would terminate
* the debugging transport, and so it is avoided here.
*
* Overall, this *works*, but clearly the optimal solution would be
* able to just use the streams directly. If it were possible to fully
* implement nsIInputStream/nsIOutputStream in JS, wrapper streams could
* be created to enforce the length and avoid closing, and consumers could
* use familiar stream utilities like NetUtil.asyncCopy.
*
* The function takes two async streams and copies a precise number
* of bytes from one to the other. Copying begins immediately, but may
* complete at some future time depending on data size. Use the returned
* promise to know when it's complete.
*
* @param {nsIAsyncInputStream} input
* Stream to copy from.
* @param {nsIAsyncOutputStream} output
* Stream to copy to.
* @param {number} length
* Amount of data that needs to be copied.
*
* @return {Promise}
* Promise is resolved when copying completes or rejected if any
* (unexpected) errors occur.
*/
function copyStream(input, output, length) {
let copier = new StreamCopier(input, output, length);
return copier.copy();
}
/** @class */
function StreamCopier(input, output, length) {
EventEmitter.decorate(this);
this._id = StreamCopier._nextId++;
this.input = input;
// Save off the base output stream, since we know it's async as we've
// required
this.baseAsyncOutput = output;
if (IOUtil.outputStreamIsBuffered(output)) {
this.output = output;
} else {
this.output = Cc["@mozilla.org/network/buffered-output-stream;1"]
.createInstance(Ci.nsIBufferedOutputStream);
this.output.init(output, BUFFER_SIZE);
}
this._length = length;
this._amountLeft = length;
this._deferred = {
promise: new Promise((resolve, reject) => {
this._deferred.resolve = resolve;
this._deferred.reject = reject;
}),
};
this._copy = this._copy.bind(this);
this._flush = this._flush.bind(this);
this._destroy = this._destroy.bind(this);
// Copy promise's then method up to this object.
//
// Allows the copier to offer a promise interface for the simple succeed
// or fail scenarios, but also emit events (due to the EventEmitter)
// for other states, like progress.
this.then = this._deferred.promise.then.bind(this._deferred.promise);
this.then(this._destroy, this._destroy);
// Stream ready callback starts as |_copy|, but may switch to |_flush|
// at end if flushing would block the output stream.
this._streamReadyCallback = this._copy;
}
StreamCopier._nextId = 0;
StreamCopier.prototype = {
copy() {
// Dispatch to the next tick so that it's possible to attach a progress
// event listener, even for extremely fast copies (like when testing).
Services.tm.currentThread.dispatch(() => {
try {
this._copy();
} catch (e) {
this._deferred.reject(e);
}
}, 0);
return this;
},
_copy() {
let bytesAvailable = this.input.available();
let amountToCopy = Math.min(bytesAvailable, this._amountLeft);
this._debug("Trying to copy: " + amountToCopy);
let bytesCopied;
try {
bytesCopied = this.output.writeFrom(this.input, amountToCopy);
} catch (e) {
if (e.result == Cr.NS_BASE_STREAM_WOULD_BLOCK) {
this._debug("Base stream would block, will retry");
this._debug("Waiting for output stream");
this.baseAsyncOutput.asyncWait(this, 0, 0, Services.tm.currentThread);
return;
}
throw e;
}
this._amountLeft -= bytesCopied;
this._debug("Copied: " + bytesCopied +
", Left: " + this._amountLeft);
this._emitProgress();
if (this._amountLeft === 0) {
this._debug("Copy done!");
this._flush();
return;
}
this._debug("Waiting for input stream");
this.input.asyncWait(this, 0, 0, Services.tm.currentThread);
},
_emitProgress() {
this.emit("progress", {
bytesSent: this._length - this._amountLeft,
totalBytes: this._length,
});
},
_flush() {
try {
this.output.flush();
} catch (e) {
if (e.result == Cr.NS_BASE_STREAM_WOULD_BLOCK ||
e.result == Cr.NS_ERROR_FAILURE) {
this._debug("Flush would block, will retry");
this._streamReadyCallback = this._flush;
this._debug("Waiting for output stream");
this.baseAsyncOutput.asyncWait(this, 0, 0, Services.tm.currentThread);
return;
}
throw e;
}
this._deferred.resolve();
},
_destroy() {
this._destroy = null;
this._copy = null;
this._flush = null;
this.input = null;
this.output = null;
},
// nsIInputStreamCallback
onInputStreamReady() {
this._streamReadyCallback();
},
// nsIOutputStreamCallback
onOutputStreamReady() {
this._streamReadyCallback();
},
_debug() {
},
};
/**
* Read from a stream, one byte at a time, up to the next
* <var>delimiter</var> character, but stopping if we've read |count|
* without finding it. Reading also terminates early if there are less
* than <var>count</var> bytes available on the stream. In that case,
* we only read as many bytes as the stream currently has to offer.
*
* @param {nsIInputStream} stream
* Input stream to read from.
* @param {string} delimiter
* Character we're trying to find.
* @param {number} count
* Max number of characters to read while searching.
*
* @return {string}
* Collected data. If the delimiter was found, this string will
* end with it.
*/
// TODO: This implementation could be removed if bug 984651 is fixed,
// which provides a native version of the same idea.
function delimitedRead(stream, delimiter, count) {
let scriptableStream;
if (stream instanceof Ci.nsIScriptableInputStream) {
scriptableStream = stream;
} else {
scriptableStream = new ScriptableInputStream(stream);
}
let data = "";
// Don't exceed what's available on the stream
count = Math.min(count, stream.available());
if (count <= 0) {
return data;
}
let char;
while (char !== delimiter && count > 0) {
char = scriptableStream.readBytes(1);
count--;
data += char;
}
return data;
}
this.StreamUtils = {
copyStream,
delimitedRead,
};

View File

@ -1,523 +0,0 @@
/* 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";
/* global Pipe, ScriptableInputStream */
const CC = Components.Constructor;
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/EventEmitter.jsm");
const {StreamUtils} =
ChromeUtils.import("chrome://juggler/content/server/stream-utils.js", {});
const {Packet, JSONPacket, BulkPacket} =
ChromeUtils.import("chrome://juggler/content/server/packets.js", {});
const executeSoon = function(func) {
Services.tm.dispatchToMainThread(func);
};
const flags = {wantVerbose: false, wantLogging: false};
const dumpv =
flags.wantVerbose ?
function(msg) { dump(msg + "\n"); } :
function() {};
const Pipe = CC("@mozilla.org/pipe;1", "nsIPipe", "init");
const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1",
"nsIScriptableInputStream", "init");
this.EXPORTED_SYMBOLS = ["DebuggerTransport"];
const PACKET_HEADER_MAX = 200;
/**
* An adapter that handles data transfers between the debugger client
* and server. It can work with both nsIPipe and nsIServerSocket
* transports so long as the properly created input and output streams
* are specified. (However, for intra-process connections,
* LocalDebuggerTransport, below, is more efficient than using an nsIPipe
* pair with DebuggerTransport.)
*
* @param {nsIAsyncInputStream} input
* The input stream.
* @param {nsIAsyncOutputStream} output
* The output stream.
*
* Given a DebuggerTransport instance dt:
* 1) Set dt.hooks to a packet handler object (described below).
* 2) Call dt.ready() to begin watching for input packets.
* 3) Call dt.send() / dt.startBulkSend() to send packets.
* 4) Call dt.close() to close the connection, and disengage from
* the event loop.
*
* A packet handler is an object with the following methods:
*
* - onPacket(packet) - called when we have received a complete packet.
* |packet| is the parsed form of the packet --- a JavaScript value, not
* a JSON-syntax string.
*
* - onBulkPacket(packet) - called when we have switched to bulk packet
* receiving mode. |packet| is an object containing:
* * actor: Name of actor that will receive the packet
* * type: Name of actor's method that should be called on receipt
* * length: Size of the data to be read
* * stream: This input stream should only be used directly if you
* can ensure that you will read exactly |length| bytes and
* will not close the stream when reading is complete
* * done: If you use the stream directly (instead of |copyTo|
* below), you must signal completion by resolving/rejecting
* this deferred. If it's rejected, the transport will
* be closed. If an Error is supplied as a rejection value,
* it will be logged via |dump|. If you do use |copyTo|,
* resolving is taken care of for you when copying completes.
* * copyTo: A helper function for getting your data out of the
* stream that meets the stream handling requirements above,
* and has the following signature:
*
* @param nsIAsyncOutputStream {output}
* The stream to copy to.
*
* @return {Promise}
* The promise is resolved when copying completes or
* rejected if any (unexpected) errors occur. This object
* also emits "progress" events for each chunk that is
* copied. See stream-utils.js.
*
* - onClosed(reason) - called when the connection is closed. |reason|
* is an optional nsresult or object, typically passed when the
* transport is closed due to some error in a underlying stream.
*
* See ./packets.js and the Remote Debugging Protocol specification for
* more details on the format of these packets.
*
* @class
*/
function DebuggerTransport(input, output) {
EventEmitter.decorate(this);
this._input = input;
this._scriptableInput = new ScriptableInputStream(input);
this._output = output;
// The current incoming (possibly partial) header, which will determine
// which type of Packet |_incoming| below will become.
this._incomingHeader = "";
// The current incoming Packet object
this._incoming = null;
// A queue of outgoing Packet objects
this._outgoing = [];
this.hooks = null;
this.active = false;
this._incomingEnabled = true;
this._outgoingEnabled = true;
this.close = this.close.bind(this);
}
DebuggerTransport.prototype = {
/**
* Transmit an object as a JSON packet.
*
* This method returns immediately, without waiting for the entire
* packet to be transmitted, registering event handlers as needed to
* transmit the entire packet. Packets are transmitted in the order they
* are passed to this method.
*/
send(object) {
this.emit("send", object);
let packet = new JSONPacket(this);
packet.object = object;
this._outgoing.push(packet);
this._flushOutgoing();
},
/**
* Transmit streaming data via a bulk packet.
*
* This method initiates the bulk send process by queuing up the header
* data. The caller receives eventual access to a stream for writing.
*
* N.B.: Do *not* attempt to close the stream handed to you, as it
* will continue to be used by this transport afterwards. Most users
* should instead use the provided |copyFrom| function instead.
*
* @param {Object} header
* This is modeled after the format of JSON packets above, but does
* not actually contain the data, but is instead just a routing
* header:
*
* - actor: Name of actor that will receive the packet
* - type: Name of actor's method that should be called on receipt
* - length: Size of the data to be sent
*
* @return {Promise}
* The promise will be resolved when you are allowed to write to
* the stream with an object containing:
*
* - stream: This output stream should only be used directly
* if you can ensure that you will write exactly
* |length| bytes and will not close the stream when
* writing is complete.
* - done: If you use the stream directly (instead of
* |copyFrom| below), you must signal completion by
* resolving/rejecting this deferred. If it's
* rejected, the transport will be closed. If an
* Error is supplied as a rejection value, it will
* be logged via |dump|. If you do use |copyFrom|,
* resolving is taken care of for you when copying
* completes.
* - copyFrom: A helper function for getting your data onto the
* stream that meets the stream handling requirements
* above, and has the following signature:
*
* @param {nsIAsyncInputStream} input
* The stream to copy from.
*
* @return {Promise}
* The promise is resolved when copying completes
* or rejected if any (unexpected) errors occur.
* This object also emits "progress" events for
* each chunkthat is copied. See stream-utils.js.
*/
startBulkSend(header) {
this.emit("startbulksend", header);
let packet = new BulkPacket(this);
packet.header = header;
this._outgoing.push(packet);
this._flushOutgoing();
return packet.streamReadyForWriting;
},
/**
* Close the transport.
*
* @param {(nsresult|object)=} reason
* The status code or error message that corresponds to the reason
* for closing the transport (likely because a stream closed
* or failed).
*/
close(reason) {
this.emit("close", reason);
this.active = false;
this._input.close();
this._scriptableInput.close();
this._output.close();
this._destroyIncoming();
this._destroyAllOutgoing();
if (this.hooks) {
this.hooks.onClosed(reason);
this.hooks = null;
}
if (reason) {
dumpv("Transport closed: " + reason);
} else {
dumpv("Transport closed.");
}
},
/**
* The currently outgoing packet (at the top of the queue).
*/
get _currentOutgoing() {
return this._outgoing[0];
},
/**
* Flush data to the outgoing stream. Waits until the output
* stream notifies us that it is ready to be written to (via
* onOutputStreamReady).
*/
_flushOutgoing() {
if (!this._outgoingEnabled || this._outgoing.length === 0) {
return;
}
// If the top of the packet queue has nothing more to send, remove it.
if (this._currentOutgoing.done) {
this._finishCurrentOutgoing();
}
if (this._outgoing.length > 0) {
let threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
this._output.asyncWait(this, 0, 0, threadManager.currentThread);
}
},
/**
* Pause this transport's attempts to write to the output stream.
* This is used when we've temporarily handed off our output stream for
* writing bulk data.
*/
pauseOutgoing() {
this._outgoingEnabled = false;
},
/**
* Resume this transport's attempts to write to the output stream.
*/
resumeOutgoing() {
this._outgoingEnabled = true;
this._flushOutgoing();
},
// nsIOutputStreamCallback
/**
* This is called when the output stream is ready for more data to
* be written. The current outgoing packet will attempt to write some
* amount of data, but may not complete.
*/
onOutputStreamReady(stream) {
if (!this._outgoingEnabled || this._outgoing.length === 0) {
return;
}
try {
this._currentOutgoing.write(stream);
} catch (e) {
if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) {
this.close(e.result);
return;
}
throw e;
}
this._flushOutgoing();
},
/**
* Remove the current outgoing packet from the queue upon completion.
*/
_finishCurrentOutgoing() {
if (this._currentOutgoing) {
this._currentOutgoing.destroy();
this._outgoing.shift();
}
},
/**
* Clear the entire outgoing queue.
*/
_destroyAllOutgoing() {
for (let packet of this._outgoing) {
packet.destroy();
}
this._outgoing = [];
},
/**
* Initialize the input stream for reading. Once this method has been
* called, we watch for packets on the input stream, and pass them to
* the appropriate handlers via this.hooks.
*/
ready() {
this.active = true;
this._waitForIncoming();
},
/**
* Asks the input stream to notify us (via onInputStreamReady) when it is
* ready for reading.
*/
_waitForIncoming() {
if (this._incomingEnabled) {
let threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
this._input.asyncWait(this, 0, 0, threadManager.currentThread);
}
},
/**
* Pause this transport's attempts to read from the input stream.
* This is used when we've temporarily handed off our input stream for
* reading bulk data.
*/
pauseIncoming() {
this._incomingEnabled = false;
},
/**
* Resume this transport's attempts to read from the input stream.
*/
resumeIncoming() {
this._incomingEnabled = true;
this._flushIncoming();
this._waitForIncoming();
},
// nsIInputStreamCallback
/**
* Called when the stream is either readable or closed.
*/
onInputStreamReady(stream) {
try {
while (stream.available() && this._incomingEnabled &&
this._processIncoming(stream, stream.available())) {
// Loop until there is nothing more to process
}
this._waitForIncoming();
} catch (e) {
if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) {
this.close(e.result);
} else {
throw e;
}
}
},
/**
* Process the incoming data. Will create a new currently incoming
* Packet if needed. Tells the incoming Packet to read as much data
* as it can, but reading may not complete. The Packet signals that
* its data is ready for delivery by calling one of this transport's
* _on*Ready methods (see ./packets.js and the _on*Ready methods below).
*
* @return {boolean}
* Whether incoming stream processing should continue for any
* remaining data.
*/
_processIncoming(stream, count) {
dumpv("Data available: " + count);
if (!count) {
dumpv("Nothing to read, skipping");
return false;
}
try {
if (!this._incoming) {
dumpv("Creating a new packet from incoming");
if (!this._readHeader(stream)) {
// Not enough data to read packet type
return false;
}
// Attempt to create a new Packet by trying to parse each possible
// header pattern.
this._incoming = Packet.fromHeader(this._incomingHeader, this);
if (!this._incoming) {
throw new Error("No packet types for header: " +
this._incomingHeader);
}
}
if (!this._incoming.done) {
// We have an incomplete packet, keep reading it.
dumpv("Existing packet incomplete, keep reading");
this._incoming.read(stream, this._scriptableInput);
}
} catch (e) {
dump(`Error reading incoming packet: (${e} - ${e.stack})\n`);
// Now in an invalid state, shut down the transport.
this.close();
return false;
}
if (!this._incoming.done) {
// Still not complete, we'll wait for more data.
dumpv("Packet not done, wait for more");
return true;
}
// Ready for next packet
this._flushIncoming();
return true;
},
/**
* Read as far as we can into the incoming data, attempting to build
* up a complete packet header (which terminates with ":"). We'll only
* read up to PACKET_HEADER_MAX characters.
*
* @return {boolean}
* True if we now have a complete header.
*/
_readHeader() {
let amountToRead = PACKET_HEADER_MAX - this._incomingHeader.length;
this._incomingHeader +=
StreamUtils.delimitedRead(this._scriptableInput, ":", amountToRead);
if (flags.wantVerbose) {
dumpv("Header read: " + this._incomingHeader);
}
if (this._incomingHeader.endsWith(":")) {
if (flags.wantVerbose) {
dumpv("Found packet header successfully: " + this._incomingHeader);
}
return true;
}
if (this._incomingHeader.length >= PACKET_HEADER_MAX) {
throw new Error("Failed to parse packet header!");
}
// Not enough data yet.
return false;
},
/**
* If the incoming packet is done, log it as needed and clear the buffer.
*/
_flushIncoming() {
if (!this._incoming.done) {
return;
}
if (flags.wantLogging) {
dumpv("Got: " + this._incoming);
}
this._destroyIncoming();
},
/**
* Handler triggered by an incoming JSONPacket completing it's |read|
* method. Delivers the packet to this.hooks.onPacket.
*/
_onJSONObjectReady(object) {
executeSoon(() => {
// Ensure the transport is still alive by the time this runs.
if (this.active) {
this.emit("packet", object);
this.hooks.onPacket(object);
}
});
},
/**
* Handler triggered by an incoming BulkPacket entering the |read|
* phase for the stream portion of the packet. Delivers info about the
* incoming streaming data to this.hooks.onBulkPacket. See the main
* comment on the transport at the top of this file for more details.
*/
_onBulkReadReady(...args) {
executeSoon(() => {
// Ensure the transport is still alive by the time this runs.
if (this.active) {
this.emit("bulkpacket", ...args);
this.hooks.onBulkPacket(...args);
}
});
},
/**
* Remove all handlers and references related to the current incoming
* packet, either because it is now complete or because the transport
* is closing.
*/
_destroyIncoming() {
if (this._incoming) {
this._incoming.destroy();
}
this._incomingHeader = "";
this._incoming = null;
},
};

View File

@ -17,7 +17,7 @@ npm i puppeteer-firefox
# or "yarn add puppeteer-firefox" # or "yarn add puppeteer-firefox"
``` ```
Note: When you install puppeteer-firefox, it downloads a [custom-built Firefox](https://github.com/GoogleChrome/puppeteer/tree/master/experimental/juggler) (Firefox/63.0.4) that is guaranteed to work with the API. Note: When you install puppeteer-firefox, it downloads a [custom-built Firefox](https://github.com/puppeteer/juggler) (Firefox/63.0.4) that is guaranteed to work with the API.
### Usage ### Usage
@ -46,152 +46,9 @@ node example.js
### API Status ### API Status
Big lacking parts: Current tip-of-tree status of Puppeteer-Firefox is availabe at [isPuppeteerFirefoxReady?](https://aslushnikov.github.io/ispuppeteerfirefoxready/)
- `page.emulate`
- `page.pdf`
- all network-related APIs: `page.on('request')`, `page.on('response')`, and request interception
Supported API:
- class: Puppeteer
* puppeteer.executablePath()
* puppeteer.launch([options])
- class: Browser
* event: 'targetchanged'
* event: 'targetcreated'
* event: 'targetdestroyed'
* browser.close()
* browser.newPage()
* browser.pages()
* browser.process()
* browser.targets()
* browser.userAgent()
* browser.version()
* browser.waitForTarget(predicate[, options])
- class: Target
* target.browser()
* target.page()
* target.type()
* target.url()
- class: Page
* event: 'close'
* event: 'console'
* event: 'dialog'
* event: 'domcontentloaded'
* event: 'frameattached'
* event: 'framedetached'
* event: 'framenavigated'
* event: 'load'
* event: 'pageerror'
* page.$(selector)
* page.$$(selector)
* page.$$eval(selector, pageFunction[, ...args])
* page.$eval(selector, pageFunction[, ...args])
* page.$x(expression)
* page.addScriptTag(options)
* page.addStyleTag(options)
* page.browser()
* page.click(selector[, options])
* page.close(options)
* page.content()
* page.evaluate(pageFunction, ...args)
* page.evaluateOnNewDocument(pageFunction, ...args)
* page.focus(selector)
* page.frames()
* page.goBack(options)
* page.goForward(options)
* page.goto(url, options)
* page.hover(selector)
* page.isClosed()
* page.keyboard
* page.mainFrame()
* page.mouse
* page.reload(options)
* page.screenshot([options])
* page.select(selector, ...values)
* page.setContent(html)
* page.setViewport(viewport)
* page.target()
* page.title()
* page.type(selector, text[, options])
* page.url()
* page.viewport()
* page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])
* page.waitForFunction(pageFunction[, options[, ...args]])
* page.waitForNavigation(options)
* page.waitForSelector(selector[, options])
* page.waitForXPath(xpath[, options])
- class: Frame
* frame.$(selector)
* frame.$$(selector)
* frame.$$eval(selector, pageFunction[, ...args])
* frame.$eval(selector, pageFunction[, ...args])
* frame.$x(expression)
* frame.addScriptTag(options)
* frame.addStyleTag(options)
* frame.childFrames()
* frame.click(selector[, options])
* frame.content()
* frame.evaluate(pageFunction, ...args)
* frame.focus(selector)
* frame.hover(selector)
* frame.isDetached()
* frame.name()
* frame.parentFrame()
* frame.select(selector, ...values)
* frame.setContent(html)
* frame.title()
* frame.type(selector, text[, options])
* frame.url()
* frame.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])
* frame.waitForFunction(pageFunction[, options[, ...args]])
* frame.waitForSelector(selector[, options])
* frame.waitForXPath(xpath[, options])
- class: JSHandle
* jsHandle.asElement()
* jsHandle.dispose()
* jsHandle.getProperties()
* jsHandle.getProperty(propertyName)
* jsHandle.jsonValue()
* jsHandle.toString()
- class: ElementHandle
* elementHandle.$(selector)
* elementHandle.$$(selector)
* elementHandle.$$eval(selector, pageFunction, ...args)
* elementHandle.$eval(selector, pageFunction, ...args)
* elementHandle.$x(expression)
* elementHandle.boundingBox()
* elementHandle.click([options])
* elementHandle.dispose()
* elementHandle.focus()
* elementHandle.hover()
* elementHandle.isIntersectingViewport()
* elementHandle.press(key[, options])
* elementHandle.screenshot([options])
* elementHandle.type(text[, options])
- class: Keyboard
* keyboard.down(key[, options])
* keyboard.press(key[, options])
* keyboard.sendCharacter(char)
* keyboard.type(text, options)
* keyboard.up(key)
- class: Mouse
* mouse.click(x, y, [options])
* mouse.down([options])
* mouse.move(x, y, [options])
* mouse.up([options])
- class: Dialog
* dialog.accept([promptText])
* dialog.defaultValue()
* dialog.dismiss()
* dialog.message()
* dialog.type()
- class: ConsoleMessage
* consoleMessage.args()
* consoleMessage.text()
* consoleMessage.type()
- class: TimeoutError
### Credits
Special thanks to [Amine Bouhlali](https://bitbucket.org/aminerop/) who volunteered the [`puppeteer-firefox`](https://www.npmjs.com/package/puppeteer-firefox) NPM package. Special thanks to [Amine Bouhlali](https://bitbucket.org/aminerop/) who volunteered the [`puppeteer-firefox`](https://www.npmjs.com/package/puppeteer-firefox) NPM package.