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:
parent
72c00474e7
commit
07b43ff29a
@ -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
|
|
@ -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
|
|
||||||
|
|
1
experimental/juggler/.gitignore
vendored
1
experimental/juggler/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
firefox/
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||||||
120450a2c56c25e7c410a909192b5b9ad7b0dff2
|
|
@ -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`
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
@ -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
|
|
@ -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)/
|
|
@ -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)/
|
|
@ -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;
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
@ -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;
|
|
@ -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'];
|
|
@ -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]);
|
|
||||||
|
|
@ -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
|
|
@ -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",
|
|
||||||
]
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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")
|
|
||||||
|
|
@ -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;
|
|
||||||
},
|
|
||||||
});
|
|
@ -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;
|
|
@ -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,
|
|
||||||
};
|
|
@ -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;
|
|
||||||
},
|
|
||||||
};
|
|
@ -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.
|
||||||
|
Loading…
Reference in New Issue
Block a user