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