appium 2.0.0-beta.18 → 2.0.0-beta.21

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 (54) hide show
  1. package/build/lib/appium-config.schema.json +0 -0
  2. package/build/lib/appium.js +84 -69
  3. package/build/lib/cli/argparse-actions.js +1 -1
  4. package/build/lib/cli/args.js +87 -223
  5. package/build/lib/cli/extension-command.js +2 -2
  6. package/build/lib/cli/extension.js +14 -6
  7. package/build/lib/cli/parser.js +142 -106
  8. package/build/lib/cli/utils.js +1 -1
  9. package/build/lib/config-file.js +141 -0
  10. package/build/lib/config.js +42 -64
  11. package/build/lib/driver-config.js +41 -20
  12. package/build/lib/drivers.js +1 -1
  13. package/build/lib/ext-config-io.js +165 -0
  14. package/build/lib/extension-config.js +110 -60
  15. package/build/lib/grid-register.js +19 -21
  16. package/build/lib/logsink.js +1 -1
  17. package/build/lib/main.js +135 -72
  18. package/build/lib/plugin-config.js +17 -8
  19. package/build/lib/schema/appium-config-schema.js +252 -0
  20. package/build/lib/schema/arg-spec.js +120 -0
  21. package/build/lib/schema/cli-args.js +173 -0
  22. package/build/lib/schema/cli-transformers.js +76 -0
  23. package/build/lib/schema/index.js +36 -0
  24. package/build/lib/schema/keywords.js +62 -0
  25. package/build/lib/schema/schema.js +357 -0
  26. package/build/lib/utils.js +26 -35
  27. package/lib/appium-config.schema.json +277 -0
  28. package/lib/appium.js +99 -75
  29. package/lib/cli/args.js +138 -335
  30. package/lib/cli/extension-command.js +7 -6
  31. package/lib/cli/extension.js +12 -4
  32. package/lib/cli/parser.js +248 -96
  33. package/lib/config-file.js +227 -0
  34. package/lib/config.js +71 -61
  35. package/lib/driver-config.js +66 -11
  36. package/lib/ext-config-io.js +287 -0
  37. package/lib/extension-config.js +209 -66
  38. package/lib/grid-register.js +24 -21
  39. package/lib/main.js +139 -68
  40. package/lib/plugin-config.js +32 -2
  41. package/lib/schema/appium-config-schema.js +286 -0
  42. package/lib/schema/arg-spec.js +218 -0
  43. package/lib/schema/cli-args.js +273 -0
  44. package/lib/schema/cli-transformers.js +123 -0
  45. package/lib/schema/index.js +2 -0
  46. package/lib/schema/keywords.js +119 -0
  47. package/lib/schema/schema.js +577 -0
  48. package/lib/utils.js +29 -52
  49. package/package.json +17 -11
  50. package/types/appium-config.d.ts +197 -0
  51. package/types/types.d.ts +201 -0
  52. package/LICENSE +0 -201
  53. package/build/lib/cli/parser-helpers.js +0 -106
  54. package/lib/cli/parser-helpers.js +0 -106
package/lib/appium.js CHANGED
@@ -2,10 +2,10 @@ import _ from 'lodash';
2
2
  import log from './logger';
3
3
  import { getBuildInfo, updateBuildInfo, APPIUM_VER } from './config';
4
4
  import { findMatchingDriver } from './drivers';
5
- import { BaseDriver, errors, isSessionCommand } from '@appium/base-driver';
6
- import B from 'bluebird';
5
+ import { BaseDriver, errors, isSessionCommand,
6
+ CREATE_SESSION_COMMAND } from '@appium/base-driver';
7
7
  import AsyncLock from 'async-lock';
8
- import { getExtensionArgs, parseCapsForInnerDriver, pullSettings, validateExtensionArgs } from './utils';
8
+ import { parseCapsForInnerDriver, pullSettings } from './utils';
9
9
  import { util } from '@appium/support';
10
10
 
11
11
  const desiredCapabilityConstraints = {
@@ -39,7 +39,7 @@ class AppiumDriver extends BaseDriver {
39
39
  // the main Appium Driver has no new command timeout
40
40
  this.newCommandTimeoutMs = 0;
41
41
 
42
- this.args = Object.assign({}, args);
42
+ this.args = {...args};
43
43
 
44
44
  // Access to sessions list must be guarded with a Semaphore, because
45
45
  // it might be changed by other async calls at any time
@@ -59,6 +59,9 @@ class AppiumDriver extends BaseDriver {
59
59
  updateBuildInfo();
60
60
  }
61
61
 
62
+ /** @type {DriverConfig|undefined} */
63
+ driverConfig;
64
+
62
65
  /**
63
66
  * Cancel commands queueing for the umbrella Appium driver
64
67
  */
@@ -113,21 +116,16 @@ class AppiumDriver extends BaseDriver {
113
116
 
114
117
  /**
115
118
  * Validate and assign CLI args for a driver or plugin
116
- * @param {string} extType 'driver' or 'plugin'
119
+ *
120
+ * If the extension has provided a schema, validation has already happened.
121
+ * @param {import('./ext-config-io').ExtensionType} extType 'driver' or 'plugin'
117
122
  * @param {string} extName the name of the extension
118
- * @param {ObjectConstructor} extClass the class of the extension
119
123
  * @param {Object} extInstance the driver or plugin instance
120
124
  */
121
- assignCliArgsToExtension (extType, extName, extClass, extInstance) {
122
- const cliArgs = getExtensionArgs(this.args[`${extType}Args`], extName);
125
+ assignCliArgsToExtension (extType, extName, extInstance) {
126
+ const cliArgs = this.args[extType]?.[extName];
123
127
  if (!_.isEmpty(cliArgs)) {
124
- if (!_.has(extClass, 'argsConstraints')) {
125
- throw new Error(`You sent in CLI args for the ${extName} ${extType}, but it ` +
126
- `does not define any`);
127
- }
128
- validateExtensionArgs(cliArgs, extClass.argsConstraints);
129
128
  extInstance.cliArgs = cliArgs;
130
- log.debug(`Set CLI arguments on ${extName} ${extType} instance: ${JSON.stringify(cliArgs)}`);
131
129
  }
132
130
  }
133
131
 
@@ -213,7 +211,7 @@ class AppiumDriver extends BaseDriver {
213
211
 
214
212
  // Likewise, any driver-specific CLI args that were passed in should be assigned directly to
215
213
  // the driver so that they cannot be mimicked by a malicious user sending in capabilities
216
- this.assignCliArgsToExtension('driver', driverName, InnerDriver, driverInstance);
214
+ this.assignCliArgsToExtension('driver', driverName, driverInstance);
217
215
 
218
216
 
219
217
  // This assignment is required for correct web sockets functionality inside the driver
@@ -278,34 +276,31 @@ class AppiumDriver extends BaseDriver {
278
276
  }
279
277
 
280
278
  attachUnexpectedShutdownHandler (driver, innerSessionId) {
281
- const removeSessionFromMasterList = (cause = new Error('Unknown error')) => {
282
- log.warn(`Closing session, cause was '${cause.message}'`);
279
+ const onShutdown = (cause = new Error('Unknown error')) => {
280
+ log.warn(`Ending session, cause was '${cause.message}'`);
281
+
282
+ if (this.sessionPlugins[innerSessionId]) {
283
+ for (const plugin of this.sessionPlugins[innerSessionId]) {
284
+ if (_.isFunction(plugin.onUnexpectedShutdown)) {
285
+ log.debug(`Plugin ${plugin.name} defines an unexpected shutdown handler; calling it now`);
286
+ try {
287
+ plugin.onUnexpectedShutdown(driver, cause);
288
+ } catch (e) {
289
+ log.warn(`Got an error when running plugin ${plugin.name} shutdown handler: ${e}`);
290
+ }
291
+ } else {
292
+ log.debug(`Plugin ${plugin.name} does not define an unexpected shutdown handler`);
293
+ }
294
+ }
295
+ }
296
+
283
297
  log.info(`Removing session '${innerSessionId}' from our master session list`);
284
298
  delete this.sessions[innerSessionId];
285
299
  delete this.sessionPlugins[innerSessionId];
286
300
  };
287
301
 
288
- // eslint-disable-next-line promise/prefer-await-to-then
289
- if (_.isFunction((driver.onUnexpectedShutdown || {}).then)) {
290
- // TODO: Remove this block after all the drivers use base driver above v 5.0.0
291
- // Remove the session on unexpected shutdown, so that we are in a position
292
- // to open another session later on.
293
- driver.onUnexpectedShutdown
294
- // eslint-disable-next-line promise/prefer-await-to-then
295
- .then(() => {
296
- // if we get here, we've had an unexpected shutdown, so error
297
- throw new Error('Unexpected shutdown');
298
- })
299
- .catch((e) => {
300
- // if we cancelled the unexpected shutdown promise, that means we
301
- // no longer care about it, and can safely ignore it
302
- if (!(e instanceof B.CancellationError)) {
303
- removeSessionFromMasterList(e);
304
- }
305
- }); // this is a cancellable promise
306
- } else if (_.isFunction(driver.onUnexpectedShutdown)) {
307
- // since base driver v 5.0.0
308
- driver.onUnexpectedShutdown(removeSessionFromMasterList);
302
+ if (_.isFunction(driver.onUnexpectedShutdown)) {
303
+ driver.onUnexpectedShutdown(onShutdown);
309
304
  } else {
310
305
  log.warn(`Failed to attach the unexpected shutdown listener. ` +
311
306
  `Is 'onUnexpectedShutdown' method available for '${driver.constructor.name}'?`);
@@ -393,19 +388,17 @@ class AppiumDriver extends BaseDriver {
393
388
  * @returns {Array} - array of plugin instances
394
389
  */
395
390
  pluginsForSession (sessionId = null) {
396
- let plugins = [];
397
391
  if (sessionId) {
398
392
  if (!this.sessionPlugins[sessionId]) {
399
393
  this.sessionPlugins[sessionId] = this.createPluginInstances();
400
394
  }
401
- plugins = this.sessionPlugins[sessionId];
402
- } else {
403
- if (_.isEmpty(this.sessionlessPlugins)) {
404
- this.sessionlessPlugins = this.createPluginInstances();
405
- }
406
- plugins = this.sessionlessPlugins;
395
+ return this.sessionPlugins[sessionId];
396
+ }
397
+
398
+ if (_.isEmpty(this.sessionlessPlugins)) {
399
+ this.sessionlessPlugins = this.createPluginInstances();
407
400
  }
408
- return plugins;
401
+ return this.sessionlessPlugins;
409
402
  }
410
403
 
411
404
  /**
@@ -425,16 +418,11 @@ class AppiumDriver extends BaseDriver {
425
418
  .filter((p) => _.isFunction(p[cmd]) || _.isFunction(p.handle));
426
419
  }
427
420
 
428
- createPluginInstances (sessionId) {
429
- if (!sessionId) {
430
- sessionId = 'sessionless';
431
- }
432
- sessionId = _.truncate(sessionId, {length: 11});
421
+ createPluginInstances () {
433
422
  return this.pluginClasses.map((PluginClass) => {
434
- const generalName = PluginClass.pluginName;
435
- const name = `${generalName} (${sessionId})`;
423
+ const name = PluginClass.pluginName;
436
424
  const plugin = new PluginClass(name);
437
- this.assignCliArgsToExtension('plugin', generalName, PluginClass, plugin);
425
+ this.assignCliArgsToExtension('plugin', name, plugin);
438
426
  return plugin;
439
427
  });
440
428
  }
@@ -453,6 +441,15 @@ class AppiumDriver extends BaseDriver {
453
441
  const isUmbrellaCmd = !isGetStatus && isAppiumDriverCommand(cmd);
454
442
  const isSessionCmd = !isGetStatus && !isUmbrellaCmd;
455
443
 
444
+ // if a plugin override proxying for this command and that is why we are here instead of just
445
+ // letting the protocol proxy the command entirely, determine that, get the request object for
446
+ // use later on, then clean up the args
447
+ const reqForProxy = _.last(args)?.reqForProxy;
448
+ if (reqForProxy) {
449
+ args.pop();
450
+ }
451
+
452
+
456
453
  // first do some error checking. If we're requesting a session command execution, then make
457
454
  // sure that session actually exists on the session driver, and set the session driver itself
458
455
  let sessionId = null;
@@ -494,6 +491,18 @@ class AppiumDriver extends BaseDriver {
494
491
  // if we make it here, we know that the default behavior is handled
495
492
  cmdHandledBy.default = true;
496
493
 
494
+ if (reqForProxy) {
495
+ // we would have proxied this command had a plugin not handled it, so the default behavior
496
+ // is to do the proxy and retrieve the result internally so it can be passed to the plugin
497
+ // in case it calls 'await next()'. This requires that the driver have defined
498
+ // 'proxyCommand' and not just 'proxyReqRes'.
499
+ if (!dstSession.proxyCommand) {
500
+ throw new NoDriverProxyCommandError();
501
+ }
502
+ return await dstSession.proxyCommand(reqForProxy.originalUrl, reqForProxy.method,
503
+ reqForProxy.body);
504
+ }
505
+
497
506
  if (isGetStatus) {
498
507
  return await this.getStatus();
499
508
  }
@@ -516,7 +525,18 @@ class AppiumDriver extends BaseDriver {
516
525
 
517
526
  // if we had plugins, make sure to log out the helpful report about which plugins ended up
518
527
  // handling the command and which didn't
519
- plugins.length && this.logPluginHandlerReport({cmd, cmdHandledBy});
528
+ this.logPluginHandlerReport(plugins, {cmd, cmdHandledBy});
529
+
530
+ // And finally, if the command was createSession, we want to migrate any plugins which were
531
+ // previously sessionless to use the new sessionId, so that plugins can share state between
532
+ // their createSession method and other instance methods
533
+ if (cmd === CREATE_SESSION_COMMAND && this.sessionlessPlugins.length && !res.error) {
534
+ const sessionId = _.first(res.value);
535
+ log.info(`Promoting ${this.sessionlessPlugins.length} sessionless plugins to be attached ` +
536
+ `to session ID ${sessionId}`);
537
+ this.sessionPlugins[sessionId] = this.sessionlessPlugins;
538
+ this.sessionlessPlugins = [];
539
+ }
520
540
 
521
541
  return res;
522
542
  }
@@ -546,7 +566,11 @@ class AppiumDriver extends BaseDriver {
546
566
  return next;
547
567
  }
548
568
 
549
- logPluginHandlerReport ({cmd, cmdHandledBy}) {
569
+ logPluginHandlerReport (plugins, {cmd, cmdHandledBy}) {
570
+ if (!plugins.length) {
571
+ return;
572
+ }
573
+
550
574
  // at the end of the day, we have an object representing which plugins ended up getting
551
575
  // their code run as part of handling this command. Because plugins can choose *not* to
552
576
  // pass control to other plugins or to the default driver behavior, this is information
@@ -556,7 +580,7 @@ class AppiumDriver extends BaseDriver {
556
580
  const didHandle = Object.keys(cmdHandledBy).filter((k) => cmdHandledBy[k]);
557
581
  const didntHandle = Object.keys(cmdHandledBy).filter((k) => !cmdHandledBy[k]);
558
582
  if (didntHandle.length > 0) {
559
- log.info(`Command '${cmd}' was not handled by the following beahviors or plugins, even ` +
583
+ log.info(`Command '${cmd}' was *not* handled by the following behaviours or plugins, even ` +
560
584
  `though they were registered to handle it: ${JSON.stringify(didntHandle)}. The ` +
561
585
  `command *was* handled by these: ${JSON.stringify(didHandle)}.`);
562
586
  }
@@ -586,24 +610,6 @@ class AppiumDriver extends BaseDriver {
586
610
  return res;
587
611
  }
588
612
 
589
- // We want to override basedriver's proxy avoidance detection since plugins might want to avoid
590
- // proxying in order to handle a command, so we use this function to make an extra check whether
591
- // plugins might want to do this.
592
- proxyRouteIsAvoided (sessionId, method, url, body) {
593
- if (super.proxyRouteIsAvoided(sessionId, method, url, body)) {
594
- return true;
595
- }
596
- // if even just one plugin needs to avoid proxying, do so
597
- for (const plugin of this.pluginsForSession(sessionId)) {
598
- if (plugin.shouldAvoidProxy?.(method, url, body)) {
599
- log.info(`Request to ${url} would normally be proxied, but plugin ${plugin.name} wants ` +
600
- `to handle it internally. We'll avoid proxying in this case.`);
601
- return true;
602
- }
603
- }
604
- return false;
605
- }
606
-
607
613
  proxyActive (sessionId) {
608
614
  const dstSession = this.sessions[sessionId];
609
615
  return dstSession && _.isFunction(dstSession.proxyActive) && dstSession.proxyActive(sessionId);
@@ -626,4 +632,22 @@ function isAppiumDriverCommand (cmd) {
626
632
  return !isSessionCommand(cmd) || cmd === 'deleteSession';
627
633
  }
628
634
 
635
+ /**
636
+ * Thrown when Appium tried to proxy a command using a driver's `proxyCommand` method but the
637
+ * method did not exist
638
+ */
639
+ export class NoDriverProxyCommandError extends Error {
640
+ /**
641
+ * @type {Readonly<string>}
642
+ */
643
+ code = 'APPIUMERR_NO_DRIVER_PROXYCOMMAND';
644
+
645
+ constructor () {
646
+ super(`The default behavior for this command was to proxy, but the driver ` +
647
+ `did not have the 'proxyCommand' method defined. To fully support ` +
648
+ `plugins, drivers should have 'proxyCommand' set to a jwpProxy object's ` +
649
+ `'command()' method, in addition to the normal 'proxyReqRes'`);
650
+ }
651
+ }
652
+
629
653
  export { AppiumDriver };