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 spawn = require('child_process').spawn,
18 os = require('os'),
19 path = require('path'),
20 url = require('url'),
21 util = require('util');
22
23var promise = require('../').promise,
24 httpUtil = require('../http/util'),
25 net = require('../net'),
26 portprober = require('../net/portprober');
27
28
29
30/**
31 * Configuration options for a DriverService instance.
32 * <ul>
33 * <li>
34 * <li>{@code loopback} - Whether the service should only be accessed on this
35 * host's loopback address.
36 * <li>{@code port} - The port to start the server on (must be > 0). If the
37 * port is provided as a promise, the service will wait for the promise to
38 * resolve before starting.
39 * <li>{@code args} - The arguments to pass to the service. If a promise is
40 * provided, the service will wait for it to resolve before starting.
41 * <li>{@code path} - The base path on the server for the WebDriver wire
42 * protocol (e.g. '/wd/hub'). Defaults to '/'.
43 * <li>{@code env} - The environment variables that should be visible to the
44 * server process. Defaults to inheriting the current process's
45 * environment.
46 * <li>{@code stdio} - IO configuration for the spawned server process. For
47 * more information, refer to the documentation of
48 * {@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;
60
61
62/**
63 * Manages the life and death of a native executable WebDriver server.
64 *
65 * <p>It is expected that the driver server implements the
66 * <a href="http://code.google.com/p/selenium/wiki/JsonWireProtocol">WebDriver
67 * Wire Protocol</a>. Furthermore, the managed server should support multiple
68 * 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 * @constructor
73 */
74function DriverService(executable, options) {
75
76 /** @private {string} */
77 this.executable_ = executable;
78
79 /** @private {boolean} */
80 this.loopbackOnly_ = !!options.loopback;
81
82 /** @private {(number|!webdriver.promise.Promise.<number>)} */
83 this.port_ = options.port;
84
85 /**
86 * @private {!(Array.<string>|webdriver.promise.Promise.<!Array.<string>>)}
87 */
88 this.args_ = options.args;
89
90 /** @private {string} */
91 this.path_ = options.path || '/';
92
93 /** @private {!Object.<string, string>} */
94 this.env_ = options.env || process.env;
95
96 /** @private {(string|!Array.<string|number|!Stream|null|undefined>)} */
97 this.stdio_ = options.stdio || 'ignore';
98}
99
100
101/**
102 * The default amount of time, in milliseconds, to wait for the server to
103 * start.
104 * @type {number}
105 */
106DriverService.DEFAULT_START_TIMEOUT_MS = 30 * 1000;
107
108
109/** @private {child_process.ChildProcess} */
110DriverService.prototype.process_ = null;
111
112
113/**
114 * Promise that resolves to the server's address or null if the server has not
115 * been started.
116 * @private {webdriver.promise.Promise.<string>}
117 */
118DriverService.prototype.address_ = null;
119
120
121/**
122 * Promise that tracks the status of shutting down the server, or null if the
123 * server is not currently shutting down.
124 * @private {webdriver.promise.Promise}
125 */
126DriverService.prototype.shutdownHook_ = null;
127
128
129/**
130 * @return {!webdriver.promise.Promise.<string>} A promise that resolves to
131 * 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};
140
141
142/**
143 * @return {boolean} Whether the underlying service process is running.
144 */
145DriverService.prototype.isRunning = function() {
146 return !!this.address_;
147};
148
149
150/**
151 * Starts the server if it is not already running.
152 * @param {number=} opt_timeoutMs How long to wait, in milliseconds, for the
153 * server to start accepting requests. Defaults to 30 seconds.
154 * @return {!webdriver.promise.Promise.<string>} A promise that will resolve
155 * to the server's base URL when it has started accepting requests. If the
156 * timeout expires before the server has started, the promise will be
157 * rejected.
158 */
159DriverService.prototype.start = function(opt_timeoutMs) {
160 if (this.address_) {
161 return this.address_;
162 }
163
164 var timeout = opt_timeoutMs || DriverService.DEFAULT_START_TIMEOUT_MS;
165
166 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);
177
178 // This process should not wait on the spawned child, however, we do
179 // want to ensure the child is killed when this process exits.
180 self.process_.unref();
181 process.once('exit', killServer);
182
183 var serverUrl = url.format({
184 protocol: 'http',
185 hostname: !self.loopbackOnly_ && net.getAddress() ||
186 net.getLoopbackAddress(),
187 port: port,
188 pathname: self.path_
189 });
190
191 return httpUtil.waitForServer(serverUrl, timeout).then(function() {
192 return serverUrl;
193 });
194 });
195 }));
196
197 return this.address_;
198
199 function onServerExit(code, signal) {
200 self.address_.reject(code == null ?
201 Error('Server was killed with ' + signal) :
202 Error('Server exited with ' + code));
203
204 if (self.shutdownHook_) {
205 self.shutdownHook_.fulfill();
206 }
207
208 self.shutdownHook_ = null;
209 self.address_ = null;
210 self.process_ = null;
211 process.removeListener('exit', killServer);
212 }
213
214 function killServer() {
215 process.removeListener('exit', killServer);
216 self.process_ && self.process_.kill('SIGTERM');
217 }
218};
219
220
221/**
222 * Stops the service if it is not currently running. This function will kill
223 * the server immediately. To synchronize with the active control flow, use
224 * {@link #stop()}.
225 * @return {!webdriver.promise.Promise} A promise that will be resolved when
226 * the server has been stopped.
227 */
228DriverService.prototype.kill = function() {
229 if (!this.address_) {
230 return promise.fulfilled(); // Not currently running.
231 }
232
233 if (!this.shutdownHook_) {
234 // No process: still starting; wait on address.
235 // Otherwise, kill the process now. Exit handler will resolve the
236 // 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 }
247
248 return this.shutdownHook_;
249};
250
251
252/**
253 * Schedules a task in the current control flow to stop the server if it is
254 * currently running.
255 * @return {!webdriver.promise.Promise} A promise that will be resolved when
256 * the server has been stopped.
257 */
258DriverService.prototype.stop = function() {
259 return promise.controlFlow().execute(this.kill.bind(this));
260};
261
262
263
264/**
265 * Manages the life and death of the Selenium standalone server. The server
266 * 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 the
269 * server.
270 * @throws {Error} If an invalid port is specified.
271 * @constructor
272 * @extends {DriverService}
273 */
274function SeleniumServer(jar, options) {
275 if (options.port < 0)
276 throw Error('Port must be >= 0: ' + options.port);
277
278 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 });
286
287 DriverService.call(this, 'java', {
288 port: port,
289 args: args,
290 path: '/wd/hub',
291 env: options.env,
292 stdio: options.stdio
293 });
294}
295util.inherits(SeleniumServer, DriverService);
296
297
298/**
299 * Options for the Selenium server:
300 * <ul>
301 * <li>{@code port} - The port to start the server on (must be > 0). If the
302 * port is provided as a promise, the service will wait for the promise to
303 * resolve before starting.
304 * <li>{@code args} - The arguments to pass to the service. If a promise is
305 * 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 is
307 * provided, the service will wait for it to resolve before starting.
308 * <li>{@code env} - The environment variables that should be visible to the
309 * server process. Defaults to inheriting the current process's
310 * environment.
311 * <li>{@code stdio} - IO configuration for the spawned server process. For
312 * more information, refer to the documentation of
313 * {@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;
327
328
329// PUBLIC API
330
331exports.DriverService = DriverService;
332exports.SeleniumServer = SeleniumServer;