index.js 18 KB

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