123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604 |
- // Licensed to the Software Freedom Conservancy (SFC) under one
- // or more contributor license agreements. See the NOTICE file
- // distributed with this work for additional information
- // regarding copyright ownership. The SFC licenses this file
- // to you under the Apache License, Version 2.0 (the
- // "License"); you may not use this file except in compliance
- // with the License. You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing,
- // software distributed under the License is distributed on an
- // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- // KIND, either express or implied. See the License for the
- // specific language governing permissions and limitations
- // under the License.
- 'use strict';
- const fs = require('fs');
- const path = require('path');
- const url = require('url');
- const util = require('util');
- const httpUtil = require('../http/util');
- const io = require('../io');
- const exec = require('../io/exec');
- const {Zip} = require('../io/zip');
- const cmd = require('../lib/command');
- const input = require('../lib/input');
- const promise = require('../lib/promise');
- const webdriver = require('../lib/webdriver');
- const net = require('../net');
- const portprober = require('../net/portprober');
- /**
- * @typedef {(string|!Array<string|number|!stream.Stream|null|undefined>)}
- */
- var StdIoOptions;
- /**
- * @typedef {(string|!IThenable<string>)}
- */
- var CommandLineFlag;
- /**
- * A record object that defines the configuration options for a DriverService
- * instance.
- *
- * @record
- */
- function ServiceOptions() {}
- /**
- * Whether the service should only be accessed on this host's loopback address.
- *
- * @type {(boolean|undefined)}
- */
- ServiceOptions.prototype.loopback;
- /**
- * The host name to access the server on. If this option is specified, the
- * {@link #loopback} option will be ignored.
- *
- * @type {(string|undefined)}
- */
- ServiceOptions.prototype.hostname;
- /**
- * The port to start the server on (must be > 0). If the port is provided as a
- * promise, the service will wait for the promise to resolve before starting.
- *
- * @type {(number|!IThenable<number>)}
- */
- ServiceOptions.prototype.port;
- /**
- * The arguments to pass to the service. If a promise is provided, the service
- * will wait for it to resolve before starting.
- *
- * @type {!(Array<CommandLineFlag>|IThenable<!Array<CommandLineFlag>>)}
- */
- ServiceOptions.prototype.args;
- /**
- * The base path on the server for the WebDriver wire protocol (e.g. '/wd/hub').
- * Defaults to '/'.
- *
- * @type {(string|undefined|null)}
- */
- ServiceOptions.prototype.path;
- /**
- * The environment variables that should be visible to the server process.
- * Defaults to inheriting the current process's environment.
- *
- * @type {(Object<string, string>|undefined)}
- */
- ServiceOptions.prototype.env;
- /**
- * IO configuration for the spawned server process. For more information, refer
- * to the documentation of `child_process.spawn`.
- *
- * @type {(StdIoOptions|undefined)}
- * @see https://nodejs.org/dist/latest-v4.x/docs/api/child_process.html#child_process_options_stdio
- */
- ServiceOptions.prototype.stdio;
- /**
- * Manages the life and death of a native executable WebDriver server.
- *
- * It is expected that the driver server implements the
- * https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol.
- * Furthermore, the managed server should support multiple concurrent sessions,
- * so that this class may be reused for multiple clients.
- */
- class DriverService {
- /**
- * @param {string} executable Path to the executable to run.
- * @param {!ServiceOptions} options Configuration options for the service.
- */
- constructor(executable, options) {
- /** @private {string} */
- this.executable_ = executable;
- /** @private {boolean} */
- this.loopbackOnly_ = !!options.loopback;
- /** @private {(string|undefined)} */
- this.hostname_ = options.hostname;
- /** @private {(number|!IThenable<number>)} */
- this.port_ = options.port;
- /**
- * @private {!(Array<CommandLineFlag>|
- * IThenable<!Array<CommandLineFlag>>)}
- */
- this.args_ = options.args;
- /** @private {string} */
- this.path_ = options.path || '/';
- /** @private {!Object<string, string>} */
- this.env_ = options.env || process.env;
- /**
- * @private {(string|!Array<string|number|!stream.Stream|null|undefined>)}
- */
- this.stdio_ = options.stdio || 'ignore';
- /**
- * A promise for the managed subprocess, or null if the server has not been
- * started yet. This promise will never be rejected.
- * @private {Promise<!exec.Command>}
- */
- this.command_ = null;
- /**
- * Promise that resolves to the server's address or null if the server has
- * not been started. This promise will be rejected if the server terminates
- * before it starts accepting WebDriver requests.
- * @private {Promise<string>}
- */
- this.address_ = null;
- }
- /**
- * @return {!Promise<string>} A promise that resolves to the server's address.
- * @throws {Error} If the server has not been started.
- */
- address() {
- if (this.address_) {
- return this.address_;
- }
- throw Error('Server has not been started.');
- }
- /**
- * Returns whether the underlying process is still running. This does not take
- * into account whether the process is in the process of shutting down.
- * @return {boolean} Whether the underlying service process is running.
- */
- isRunning() {
- return !!this.address_;
- }
- /**
- * Starts the server if it is not already running.
- * @param {number=} opt_timeoutMs How long to wait, in milliseconds, for the
- * server to start accepting requests. Defaults to 30 seconds.
- * @return {!Promise<string>} A promise that will resolve to the server's base
- * URL when it has started accepting requests. If the timeout expires
- * before the server has started, the promise will be rejected.
- */
- start(opt_timeoutMs) {
- if (this.address_) {
- return this.address_;
- }
- var timeout = opt_timeoutMs || DriverService.DEFAULT_START_TIMEOUT_MS;
- var self = this;
- let resolveCommand;
- this.command_ = new Promise(resolve => resolveCommand = resolve);
- this.address_ = new Promise((resolveAddress, rejectAddress) => {
- resolveAddress(Promise.resolve(this.port_).then(port => {
- if (port <= 0) {
- throw Error('Port must be > 0: ' + port);
- }
- return resolveCommandLineFlags(this.args_).then(args => {
- var command = exec(self.executable_, {
- args: args,
- env: self.env_,
- stdio: self.stdio_
- });
- resolveCommand(command);
- var earlyTermination = command.result().then(function(result) {
- var error = result.code == null ?
- Error('Server was killed with ' + result.signal) :
- Error('Server terminated early with status ' + result.code);
- rejectAddress(error);
- self.address_ = null;
- self.command_ = null;
- throw error;
- });
- var hostname = self.hostname_;
- if (!hostname) {
- hostname = !self.loopbackOnly_ && net.getAddress()
- || net.getLoopbackAddress();
- }
- var serverUrl = url.format({
- protocol: 'http',
- hostname: hostname,
- port: port + '',
- pathname: self.path_
- });
- return new Promise((fulfill, reject) => {
- let cancelToken =
- earlyTermination.catch(e => reject(Error(e.message)));
- httpUtil.waitForServer(serverUrl, timeout, cancelToken)
- .then(_ => fulfill(serverUrl), err => {
- if (err instanceof promise.CancellationError) {
- fulfill(serverUrl);
- } else {
- reject(err);
- }
- });
- });
- });
- }));
- });
- return this.address_;
- }
- /**
- * Stops the service if it is not currently running. This function will kill
- * the server immediately. To synchronize with the active control flow, use
- * {@link #stop()}.
- * @return {!Promise} A promise that will be resolved when the server has been
- * stopped.
- */
- kill() {
- if (!this.address_ || !this.command_) {
- return Promise.resolve(); // Not currently running.
- }
- let cmd = this.command_;
- this.address_ = null;
- this.command_ = null;
- return cmd.then(c => c.kill('SIGTERM'));
- }
- /**
- * Schedules a task in the current control flow to stop the server if it is
- * currently running.
- * @return {!promise.Thenable} A promise that will be resolved when
- * the server has been stopped.
- */
- stop() {
- return promise.controlFlow().execute(this.kill.bind(this));
- }
- }
- /**
- * @param {!(Array<CommandLineFlag>|IThenable<!Array<CommandLineFlag>>)} args
- * @return {!Promise<!Array<string>>}
- */
- function resolveCommandLineFlags(args) {
- // Resolve the outer array, then the individual flags.
- return Promise.resolve(args)
- .then(/** !Array<CommandLineFlag> */args => Promise.all(args));
- }
- /**
- * The default amount of time, in milliseconds, to wait for the server to
- * start.
- * @const {number}
- */
- DriverService.DEFAULT_START_TIMEOUT_MS = 30 * 1000;
- /**
- * Creates {@link DriverService} objects that manage a WebDriver server in a
- * child process.
- */
- DriverService.Builder = class {
- /**
- * @param {string} exe Path to the executable to use. This executable must
- * accept the `--port` flag for defining the port to start the server on.
- * @throws {Error} If the provided executable path does not exist.
- */
- constructor(exe) {
- if (!fs.existsSync(exe)) {
- throw Error(`The specified executable path does not exist: ${exe}`);
- }
- /** @private @const {string} */
- this.exe_ = exe;
- /** @private {!ServiceOptions} */
- this.options_ = {
- args: [],
- port: 0,
- env: null,
- stdio: 'ignore'
- };
- }
- /**
- * Define additional command line arguments to use when starting the server.
- *
- * @param {...CommandLineFlag} var_args The arguments to include.
- * @return {!THIS} A self reference.
- * @this {THIS}
- * @template THIS
- */
- addArguments(var_args) {
- let args = Array.prototype.slice.call(arguments, 0);
- this.options_.args = this.options_.args.concat(args);
- return this;
- }
- /**
- * Sets the host name to access the server on. If specified, the
- * {@linkplain #setLoopback() loopback} setting will be ignored.
- *
- * @param {string} hostname
- * @return {!DriverService.Builder} A self reference.
- */
- setHostname(hostname) {
- this.options_.hostname = hostname;
- return this;
- }
- /**
- * Sets whether the service should be accessed at this host's loopback
- * address.
- *
- * @param {boolean} loopback
- * @return {!DriverService.Builder} A self reference.
- */
- setLoopback(loopback) {
- this.options_.loopback = loopback;
- return this;
- }
- /**
- * Sets the base path for WebDriver REST commands (e.g. "/wd/hub").
- * By default, the driver will accept commands relative to "/".
- *
- * @param {?string} basePath The base path to use, or `null` to use the
- * default.
- * @return {!DriverService.Builder} A self reference.
- */
- setPath(basePath) {
- this.options_.path = basePath;
- return this;
- }
- /**
- * Sets the port to start the server on.
- *
- * @param {number} port The port to use, or 0 for any free port.
- * @return {!DriverService.Builder} A self reference.
- * @throws {Error} If an invalid port is specified.
- */
- setPort(port) {
- if (port < 0) {
- throw Error(`port must be >= 0: ${port}`);
- }
- this.options_.port = port;
- return this;
- }
- /**
- * Defines the environment to start the server under. This setting will be
- * inherited by every browser session started by the server. By default, the
- * server will inherit the enviroment of the current process.
- *
- * @param {(Map<string, string>|Object<string, string>|null)} env The desired
- * environment to use, or `null` if the server should inherit the
- * current environment.
- * @return {!DriverService.Builder} A self reference.
- */
- setEnvironment(env) {
- if (env instanceof Map) {
- let tmp = {};
- env.forEach((value, key) => tmp[key] = value);
- env = tmp;
- }
- this.options_.env = env;
- return this;
- }
- /**
- * IO configuration for the spawned server process. For more information,
- * refer to the documentation of `child_process.spawn`.
- *
- * @param {StdIoOptions} config The desired IO configuration.
- * @return {!DriverService.Builder} A self reference.
- * @see https://nodejs.org/dist/latest-v4.x/docs/api/child_process.html#child_process_options_stdio
- */
- setStdio(config) {
- this.options_.stdio = config;
- return this;
- }
- /**
- * Creates a new DriverService using this instance's current configuration.
- *
- * @return {!DriverService} A new driver service.
- */
- build() {
- let port = this.options_.port || portprober.findFreePort();
- let args = Promise.resolve(port).then(port => {
- return this.options_.args.concat('--port=' + port);
- });
- let options =
- /** @type {!ServiceOptions} */
- (Object.assign({}, this.options_, {args, port}));
- return new DriverService(this.exe_, options);
- }
- };
- /**
- * Manages the life and death of the
- * <a href="http://selenium-release.storage.googleapis.com/index.html">
- * standalone Selenium server</a>.
- */
- class SeleniumServer extends DriverService {
- /**
- * @param {string} jar Path to the Selenium server jar.
- * @param {SeleniumServer.Options=} opt_options Configuration options for the
- * server.
- * @throws {Error} If the path to the Selenium jar is not specified or if an
- * invalid port is specified.
- */
- constructor(jar, opt_options) {
- if (!jar) {
- throw Error('Path to the Selenium jar not specified');
- }
- var options = opt_options || {};
- if (options.port < 0) {
- throw Error('Port must be >= 0: ' + options.port);
- }
- let port = options.port || portprober.findFreePort();
- let args = Promise.all([port, options.jvmArgs || [], options.args || []])
- .then(resolved => {
- let port = resolved[0];
- let jvmArgs = resolved[1];
- let args = resolved[2];
- return jvmArgs.concat('-jar', jar, '-port', port).concat(args);
- });
- let java = 'java';
- if (process.env['JAVA_HOME']) {
- java = path.join(process.env['JAVA_HOME'], 'bin/java');
- }
- super(java, {
- loopback: options.loopback,
- port: port,
- args: args,
- path: '/wd/hub',
- env: options.env,
- stdio: options.stdio
- });
- }
- }
- /**
- * Options for the Selenium server:
- *
- * - `loopback` - Whether the server should only be accessed on this host's
- * loopback address.
- * - `port` - The port to start the server on (must be > 0). If the port is
- * provided as a promise, the service will wait for the promise to resolve
- * before starting.
- * - `args` - The arguments to pass to the service. If a promise is provided,
- * the service will wait for it to resolve before starting.
- * - `jvmArgs` - The arguments to pass to the JVM. If a promise is provided,
- * the service will wait for it to resolve before starting.
- * - `env` - The environment variables that should be visible to the server
- * process. Defaults to inheriting the current process's environment.
- * - `stdio` - IO configuration for the spawned server process. For more
- * information, refer to the documentation of `child_process.spawn`.
- *
- * @typedef {{
- * loopback: (boolean|undefined),
- * port: (number|!promise.Promise<number>),
- * args: !(Array<string>|promise.Promise<!Array<string>>),
- * jvmArgs: (!Array<string>|
- * !promise.Promise<!Array<string>>|
- * undefined),
- * env: (!Object<string, string>|undefined),
- * stdio: (string|!Array<string|number|!stream.Stream|null|undefined>|
- * undefined)
- * }}
- */
- SeleniumServer.Options;
- /**
- * A {@link webdriver.FileDetector} that may be used when running
- * against a remote
- * [Selenium server](http://selenium-release.storage.googleapis.com/index.html).
- *
- * When a file path on the local machine running this script is entered with
- * {@link webdriver.WebElement#sendKeys WebElement#sendKeys}, this file detector
- * will transfer the specified file to the Selenium server's host; the sendKeys
- * command will be updated to use the transfered file's path.
- *
- * __Note:__ This class depends on a non-standard command supported on the
- * Java Selenium server. The file detector will fail if used with a server that
- * only supports standard WebDriver commands (such as the ChromeDriver).
- *
- * @final
- */
- class FileDetector extends input.FileDetector {
- /**
- * Prepares a `file` for use with the remote browser. If the provided path
- * does not reference a normal file (i.e. it does not exist or is a
- * directory), then the promise returned by this method will be resolved with
- * the original file path. Otherwise, this method will upload the file to the
- * remote server, which will return the file's path on the remote system so
- * it may be referenced in subsequent commands.
- *
- * @override
- */
- handleFile(driver, file) {
- return io.stat(file).then(function(stats) {
- if (stats.isDirectory()) {
- return file; // Not a valid file, return original input.
- }
- let zip = new Zip;
- return zip.addFile(file)
- .then(() => zip.toBuffer())
- .then(buf => buf.toString('base64'))
- .then(encodedZip => {
- let command = new cmd.Command(cmd.Name.UPLOAD_FILE)
- .setParameter('file', encodedZip);
- return driver.schedule(command,
- 'remote.FileDetector.handleFile(' + file + ')');
- });
- }, function(err) {
- if (err.code === 'ENOENT') {
- return file; // Not a file; return original input.
- }
- throw err;
- });
- }
- }
- // PUBLIC API
- exports.DriverService = DriverService;
- exports.FileDetector = FileDetector;
- exports.SeleniumServer = SeleniumServer;
- exports.ServiceOptions = ServiceOptions; // Exported for API docs.
|