Path: blob/trunk/javascript/selenium-webdriver/remote/index.js
1864 views
// Licensed to the Software Freedom Conservancy (SFC) under one1// or more contributor license agreements. See the NOTICE file2// distributed with this work for additional information3// regarding copyright ownership. The SFC licenses this file4// to you under the Apache License, Version 2.0 (the5// "License"); you may not use this file except in compliance6// with the License. You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing,11// software distributed under the License is distributed on an12// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13// KIND, either express or implied. See the License for the14// specific language governing permissions and limitations15// under the License.1617'use strict'1819const url = require('node:url')2021const httpUtil = require('../http/util')22const io = require('../io')23const { exec } = require('../io/exec')24const { Zip } = require('../io/zip')25const cmd = require('../lib/command')26const input = require('../lib/input')27const net = require('../net')28const portprober = require('../net/portprober')29const logging = require('../lib/logging')3031const { getJavaPath, formatSpawnArgs } = require('./util')3233/**34* @typedef {(string|!Array<string|number|!stream.Stream|null|undefined>)}35*/36let StdIoOptions // eslint-disable-line3738/**39* @typedef {(string|!IThenable<string>)}40*/41let CommandLineFlag // eslint-disable-line4243/**44* A record object that defines the configuration options for a DriverService45* instance.46*47* @record48*/49function ServiceOptions() {}5051/**52* Whether the service should only be accessed on this host's loopback address.53*54* @type {(boolean|undefined)}55*/56ServiceOptions.prototype.loopback5758/**59* The host name to access the server on. If this option is specified, the60* {@link #loopback} option will be ignored.61*62* @type {(string|undefined)}63*/64ServiceOptions.prototype.hostname6566/**67* The port to start the server on (must be > 0). If the port is provided as a68* promise, the service will wait for the promise to resolve before starting.69*70* @type {(number|!IThenable<number>)}71*/72ServiceOptions.prototype.port7374/**75* The arguments to pass to the service. If a promise is provided, the service76* will wait for it to resolve before starting.77*78* @type {!(Array<CommandLineFlag>|IThenable<!Array<CommandLineFlag>>)}79*/80ServiceOptions.prototype.args8182/**83* The base path on the server for the WebDriver wire protocol (e.g. '/wd/hub').84* Defaults to '/'.85*86* @type {(string|undefined|null)}87*/88ServiceOptions.prototype.path8990/**91* The environment variables that should be visible to the server process.92* Defaults to inheriting the current process's environment.93*94* @type {(Object<string, string>|undefined)}95*/96ServiceOptions.prototype.env9798/**99* IO configuration for the spawned server process. For more information, refer100* to the documentation of `child_process.spawn`.101*102* @type {(StdIoOptions|undefined)}103* @see https://nodejs.org/dist/latest-v4.x/docs/api/child_process.html#child_process_options_stdio104*/105ServiceOptions.prototype.stdio106107/**108* Manages the life and death of a native executable WebDriver server.109*110* It is expected that the driver server implements the111* https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol.112* Furthermore, the managed server should support multiple concurrent sessions,113* so that this class may be reused for multiple clients.114*/115class DriverService {116/**117* @param {string} executable Path to the executable to run.118* @param {!ServiceOptions} options Configuration options for the service.119*/120constructor(executable, options) {121/** @private @const */122this.log_ = logging.getLogger(`${logging.Type.DRIVER}.DriverService`)123/** @private {string} */124this.executable_ = executable125126/** @private {boolean} */127this.loopbackOnly_ = !!options.loopback128129/** @private {(string|undefined)} */130this.hostname_ = options.hostname131132/** @private {(number|!IThenable<number>)} */133this.port_ = options.port134135/**136* @private {!(Array<CommandLineFlag>|137* IThenable<!Array<CommandLineFlag>>)}138*/139this.args_ = options.args140141/** @private {string} */142this.path_ = options.path || '/'143144/** @private {!Object<string, string>} */145this.env_ = options.env || process.env146147/**148* @private {(string|!Array<string|number|!stream.Stream|null|undefined>)}149*/150this.stdio_ = options.stdio || 'ignore'151152/**153* A promise for the managed subprocess, or null if the server has not been154* started yet. This promise will never be rejected.155* @private {Promise<!exec.Command>}156*/157this.command_ = null158159/**160* Promise that resolves to the server's address or null if the server has161* not been started. This promise will be rejected if the server terminates162* before it starts accepting WebDriver requests.163* @private {Promise<string>}164*/165this.address_ = null166}167168getExecutable() {169return this.executable_170}171172setExecutable(value) {173this.executable_ = value174}175176/**177* @return {!Promise<string>} A promise that resolves to the server's address.178* @throws {Error} If the server has not been started.179*/180address() {181if (this.address_) {182return this.address_183}184throw Error('Server has not been started.')185}186187/**188* Returns whether the underlying process is still running. This does not take189* into account whether the process is in the process of shutting down.190* @return {boolean} Whether the underlying service process is running.191*/192isRunning() {193return !!this.address_194}195196/**197* Starts the server if it is not already running.198* @param {number=} opt_timeoutMs How long to wait, in milliseconds, for the199* server to start accepting requests. Defaults to 30 seconds.200* @return {!Promise<string>} A promise that will resolve to the server's base201* URL when it has started accepting requests. If the timeout expires202* before the server has started, the promise will be rejected.203*/204start(opt_timeoutMs) {205if (this.address_) {206return this.address_207}208209const timeout = opt_timeoutMs || DriverService.DEFAULT_START_TIMEOUT_MS210const self = this211212let resolveCommand213this.command_ = new Promise((resolve) => (resolveCommand = resolve))214215this.address_ = new Promise((resolveAddress, rejectAddress) => {216resolveAddress(217Promise.resolve(this.port_).then((port) => {218if (port <= 0) {219throw Error('Port must be > 0: ' + port)220}221222return resolveCommandLineFlags(this.args_).then((args) => {223const command = exec(self.executable_, {224args: args,225env: self.env_,226stdio: self.stdio_,227})228229resolveCommand(command)230231const earlyTermination = command.result().then(function (result) {232const error =233result.code == null234? Error('Server was killed with ' + result.signal)235: Error('Server terminated early with status ' + result.code)236rejectAddress(error)237self.address_ = null238self.command_ = null239throw error240})241242let hostname = self.hostname_243if (!hostname) {244hostname = (!self.loopbackOnly_ && net.getAddress()) || net.getLoopbackAddress()245}246247const serverUrl = url.format({248protocol: 'http',249hostname: hostname,250port: port + '',251pathname: self.path_,252})253254return new Promise((fulfill, reject) => {255let cancelToken = earlyTermination.catch((e) => reject(Error(e.message)))256257httpUtil.waitForServer(serverUrl, timeout, cancelToken).then(258(_) => fulfill(serverUrl),259(err) => {260if (err instanceof httpUtil.CancellationError) {261fulfill(serverUrl)262} else {263reject(err)264}265},266)267})268})269}),270)271})272273return this.address_274}275276/**277* Stops the service if it is not currently running. This function will kill278* the server immediately. To synchronize with the active control flow, use279* {@link #stop()}.280* @return {!Promise} A promise that will be resolved when the server has been281* stopped.282*/283kill() {284if (!this.address_ || !this.command_) {285return Promise.resolve() // Not currently running.286}287let cmd = this.command_288this.address_ = null289this.command_ = null290return cmd.then((c) => c.kill('SIGTERM'))291}292}293294/**295* @param {!(Array<CommandLineFlag>|IThenable<!Array<CommandLineFlag>>)} args296* @return {!Promise<!Array<string>>}297*/298function resolveCommandLineFlags(args) {299// Resolve the outer array, then the individual flags.300return Promise.resolve(args).then(/** !Array<CommandLineFlag> */ (args) => Promise.all(args))301}302303/**304* The default amount of time, in milliseconds, to wait for the server to305* start.306* @const {number}307*/308DriverService.DEFAULT_START_TIMEOUT_MS = 30 * 1000309310/**311* Creates {@link DriverService} objects that manage a WebDriver server in a312* child process.313*/314DriverService.Builder = class {315/**316* @param {string} exe Path to the executable to use. This executable must317* accept the `--port` flag for defining the port to start the server on.318* @throws {Error} If the provided executable path does not exist.319*/320constructor(exe) {321/** @private @const {string} */322this.exe_ = exe323324/** @private {!ServiceOptions} */325this.options_ = {326args: [],327port: 0,328env: null,329stdio: 'ignore',330}331}332333/**334* Define additional command line arguments to use when starting the server.335*336* @param {...CommandLineFlag} var_args The arguments to include.337* @return {!THIS} A self reference.338* @this {THIS}339* @template THIS340*/341addArguments(...arguments_) {342this.options_.args = this.options_.args.concat(arguments_)343return this344}345346/**347* Sets the host name to access the server on. If specified, the348* {@linkplain #setLoopback() loopback} setting will be ignored.349*350* @param {string} hostname351* @return {!DriverService.Builder} A self reference.352*/353setHostname(hostname) {354this.options_.hostname = hostname355return this356}357358/**359* Sets whether the service should be accessed at this host's loopback360* address.361*362* @param {boolean} loopback363* @return {!DriverService.Builder} A self reference.364*/365setLoopback(loopback) {366this.options_.loopback = loopback367return this368}369370/**371* Sets the base path for WebDriver REST commands (e.g. "/wd/hub").372* By default, the driver will accept commands relative to "/".373*374* @param {?string} basePath The base path to use, or `null` to use the375* default.376* @return {!DriverService.Builder} A self reference.377*/378setPath(basePath) {379this.options_.path = basePath380return this381}382383/**384* Sets the port to start the server on.385*386* @param {number} port The port to use, or 0 for any free port.387* @return {!DriverService.Builder} A self reference.388* @throws {Error} If an invalid port is specified.389*/390setPort(port) {391if (port < 0) {392throw Error(`port must be >= 0: ${port}`)393}394this.options_.port = port395return this396}397398/**399* Defines the environment to start the server under. This setting will be400* inherited by every browser session started by the server. By default, the401* server will inherit the environment of the current process.402*403* @param {(Map<string, string>|Object<string, string>|null)} env The desired404* environment to use, or `null` if the server should inherit the405* current environment.406* @return {!DriverService.Builder} A self reference.407*/408setEnvironment(env) {409if (env instanceof Map) {410let tmp = {}411env.forEach((value, key) => (tmp[key] = value))412env = tmp413}414this.options_.env = env415return this416}417418/**419* IO configuration for the spawned server process. For more information,420* refer to the documentation of `child_process.spawn`.421*422* @param {StdIoOptions} config The desired IO configuration.423* @return {!DriverService.Builder} A self reference.424* @see https://nodejs.org/dist/latest-v4.x/docs/api/child_process.html#child_process_options_stdio425*/426setStdio(config) {427this.options_.stdio = config428return this429}430431/**432* Creates a new DriverService using this instance's current configuration.433*434* @return {!DriverService} A new driver service.435*/436build() {437let port = this.options_.port || portprober.findFreePort()438let args = Promise.resolve(port).then((port) => {439return this.options_.args.concat('--port=' + port)440})441442let options =443/** @type {!ServiceOptions} */444(Object.assign({}, this.options_, { args, port }))445return new DriverService(this.exe_, options)446}447}448449/**450* Manages the life and death of the451* <a href="https://www.selenium.dev/downloads/">452* standalone Selenium server</a>.453*/454class SeleniumServer extends DriverService {455/**456* @param {string} jar Path to the Selenium server jar.457* @param {SeleniumServer.Options=} opt_options Configuration options for the458* server.459* @throws {Error} If the path to the Selenium jar is not specified or if an460* invalid port is specified.461*/462constructor(jar, opt_options) {463if (!jar) {464throw Error('Path to the Selenium jar not specified')465}466467const options = opt_options || {}468469if (options.port < 0) {470throw Error('Port must be >= 0: ' + options.port)471}472473let port = options.port || portprober.findFreePort()474let args = Promise.all([port, options.jvmArgs || [], options.args || []]).then((resolved) => {475let port = resolved[0]476let jvmArgs = resolved[1]477let args = resolved[2]478479const fullArgsList = jvmArgs.concat('-jar', jar, '-port', port).concat(args)480481return formatSpawnArgs(jar, fullArgsList)482})483484const java = getJavaPath()485486super(java, {487loopback: options.loopback,488port: port,489args: args,490path: '/wd/hub',491env: options.env,492stdio: options.stdio,493})494}495}496497/**498* A record object describing configuration options for a {@link SeleniumServer}499* instance.500*501* @record502*/503SeleniumServer.Options = class {504constructor() {505/**506* Whether the server should only be accessible on this host's loopback507* address.508*509* @type {(boolean|undefined)}510*/511this.loopback512513/**514* The port to start the server on (must be > 0). If the port is provided as515* a promise, the service will wait for the promise to resolve before516* starting.517*518* @type {(number|!IThenable<number>)}519*/520this.port521522/**523* The arguments to pass to the service. If a promise is provided,524* the service will wait for it to resolve before starting.525*526* @type {!(Array<string>|IThenable<!Array<string>>)}527*/528this.args529530/**531* The arguments to pass to the JVM. If a promise is provided,532* the service will wait for it to resolve before starting.533*534* @type {(!Array<string>|!IThenable<!Array<string>>|undefined)}535*/536this.jvmArgs537538/**539* The environment variables that should be visible to the server540* process. Defaults to inheriting the current process's environment.541*542* @type {(!Object<string, string>|undefined)}543*/544this.env545546/**547* IO configuration for the spawned server process. If unspecified, IO will548* be ignored.549*550* @type {(string|!Array<string|number|!stream.Stream|null|undefined>|551* undefined)}552* @see <https://nodejs.org/dist/latest-v8.x/docs/api/child_process.html#child_process_options_stdio>553*/554this.stdio555}556}557558/**559* A {@link webdriver.FileDetector} that may be used when running560* against a remote561* [Selenium server](https://www.selenium.dev/downloads/).562*563* When a file path on the local machine running this script is entered with564* {@link webdriver.WebElement#sendKeys WebElement#sendKeys}, this file detector565* will transfer the specified file to the Selenium server's host; the sendKeys566* command will be updated to use the transferred file's path.567*568* __Note:__ This class depends on a non-standard command supported on the569* Java Selenium server. The file detector will fail if used with a server that570* only supports standard WebDriver commands (such as the ChromeDriver).571*572* @final573*/574class FileDetector extends input.FileDetector {575/**576* Prepares a `file` for use with the remote browser. If the provided path577* does not reference a normal file (i.e. it does not exist or is a578* directory), then the promise returned by this method will be resolved with579* the original file path. Otherwise, this method will upload the file to the580* remote server, which will return the file's path on the remote system so581* it may be referenced in subsequent commands.582*583* @override584*/585handleFile(driver, file) {586return io.stat(file).then(587function (stats) {588if (stats.isDirectory()) {589return file // Not a valid file, return original input.590}591592let zip = new Zip()593return zip594.addFile(file)595.then(() => zip.toBuffer())596.then((buf) => buf.toString('base64'))597.then((encodedZip) => {598let command = new cmd.Command(cmd.Name.UPLOAD_FILE).setParameter('file', encodedZip)599return driver.execute(command)600})601},602function (err) {603if (err.code === 'ENOENT') {604return file // Not a file; return original input.605}606throw err607},608)609}610}611612// PUBLIC API613614module.exports = {615DriverService,616FileDetector,617SeleniumServer,618// Exported for API docs.619ServiceOptions,620}621622623