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