remote/index.js

1// Licensed to the Software Freedom Conservancy (SFC) under one
2// or more contributor license agreements. See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership. The SFC licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License. You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied. See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18'use strict';
19
20var AdmZip = require('adm-zip'),
21 fs = require('fs'),
22 path = require('path'),
23 url = require('url'),
24 util = require('util');
25
26var _base = require('../_base'),
27 webdriver = require('../'),
28 promise = require('../').promise,
29 httpUtil = require('../http/util'),
30 exec = require('../io/exec'),
31 net = require('../net'),
32 portprober = require('../net/portprober');
33
34
35
36/**
37 * Configuration options for a DriverService instance.
38 *
39 * - `loopback` - Whether the service should only be accessed on this host's
40 * loopback address.
41 * - `port` - The port to start the server on (must be > 0). If the port is
42 * provided as a promise, the service will wait for the promise to resolve
43 * before starting.
44 * - `args` - The arguments to pass to the service. If a promise is provided,
45 * the service will wait for it to resolve before starting.
46 * - `path` - The base path on the server for the WebDriver wire protocol
47 * (e.g. '/wd/hub'). Defaults to '/'.
48 * - `env` - The environment variables that should be visible to the server
49 * process. Defaults to inheriting the current process's environment.
50 * - `stdio` - IO configuration for the spawned server process. For more
51 * information, refer to the documentation of `child_process.spawn`.
52 *
53 * @typedef {{
54 * port: (number|!webdriver.promise.Promise.<number>),
55 * args: !(Array.<string>|webdriver.promise.Promise.<!Array.<string>>),
56 * path: (string|undefined),
57 * env: (!Object.<string, string>|undefined),
58 * stdio: (string|!Array.<string|number|!Stream|null|undefined>|undefined)
59 * }}
60 */
61var ServiceOptions;
62
63
64/**
65 * Manages the life and death of a native executable WebDriver server.
66 *
67 * It is expected that the driver server implements the
68 * https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol.
69 * Furthermore, the managed server should support multiple concurrent sessions,
70 * so that this class may be reused for multiple clients.
71 *
72 * @param {string} executable Path to the executable to run.
73 * @param {!ServiceOptions} options Configuration options for the service.
74 * @constructor
75 */
76function DriverService(executable, options) {
77
78 /** @private {string} */
79 this.executable_ = executable;
80
81 /** @private {boolean} */
82 this.loopbackOnly_ = !!options.loopback;
83
84 /** @private {(number|!webdriver.promise.Promise.<number>)} */
85 this.port_ = options.port;
86
87 /**
88 * @private {!(Array.<string>|webdriver.promise.Promise.<!Array.<string>>)}
89 */
90 this.args_ = options.args;
91
92 /** @private {string} */
93 this.path_ = options.path || '/';
94
95 /** @private {!Object.<string, string>} */
96 this.env_ = options.env || process.env;
97
98 /** @private {(string|!Array.<string|number|!Stream|null|undefined>)} */
99 this.stdio_ = options.stdio || 'ignore';
100
101 /**
102 * A promise for the managed subprocess, or null if the server has not been
103 * started yet. This promise will never be rejected.
104 * @private {promise.Promise.<!exec.Command>}
105 */
106 this.command_ = null;
107
108 /**
109 * Promise that resolves to the server's address or null if the server has
110 * not been started. This promise will be rejected if the server terminates
111 * before it starts accepting WebDriver requests.
112 * @private {promise.Promise.<string>}
113 */
114 this.address_ = null;
115}
116
117
118/**
119 * The default amount of time, in milliseconds, to wait for the server to
120 * start.
121 * @type {number}
122 */
123DriverService.DEFAULT_START_TIMEOUT_MS = 30 * 1000;
124
125
126/**
127 * @return {!webdriver.promise.Promise.<string>} A promise that resolves to
128 * the server's address.
129 * @throws {Error} If the server has not been started.
130 */
131DriverService.prototype.address = function() {
132 if (this.address_) {
133 return this.address_;
134 }
135 throw Error('Server has not been started.');
136};
137
138
139/**
140 * Returns whether the underlying process is still running. This does not take
141 * into account whether the process is in the process of shutting down.
142 * @return {boolean} Whether the underlying service process is running.
143 */
144DriverService.prototype.isRunning = function() {
145 return !!this.address_;
146};
147
148
149/**
150 * Starts the server if it is not already running.
151 * @param {number=} opt_timeoutMs How long to wait, in milliseconds, for the
152 * server to start accepting requests. Defaults to 30 seconds.
153 * @return {!promise.Promise.<string>} A promise that will resolve
154 * to the server's base URL when it has started accepting requests. If the
155 * timeout expires before the server has started, the promise will be
156 * rejected.
157 */
158DriverService.prototype.start = function(opt_timeoutMs) {
159 if (this.address_) {
160 return this.address_;
161 }
162
163 var timeout = opt_timeoutMs || DriverService.DEFAULT_START_TIMEOUT_MS;
164
165 var self = this;
166 this.command_ = promise.defer();
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 var command = exec(self.executable_, {
174 args: args,
175 env: self.env_,
176 stdio: self.stdio_
177 });
178
179 self.command_.fulfill(command);
180
181 var earlyTermination = command.result().then(function(result) {
182 var error = result.code == null ?
183 Error('Server was killed with ' + result.signal) :
184 Error('Server terminated early with status ' + result.code);
185 self.address_.reject(error);
186 self.address_ = null;
187 self.command_ = null;
188 throw error;
189 });
190
191 var serverUrl = url.format({
192 protocol: 'http',
193 hostname: !self.loopbackOnly_ && net.getAddress() ||
194 net.getLoopbackAddress(),
195 port: port,
196 pathname: self.path_
197 });
198
199 return new promise.Promise(function(fulfill, reject) {
200 var ready = httpUtil.waitForServer(serverUrl, timeout)
201 .then(fulfill, reject);
202 earlyTermination.thenCatch(function(e) {
203 ready.cancel(e);
204 reject(Error(e.message));
205 });
206 }).then(function() {
207 return serverUrl;
208 });
209 });
210 }));
211
212 return this.address_;
213};
214
215
216/**
217 * Stops the service if it is not currently running. This function will kill
218 * the server immediately. To synchronize with the active control flow, use
219 * {@link #stop()}.
220 * @return {!webdriver.promise.Promise} A promise that will be resolved when
221 * the server has been stopped.
222 */
223DriverService.prototype.kill = function() {
224 if (!this.address_ || !this.command_) {
225 return promise.fulfilled(); // Not currently running.
226 }
227 return this.command_.then(function(command) {
228 command.kill('SIGTERM');
229 });
230};
231
232
233/**
234 * Schedules a task in the current control flow to stop the server if it is
235 * currently running.
236 * @return {!webdriver.promise.Promise} A promise that will be resolved when
237 * the server has been stopped.
238 */
239DriverService.prototype.stop = function() {
240 return promise.controlFlow().execute(this.kill.bind(this));
241};
242
243
244
245/**
246 * Manages the life and death of the
247 * <a href="http://selenium-release.storage.googleapis.com/index.html">
248 * standalone Selenium server</a>.
249 *
250 * @param {string} jar Path to the Selenium server jar.
251 * @param {SeleniumServer.Options=} opt_options Configuration options for the
252 * server.
253 * @throws {Error} If the path to the Selenium jar is not specified or if an
254 * invalid port is specified.
255 * @constructor
256 * @extends {DriverService}
257 */
258function SeleniumServer(jar, opt_options) {
259 if (!jar) {
260 throw Error('Path to the Selenium jar not specified');
261 }
262
263 var options = opt_options || {};
264
265 if (options.port < 0) {
266 throw Error('Port must be >= 0: ' + options.port);
267 }
268
269 var port = options.port || portprober.findFreePort();
270 var args = promise.when(options.jvmArgs || [], function(jvmArgs) {
271 return promise.when(options.args || [], function(args) {
272 return promise.when(port, function(port) {
273 return jvmArgs.concat(['-jar', jar, '-port', port]).concat(args);
274 });
275 });
276 });
277
278 DriverService.call(this, 'java', {
279 port: port,
280 args: args,
281 path: '/wd/hub',
282 env: options.env,
283 stdio: options.stdio
284 });
285}
286util.inherits(SeleniumServer, DriverService);
287
288
289/**
290 * Options for the Selenium server:
291 *
292 * - `port` - The port to start the server on (must be > 0). If the port is
293 * provided as a promise, the service will wait for the promise to resolve
294 * before starting.
295 * - `args` - The arguments to pass to the service. If a promise is provided,
296 * the service will wait for it to resolve before starting.
297 * - `jvmArgs` - The arguments to pass to the JVM. If a promise is provided,
298 * the service will wait for it to resolve before starting.
299 * - `env` - The environment variables that should be visible to the server
300 * process. Defaults to inheriting the current process's environment.
301 * - `stdio` - IO configuration for the spawned server process. For more
302 * information, refer to the documentation of `child_process.spawn`.
303 *
304 * @typedef {{
305 * port: (number|!webdriver.promise.Promise.<number>),
306 * args: !(Array.<string>|webdriver.promise.Promise.<!Array.<string>>),
307 * jvmArgs: (!Array.<string>|
308 * !webdriver.promise.Promise.<!Array.<string>>|
309 * undefined),
310 * env: (!Object.<string, string>|undefined),
311 * stdio: (string|!Array.<string|number|!Stream|null|undefined>|undefined)
312 * }}
313 */
314SeleniumServer.Options;
315
316
317
318/**
319 * A {@link webdriver.FileDetector} that may be used when running
320 * against a remote
321 * [Selenium server](http://selenium-release.storage.googleapis.com/index.html).
322 *
323 * When a file path on the local machine running this script is entered with
324 * {@link webdriver.WebElement#sendKeys WebElement#sendKeys}, this file detector
325 * will transfer the specified file to the Selenium server's host; the sendKeys
326 * command will be updated to use the transfered file's path.
327 *
328 * __Note:__ This class depends on a non-standard command supported on the
329 * Java Selenium server. The file detector will fail if used with a server that
330 * only supports standard WebDriver commands (such as the ChromeDriver).
331 *
332 * @constructor
333 * @extends {webdriver.FileDetector}
334 * @final
335 */
336var FileDetector = function() {};
337util.inherits(webdriver.FileDetector, FileDetector);
338
339
340/** @override */
341FileDetector.prototype.handleFile = function(driver, filePath) {
342 return promise.checkedNodeCall(fs.stat, filePath).then(function(stats) {
343 if (stats.isDirectory()) {
344 throw TypeError('Uploading directories is not supported: ' + filePath);
345 }
346
347 var zip = new AdmZip();
348 zip.addLocalFile(filePath);
349
350 var command = new webdriver.Command(webdriver.CommandName.UPLOAD_FILE)
351 .setParameter('file', zip.toBuffer().toString('base64'));
352 return driver.schedule(command,
353 'remote.FileDetector.handleFile(' + filePath + ')');
354 }, function(err) {
355 if (err.code === 'ENOENT') {
356 return filePath; // Not a file; return original input.
357 }
358 throw err;
359 });
360};
361
362// PUBLIC API
363
364exports.DriverService = DriverService;
365exports.FileDetector = FileDetector;
366exports.SeleniumServer = SeleniumServer;