remote/index.js

1// Copyright 2013 Software Freedom Conservancy
2//
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 at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// 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 and
13// limitations under the License.
14
15'use strict';
16
17var path = require('path'),
18 url = require('url'),
19 util = require('util');
20
21var promise = require('../').promise,
22 httpUtil = require('../http/util'),
23 exec = require('../io/exec'),
24 net = require('../net'),
25 portprober = require('../net/portprober');
26
27
28
29/**
30 * Configuration options for a DriverService instance.
31 * <ul>
32 * <li>
33 * <li>{@code loopback} - Whether the service should only be accessed on this
34 * host's loopback address.
35 * <li>{@code port} - The port to start the server on (must be > 0). If the
36 * port is provided as a promise, the service will wait for the promise to
37 * resolve before starting.
38 * <li>{@code args} - The arguments to pass to the service. If a promise is
39 * provided, the service will wait for it to resolve before starting.
40 * <li>{@code path} - The base path on the server for the WebDriver wire
41 * protocol (e.g. '/wd/hub'). Defaults to '/'.
42 * <li>{@code env} - The environment variables that should be visible to the
43 * server process. Defaults to inheriting the current process's
44 * environment.
45 * <li>{@code stdio} - IO configuration for the spawned server process. For
46 * more information, refer to the documentation of
47 * {@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;
59
60
61/**
62 * Manages the life and death of a native executable WebDriver server.
63 *
64 * <p>It is expected that the driver server implements the
65 * <a href="http://code.google.com/p/selenium/wiki/JsonWireProtocol">WebDriver
66 * Wire Protocol</a>. Furthermore, the managed server should support multiple
67 * 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 * @constructor
72 */
73function DriverService(executable, options) {
74
75 /** @private {string} */
76 this.executable_ = executable;
77
78 /** @private {boolean} */
79 this.loopbackOnly_ = !!options.loopback;
80
81 /** @private {(number|!webdriver.promise.Promise.<number>)} */
82 this.port_ = options.port;
83
84 /**
85 * @private {!(Array.<string>|webdriver.promise.Promise.<!Array.<string>>)}
86 */
87 this.args_ = options.args;
88
89 /** @private {string} */
90 this.path_ = options.path || '/';
91
92 /** @private {!Object.<string, string>} */
93 this.env_ = options.env || process.env;
94
95 /** @private {(string|!Array.<string|number|!Stream|null|undefined>)} */
96 this.stdio_ = options.stdio || 'ignore';
97
98 /**
99 * A promise for the managed subprocess, or null if the server has not been
100 * started yet. This promise will never be rejected.
101 * @private {promise.Promise.<!exec.Command>}
102 */
103 this.command_ = null;
104
105 /**
106 * Promise that resolves to the server's address or null if the server has
107 * not been started. This promise will be rejected if the server terminates
108 * before it starts accepting WebDriver requests.
109 * @private {promise.Promise.<string>}
110 */
111 this.address_ = null;
112}
113
114
115/**
116 * The default amount of time, in milliseconds, to wait for the server to
117 * start.
118 * @type {number}
119 */
120DriverService.DEFAULT_START_TIMEOUT_MS = 30 * 1000;
121
122
123/**
124 * @return {!webdriver.promise.Promise.<string>} A promise that resolves to
125 * 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};
134
135
136/**
137 * Returns whether the underlying process is still running. This does not take
138 * 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};
144
145
146/**
147 * Starts the server if it is not already running.
148 * @param {number=} opt_timeoutMs How long to wait, in milliseconds, for the
149 * server to start accepting requests. Defaults to 30 seconds.
150 * @return {!promise.Promise.<string>} A promise that will resolve
151 * to the server's base URL when it has started accepting requests. If the
152 * timeout expires before the server has started, the promise will be
153 * rejected.
154 */
155DriverService.prototype.start = function(opt_timeoutMs) {
156 if (this.address_) {
157 return this.address_;
158 }
159
160 var timeout = opt_timeoutMs || DriverService.DEFAULT_START_TIMEOUT_MS;
161
162 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 });
175
176 self.command_.fulfill(command);
177
178 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 });
185
186 var serverUrl = url.format({
187 protocol: 'http',
188 hostname: !self.loopbackOnly_ && net.getAddress() ||
189 net.getLoopbackAddress(),
190 port: port,
191 pathname: self.path_
192 });
193
194 return httpUtil.waitForServer(serverUrl, timeout).then(function() {
195 return serverUrl;
196 });
197 });
198 }));
199
200 return this.address_;
201};
202
203
204/**
205 * Stops the service if it is not currently running. This function will kill
206 * the server immediately. To synchronize with the active control flow, use
207 * {@link #stop()}.
208 * @return {!webdriver.promise.Promise} A promise that will be resolved when
209 * 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};
219
220
221/**
222 * Schedules a task in the current control flow to stop the server if it is
223 * currently running.
224 * @return {!webdriver.promise.Promise} A promise that will be resolved when
225 * the server has been stopped.
226 */
227DriverService.prototype.stop = function() {
228 return promise.controlFlow().execute(this.kill.bind(this));
229};
230
231
232
233/**
234 * Manages the life and death of the Selenium standalone server. The server
235 * 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 the
238 * server.
239 * @throws {Error} If an invalid port is specified.
240 * @constructor
241 * @extends {DriverService}
242 */
243function SeleniumServer(jar, options) {
244 if (options.port < 0)
245 throw Error('Port must be >= 0: ' + options.port);
246
247 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 });
255
256 DriverService.call(this, 'java', {
257 port: port,
258 args: args,
259 path: '/wd/hub',
260 env: options.env,
261 stdio: options.stdio
262 });
263}
264util.inherits(SeleniumServer, DriverService);
265
266
267/**
268 * Options for the Selenium server:
269 * <ul>
270 * <li>{@code port} - The port to start the server on (must be > 0). If the
271 * port is provided as a promise, the service will wait for the promise to
272 * resolve before starting.
273 * <li>{@code args} - The arguments to pass to the service. If a promise is
274 * 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 is
276 * provided, the service will wait for it to resolve before starting.
277 * <li>{@code env} - The environment variables that should be visible to the
278 * server process. Defaults to inheriting the current process's
279 * environment.
280 * <li>{@code stdio} - IO configuration for the spawned server process. For
281 * more information, refer to the documentation of
282 * {@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;
296
297
298// PUBLIC API
299
300exports.DriverService = DriverService;
301exports.SeleniumServer = SeleniumServer;