appium 2.13.0 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/build/lib/appium.d.ts +4 -64
  2. package/build/lib/appium.d.ts.map +1 -1
  3. package/build/lib/appium.js +46 -316
  4. package/build/lib/appium.js.map +1 -1
  5. package/build/lib/bidi.d.ts +32 -0
  6. package/build/lib/bidi.d.ts.map +1 -0
  7. package/build/lib/bidi.js +348 -0
  8. package/build/lib/bidi.js.map +1 -0
  9. package/build/lib/cli/args.d.ts.map +1 -1
  10. package/build/lib/cli/args.js +21 -31
  11. package/build/lib/cli/args.js.map +1 -1
  12. package/build/lib/cli/extension-command.d.ts.map +1 -1
  13. package/build/lib/cli/extension-command.js +38 -5
  14. package/build/lib/cli/extension-command.js.map +1 -1
  15. package/build/lib/cli/parser.d.ts.map +1 -1
  16. package/build/lib/cli/parser.js +26 -15
  17. package/build/lib/cli/parser.js.map +1 -1
  18. package/build/lib/cli/utils.d.ts.map +1 -1
  19. package/build/lib/cli/utils.js +3 -1
  20. package/build/lib/cli/utils.js.map +1 -1
  21. package/build/lib/config.js +42 -9
  22. package/build/lib/config.js.map +1 -1
  23. package/build/lib/extension/extension-config.d.ts +7 -0
  24. package/build/lib/extension/extension-config.d.ts.map +1 -1
  25. package/build/lib/extension/extension-config.js +42 -11
  26. package/build/lib/extension/extension-config.js.map +1 -1
  27. package/build/lib/extension/manifest.js +2 -2
  28. package/build/lib/extension/manifest.js.map +1 -1
  29. package/build/lib/schema/schema.js +3 -3
  30. package/build/lib/schema/schema.js.map +1 -1
  31. package/build/lib/utils.js +1 -2
  32. package/build/lib/utils.js.map +1 -1
  33. package/lib/appium.js +16 -377
  34. package/lib/bidi.ts +436 -0
  35. package/lib/cli/args.js +22 -32
  36. package/lib/cli/extension-command.js +4 -4
  37. package/lib/cli/parser.js +33 -17
  38. package/lib/cli/utils.js +3 -1
  39. package/lib/config.js +7 -7
  40. package/lib/extension/extension-config.js +49 -11
  41. package/lib/extension/manifest.js +2 -2
  42. package/lib/schema/schema.js +2 -2
  43. package/lib/utils.js +2 -2
  44. package/package.json +13 -13
  45. package/scripts/autoinstall-extensions.js +5 -5
  46. package/LICENSE +0 -201
package/lib/appium.js CHANGED
@@ -1,6 +1,4 @@
1
- /* eslint-disable no-unused-vars */
2
1
  import _ from 'lodash';
3
- import B from 'bluebird';
4
2
  import {getBuildInfo, updateBuildInfo, APPIUM_VER} from './config';
5
3
  import {
6
4
  BaseDriver,
@@ -10,7 +8,6 @@ import {
10
8
  CREATE_SESSION_COMMAND,
11
9
  DELETE_SESSION_COMMAND,
12
10
  GET_STATUS_COMMAND,
13
- MAX_LOG_BODY_LENGTH,
14
11
  promoteAppiumOptions,
15
12
  promoteAppiumOptionsForObject,
16
13
  } from '@appium/base-driver';
@@ -19,20 +16,12 @@ import {
19
16
  parseCapsForInnerDriver,
20
17
  pullSettings,
21
18
  makeNonW3cCapsError,
22
- isBroadcastIp,
23
- fetchInterfaces,
24
- V4_BROADCAST_IP,
25
19
  validateFeatures,
26
20
  } from './utils';
27
21
  import {util, node, logger} from '@appium/support';
28
22
  import {getDefaultsForExtension} from './schema';
29
- import {DRIVER_TYPE, BIDI_BASE_PATH, BIDI_EVENT_NAME} from './constants';
30
- import WebSocket from 'ws';
31
- import os from 'node:os';
32
-
33
- const MIN_WS_CODE_VAL = 1000;
34
- const MAX_WS_CODE_VAL = 1015;
35
- const WS_FALLBACK_CODE = 1011; // server encountered an error while fulfilling request
23
+ import {DRIVER_TYPE, BIDI_BASE_PATH} from './constants';
24
+ import * as bidiHelpers from './bidi';
36
25
 
37
26
  const desiredCapabilityConstraints = /** @type {const} */ ({
38
27
  automationName: {
@@ -178,7 +167,6 @@ class AppiumDriver extends DriverCore {
178
167
  return this.sessions[sessionId];
179
168
  }
180
169
 
181
- // eslint-disable-next-line require-await
182
170
  async getStatus() {
183
171
  // https://www.w3.org/TR/webdriver/#dfn-status
184
172
  const statusObj = this._isShuttingDown
@@ -265,347 +253,6 @@ class AppiumDriver extends DriverCore {
265
253
  }
266
254
  }
267
255
 
268
- /**
269
- * Initialize a new bidi connection and set up handlers
270
- * @param {import('ws').WebSocket} ws The websocket connection object
271
- * @param {import('http').IncomingMessage} req The connection pathname, which might include the session id
272
- */
273
- onBidiConnection(ws, req) {
274
- // TODO put bidi-related functionality into a mixin/helper class
275
- // wrap all of the handler logic with exception handling so if something blows up we can log
276
- // and close the websocket
277
- try {
278
- const {bidiHandlerDriver, proxyClient, send, sendToProxy, logSocketErr} = this.initBidiSocket(
279
- ws,
280
- req,
281
- );
282
-
283
- this.initBidiSocketHandlers(
284
- ws,
285
- proxyClient,
286
- send,
287
- sendToProxy,
288
- bidiHandlerDriver,
289
- logSocketErr,
290
- );
291
- this.initBidiProxyHandlers(proxyClient, ws, send);
292
- this.initBidiEventListeners(ws, bidiHandlerDriver, send);
293
- } catch (err) {
294
- this.log.error(err);
295
- try {
296
- ws.close();
297
- } catch (ign) {}
298
- }
299
- }
300
-
301
- /**
302
- * Initialize a new bidi connection
303
- * @param {import('ws').WebSocket} ws The websocket connection object
304
- * @param {import('http').IncomingMessage} req The connection pathname, which might include the session id
305
- */
306
- initBidiSocket(ws, req) {
307
- let outOfBandErrorPrefix = '';
308
- const pathname = req.url;
309
- if (!pathname) {
310
- throw new Error('Invalid connection request: pathname missing from request');
311
- }
312
- const bidiSessionRe = new RegExp(`${BIDI_BASE_PATH}/([^/]+)$`);
313
- const bidiNoSessionRe = new RegExp(`${BIDI_BASE_PATH}/?$`);
314
- const sessionMatch = bidiSessionRe.exec(pathname);
315
- const noSessionMatch = bidiNoSessionRe.exec(pathname);
316
-
317
- if (!sessionMatch && !noSessionMatch) {
318
- throw new Error(
319
- `Got websocket connection for path ${pathname} but didn't know what to do with it. ` +
320
- `Ignoring and will close the connection`,
321
- );
322
- }
323
-
324
- // Let's figure out which driver is going to handle this socket connection. It's either going
325
- // to be a driver matching a session id appended to the bidi base path, or this umbrella driver
326
- // (if no session id is included in the bidi connection request)
327
-
328
- /** @type {import('@appium/types').ExternalDriver | AppiumDriver} */
329
- let bidiHandlerDriver;
330
-
331
- /** @type {import('ws').WebSocket | null} */
332
- let proxyClient = null;
333
-
334
- if (sessionMatch) {
335
- // If we found a session id, see if it matches an active session
336
- const sessionId = sessionMatch[1];
337
- bidiHandlerDriver = this.sessions[sessionId];
338
- if (!bidiHandlerDriver) {
339
- // The session ID sent in doesn't match an active session; just ignore this socket
340
- // connection in that case
341
- throw new Error(
342
- `Got bidi connection request for session with id ${sessionId} which is closed ` +
343
- `or does not exist. Closing the socket connection.`,
344
- );
345
- }
346
- const driverName = bidiHandlerDriver.constructor.name;
347
- outOfBandErrorPrefix = `[session ${sessionId}] `;
348
- this.log.info(`Bidi websocket connection made for session ${sessionId}`);
349
- // store this socket connection for later removal on session deletion. theoretically there
350
- // can be multiple sockets per session
351
- if (!this.bidiSockets[sessionId]) {
352
- this.bidiSockets[sessionId] = [];
353
- }
354
- this.bidiSockets[sessionId].push(ws);
355
-
356
- const bidiProxyUrl = bidiHandlerDriver.bidiProxyUrl;
357
- if (bidiProxyUrl) {
358
- try {
359
- new URL(bidiProxyUrl);
360
- } catch (ign) {
361
- throw new Error(
362
- `Got request for ${driverName} to proxy bidi connections to upstream socket with ` +
363
- `url ${bidiProxyUrl}, but this was not a valid url`,
364
- );
365
- }
366
- this.log.info(`Bidi connection for ${driverName} will be proxied to ${bidiProxyUrl}`);
367
- proxyClient = new WebSocket(bidiProxyUrl);
368
- this.bidiProxyClients[sessionId] = proxyClient;
369
- }
370
- } else {
371
- this.log.info('Bidi websocket connection made to main server');
372
- // no need to store the socket connection if it's to the main server since it will just
373
- // stay open as long as the server itself is and will close when the server closes.
374
- bidiHandlerDriver = this; // eslint-disable-line @typescript-eslint/no-this-alias
375
- }
376
-
377
- const logSocketErr = (/** @type {Error} */ err) =>
378
- this.log.error(`${outOfBandErrorPrefix}${err}`);
379
-
380
- // This is a function which wraps the 'send' method on a web socket for two reasons:
381
- // 1. Make it async-await friendly
382
- // 2. Do some logging if there's a send error
383
- const sendFactory = (/** @type {import('ws').WebSocket} */ socket) => {
384
- const socketSend = B.promisify(socket.send, {context: socket});
385
- return async (/** @type {string|Buffer} */ data) => {
386
- try {
387
- await socketSend(data);
388
- } catch (err) {
389
- logSocketErr(err);
390
- }
391
- };
392
- };
393
-
394
- // Construct our send method for sending messages to the client
395
- const send = sendFactory(ws);
396
-
397
- // Construct a conditional send method for proxying messages from the client to an upstream
398
- // bidi socket server (e.g. on a browser)
399
- const sendToProxy = proxyClient ? sendFactory(proxyClient) : null;
400
-
401
- return {bidiHandlerDriver, proxyClient, send, sendToProxy, logSocketErr};
402
- }
403
-
404
- /**
405
- * Set up handlers on upstream bidi socket we are proxying to/from
406
- *
407
- * @param {import('ws').WebSocket | null} proxyClient - the websocket connection to/from the
408
- * upstream socket (the one we're proxying to/from)
409
- * @param {import('ws').WebSocket} ws - the websocket connection to/from the client
410
- * @param {(data: string | Buffer) => Promise<void>} send - a method used to send data to the
411
- * client
412
- */
413
- initBidiProxyHandlers(proxyClient, ws, send) {
414
- // Set up handlers for events that might come from the upstream bidi socket connection if
415
- // we're in proxy mode
416
- if (proxyClient) {
417
- // Here we're receiving a message from the upstream socket server. We want to pass it on to
418
- // the client
419
- proxyClient.on('message', async (/** @type {Buffer|string} */ data) => {
420
- const logData = _.truncate(data.toString('utf8'), {length: MAX_LOG_BODY_LENGTH});
421
- this.log.debug(
422
- `<-- BIDI Received data from proxied bidi socket, sending to client. Data: ${logData}`,
423
- );
424
- await send(data);
425
- });
426
-
427
- // If the upstream socket server closes the connection, should close the connection to the
428
- // client as well
429
- proxyClient.on('close', (code, reason) => {
430
- this.log.debug(
431
- `Upstream bidi socket closed connection (code ${code}, reason: '${reason}'). ` +
432
- `Closing proxy connection to client`,
433
- );
434
- if (!_.isNumber(code)) {
435
- code = parseInt(code, 10);
436
- }
437
- if (_.isNaN(code) || code < MIN_WS_CODE_VAL || code > MAX_WS_CODE_VAL) {
438
- this.log.warn(
439
- `Received code ${code} from upstream socket, but this is not a valid ` +
440
- `websocket code. Rewriting to ${WS_FALLBACK_CODE} for ws compatibility`,
441
- );
442
- code = WS_FALLBACK_CODE;
443
- }
444
- ws.close(code, reason);
445
- });
446
-
447
- proxyClient.on('error', (err) => {
448
- this.log.error(`Got error on upstream bidi socket connection: ${err}`);
449
- });
450
- }
451
- }
452
-
453
- /**
454
- * Set up handlers on the bidi socket connection to the client
455
- *
456
- * @param {import('ws').WebSocket} ws - the websocket connection to/from the client
457
- * @param {import('ws').WebSocket | null} proxyClient - the websocket connection to/from the
458
- * upstream socket (the one we're proxying to/from, if we're proxying)
459
- * @param {(data: string | Buffer) => Promise<void>} send - a method used to send data to the
460
- * client
461
- * @param {((data: string | Buffer) => Promise<void>) | null} sendToProxy - a method used to send data to the
462
- * upstream socket
463
- * @param {import('@appium/types').ExternalDriver | AppiumDriver} bidiHandlerDriver - the driver
464
- * handling the bidi commands
465
- * @param {(err: Error) => void} logSocketErr - a special prefixed logger
466
- */
467
- initBidiSocketHandlers(ws, proxyClient, send, sendToProxy, bidiHandlerDriver, logSocketErr) {
468
- ws.on('error', (err) => {
469
- // Can't do much with random errors on the connection other than log them
470
- logSocketErr(err);
471
- });
472
-
473
- ws.on('open', () => {
474
- this.log.info('Bidi websocket connection is now open');
475
- });
476
-
477
- // Now set up handlers for the various events that might happen on the websocket connection
478
- // coming from the client
479
- // First is incoming messages from the client
480
- ws.on('message', async (/** @type {Buffer} */ data) => {
481
- if (proxyClient) {
482
- const logData = _.truncate(data.toString('utf8'), {length: MAX_LOG_BODY_LENGTH});
483
- this.log.debug(
484
- `--> BIDI Received data from client, sending to upstream bidi socket. Data: ${logData}`,
485
- );
486
- // if we're meant to proxy to an upstream bidi socket, just do that
487
- // @ts-ignore sendToProxy is never null if proxyClient is truthy, but ts doesn't know
488
- // that
489
- await sendToProxy(data.toString('utf8'));
490
- } else {
491
- const res = await this.onBidiMessage(data, bidiHandlerDriver);
492
- await send(JSON.stringify(res));
493
- }
494
- });
495
-
496
- // Next consider if the client closes the socket connection on us
497
- ws.on('close', (code, reason) => {
498
- // Not sure if we need to do anything here if the client closes the websocket connection.
499
- // Probably if a session was started via the socket, and the socket closes, we should end the
500
- // associated session to free up resources. But otherwise, for sockets attached to existing
501
- // sessions, doing nothing is probably right.
502
- this.log.debug(`Bidi socket connection closed (code ${code}, reason: '${reason}')`);
503
-
504
- // If we're proxying, might as well close the upstream connection and clean it up
505
- if (proxyClient) {
506
- this.log.debug('Also closing bidi proxy socket connection');
507
- proxyClient.close(code, reason);
508
- }
509
- });
510
- }
511
-
512
- /**
513
- * Set up bidi event listeners
514
- *
515
- * @param {import('ws').WebSocket} ws - the websocket connection to/from the client
516
- * @param {import('@appium/types').ExternalDriver | AppiumDriver} bidiHandlerDriver - the driver
517
- * handling the bidi commands
518
- * @param {(data: string | Buffer) => Promise<void>} send - a method used to send data to the
519
- * client
520
- */
521
- initBidiEventListeners(ws, bidiHandlerDriver, send) {
522
- // If the driver emits a bidi event that should maybe get sent to the client, check to make
523
- // sure the client is subscribed and then pass it on
524
- let eventListener = async ({context, method, params}) => {
525
- // if the driver didn't specify a context, use the empty context
526
- if (!context) {
527
- context = '';
528
- }
529
- if (!method || !params) {
530
- throw new Error(
531
- `Driver emitted a bidi event that was malformed. Require method and params keys ` +
532
- `(with optional context). But instead received: ${JSON.stringify({
533
- context,
534
- method,
535
- params,
536
- })}`,
537
- );
538
- }
539
- if (ws.readyState !== WebSocket.OPEN) {
540
- // if the websocket is not still 'open', then we can ignore sending these events
541
- if (ws.readyState > WebSocket.OPEN) {
542
- // if the websocket is closed or closing, we can remove this listener as well to avoid
543
- // leaks
544
- bidiHandlerDriver.eventEmitter.removeListener(BIDI_EVENT_NAME, eventListener);
545
- }
546
- return;
547
- }
548
-
549
- if (bidiHandlerDriver.bidiEventSubs[method]?.includes(context)) {
550
- this.log.info(
551
- `<-- BIDI EVENT ${method} (context: '${context}', params: ${JSON.stringify(params)})`,
552
- );
553
- // now we can send the event onto the socket
554
- const ev = {type: 'event', context, method, params};
555
- await send(JSON.stringify(ev));
556
- }
557
- };
558
- bidiHandlerDriver.eventEmitter.on(BIDI_EVENT_NAME, eventListener);
559
- }
560
-
561
- /**
562
- * @param {Buffer} data
563
- * @param {ExternalDriver | AppiumDriver} driver
564
- */
565
- async onBidiMessage(data, driver) {
566
- let resMessage, id, method, params;
567
- const dataTruncated = _.truncate(data.toString(), {length: 100});
568
- try {
569
- try {
570
- ({id, method, params} = JSON.parse(data.toString('utf8')));
571
- } catch (err) {
572
- throw new errors.InvalidArgumentError(
573
- `Could not parse Bidi command '${dataTruncated}': ${err.message}`,
574
- );
575
- }
576
- driver.log.info(`--> BIDI message #${id}`);
577
- if (!method) {
578
- throw new errors.InvalidArgumentError(
579
- `Missing method for BiDi operation in '${dataTruncated}'`,
580
- );
581
- }
582
- if (!params) {
583
- throw new errors.InvalidArgumentError(
584
- `Missing params for BiDi operation in '${dataTruncated}`,
585
- );
586
- }
587
- const result = await driver.executeBidiCommand(method, params);
588
- // https://w3c.github.io/webdriver-bidi/#protocol-definition
589
- resMessage = {
590
- id,
591
- type: 'success',
592
- result,
593
- };
594
- } catch (err) {
595
- resMessage = err.bidiErrObject(id);
596
- }
597
- driver.log.info(`<-- BIDI message #${id}`);
598
- return resMessage;
599
- }
600
-
601
- /**
602
- * Log a bidi server error
603
- * @param {Error} err
604
- */
605
- onBidiServerError(err) {
606
- this.log.error(`Error from bidi websocket server: ${err}`);
607
- }
608
-
609
256
  /**
610
257
  * Create a new session
611
258
  * @param {W3CAppiumDriverCaps} jsonwpCaps JSONWP formatted desired capabilities
@@ -695,6 +342,10 @@ class AppiumDriver extends DriverCore {
695
342
  driverInstance.relaxedSecurityEnabled = true;
696
343
  }
697
344
 
345
+ // We also want to assign any new Bidi Commands that the driver has specified, including all
346
+ // the standard bidi commands
347
+ driverInstance.updateBidiCommands(InnerDriver.newBidiCommands ?? {});
348
+
698
349
  if (!_.isEmpty(this.args.denyInsecure)) {
699
350
  this.log.info('Explicitly preventing use of insecure features:');
700
351
  this.args.denyInsecure.map((a) => this.log.info(` ${a}`));
@@ -783,7 +434,7 @@ class AppiumDriver extends DriverCore {
783
434
  if (dCaps.webSocketUrl && driverInstance.doesSupportBidi) {
784
435
  const {address, port, basePath} = this.args;
785
436
  const scheme = `ws${this.server.isSecure() ? 's' : ''}`;
786
- const host = determineBiDiHost(address);
437
+ const host = bidiHelpers.determineBiDiHost(address);
787
438
  const bidiUrl = `${scheme}://${host}:${port}${basePath}${BIDI_BASE_PATH}/${innerSessionId}`;
788
439
  this.log.info(
789
440
  `Upstream driver responded with webSocketUrl ${dCaps.webSocketUrl}, will rewrite to ` +
@@ -1102,7 +753,9 @@ class AppiumDriver extends DriverCore {
1102
753
  // if we're running with plugins, make sure we log that the default behavior is actually
1103
754
  // happening so we can tell when the plugin call chain is unwrapping to the default behavior
1104
755
  // if that's what happens
1105
- plugins.length && this.log.info(`Executing default handling behavior for command '${cmd}'`);
756
+ if (plugins.length) {
757
+ this.log.info(`Executing default handling behavior for command '${cmd}'`);
758
+ }
1106
759
 
1107
760
  // if we make it here, we know that the default behavior is handled
1108
761
  cmdHandledBy.default = true;
@@ -1184,8 +837,9 @@ class AppiumDriver extends DriverCore {
1184
837
  }
1185
838
 
1186
839
  wrapCommandWithPlugins({driver, cmd, args, next, cmdHandledBy, plugins}) {
1187
- plugins.length &&
840
+ if (plugins.length) {
1188
841
  this.log.info(`Plugins which can handle cmd '${cmd}': ${plugins.map((p) => p.name)}`);
842
+ }
1189
843
 
1190
844
  // now we can go through each plugin and wrap `next` around its own handler, passing the *old*
1191
845
  // next in so that it can call it if it wants to
@@ -1276,6 +930,10 @@ class AppiumDriver extends DriverCore {
1276
930
  const dstSession = this.sessions[sessionId];
1277
931
  return dstSession && dstSession.canProxy(sessionId);
1278
932
  }
933
+
934
+ onBidiConnection = bidiHelpers.onBidiConnection;
935
+ onBidiMessage = bidiHelpers.onBidiMessage;
936
+ onBidiServerError = bidiHelpers.onBidiServerError;
1279
937
  }
1280
938
 
1281
939
  // help decide which commands should be proxied to sub-drivers and which
@@ -1284,25 +942,6 @@ function isAppiumDriverCommand(cmd) {
1284
942
  return !isSessionCommand(cmd) || cmd === DELETE_SESSION_COMMAND;
1285
943
  }
1286
944
 
1287
- /**
1288
- * Clients cannot use broadcast addresses, like 0.0.0.0 or ::
1289
- * to create connections. Thus we prefer a hostname if such
1290
- * address is provided or the actual address of a non-local interface,
1291
- * in case the host only has one such interface.
1292
- *
1293
- * @param {string} address
1294
- * @returns {string}
1295
- */
1296
- function determineBiDiHost(address) {
1297
- if (!isBroadcastIp(address)) {
1298
- return address;
1299
- }
1300
-
1301
- const nonLocalInterfaces = fetchInterfaces(address === V4_BROADCAST_IP ? 4 : 6)
1302
- .filter((iface) => !iface.internal);
1303
- return nonLocalInterfaces.length === 1 ? nonLocalInterfaces[0].address : os.hostname();
1304
- }
1305
-
1306
945
  /**
1307
946
  * Thrown when Appium tried to proxy a command using a driver's `proxyCommand` method but the
1308
947
  * method did not exist