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