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>{@code port} - The port to start the server on (must be > 0). If the
34 * port is provided as a promise, the service will wait for the promise to
35 * resolve before starting.
36 * <li>{@code args} - The arguments to pass to the service. If a promise is
37 * provided, the service will wait for it to resolve before starting.
38 * <li>{@code path} - The base path on the server for the WebDriver wire
39 * protocol (e.g. '/wd/hub'). Defaults to '/'.
40 * <li>{@code env} - The environment variables that should be visible to the
41 * server process. Defaults to inheriting the current process's
42 * environment.
43 * <li>{@code stdio} - IO configuration for the spawned server process. For
44 * more information, refer to the documentation of
45 * {@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;
57
58
59/**
60 * Manages the life and death of a native executable WebDriver server.
61 *
62 * <p>It is expected that the driver server implements the
63 * <a href="http://code.google.com/p/selenium/wiki/JsonWireProtocol">WebDriver
64 * Wire Protocol</a>. Furthermore, the managed server should support multiple
65 * 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 * @constructor
70 */
71function DriverService(executable, options) {
72
73 /** @private {string} */
74 this.executable_ = executable;
75
76 /** @private {(number|!webdriver.promise.Promise.<number>)} */
77 this.port_ = options.port;
78
79 /**
80 * @private {!(Array.<string>|webdriver.promise.Promise.<!Array.<string>>)}
81 */
82 this.args_ = options.args;
83
84 /** @private {string} */
85 this.path_ = options.path || '/';
86
87 /** @private {!Object.<string, string>} */
88 this.env_ = options.env || process.env;
89
90 /** @private {(string|!Array.<string|number|!Stream|null|undefined>)} */
91 this.stdio_ = options.stdio || 'ignore';
92}
93
94
95/**
96 * The default amount of time, in milliseconds, to wait for the server to
97 * start.
98 * @type {number}
99 */
100DriverService.DEFAULT_START_TIMEOUT_MS = 30 * 1000;
101
102
103/** @private {child_process.ChildProcess} */
104DriverService.prototype.process_ = null;
105
106
107/**
108 * Promise that resolves to the server's address or null if the server has not
109 * been started.
110 * @private {webdriver.promise.Promise.<string>}
111 */
112DriverService.prototype.address_ = null;
113
114
115/**
116 * Promise that tracks the status of shutting down the server, or null if the
117 * server is not currently shutting down.
118 * @private {webdriver.promise.Promise}
119 */
120DriverService.prototype.shutdownHook_ = null;
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 * @return {boolean} Whether the underlying service process is running.
138 */
139DriverService.prototype.isRunning = function() {
140 return !!this.address_;
141};
142
143
144/**
145 * Starts the server if it is not already running.
146 * @param {number=} opt_timeoutMs How long to wait, in milliseconds, for the
147 * server to start accepting requests. Defaults to 30 seconds.
148 * @return {!webdriver.promise.Promise.<string>} A promise that will resolve
149 * to the server's base URL when it has started accepting requests. If the
150 * timeout expires before the server has started, the promise will be
151 * rejected.
152 */
153DriverService.prototype.start = function(opt_timeoutMs) {
154 if (this.address_) {
155 return this.address_;
156 }
157
158 var timeout = opt_timeoutMs || DriverService.DEFAULT_START_TIMEOUT_MS;
159
160 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);
171
172 // This process should not wait on the spawned child, however, we do
173 // want to ensure the child is killed when this process exits.
174 self.process_.unref();
175 process.once('exit', killServer);
176
177 var serverUrl = url.format({
178 protocol: 'http',
179 hostname: net.getAddress() || net.getLoopbackAddress(),
180 port: port,
181 pathname: self.path_
182 });
183
184 return httpUtil.waitForServer(serverUrl, timeout).then(function() {
185 return serverUrl;
186 });
187 });
188 }));
189
190 return this.address_;
191
192 function onServerExit(code, signal) {
193 self.address_.reject(code == null ?
194 Error('Server was killed with ' + signal) :
195 Error('Server exited with ' + code));
196
197 if (self.shutdownHook_) {
198 self.shutdownHook_.fulfill();
199 }
200
201 self.shutdownHook_ = null;
202 self.address_ = null;
203 self.process_ = null;
204 process.removeListener('exit', killServer);
205 }
206
207 function killServer() {
208 process.removeListener('exit', killServer);
209 self.process_ && self.process_.kill('SIGTERM');
210 }
211};
212
213
214/**
215 * Stops the service if it is not currently running. This function will kill
216 * the server immediately. To synchronize with the active control flow, use
217 * {@link #stop()}.
218 * @return {!webdriver.promise.Promise} A promise that will be resolved when
219 * the server has been stopped.
220 */
221DriverService.prototype.kill = function() {
222 if (!this.address_) {
223 return promise.fulfilled(); // Not currently running.
224 }
225
226 if (!this.shutdownHook_) {
227 // No process: still starting; wait on address.
228 // Otherwise, kill the process now. Exit handler will resolve the
229 // 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 }
240
241 return this.shutdownHook_;
242};
243
244
245/**
246 * Schedules a task in the current control flow to stop the server if it is
247 * currently running.
248 * @return {!webdriver.promise.Promise} A promise that will be resolved when
249 * the server has been stopped.
250 */
251DriverService.prototype.stop = function() {
252 return promise.controlFlow().execute(this.kill.bind(this));
253};
254
255
256
257/**
258 * Manages the life and death of the Selenium standalone server. The server
259 * may be obtained from http://selenium-release.storage.googleapis.com/index.html.
260 * @param {string} jar Path to the Selenium server jar.
261 * @param {!SeleniumServer.Options} options Configuration options for the
262 * server.
263 * @throws {Error} If an invalid port is specified.
264 * @constructor
265 * @extends {DriverService}
266 */
267function SeleniumServer(jar, options) {
268 if (options.port < 0)
269 throw Error('Port must be >= 0: ' + options.port);
270
271 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 });
279
280 DriverService.call(this, 'java', {
281 port: port,
282 args: args,
283 path: '/wd/hub',
284 env: options.env,
285 stdio: options.stdio
286 });
287}
288util.inherits(SeleniumServer, DriverService);
289
290
291/**
292 * Options for the Selenium server:
293 * <ul>
294 * <li>{@code port} - The port to start the server on (must be > 0). If the
295 * port is provided as a promise, the service will wait for the promise to
296 * resolve before starting.
297 * <li>{@code args} - The arguments to pass to the service. If a promise is
298 * 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 is
300 * provided, the service will wait for it to resolve before starting.
301 * <li>{@code env} - The environment variables that should be visible to the
302 * server process. Defaults to inheriting the current process's
303 * environment.
304 * <li>{@code stdio} - IO configuration for the spawned server process. For
305 * more information, refer to the documentation of
306 * {@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;
320
321
322// PUBLIC API
323
324exports.DriverService = DriverService;
325exports.SeleniumServer = SeleniumServer;