# 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>{@code port} - The port to start the server on (must be > 0). If the34 *     port is provided as a promise, the service will wait for the promise to35 *     resolve before starting.36 * <li>{@code args} - The arguments to pass to the service. If a promise is37 *     provided, the service will wait for it to resolve before starting.38 * <li>{@code path} - The base path on the server for the WebDriver wire39 *     protocol (e.g. '/wd/hub'). Defaults to '/'.40 * <li>{@code env} - The environment variables that should be visible to the41 *     server process. Defaults to inheriting the current process's42 *     environment.43 * <li>{@code stdio} - IO configuration for the spawned server process. For44 *     more information, refer to the documentation of45 *     {@code child_process.spawn}.46 * </ul>47 *48 * @typedef {{49 *   port: (number|!webdriver.promise.Promise.<number>),50 *   args: !(Array.<string>|webdriver.promise.Promise.<!Array.<string>>),51 *   path: (string|undefined),52 *   env: (!Object.<string, string>|undefined),53 *   stdio: (string|!Array.<string|number|!Stream|null|undefined>|undefined)54 * }}55 */56var ServiceOptions;575859/**60 * Manages the life and death of a native executable WebDriver server.61 *62 * <p>It is expected that the driver server implements the63 * <a href="http://code.google.com/p/selenium/wiki/JsonWireProtocol">WebDriver64 * Wire Protocol</a>. Furthermore, the managed server should support multiple65 * concurrent sessions, so that this class may be reused for multiple clients.66 *67 * @param {string} executable Path to the executable to run.68 * @param {!ServiceOptions} options Configuration options for the service.69 * @constructor70 */71function DriverService(executable, options) {7273  /** @private {string} */74  this.executable_ = executable;7576  /** @private {(number|!webdriver.promise.Promise.<number>)} */77  this.port_ = options.port;7879  /**80   * @private {!(Array.<string>|webdriver.promise.Promise.<!Array.<string>>)}81   */82  this.args_ = options.args;8384  /** @private {string} */85  this.path_ = options.path || '/';8687  /** @private {!Object.<string, string>} */88  this.env_ = options.env || process.env;8990  /** @private {(string|!Array.<string|number|!Stream|null|undefined>)} */91  this.stdio_ = options.stdio || 'ignore';92}939495/**96 * The default amount of time, in milliseconds, to wait for the server to97 * start.98 * @type {number}99 */100DriverService.DEFAULT_START_TIMEOUT_MS = 30 * 1000;101102103/** @private {child_process.ChildProcess} */104DriverService.prototype.process_ = null;105106107/**108 * Promise that resolves to the server's address or null if the server has not109 * been started.110 * @private {webdriver.promise.Promise.<string>}111 */112DriverService.prototype.address_ = null;113114115/**116 * Promise that tracks the status of shutting down the server, or null if the117 * server is not currently shutting down.118 * @private {webdriver.promise.Promise}119 */120DriverService.prototype.shutdownHook_ = null;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 * @return {boolean} Whether the underlying service process is running.138 */139DriverService.prototype.isRunning = function() {140  return !!this.address_;141};142143144/**145 * Starts the server if it is not already running.146 * @param {number=} opt_timeoutMs How long to wait, in milliseconds, for the147 *     server to start accepting requests. Defaults to 30 seconds.148 * @return {!webdriver.promise.Promise.<string>} A promise that will resolve149 *     to the server's base URL when it has started accepting requests. If the150 *     timeout expires before the server has started, the promise will be151 *     rejected.152 */153DriverService.prototype.start = function(opt_timeoutMs) {154  if (this.address_) {155    return this.address_;156  }157158  var timeout = opt_timeoutMs || DriverService.DEFAULT_START_TIMEOUT_MS;159160  var self = this;161  this.address_ = promise.defer();162  this.address_.fulfill(promise.when(this.port_, function(port) {163    if (port <= 0) {164      throw Error('Port must be > 0: ' + port);165    }166    return promise.when(self.args_, function(args) {167      self.process_ = spawn(self.executable_, args, {168        env: self.env_,169        stdio: self.stdio_170      }).once('exit', onServerExit);171172      // This process should not wait on the spawned child, however, we do173      // want to ensure the child is killed when this process exits.174      self.process_.unref();175      process.once('exit', killServer);176177      var serverUrl = url.format({178        protocol: 'http',179        hostname: net.getAddress() || net.getLoopbackAddress(),180        port: port,181        pathname: self.path_182      });183184      return httpUtil.waitForServer(serverUrl, timeout).then(function() {185        return serverUrl;186      });187    });188  }));189190  return this.address_;191192  function onServerExit(code, signal) {193    self.address_.reject(code == null ?194        Error('Server was killed with ' + signal) :195        Error('Server exited with ' + code));196197    if (self.shutdownHook_) {198      self.shutdownHook_.fulfill();199    }200201    self.shutdownHook_ = null;202    self.address_ = null;203    self.process_ = null;204    process.removeListener('exit', killServer);205  }206207  function killServer() {208    process.removeListener('exit', killServer);209    self.process_ && self.process_.kill('SIGTERM');210  }211};212213214/**215 * Stops the service if it is not currently running. This function will kill216 * the server immediately. To synchronize with the active control flow, use217 * {@link #stop()}.218 * @return {!webdriver.promise.Promise} A promise that will be resolved when219 *     the server has been stopped.220 */221DriverService.prototype.kill = function() {222  if (!this.address_) {223    return promise.fulfilled();  // Not currently running.224  }225226  if (!this.shutdownHook_) {227    // No process: still starting; wait on address.228    // Otherwise, kill the process now. Exit handler will resolve the229    // shutdown hook.230    if (this.process_) {231      this.shutdownHook_ = promise.defer();232      this.process_.kill('SIGTERM');233    } else {234      var self = this;235      this.shutdownHook_ = this.address_.thenFinally(function() {236        self.process_ && self.process_.kill('SIGTERM');237      });238    }239  }240241  return this.shutdownHook_;242};243244245/**246 * Schedules a task in the current control flow to stop the server if it is247 * currently running.248 * @return {!webdriver.promise.Promise} A promise that will be resolved when249 *     the server has been stopped.250 */251DriverService.prototype.stop = function() {252  return promise.controlFlow().execute(this.kill.bind(this));253};254255256257/**258 * Manages the life and death of the Selenium standalone server. The server259 * may be obtained from https://code.google.com/p/selenium/downloads/list.260 * @param {string} jar Path to the Selenium server jar.261 * @param {!SeleniumServer.Options} options Configuration options for the262 *     server.263 * @throws {Error} If an invalid port is specified.264 * @constructor265 * @extends {DriverService}266 */267function SeleniumServer(jar, options) {268  if (options.port < 0)269    throw Error('Port must be >= 0: ' + options.port);270271  var port = options.port || portprober.findFreePort();272  var args = promise.when(options.jvmArgs || [], function(jvmArgs) {273    return promise.when(options.args || [], function(args) {274      return promise.when(port, function(port) {275        return jvmArgs.concat(['-jar', jar, '-port', port]).concat(args);276      });277    });278  });279280  DriverService.call(this, 'java', {281    port: port,282    args: args,283    path: '/wd/hub',284    env: options.env,285    stdio: options.stdio286  });287}288util.inherits(SeleniumServer, DriverService);289290291/**292 * Options for the Selenium server:293 * <ul>294 * <li>{@code port} - The port to start the server on (must be > 0). If the295 *     port is provided as a promise, the service will wait for the promise to296 *     resolve before starting.297 * <li>{@code args} - The arguments to pass to the service. If a promise is298 *     provided, the service will wait for it to resolve before starting.299 * <li>{@code jvmArgs} - The arguments to pass to the JVM. If a promise is300 *     provided, the service will wait for it to resolve before starting.301 * <li>{@code env} - The environment variables that should be visible to the302 *     server process. Defaults to inheriting the current process's303 *     environment.304 * <li>{@code stdio} - IO configuration for the spawned server process. For305 *     more information, refer to the documentation of306 *     {@code child_process.spawn}.307 * </ul>308 *309 * @typedef {{310 *   port: (number|!webdriver.promise.Promise.<number>),311 *   args: !(Array.<string>|webdriver.promise.Promise.<!Array.<string>>),312 *   jvmArgs: (!Array.<string>|313 *             !webdriver.promise.Promise.<!Array.<string>>|314 *             undefined),315 *   env: (!Object.<string, string>|undefined),316 *   stdio: (string|!Array.<string|number|!Stream|null|undefined>|undefined)317 * }}318 */319SeleniumServer.Options;320321322// PUBLIC API323324exports.DriverService = DriverService;325exports.SeleniumServer = SeleniumServer;