# remote/index.js

    1// Copyright 2013 Software Freedom Conservancy2//3// Licensed under the Apache License, Version 2.0 (the "License");4// you may not use this file except in compliance with the License.5//     You may obtain a copy of the License at6//7// http://www.apache.org/licenses/LICENSE-2.08//9// Unless required by applicable law or agreed to in writing, software10// distributed under the License is distributed on an "AS IS" BASIS,11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12// See the License for the specific language governing permissions and13// limitations under the License.1415'use strict';1617var path = require('path'),18    url = require('url'),19    util = require('util');2021var promise = require('../').promise,22    httpUtil = require('../http/util'),23    exec = require('../io/exec'),24    net = require('../net'),25    portprober = require('../net/portprober');26272829/**30 * Configuration options for a DriverService instance.31 * <ul>32 * <li>33 * <li>{@code loopback} - Whether the service should only be accessed on this34 *     host's loopback address.35 * <li>{@code port} - The port to start the server on (must be > 0). If the36 *     port is provided as a promise, the service will wait for the promise to37 *     resolve before starting.38 * <li>{@code args} - The arguments to pass to the service. If a promise is39 *     provided, the service will wait for it to resolve before starting.40 * <li>{@code path} - The base path on the server for the WebDriver wire41 *     protocol (e.g. '/wd/hub'). Defaults to '/'.42 * <li>{@code env} - The environment variables that should be visible to the43 *     server process. Defaults to inheriting the current process's44 *     environment.45 * <li>{@code stdio} - IO configuration for the spawned server process. For46 *     more information, refer to the documentation of47 *     {@code child_process.spawn}.48 * </ul>49 *50 * @typedef {{51 *   port: (number|!webdriver.promise.Promise.<number>),52 *   args: !(Array.<string>|webdriver.promise.Promise.<!Array.<string>>),53 *   path: (string|undefined),54 *   env: (!Object.<string, string>|undefined),55 *   stdio: (string|!Array.<string|number|!Stream|null|undefined>|undefined)56 * }}57 */58var ServiceOptions;596061/**62 * Manages the life and death of a native executable WebDriver server.63 *64 * <p>It is expected that the driver server implements the65 * <a href="http://code.google.com/p/selenium/wiki/JsonWireProtocol">WebDriver66 * Wire Protocol</a>. Furthermore, the managed server should support multiple67 * concurrent sessions, so that this class may be reused for multiple clients.68 *69 * @param {string} executable Path to the executable to run.70 * @param {!ServiceOptions} options Configuration options for the service.71 * @constructor72 */73function DriverService(executable, options) {7475  /** @private {string} */76  this.executable_ = executable;7778  /** @private {boolean} */79  this.loopbackOnly_ = !!options.loopback;8081  /** @private {(number|!webdriver.promise.Promise.<number>)} */82  this.port_ = options.port;8384  /**85   * @private {!(Array.<string>|webdriver.promise.Promise.<!Array.<string>>)}86   */87  this.args_ = options.args;8889  /** @private {string} */90  this.path_ = options.path || '/';9192  /** @private {!Object.<string, string>} */93  this.env_ = options.env || process.env;9495  /** @private {(string|!Array.<string|number|!Stream|null|undefined>)} */96  this.stdio_ = options.stdio || 'ignore';9798  /**99   * A promise for the managed subprocess, or null if the server has not been100   * started yet. This promise will never be rejected.101   * @private {promise.Promise.<!exec.Command>}102   */103  this.command_ = null;104105  /**106   * Promise that resolves to the server's address or null if the server has107   * not been started. This promise will be rejected if the server terminates108   * before it starts accepting WebDriver requests.109   * @private {promise.Promise.<string>}110   */111  this.address_ = null;112}113114115/**116 * The default amount of time, in milliseconds, to wait for the server to117 * start.118 * @type {number}119 */120DriverService.DEFAULT_START_TIMEOUT_MS = 30 * 1000;121122123/**124 * @return {!webdriver.promise.Promise.<string>} A promise that resolves to125 *    the server's address.126 * @throws {Error} If the server has not been started.127 */128DriverService.prototype.address = function() {129  if (this.address_) {130    return this.address_;131  }132  throw Error('Server has not been started.');133};134135136/**137 * Returns whether the underlying process is still running. This does not take138 * into account whether the process is in the process of shutting down.139 * @return {boolean} Whether the underlying service process is running.140 */141DriverService.prototype.isRunning = function() {142  return !!this.address_;143};144145146/**147 * Starts the server if it is not already running.148 * @param {number=} opt_timeoutMs How long to wait, in milliseconds, for the149 *     server to start accepting requests. Defaults to 30 seconds.150 * @return {!promise.Promise.<string>} A promise that will resolve151 *     to the server's base URL when it has started accepting requests. If the152 *     timeout expires before the server has started, the promise will be153 *     rejected.154 */155DriverService.prototype.start = function(opt_timeoutMs) {156  if (this.address_) {157    return this.address_;158  }159160  var timeout = opt_timeoutMs || DriverService.DEFAULT_START_TIMEOUT_MS;161162  var self = this;163  this.command_ = promise.defer();164  this.address_ = promise.defer();165  this.address_.fulfill(promise.when(this.port_, function(port) {166    if (port <= 0) {167      throw Error('Port must be > 0: ' + port);168    }169    return promise.when(self.args_, function(args) {170      var command = exec(self.executable_, {171        args: args,172        env: self.env_,173        stdio: self.stdio_174      });175176      self.command_.fulfill(command);177178      command.result().then(function(result) {179        self.address_.reject(result.code == null ?180            Error('Server was killed with ' + result.signal) :181            Error('Server exited with ' + result.code));182        self.address_ = null;183        self.command_ = null;184      });185186      var serverUrl = url.format({187        protocol: 'http',188        hostname: !self.loopbackOnly_ && net.getAddress() ||189            net.getLoopbackAddress(),190        port: port,191        pathname: self.path_192      });193194      return httpUtil.waitForServer(serverUrl, timeout).then(function() {195        return serverUrl;196      });197    });198  }));199200  return this.address_;201};202203204/**205 * Stops the service if it is not currently running. This function will kill206 * the server immediately. To synchronize with the active control flow, use207 * {@link #stop()}.208 * @return {!webdriver.promise.Promise} A promise that will be resolved when209 *     the server has been stopped.210 */211DriverService.prototype.kill = function() {212  if (!this.address_ || !this.command_) {213    return promise.fulfilled();  // Not currently running.214  }215  return this.command_.then(function(command) {216    command.kill('SIGTERM');217  });218};219220221/**222 * Schedules a task in the current control flow to stop the server if it is223 * currently running.224 * @return {!webdriver.promise.Promise} A promise that will be resolved when225 *     the server has been stopped.226 */227DriverService.prototype.stop = function() {228  return promise.controlFlow().execute(this.kill.bind(this));229};230231232233/**234 * Manages the life and death of the Selenium standalone server. The server235 * may be obtained from http://selenium-release.storage.googleapis.com/index.html.236 * @param {string} jar Path to the Selenium server jar.237 * @param {!SeleniumServer.Options} options Configuration options for the238 *     server.239 * @throws {Error} If an invalid port is specified.240 * @constructor241 * @extends {DriverService}242 */243function SeleniumServer(jar, options) {244  if (options.port < 0)245    throw Error('Port must be >= 0: ' + options.port);246247  var port = options.port || portprober.findFreePort();248  var args = promise.when(options.jvmArgs || [], function(jvmArgs) {249    return promise.when(options.args || [], function(args) {250      return promise.when(port, function(port) {251        return jvmArgs.concat(['-jar', jar, '-port', port]).concat(args);252      });253    });254  });255256  DriverService.call(this, 'java', {257    port: port,258    args: args,259    path: '/wd/hub',260    env: options.env,261    stdio: options.stdio262  });263}264util.inherits(SeleniumServer, DriverService);265266267/**268 * Options for the Selenium server:269 * <ul>270 * <li>{@code port} - The port to start the server on (must be > 0). If the271 *     port is provided as a promise, the service will wait for the promise to272 *     resolve before starting.273 * <li>{@code args} - The arguments to pass to the service. If a promise is274 *     provided, the service will wait for it to resolve before starting.275 * <li>{@code jvmArgs} - The arguments to pass to the JVM. If a promise is276 *     provided, the service will wait for it to resolve before starting.277 * <li>{@code env} - The environment variables that should be visible to the278 *     server process. Defaults to inheriting the current process's279 *     environment.280 * <li>{@code stdio} - IO configuration for the spawned server process. For281 *     more information, refer to the documentation of282 *     {@code child_process.spawn}.283 * </ul>284 *285 * @typedef {{286 *   port: (number|!webdriver.promise.Promise.<number>),287 *   args: !(Array.<string>|webdriver.promise.Promise.<!Array.<string>>),288 *   jvmArgs: (!Array.<string>|289 *             !webdriver.promise.Promise.<!Array.<string>>|290 *             undefined),291 *   env: (!Object.<string, string>|undefined),292 *   stdio: (string|!Array.<string|number|!Stream|null|undefined>|undefined)293 * }}294 */295SeleniumServer.Options;296297298// PUBLIC API299300exports.DriverService = DriverService;301exports.SeleniumServer = SeleniumServer;