# 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 |
17 | var AdmZip = require('adm-zip'),
18 | fs = require('fs'),
19 | path = require('path'),
20 | url = require('url'),
21 | util = require('util');
22 |
23 | var _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 | */
58 | var 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 | */
73 | function 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 | */
120 | DriverService.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 | */
128 | DriverService.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 | */
141 | DriverService.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 | */
155 | DriverService.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 | */
211 | DriverService.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 | */
227 | DriverService.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 | */
246 | function 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 | }
274 | util.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 | */
302 | SeleniumServer.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 | */
324 | var FileDetector = function() {};
325 | util.inherits(webdriver.FileDetector, FileDetector);
326 |
327 |
328 | /** @override */
329 | FileDetector.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 |
352 | exports.DriverService = DriverService;
353 | exports.FileDetector = FileDetector;
354 | exports.SeleniumServer = SeleniumServer;