# 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 |
20 | var 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 |
27 | var _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 | */
63 | var 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 | */
78 | function 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 | */
125 | DriverService.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 | */
133 | DriverService.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 | */
146 | DriverService.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 | */
160 | DriverService.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 | */
225 | DriverService.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 | */
241 | DriverService.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 | */
260 | function 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 | }
289 | util.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 | */
320 | SeleniumServer.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 | */
342 | var FileDetector = function() {};
343 | util.inherits(webdriver.FileDetector, FileDetector);
344 |
345 |
346 | /** @override */
347 | FileDetector.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 |
371 | exports.DriverService = DriverService;
372 | exports.FileDetector = FileDetector;
373 | exports.SeleniumServer = SeleniumServer;