зеркало из https://github.com/mozilla/gecko-dev.git
679 строки
20 KiB
JavaScript
679 строки
20 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
const {Constructor: CC, classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
|
|
|
const loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
|
|
.getService(Ci.mozIJSSubScriptLoader);
|
|
const ServerSocket = CC(
|
|
"@mozilla.org/network/server-socket;1",
|
|
"nsIServerSocket",
|
|
"initSpecialConnection");
|
|
|
|
Cu.import("resource://gre/modules/Log.jsm");
|
|
Cu.import("resource://gre/modules/Preferences.jsm");
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
Cu.import("chrome://marionette/content/assert.js");
|
|
const {GeckoDriver} = Cu.import("chrome://marionette/content/driver.js", {});
|
|
const {
|
|
error,
|
|
UnknownCommandError,
|
|
} = Cu.import("chrome://marionette/content/error.js", {});
|
|
Cu.import("chrome://marionette/content/message.js");
|
|
const {DebuggerTransport} =
|
|
Cu.import("chrome://marionette/content/transport.js", {});
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
this, "env", "@mozilla.org/process/environment;1", "nsIEnvironment");
|
|
|
|
const logger = Log.repository.getLogger("Marionette");
|
|
|
|
const {KeepWhenOffline, LoopbackOnly} = Ci.nsIServerSocket;
|
|
|
|
this.EXPORTED_SYMBOLS = ["server"];
|
|
|
|
/** @namespace */
|
|
this.server = {};
|
|
|
|
const PROTOCOL_VERSION = 3;
|
|
|
|
const ENV_ENABLED = "MOZ_MARIONETTE";
|
|
|
|
const PREF_CONTENT_LISTENER = "marionette.contentListener";
|
|
const PREF_PORT = "marionette.port";
|
|
const PREF_RECOMMENDED = "marionette.prefs.recommended";
|
|
|
|
const NOTIFY_RUNNING = "remote-active";
|
|
|
|
// Marionette sets preferences recommended for automation when it starts,
|
|
// unless |marionette.prefs.recommended| has been set to false.
|
|
// Where noted, some prefs should also be set in the profile passed to
|
|
// Marionette to prevent them from affecting startup, since some of these
|
|
// are checked before Marionette initialises.
|
|
const RECOMMENDED_PREFS = new Map([
|
|
|
|
// Disable automatic downloading of new releases.
|
|
//
|
|
// This should also be set in the profile prior to starting Firefox,
|
|
// as it is picked up at runtime.
|
|
["app.update.auto", false],
|
|
|
|
// Disable automatically upgrading Firefox.
|
|
//
|
|
// This should also be set in the profile prior to starting Firefox,
|
|
// as it is picked up at runtime.
|
|
["app.update.enabled", false],
|
|
|
|
// Increase the APZ content response timeout in tests to 1 minute.
|
|
// This is to accommodate the fact that test environments tends to be
|
|
// slower than production environments (with the b2g emulator being
|
|
// the slowest of them all), resulting in the production timeout value
|
|
// sometimes being exceeded and causing false-positive test failures.
|
|
//
|
|
// (bug 1176798, bug 1177018, bug 1210465)
|
|
["apz.content_response_timeout", 60000],
|
|
|
|
// Indicate that the download panel has been shown once so that
|
|
// whichever download test runs first doesn't show the popup
|
|
// inconsistently.
|
|
["browser.download.panel.shown", true],
|
|
|
|
// Do not show the EULA notification.
|
|
//
|
|
// This should also be set in the profile prior to starting Firefox,
|
|
// as it is picked up at runtime.
|
|
["browser.EULA.override", true],
|
|
|
|
// Turn off about:newtab and make use of about:blank instead for new
|
|
// opened tabs.
|
|
//
|
|
// This should also be set in the profile prior to starting Firefox,
|
|
// as it is picked up at runtime.
|
|
["browser.newtabpage.enabled", false],
|
|
|
|
// Assume the about:newtab page's intro panels have been shown to not
|
|
// depend on which test runs first and happens to open about:newtab
|
|
["browser.newtabpage.introShown", true],
|
|
|
|
// Never start the browser in offline mode
|
|
//
|
|
// This should also be set in the profile prior to starting Firefox,
|
|
// as it is picked up at runtime.
|
|
["browser.offline", false],
|
|
|
|
// Background thumbnails in particular cause grief, and disabling
|
|
// thumbnails in general cannot hurt
|
|
["browser.pagethumbnails.capturing_disabled", true],
|
|
|
|
// Disable safebrowsing components.
|
|
//
|
|
// These should also be set in the profile prior to starting Firefox,
|
|
// as it is picked up at runtime.
|
|
["browser.safebrowsing.blockedURIs.enabled", false],
|
|
["browser.safebrowsing.downloads.enabled", false],
|
|
["browser.safebrowsing.passwords.enabled", false],
|
|
["browser.safebrowsing.malware.enabled", false],
|
|
["browser.safebrowsing.phishing.enabled", false],
|
|
|
|
// Disable updates to search engines.
|
|
//
|
|
// Should be set in profile.
|
|
["browser.search.update", false],
|
|
|
|
// Do not restore the last open set of tabs if the browser has crashed
|
|
["browser.sessionstore.resume_from_crash", false],
|
|
|
|
// Don't check for the default web browser during startup.
|
|
//
|
|
// These should also be set in the profile prior to starting Firefox,
|
|
// as it is picked up at runtime.
|
|
["browser.shell.checkDefaultBrowser", false],
|
|
|
|
// Start with a blank page (about:blank)
|
|
["browser.startup.page", 0],
|
|
|
|
// Do not redirect user when a milstone upgrade of Firefox is detected
|
|
["browser.startup.homepage_override.mstone", "ignore"],
|
|
|
|
// Disable browser animations
|
|
["toolkit.cosmeticAnimations.enabled", false],
|
|
|
|
// Do not allow background tabs to be zombified, otherwise for tests
|
|
// that open additional tabs, the test harness tab itself might get
|
|
// unloaded
|
|
["browser.tabs.disableBackgroundZombification", false],
|
|
|
|
// Do not warn when closing all other open tabs
|
|
["browser.tabs.warnOnCloseOtherTabs", false],
|
|
|
|
// Do not warn when multiple tabs will be opened
|
|
["browser.tabs.warnOnOpen", false],
|
|
|
|
// Disable first run splash page on Windows 10
|
|
["browser.usedOnWindows10.introURL", ""],
|
|
|
|
// Disable the UI tour.
|
|
//
|
|
// Should be set in profile.
|
|
["browser.uitour.enabled", false],
|
|
|
|
// Turn off search suggestions in the location bar so as not to trigger
|
|
// network connections.
|
|
["browser.urlbar.suggest.searches", false],
|
|
|
|
// Turn off the location bar search suggestions opt-in. It interferes with
|
|
// tests that don't expect it to be there.
|
|
["browser.urlbar.userMadeSearchSuggestionsChoice", true],
|
|
|
|
// Do not show datareporting policy notifications which can
|
|
// interfere with tests
|
|
[
|
|
"datareporting.healthreport.about.reportUrl",
|
|
"http://%(server)s/dummy/abouthealthreport/",
|
|
],
|
|
[
|
|
"datareporting.healthreport.documentServerURI",
|
|
"http://%(server)s/dummy/healthreport/",
|
|
],
|
|
["datareporting.healthreport.logging.consoleEnabled", false],
|
|
["datareporting.healthreport.service.enabled", false],
|
|
["datareporting.healthreport.service.firstRun", false],
|
|
["datareporting.healthreport.uploadEnabled", false],
|
|
["datareporting.policy.dataSubmissionEnabled", false],
|
|
["datareporting.policy.dataSubmissionPolicyAccepted", false],
|
|
["datareporting.policy.dataSubmissionPolicyBypassNotification", true],
|
|
|
|
// Disable popup-blocker
|
|
["dom.disable_open_during_load", false],
|
|
|
|
// Enabling the support for File object creation in the content process
|
|
["dom.file.createInChild", true],
|
|
|
|
// Disable the ProcessHangMonitor
|
|
["dom.ipc.reportProcessHangs", false],
|
|
|
|
// Disable slow script dialogues
|
|
["dom.max_chrome_script_run_time", 0],
|
|
["dom.max_script_run_time", 0],
|
|
|
|
// Only load extensions from the application and user profile
|
|
// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
|
|
//
|
|
// Should be set in profile.
|
|
["extensions.autoDisableScopes", 0],
|
|
["extensions.enabledScopes", 5],
|
|
|
|
// Do not block add-ons for e10s
|
|
["extensions.e10sBlocksEnabling", false],
|
|
|
|
// Disable metadata caching for installed add-ons by default
|
|
["extensions.getAddons.cache.enabled", false],
|
|
|
|
// Disable installing any distribution extensions or add-ons.
|
|
// Should be set in profile.
|
|
["extensions.installDistroAddons", false],
|
|
|
|
// Make sure Shield doesn't hit the network.
|
|
["extensions.shield-recipe-client.api_url", ""],
|
|
|
|
["extensions.showMismatchUI", false],
|
|
|
|
// Turn off extension updates so they do not bother tests
|
|
["extensions.update.enabled", false],
|
|
["extensions.update.notifyUser", false],
|
|
|
|
// Make sure opening about:addons will not hit the network
|
|
[
|
|
"extensions.webservice.discoverURL",
|
|
"http://%(server)s/dummy/discoveryURL",
|
|
],
|
|
|
|
// Allow the application to have focus even it runs in the background
|
|
["focusmanager.testmode", true],
|
|
|
|
// Disable useragent updates
|
|
["general.useragent.updates.enabled", false],
|
|
|
|
// Always use network provider for geolocation tests so we bypass the
|
|
// macOS dialog raised by the corelocation provider
|
|
["geo.provider.testing", true],
|
|
|
|
// Do not scan Wifi
|
|
["geo.wifi.scan", false],
|
|
|
|
// No hang monitor
|
|
["hangmonitor.timeout", 0],
|
|
|
|
// Show chrome errors and warnings in the error console
|
|
["javascript.options.showInConsole", true],
|
|
|
|
// Do not prompt for temporary redirects
|
|
["network.http.prompt-temp-redirect", false],
|
|
|
|
// Disable speculative connections so they are not reported as leaking
|
|
// when they are hanging around
|
|
["network.http.speculative-parallel-limit", 0],
|
|
|
|
// Do not automatically switch between offline and online
|
|
["network.manage-offline-status", false],
|
|
|
|
// Make sure SNTP requests do not hit the network
|
|
["network.sntp.pools", "%(server)s"],
|
|
|
|
// Local documents have access to all other local documents,
|
|
// including directory listings
|
|
["security.fileuri.strict_origin_policy", false],
|
|
|
|
// Tests do not wait for the notification button security delay
|
|
["security.notification_enable_delay", 0],
|
|
|
|
// Ensure blocklist updates do not hit the network
|
|
["services.settings.server", "http://%(server)s/dummy/blocklist/"],
|
|
|
|
// Do not automatically fill sign-in forms with known usernames and
|
|
// passwords
|
|
["signon.autofillForms", false],
|
|
|
|
// Disable password capture, so that tests that include forms are not
|
|
// influenced by the presence of the persistent doorhanger notification
|
|
["signon.rememberSignons", false],
|
|
|
|
// Disable first-run welcome page
|
|
["startup.homepage_welcome_url", "about:blank"],
|
|
["startup.homepage_welcome_url.additional", ""],
|
|
|
|
// Prevent starting into safe mode after application crashes
|
|
["toolkit.startup.max_resumed_crashes", -1],
|
|
|
|
]);
|
|
|
|
/**
|
|
* Bootstraps Marionette and handles incoming client connections.
|
|
*
|
|
* Starting the Marionette server will open a TCP socket sporting the
|
|
* debugger transport interface on the provided |port|. For every new
|
|
* connection, a |server.TCPConnection| is created.
|
|
*/
|
|
server.TCPListener = class {
|
|
/**
|
|
* @param {number} port
|
|
* Port for server to listen to.
|
|
*/
|
|
constructor(port) {
|
|
this.port = port;
|
|
this.socket = null;
|
|
this.conns = new Set();
|
|
this.nextConnID = 0;
|
|
this.alive = false;
|
|
this._acceptConnections = false;
|
|
this.alteredPrefs = new Set();
|
|
}
|
|
|
|
/**
|
|
* Function produces a GeckoDriver.
|
|
*
|
|
* Determines application name to initialise the driver with.
|
|
*
|
|
* @return {GeckoDriver}
|
|
* A driver instance.
|
|
*/
|
|
driverFactory() {
|
|
Preferences.set(PREF_CONTENT_LISTENER, false);
|
|
return new GeckoDriver(Services.appinfo.name, this);
|
|
}
|
|
|
|
set acceptConnections(value) {
|
|
if (!value) {
|
|
logger.info("New connections will no longer be accepted");
|
|
} else {
|
|
logger.info("New connections are accepted again");
|
|
}
|
|
|
|
this._acceptConnections = value;
|
|
}
|
|
|
|
/**
|
|
* Bind this listener to |port| and start accepting incoming socket
|
|
* connections on |onSocketAccepted|.
|
|
*
|
|
* The marionette.port preference will be populated with the value
|
|
* of |this.port|.
|
|
*/
|
|
start() {
|
|
if (this.alive) {
|
|
return;
|
|
}
|
|
|
|
Services.obs.notifyObservers(this, NOTIFY_RUNNING, true);
|
|
|
|
if (Preferences.get(PREF_RECOMMENDED)) {
|
|
// set recommended prefs if they are not already user-defined
|
|
for (let [k, v] of RECOMMENDED_PREFS) {
|
|
if (!Preferences.isSet(k)) {
|
|
logger.debug(`Setting recommended pref ${k} to ${v}`);
|
|
Preferences.set(k, v);
|
|
this.alteredPrefs.add(k);
|
|
}
|
|
}
|
|
}
|
|
|
|
const flags = KeepWhenOffline | LoopbackOnly;
|
|
const backlog = 1;
|
|
this.socket = new ServerSocket(this.port, flags, backlog);
|
|
this.socket.asyncListen(this);
|
|
this.port = this.socket.port;
|
|
Preferences.set(PREF_PORT, this.port);
|
|
|
|
this.alive = true;
|
|
this._acceptConnections = true;
|
|
env.set(ENV_ENABLED, "1");
|
|
}
|
|
|
|
stop() {
|
|
if (!this.alive) {
|
|
return;
|
|
}
|
|
|
|
this._acceptConnections = false;
|
|
|
|
this.socket.close();
|
|
this.socket = null;
|
|
|
|
for (let k of this.alteredPrefs) {
|
|
logger.debug(`Resetting recommended pref ${k}`);
|
|
Preferences.reset(k);
|
|
}
|
|
this.alteredPrefs.clear();
|
|
|
|
Services.obs.notifyObservers(this, NOTIFY_RUNNING);
|
|
|
|
this.alive = false;
|
|
}
|
|
|
|
onSocketAccepted(serverSocket, clientSocket) {
|
|
if (!this._acceptConnections) {
|
|
logger.warn("New connections are currently not accepted");
|
|
return;
|
|
}
|
|
|
|
let input = clientSocket.openInputStream(0, 0, 0);
|
|
let output = clientSocket.openOutputStream(0, 0, 0);
|
|
let transport = new DebuggerTransport(input, output);
|
|
|
|
let conn = new server.TCPConnection(
|
|
this.nextConnID++, transport, this.driverFactory.bind(this));
|
|
conn.onclose = this.onConnectionClosed.bind(this);
|
|
this.conns.add(conn);
|
|
|
|
logger.debug(`Accepted connection ${conn.id} ` +
|
|
`from ${clientSocket.host}:${clientSocket.port}`);
|
|
conn.sayHello();
|
|
transport.ready();
|
|
}
|
|
|
|
onConnectionClosed(conn) {
|
|
logger.debug(`Closed connection ${conn.id}`);
|
|
this.conns.delete(conn);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Marionette client connection.
|
|
*
|
|
* Dispatches packets received to their correct service destinations
|
|
* and sends back the service endpoint's return values.
|
|
*
|
|
* @param {number} connID
|
|
* Unique identifier of the connection this dispatcher should handle.
|
|
* @param {DebuggerTransport} transport
|
|
* Debugger transport connection to the client.
|
|
* @param {function(): GeckoDriver} driverFactory
|
|
* Factory function that produces a |GeckoDriver|.
|
|
*/
|
|
server.TCPConnection = class {
|
|
constructor(connID, transport, driverFactory) {
|
|
this.id = connID;
|
|
this.conn = transport;
|
|
|
|
// transport hooks are TCPConnection#onPacket
|
|
// and TCPConnection#onClosed
|
|
this.conn.hooks = this;
|
|
|
|
// callback for when connection is closed
|
|
this.onclose = null;
|
|
|
|
// last received/sent message ID
|
|
this.lastID = 0;
|
|
|
|
this.driver = driverFactory();
|
|
|
|
// lookup of commands sent by server to client by message ID
|
|
this.commands_ = new Map();
|
|
}
|
|
|
|
/**
|
|
* Debugger transport callback that cleans up
|
|
* after a connection is closed.
|
|
*/
|
|
onClosed(reason) {
|
|
this.driver.deleteSession();
|
|
if (this.onclose) {
|
|
this.onclose(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Callback that receives data packets from the client.
|
|
*
|
|
* If the message is a Response, we look up the command previously
|
|
* issued to the client and run its callback, if any. In case of
|
|
* a Command, the corresponding is executed.
|
|
*
|
|
* @param {Array.<number, number, ?, ?>} data
|
|
* A four element array where the elements, in sequence, signifies
|
|
* message type, message ID, method name or error, and parameters
|
|
* or result.
|
|
*/
|
|
onPacket(data) {
|
|
// unable to determine how to respond
|
|
if (!Array.isArray(data)) {
|
|
let e = new TypeError(
|
|
"Unable to unmarshal packet data: " + JSON.stringify(data));
|
|
error.report(e);
|
|
return;
|
|
}
|
|
|
|
// return immediately with any error trying to unmarshal message
|
|
let msg;
|
|
try {
|
|
msg = Message.fromMsg(data);
|
|
msg.origin = MessageOrigin.Client;
|
|
this.log_(msg);
|
|
} catch (e) {
|
|
let resp = this.createResponse(data[1]);
|
|
resp.sendError(e);
|
|
return;
|
|
}
|
|
|
|
// look up previous command we received a response for
|
|
if (msg instanceof Response) {
|
|
let cmd = this.commands_.get(msg.id);
|
|
this.commands_.delete(msg.id);
|
|
cmd.onresponse(msg);
|
|
|
|
// execute new command
|
|
} else if (msg instanceof Command) {
|
|
(async () => {
|
|
await this.execute(msg);
|
|
})();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Executes a Marionette command and sends back a response when it
|
|
* has finished executing.
|
|
*
|
|
* If the command implementation sends the response itself by calling
|
|
* <code>resp.send()</code>, the response is guaranteed to not be
|
|
* sent twice.
|
|
*
|
|
* Errors thrown in commands are marshaled and sent back, and if they
|
|
* are not {@link WebDriverError} instances, they are additionally
|
|
* propagated and reported to {@link Components.utils.reportError}.
|
|
*
|
|
* @param {Command} cmd
|
|
* Command to execute.
|
|
*/
|
|
async execute(cmd) {
|
|
let resp = this.createResponse(cmd.id);
|
|
let sendResponse = () => resp.sendConditionally(resp => !resp.sent);
|
|
let sendError = resp.sendError.bind(resp);
|
|
|
|
await this.despatch(cmd, resp)
|
|
.then(sendResponse, sendError).catch(error.report);
|
|
}
|
|
|
|
/**
|
|
* Despatches command to appropriate Marionette service.
|
|
*
|
|
* @param {Command} cmd
|
|
* Command to run.
|
|
* @param {Response} resp
|
|
* Mutable response where the command's return value will be
|
|
* assigned.
|
|
*
|
|
* @throws {Error}
|
|
* A command's implementation may throw at any time.
|
|
*/
|
|
async despatch(cmd, resp) {
|
|
let fn = this.driver.commands[cmd.name];
|
|
if (typeof fn == "undefined") {
|
|
throw new UnknownCommandError(cmd.name);
|
|
}
|
|
|
|
if (!["newSession", "WebDriver:NewSession"].includes(cmd.name)) {
|
|
assert.session(this.driver);
|
|
}
|
|
|
|
let rv = await fn.bind(this.driver)(cmd, resp);
|
|
|
|
if (typeof rv != "undefined") {
|
|
if (typeof rv != "object") {
|
|
resp.body = {value: rv};
|
|
} else {
|
|
resp.body = rv;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fail-safe creation of a new instance of |message.Response|.
|
|
*
|
|
* @param {number} msgID
|
|
* Message ID to respond to. If it is not a number, -1 is used.
|
|
*
|
|
* @return {message.Response}
|
|
* Response to the message with |msgID|.
|
|
*/
|
|
createResponse(msgID) {
|
|
if (typeof msgID != "number") {
|
|
msgID = -1;
|
|
}
|
|
return new Response(msgID, this.send.bind(this));
|
|
}
|
|
|
|
sendError(err, cmdID) {
|
|
let resp = new Response(cmdID, this.send.bind(this));
|
|
resp.sendError(err);
|
|
}
|
|
|
|
/**
|
|
* When a client connects we send across a JSON Object defining the
|
|
* protocol level.
|
|
*
|
|
* This is the only message sent by Marionette that does not follow
|
|
* the regular message format.
|
|
*/
|
|
sayHello() {
|
|
let whatHo = {
|
|
applicationType: "gecko",
|
|
marionetteProtocol: PROTOCOL_VERSION,
|
|
};
|
|
this.sendRaw(whatHo);
|
|
}
|
|
|
|
/**
|
|
* Delegates message to client based on the provided {@code cmdID}.
|
|
* The message is sent over the debugger transport socket.
|
|
*
|
|
* The command ID is a unique identifier assigned to the client's request
|
|
* that is used to distinguish the asynchronous responses.
|
|
*
|
|
* Whilst responses to commands are synchronous and must be sent in the
|
|
* correct order.
|
|
*
|
|
* @param {Message} msg
|
|
* The command or response to send.
|
|
*/
|
|
send(msg) {
|
|
msg.origin = MessageOrigin.Server;
|
|
if (msg instanceof Command) {
|
|
this.commands_.set(msg.id, msg);
|
|
this.sendToEmulator(msg);
|
|
} else if (msg instanceof Response) {
|
|
this.sendToClient(msg);
|
|
}
|
|
}
|
|
|
|
// Low-level methods:
|
|
|
|
/**
|
|
* Send given response to the client over the debugger transport socket.
|
|
*
|
|
* @param {Response} resp
|
|
* The response to send back to the client.
|
|
*/
|
|
sendToClient(resp) {
|
|
this.driver.responseCompleted();
|
|
this.sendMessage(resp);
|
|
}
|
|
|
|
/**
|
|
* Marshal message to the Marionette message format and send it.
|
|
*
|
|
* @param {Message} msg
|
|
* The message to send.
|
|
*/
|
|
sendMessage(msg) {
|
|
this.log_(msg);
|
|
let payload = msg.toMsg();
|
|
this.sendRaw(payload);
|
|
}
|
|
|
|
/**
|
|
* Send the given payload over the debugger transport socket to the
|
|
* connected client.
|
|
*
|
|
* @param {Object.<string, ?>} payload
|
|
* The payload to ship.
|
|
*/
|
|
sendRaw(payload) {
|
|
this.conn.send(payload);
|
|
}
|
|
|
|
log_(msg) {
|
|
let a = (msg.origin == MessageOrigin.Client ? " -> " : " <- ");
|
|
let s = JSON.stringify(msg.toMsg());
|
|
logger.trace(this.id + a + s);
|
|
}
|
|
|
|
toString() {
|
|
return `[object server.TCPConnection ${this.id}]`;
|
|
}
|
|
};
|