UNPKG

18.3 kBJavaScriptView Raw
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
20const AdmZip = require('adm-zip');
21const fs = require('fs');
22const path = require('path');
23const url = require('url');
24const util = require('util');
25
26const httpUtil = require('../http/util');
27const io = require('../io');
28const exec = require('../io/exec');
29const cmd = require('../lib/command');
30const input = require('../lib/input');
31const promise = require('../lib/promise');
32const webdriver = require('../lib/webdriver');
33const net = require('../net');
34const portprober = require('../net/portprober');
35
36
37/**
38 * @typedef {(string|!Array<string|number|!stream.Stream|null|undefined>)}
39 */
40var StdIoOptions;
41
42
43/**
44 * @typedef {(string|!IThenable<string>)}
45 */
46var CommandLineFlag;
47
48
49/**
50 * A record object that defines the configuration options for a DriverService
51 * instance.
52 *
53 * @record
54 */
55function ServiceOptions() {}
56
57/**
58 * Whether the service should only be accessed on this host's loopback address.
59 *
60 * @type {(boolean|undefined)}
61 */
62ServiceOptions.prototype.loopback;
63
64/**
65 * The host name to access the server on. If this option is specified, the
66 * {@link #loopback} option will be ignored.
67 *
68 * @type {(string|undefined)}
69 */
70ServiceOptions.prototype.hostname;
71
72/**
73 * The port to start the server on (must be > 0). If the port is provided as a
74 * promise, the service will wait for the promise to resolve before starting.
75 *
76 * @type {(number|!IThenable<number>)}
77 */
78ServiceOptions.prototype.port;
79
80/**
81 * The arguments to pass to the service. If a promise is provided, the service
82 * will wait for it to resolve before starting.
83 *
84 * @type {!(Array<CommandLineFlag>|IThenable<!Array<CommandLineFlag>>)}
85 */
86ServiceOptions.prototype.args;
87
88/**
89 * The base path on the server for the WebDriver wire protocol (e.g. '/wd/hub').
90 * Defaults to '/'.
91 *
92 * @type {(string|undefined|null)}
93 */
94ServiceOptions.prototype.path;
95
96/**
97 * The environment variables that should be visible to the server process.
98 * Defaults to inheriting the current process's environment.
99 *
100 * @type {(Object<string, string>|undefined)}
101 */
102ServiceOptions.prototype.env;
103
104/**
105 * IO configuration for the spawned server process. For more information, refer
106 * to the documentation of `child_process.spawn`.
107 *
108 * @type {(StdIoOptions|undefined)}
109 * @see https://nodejs.org/dist/latest-v4.x/docs/api/child_process.html#child_process_options_stdio
110 */
111ServiceOptions.prototype.stdio;
112
113
114/**
115 * Manages the life and death of a native executable WebDriver server.
116 *
117 * It is expected that the driver server implements the
118 * https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol.
119 * Furthermore, the managed server should support multiple concurrent sessions,
120 * so that this class may be reused for multiple clients.
121 */
122class DriverService {
123 /**
124 * @param {string} executable Path to the executable to run.
125 * @param {!ServiceOptions} options Configuration options for the service.
126 */
127 constructor(executable, options) {
128 /** @private {string} */
129 this.executable_ = executable;
130
131 /** @private {boolean} */
132 this.loopbackOnly_ = !!options.loopback;
133
134 /** @private {(string|undefined)} */
135 this.hostname_ = options.hostname;
136
137 /** @private {(number|!IThenable<number>)} */
138 this.port_ = options.port;
139
140 /**
141 * @private {!(Array<CommandLineFlag>|
142 * IThenable<!Array<CommandLineFlag>>)}
143 */
144 this.args_ = options.args;
145
146 /** @private {string} */
147 this.path_ = options.path || '/';
148
149 /** @private {!Object<string, string>} */
150 this.env_ = options.env || process.env;
151
152 /**
153 * @private {(string|!Array<string|number|!stream.Stream|null|undefined>)}
154 */
155 this.stdio_ = options.stdio || 'ignore';
156
157 /**
158 * A promise for the managed subprocess, or null if the server has not been
159 * started yet. This promise will never be rejected.
160 * @private {Promise<!exec.Command>}
161 */
162 this.command_ = null;
163
164 /**
165 * Promise that resolves to the server's address or null if the server has
166 * not been started. This promise will be rejected if the server terminates
167 * before it starts accepting WebDriver requests.
168 * @private {Promise<string>}
169 */
170 this.address_ = null;
171 }
172
173 /**
174 * @return {!Promise<string>} A promise that resolves to the server's address.
175 * @throws {Error} If the server has not been started.
176 */
177 address() {
178 if (this.address_) {
179 return this.address_;
180 }
181 throw Error('Server has not been started.');
182 }
183
184 /**
185 * Returns whether the underlying process is still running. This does not take
186 * into account whether the process is in the process of shutting down.
187 * @return {boolean} Whether the underlying service process is running.
188 */
189 isRunning() {
190 return !!this.address_;
191 }
192
193 /**
194 * Starts the server if it is not already running.
195 * @param {number=} opt_timeoutMs How long to wait, in milliseconds, for the
196 * server to start accepting requests. Defaults to 30 seconds.
197 * @return {!Promise<string>} A promise that will resolve to the server's base
198 * URL when it has started accepting requests. If the timeout expires
199 * before the server has started, the promise will be rejected.
200 */
201 start(opt_timeoutMs) {
202 if (this.address_) {
203 return this.address_;
204 }
205
206 var timeout = opt_timeoutMs || DriverService.DEFAULT_START_TIMEOUT_MS;
207 var self = this;
208
209 let resolveCommand;
210 this.command_ = new Promise(resolve => resolveCommand = resolve);
211
212 this.address_ = new Promise((resolveAddress, rejectAddress) => {
213 resolveAddress(Promise.resolve(this.port_).then(port => {
214 if (port <= 0) {
215 throw Error('Port must be > 0: ' + port);
216 }
217
218 return resolveCommandLineFlags(this.args_).then(args => {
219 var command = exec(self.executable_, {
220 args: args,
221 env: self.env_,
222 stdio: self.stdio_
223 });
224
225 resolveCommand(command);
226
227 var earlyTermination = command.result().then(function(result) {
228 var error = result.code == null ?
229 Error('Server was killed with ' + result.signal) :
230 Error('Server terminated early with status ' + result.code);
231 rejectAddress(error);
232 self.address_ = null;
233 self.command_ = null;
234 throw error;
235 });
236
237 var hostname = self.hostname_;
238 if (!hostname) {
239 hostname = !self.loopbackOnly_ && net.getAddress()
240 || net.getLoopbackAddress();
241 }
242
243 var serverUrl = url.format({
244 protocol: 'http',
245 hostname: hostname,
246 port: port + '',
247 pathname: self.path_
248 });
249
250 return new Promise((fulfill, reject) => {
251 let cancelToken =
252 earlyTermination.catch(e => reject(Error(e.message)));
253
254 httpUtil.waitForServer(serverUrl, timeout, cancelToken)
255 .then(_ => fulfill(serverUrl), err => {
256 if (err instanceof promise.CancellationError) {
257 fulfill(serverUrl);
258 } else {
259 reject(err);
260 }
261 });
262 });
263 });
264 }));
265 });
266
267 return this.address_;
268 }
269
270 /**
271 * Stops the service if it is not currently running. This function will kill
272 * the server immediately. To synchronize with the active control flow, use
273 * {@link #stop()}.
274 * @return {!Promise} A promise that will be resolved when the server has been
275 * stopped.
276 */
277 kill() {
278 if (!this.address_ || !this.command_) {
279 return Promise.resolve(); // Not currently running.
280 }
281 return this.command_.then(function(command) {
282 command.kill('SIGTERM');
283 });
284 }
285
286 /**
287 * Schedules a task in the current control flow to stop the server if it is
288 * currently running.
289 * @return {!promise.Thenable} A promise that will be resolved when
290 * the server has been stopped.
291 */
292 stop() {
293 return promise.controlFlow().execute(this.kill.bind(this));
294 }
295}
296
297
298/**
299 * @param {!(Array<CommandLineFlag>|IThenable<!Array<CommandLineFlag>>)} args
300 * @return {!Promise<!Array<string>>}
301 */
302function resolveCommandLineFlags(args) {
303 // Resolve the outer array, then the individual flags.
304 return Promise.resolve(args)
305 .then(/** !Array<CommandLineFlag> */args => Promise.all(args));
306}
307
308
309/**
310 * The default amount of time, in milliseconds, to wait for the server to
311 * start.
312 * @const {number}
313 */
314DriverService.DEFAULT_START_TIMEOUT_MS = 30 * 1000;
315
316
317/**
318 * Creates {@link DriverService} objects that manage a WebDriver server in a
319 * child process.
320 */
321DriverService.Builder = class {
322 /**
323 * @param {string} exe Path to the executable to use. This executable must
324 * accept the `--port` flag for defining the port to start the server on.
325 * @throws {Error} If the provided executable path does not exist.
326 */
327 constructor(exe) {
328 if (!fs.existsSync(exe)) {
329 throw Error(`The specified executable path does not exist: ${exe}`);
330 }
331
332 /** @private @const {string} */
333 this.exe_ = exe;
334
335 /** @private {!ServiceOptions} */
336 this.options_ = {
337 args: [],
338 port: 0,
339 env: null,
340 stdio: 'ignore'
341 };
342 }
343
344 /**
345 * Define additional command line arguments to use when starting the server.
346 *
347 * @param {...CommandLineFlag} var_args The arguments to include.
348 * @return {!THIS} A self reference.
349 * @this {THIS}
350 * @template THIS
351 */
352 addArguments(var_args) {
353 let args = Array.prototype.slice.call(arguments, 0);
354 this.options_.args = this.options_.args.concat(args);
355 return this;
356 }
357
358 /**
359 * Sets the host name to access the server on. If specified, the
360 * {@linkplain #setLoopback() loopback} setting will be ignored.
361 *
362 * @param {string} hostname
363 * @return {!DriverService.Builder} A self reference.
364 */
365 setHostname(hostname) {
366 this.options_.hostname = hostname;
367 return this;
368 }
369
370 /**
371 * Sets whether the service should be accessed at this host's loopback
372 * address.
373 *
374 * @param {boolean} loopback
375 * @return {!DriverService.Builder} A self reference.
376 */
377 setLoopback(loopback) {
378 this.options_.loopback = loopback;
379 return this;
380 }
381
382 /**
383 * Sets the base path for WebDriver REST commands (e.g. "/wd/hub").
384 * By default, the driver will accept commands relative to "/".
385 *
386 * @param {?string} basePath The base path to use, or `null` to use the
387 * default.
388 * @return {!DriverService.Builder} A self reference.
389 */
390 setPath(basePath) {
391 this.options_.path = basePath;
392 return this;
393 }
394
395 /**
396 * Sets the port to start the server on.
397 *
398 * @param {number} port The port to use, or 0 for any free port.
399 * @return {!DriverService.Builder} A self reference.
400 * @throws {Error} If an invalid port is specified.
401 */
402 setPort(port) {
403 if (port < 0) {
404 throw Error(`port must be >= 0: ${port}`);
405 }
406 this.options_.port = port;
407 return this;
408 }
409
410 /**
411 * Defines the environment to start the server under. This setting will be
412 * inherited by every browser session started by the server. By default, the
413 * server will inherit the enviroment of the current process.
414 *
415 * @param {(Map<string, string>|Object<string, string>|null)} env The desired
416 * environment to use, or `null` if the server should inherit the
417 * current environment.
418 * @return {!DriverService.Builder} A self reference.
419 */
420 setEnvironment(env) {
421 if (env instanceof Map) {
422 let tmp = {};
423 env.forEach((value, key) => tmp[key] = value);
424 env = tmp;
425 }
426 this.options_.env = env;
427 return this;
428 }
429
430 /**
431 * IO configuration for the spawned server process. For more information,
432 * refer to the documentation of `child_process.spawn`.
433 *
434 * @param {StdIoOptions} config The desired IO configuration.
435 * @return {!DriverService.Builder} A self reference.
436 * @see https://nodejs.org/dist/latest-v4.x/docs/api/child_process.html#child_process_options_stdio
437 */
438 setStdio(config) {
439 this.options_.stdio = config;
440 return this;
441 }
442
443 /**
444 * Creates a new DriverService using this instance's current configuration.
445 *
446 * @return {!DriverService} A new driver service.
447 */
448 build() {
449 let port = this.options_.port || portprober.findFreePort();
450 let args = Promise.resolve(port).then(port => {
451 return this.options_.args.concat('--port=' + port);
452 });
453
454 let options =
455 /** @type {!ServiceOptions} */
456 (Object.assign({}, this.options_, {args, port}));
457 return new DriverService(this.exe_, options);
458 }
459};
460
461
462/**
463 * Manages the life and death of the
464 * <a href="http://selenium-release.storage.googleapis.com/index.html">
465 * standalone Selenium server</a>.
466 */
467class SeleniumServer extends DriverService {
468 /**
469 * @param {string} jar Path to the Selenium server jar.
470 * @param {SeleniumServer.Options=} opt_options Configuration options for the
471 * server.
472 * @throws {Error} If the path to the Selenium jar is not specified or if an
473 * invalid port is specified.
474 */
475 constructor(jar, opt_options) {
476 if (!jar) {
477 throw Error('Path to the Selenium jar not specified');
478 }
479
480 var options = opt_options || {};
481
482 if (options.port < 0) {
483 throw Error('Port must be >= 0: ' + options.port);
484 }
485
486 let port = options.port || portprober.findFreePort();
487 let args = Promise.all([port, options.jvmArgs || [], options.args || []])
488 .then(resolved => {
489 let port = resolved[0];
490 let jvmArgs = resolved[1];
491 let args = resolved[2];
492 return jvmArgs.concat('-jar', jar, '-port', port).concat(args);
493 });
494
495 super('java', {
496 loopback: options.loopback,
497 port: port,
498 args: args,
499 path: '/wd/hub',
500 env: options.env,
501 stdio: options.stdio
502 });
503 }
504}
505
506
507/**
508 * Options for the Selenium server:
509 *
510 * - `loopback` - Whether the server should only be accessed on this host's
511 * loopback address.
512 * - `port` - The port to start the server on (must be > 0). If the port is
513 * provided as a promise, the service will wait for the promise to resolve
514 * before starting.
515 * - `args` - The arguments to pass to the service. If a promise is provided,
516 * the service will wait for it to resolve before starting.
517 * - `jvmArgs` - The arguments to pass to the JVM. If a promise is provided,
518 * the service will wait for it to resolve before starting.
519 * - `env` - The environment variables that should be visible to the server
520 * process. Defaults to inheriting the current process's environment.
521 * - `stdio` - IO configuration for the spawned server process. For more
522 * information, refer to the documentation of `child_process.spawn`.
523 *
524 * @typedef {{
525 * loopback: (boolean|undefined),
526 * port: (number|!promise.Promise<number>),
527 * args: !(Array<string>|promise.Promise<!Array<string>>),
528 * jvmArgs: (!Array<string>|
529 * !promise.Promise<!Array<string>>|
530 * undefined),
531 * env: (!Object<string, string>|undefined),
532 * stdio: (string|!Array<string|number|!stream.Stream|null|undefined>|
533 * undefined)
534 * }}
535 */
536SeleniumServer.Options;
537
538
539
540/**
541 * A {@link webdriver.FileDetector} that may be used when running
542 * against a remote
543 * [Selenium server](http://selenium-release.storage.googleapis.com/index.html).
544 *
545 * When a file path on the local machine running this script is entered with
546 * {@link webdriver.WebElement#sendKeys WebElement#sendKeys}, this file detector
547 * will transfer the specified file to the Selenium server's host; the sendKeys
548 * command will be updated to use the transfered file's path.
549 *
550 * __Note:__ This class depends on a non-standard command supported on the
551 * Java Selenium server. The file detector will fail if used with a server that
552 * only supports standard WebDriver commands (such as the ChromeDriver).
553 *
554 * @final
555 */
556class FileDetector extends input.FileDetector {
557 /**
558 * Prepares a `file` for use with the remote browser. If the provided path
559 * does not reference a normal file (i.e. it does not exist or is a
560 * directory), then the promise returned by this method will be resolved with
561 * the original file path. Otherwise, this method will upload the file to the
562 * remote server, which will return the file's path on the remote system so
563 * it may be referenced in subsequent commands.
564 *
565 * @override
566 */
567 handleFile(driver, file) {
568 return io.stat(file).then(function(stats) {
569 if (stats.isDirectory()) {
570 return file; // Not a valid file, return original input.
571 }
572
573 var zip = new AdmZip();
574 zip.addLocalFile(file);
575 // Stored compression, see https://en.wikipedia.org/wiki/Zip_(file_format)
576 zip.getEntries()[0].header.method = 0;
577
578 var command = new cmd.Command(cmd.Name.UPLOAD_FILE)
579 .setParameter('file', zip.toBuffer().toString('base64'));
580 return driver.schedule(command,
581 'remote.FileDetector.handleFile(' + file + ')');
582 }, function(err) {
583 if (err.code === 'ENOENT') {
584 return file; // Not a file; return original input.
585 }
586 throw err;
587 });
588 }
589}
590
591
592// PUBLIC API
593
594exports.DriverService = DriverService;
595exports.FileDetector = FileDetector;
596exports.SeleniumServer = SeleniumServer;
597exports.ServiceOptions = ServiceOptions; // Exported for API docs.