# 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 spawn = require('child_process').spawn,18    os = require('os'),19    path = require('path'),20    url = require('url'),21    util = require('util');2223var promise = require('../').promise,24    httpUtil = require('../http/util'),25    net = require('../net'),26    portprober = require('../net/portprober');27282930/**31 * Configuration options for a DriverService instance.32 * <ul>33 * <li>34 * <li>{@code loopback} - Whether the service should only be accessed on this35 *     host's loopback address.36 * <li>{@code port} - The port to start the server on (must be > 0). If the37 *     port is provided as a promise, the service will wait for the promise to38 *     resolve before starting.39 * <li>{@code args} - The arguments to pass to the service. If a promise is40 *     provided, the service will wait for it to resolve before starting.41 * <li>{@code path} - The base path on the server for the WebDriver wire42 *     protocol (e.g. '/wd/hub'). Defaults to '/'.43 * <li>{@code env} - The environment variables that should be visible to the44 *     server process. Defaults to inheriting the current process's45 *     environment.46 * <li>{@code stdio} - IO configuration for the spawned server process. For47 *     more information, refer to the documentation of48 *     {@code child_process.spawn}.49 * </ul>50 *51 * @typedef {{52 *   port: (number|!webdriver.promise.Promise.<number>),53 *   args: !(Array.<string>|webdriver.promise.Promise.<!Array.<string>>),54 *   path: (string|undefined),55 *   env: (!Object.<string, string>|undefined),56 *   stdio: (string|!Array.<string|number|!Stream|null|undefined>|undefined)57 * }}58 */59var ServiceOptions;606162/**63 * Manages the life and death of a native executable WebDriver server.64 *65 * <p>It is expected that the driver server implements the66 * <a href="http://code.google.com/p/selenium/wiki/JsonWireProtocol">WebDriver67 * Wire Protocol</a>. Furthermore, the managed server should support multiple68 * concurrent sessions, so that this class may be reused for multiple clients.69 *70 * @param {string} executable Path to the executable to run.71 * @param {!ServiceOptions} options Configuration options for the service.72 * @constructor73 */74function DriverService(executable, options) {7576  /** @private {string} */77  this.executable_ = executable;7879  /** @private {boolean} */80  this.loopbackOnly_ = !!options.loopback;8182  /** @private {(number|!webdriver.promise.Promise.<number>)} */83  this.port_ = options.port;8485  /**86   * @private {!(Array.<string>|webdriver.promise.Promise.<!Array.<string>>)}87   */88  this.args_ = options.args;8990  /** @private {string} */91  this.path_ = options.path || '/';9293  /** @private {!Object.<string, string>} */94  this.env_ = options.env || process.env;9596  /** @private {(string|!Array.<string|number|!Stream|null|undefined>)} */97  this.stdio_ = options.stdio || 'ignore';98}99100101/**102 * The default amount of time, in milliseconds, to wait for the server to103 * start.104 * @type {number}105 */106DriverService.DEFAULT_START_TIMEOUT_MS = 30 * 1000;107108109/** @private {child_process.ChildProcess} */110DriverService.prototype.process_ = null;111112113/**114 * Promise that resolves to the server's address or null if the server has not115 * been started.116 * @private {webdriver.promise.Promise.<string>}117 */118DriverService.prototype.address_ = null;119120121/**122 * Promise that tracks the status of shutting down the server, or null if the123 * server is not currently shutting down.124 * @private {webdriver.promise.Promise}125 */126DriverService.prototype.shutdownHook_ = null;127128129/**130 * @return {!webdriver.promise.Promise.<string>} A promise that resolves to131 *    the server's address.132 * @throws {Error} If the server has not been started.133 */134DriverService.prototype.address = function() {135  if (this.address_) {136    return this.address_;137  }138  throw Error('Server has not been started.');139};140141142/**143 * @return {boolean} Whether the underlying service process is running.144 */145DriverService.prototype.isRunning = function() {146  return !!this.address_;147};148149150/**151 * Starts the server if it is not already running.152 * @param {number=} opt_timeoutMs How long to wait, in milliseconds, for the153 *     server to start accepting requests. Defaults to 30 seconds.154 * @return {!webdriver.promise.Promise.<string>} A promise that will resolve155 *     to the server's base URL when it has started accepting requests. If the156 *     timeout expires before the server has started, the promise will be157 *     rejected.158 */159DriverService.prototype.start = function(opt_timeoutMs) {160  if (this.address_) {161    return this.address_;162  }163164  var timeout = opt_timeoutMs || DriverService.DEFAULT_START_TIMEOUT_MS;165166  var self = this;167  this.address_ = promise.defer();168  this.address_.fulfill(promise.when(this.port_, function(port) {169    if (port <= 0) {170      throw Error('Port must be > 0: ' + port);171    }172    return promise.when(self.args_, function(args) {173      self.process_ = spawn(self.executable_, args, {174        env: self.env_,175        stdio: self.stdio_176      }).once('exit', onServerExit);177178      // This process should not wait on the spawned child, however, we do179      // want to ensure the child is killed when this process exits.180      self.process_.unref();181      process.once('exit', killServer);182183      var serverUrl = url.format({184        protocol: 'http',185        hostname: !self.loopbackOnly_ && net.getAddress() ||186            net.getLoopbackAddress(),187        port: port,188        pathname: self.path_189      });190191      return httpUtil.waitForServer(serverUrl, timeout).then(function() {192        return serverUrl;193      });194    });195  }));196197  return this.address_;198199  function onServerExit(code, signal) {200    self.address_.reject(code == null ?201        Error('Server was killed with ' + signal) :202        Error('Server exited with ' + code));203204    if (self.shutdownHook_) {205      self.shutdownHook_.fulfill();206    }207208    self.shutdownHook_ = null;209    self.address_ = null;210    self.process_ = null;211    process.removeListener('exit', killServer);212  }213214  function killServer() {215    process.removeListener('exit', killServer);216    self.process_ && self.process_.kill('SIGTERM');217  }218};219220221/**222 * Stops the service if it is not currently running. This function will kill223 * the server immediately. To synchronize with the active control flow, use224 * {@link #stop()}.225 * @return {!webdriver.promise.Promise} A promise that will be resolved when226 *     the server has been stopped.227 */228DriverService.prototype.kill = function() {229  if (!this.address_) {230    return promise.fulfilled();  // Not currently running.231  }232233  if (!this.shutdownHook_) {234    // No process: still starting; wait on address.235    // Otherwise, kill the process now. Exit handler will resolve the236    // shutdown hook.237    if (this.process_) {238      this.shutdownHook_ = promise.defer();239      this.process_.kill('SIGTERM');240    } else {241      var self = this;242      this.shutdownHook_ = this.address_.thenFinally(function() {243        self.process_ && self.process_.kill('SIGTERM');244      });245    }246  }247248  return this.shutdownHook_;249};250251252/**253 * Schedules a task in the current control flow to stop the server if it is254 * currently running.255 * @return {!webdriver.promise.Promise} A promise that will be resolved when256 *     the server has been stopped.257 */258DriverService.prototype.stop = function() {259  return promise.controlFlow().execute(this.kill.bind(this));260};261262263264/**265 * Manages the life and death of the Selenium standalone server. The server266 * may be obtained from http://selenium-release.storage.googleapis.com/index.html.267 * @param {string} jar Path to the Selenium server jar.268 * @param {!SeleniumServer.Options} options Configuration options for the269 *     server.270 * @throws {Error} If an invalid port is specified.271 * @constructor272 * @extends {DriverService}273 */274function SeleniumServer(jar, options) {275  if (options.port < 0)276    throw Error('Port must be >= 0: ' + options.port);277278  var port = options.port || portprober.findFreePort();279  var args = promise.when(options.jvmArgs || [], function(jvmArgs) {280    return promise.when(options.args || [], function(args) {281      return promise.when(port, function(port) {282        return jvmArgs.concat(['-jar', jar, '-port', port]).concat(args);283      });284    });285  });286287  DriverService.call(this, 'java', {288    port: port,289    args: args,290    path: '/wd/hub',291    env: options.env,292    stdio: options.stdio293  });294}295util.inherits(SeleniumServer, DriverService);296297298/**299 * Options for the Selenium server:300 * <ul>301 * <li>{@code port} - The port to start the server on (must be > 0). If the302 *     port is provided as a promise, the service will wait for the promise to303 *     resolve before starting.304 * <li>{@code args} - The arguments to pass to the service. If a promise is305 *     provided, the service will wait for it to resolve before starting.306 * <li>{@code jvmArgs} - The arguments to pass to the JVM. If a promise is307 *     provided, the service will wait for it to resolve before starting.308 * <li>{@code env} - The environment variables that should be visible to the309 *     server process. Defaults to inheriting the current process's310 *     environment.311 * <li>{@code stdio} - IO configuration for the spawned server process. For312 *     more information, refer to the documentation of313 *     {@code child_process.spawn}.314 * </ul>315 *316 * @typedef {{317 *   port: (number|!webdriver.promise.Promise.<number>),318 *   args: !(Array.<string>|webdriver.promise.Promise.<!Array.<string>>),319 *   jvmArgs: (!Array.<string>|320 *             !webdriver.promise.Promise.<!Array.<string>>|321 *             undefined),322 *   env: (!Object.<string, string>|undefined),323 *   stdio: (string|!Array.<string|number|!Stream|null|undefined>|undefined)324 * }}325 */326SeleniumServer.Options;327328329// PUBLIC API330331exports.DriverService = DriverService;332exports.SeleniumServer = SeleniumServer;