appium 2.0.0-beta.3 → 2.0.0-beta.30

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 (139) hide show
  1. package/README.md +10 -11
  2. package/build/lib/appium.d.ts +215 -0
  3. package/build/lib/appium.d.ts.map +1 -0
  4. package/build/lib/appium.js +238 -132
  5. package/build/lib/cli/args.d.ts +20 -0
  6. package/build/lib/cli/args.d.ts.map +1 -0
  7. package/build/lib/cli/args.js +96 -282
  8. package/build/lib/cli/driver-command.d.ts +36 -0
  9. package/build/lib/cli/driver-command.d.ts.map +1 -0
  10. package/build/lib/cli/driver-command.js +19 -12
  11. package/build/lib/cli/extension-command.d.ts +345 -0
  12. package/build/lib/cli/extension-command.d.ts.map +1 -0
  13. package/build/lib/cli/extension-command.js +171 -96
  14. package/build/lib/cli/extension.d.ts +14 -0
  15. package/build/lib/cli/extension.d.ts.map +1 -0
  16. package/build/lib/cli/extension.js +31 -16
  17. package/build/lib/cli/parser.d.ts +79 -0
  18. package/build/lib/cli/parser.d.ts.map +1 -0
  19. package/build/lib/cli/parser.js +152 -95
  20. package/build/lib/cli/plugin-command.d.ts +39 -0
  21. package/build/lib/cli/plugin-command.d.ts.map +1 -0
  22. package/build/lib/cli/plugin-command.js +18 -13
  23. package/build/lib/cli/utils.d.ts +29 -0
  24. package/build/lib/cli/utils.d.ts.map +1 -0
  25. package/build/lib/cli/utils.js +27 -3
  26. package/build/lib/config-file.d.ts +100 -0
  27. package/build/lib/config-file.d.ts.map +1 -0
  28. package/build/lib/config-file.js +136 -0
  29. package/build/lib/config.d.ts +40 -0
  30. package/build/lib/config.d.ts.map +1 -0
  31. package/build/lib/config.js +92 -67
  32. package/build/lib/constants.d.ts +48 -0
  33. package/build/lib/constants.d.ts.map +1 -0
  34. package/build/lib/constants.js +60 -0
  35. package/build/lib/extension/driver-config.d.ts +84 -0
  36. package/build/lib/extension/driver-config.d.ts.map +1 -0
  37. package/build/lib/extension/driver-config.js +190 -0
  38. package/build/lib/extension/extension-config.d.ts +170 -0
  39. package/build/lib/extension/extension-config.d.ts.map +1 -0
  40. package/build/lib/extension/extension-config.js +297 -0
  41. package/build/lib/extension/index.d.ts +39 -0
  42. package/build/lib/extension/index.d.ts.map +1 -0
  43. package/build/lib/extension/index.js +77 -0
  44. package/build/lib/extension/manifest.d.ts +174 -0
  45. package/build/lib/extension/manifest.d.ts.map +1 -0
  46. package/build/lib/extension/manifest.js +246 -0
  47. package/build/lib/extension/package-changed.d.ts +11 -0
  48. package/build/lib/extension/package-changed.d.ts.map +1 -0
  49. package/build/lib/extension/package-changed.js +68 -0
  50. package/build/lib/extension/plugin-config.d.ts +62 -0
  51. package/build/lib/extension/plugin-config.d.ts.map +1 -0
  52. package/build/lib/extension/plugin-config.js +87 -0
  53. package/build/lib/grid-register.d.ts +10 -0
  54. package/build/lib/grid-register.d.ts.map +1 -0
  55. package/build/lib/grid-register.js +21 -25
  56. package/build/lib/logger.d.ts +3 -0
  57. package/build/lib/logger.d.ts.map +1 -0
  58. package/build/lib/logger.js +4 -6
  59. package/build/lib/logsink.d.ts +4 -0
  60. package/build/lib/logsink.d.ts.map +1 -0
  61. package/build/lib/logsink.js +12 -16
  62. package/build/lib/main.d.ts +51 -0
  63. package/build/lib/main.d.ts.map +1 -0
  64. package/build/lib/main.js +174 -82
  65. package/build/lib/schema/arg-spec.d.ts +143 -0
  66. package/build/lib/schema/arg-spec.d.ts.map +1 -0
  67. package/build/lib/schema/arg-spec.js +119 -0
  68. package/build/lib/schema/cli-args.d.ts +19 -0
  69. package/build/lib/schema/cli-args.d.ts.map +1 -0
  70. package/build/lib/schema/cli-args.js +180 -0
  71. package/build/lib/schema/cli-transformers.d.ts +5 -0
  72. package/build/lib/schema/cli-transformers.d.ts.map +1 -0
  73. package/build/lib/schema/cli-transformers.js +74 -0
  74. package/build/lib/schema/index.d.ts +3 -0
  75. package/build/lib/schema/index.d.ts.map +1 -0
  76. package/build/lib/schema/index.js +34 -0
  77. package/build/lib/schema/keywords.d.ts +24 -0
  78. package/build/lib/schema/keywords.d.ts.map +1 -0
  79. package/build/lib/schema/keywords.js +70 -0
  80. package/build/lib/schema/schema.d.ts +259 -0
  81. package/build/lib/schema/schema.d.ts.map +1 -0
  82. package/build/lib/schema/schema.js +452 -0
  83. package/build/lib/utils.d.ts +66 -0
  84. package/build/lib/utils.d.ts.map +1 -0
  85. package/build/lib/utils.js +35 -139
  86. package/build/tsconfig.tsbuildinfo +1 -0
  87. package/index.js +11 -0
  88. package/lib/appium-config.schema.json +278 -0
  89. package/lib/appium.js +398 -155
  90. package/lib/cli/args.js +174 -377
  91. package/lib/cli/driver-command.js +22 -7
  92. package/lib/cli/extension-command.js +372 -177
  93. package/lib/cli/extension.js +32 -10
  94. package/lib/cli/parser.js +252 -83
  95. package/lib/cli/plugin-command.js +19 -6
  96. package/lib/cli/utils.js +22 -2
  97. package/lib/config-file.js +223 -0
  98. package/lib/config.js +169 -69
  99. package/lib/constants.js +78 -0
  100. package/lib/extension/driver-config.js +249 -0
  101. package/lib/extension/extension-config.js +458 -0
  102. package/lib/extension/index.js +102 -0
  103. package/lib/extension/manifest.js +486 -0
  104. package/lib/extension/package-changed.js +63 -0
  105. package/lib/extension/plugin-config.js +113 -0
  106. package/lib/grid-register.js +25 -22
  107. package/lib/logger.js +1 -1
  108. package/lib/logsink.js +14 -7
  109. package/lib/main.js +233 -83
  110. package/lib/schema/arg-spec.js +232 -0
  111. package/lib/schema/cli-args.js +261 -0
  112. package/lib/schema/cli-transformers.js +122 -0
  113. package/lib/schema/index.js +2 -0
  114. package/lib/schema/keywords.js +134 -0
  115. package/lib/schema/schema.js +734 -0
  116. package/lib/utils.js +85 -129
  117. package/package.json +62 -85
  118. package/scripts/postinstall.js +71 -0
  119. package/types/appium-manifest.d.ts +61 -0
  120. package/types/cli.d.ts +134 -0
  121. package/types/extension.d.ts +56 -0
  122. package/types/external-manifest.d.ts +58 -0
  123. package/types/index.d.ts +7 -0
  124. package/CHANGELOG.md +0 -3515
  125. package/bin/ios-webkit-debug-proxy-launcher.js +0 -71
  126. package/build/lib/cli/npm.js +0 -206
  127. package/build/lib/cli/parser-helpers.js +0 -82
  128. package/build/lib/driver-config.js +0 -77
  129. package/build/lib/drivers.js +0 -96
  130. package/build/lib/extension-config.js +0 -251
  131. package/build/lib/plugin-config.js +0 -59
  132. package/build/lib/plugins.js +0 -14
  133. package/lib/cli/npm.js +0 -183
  134. package/lib/cli/parser-helpers.js +0 -79
  135. package/lib/driver-config.js +0 -46
  136. package/lib/drivers.js +0 -81
  137. package/lib/extension-config.js +0 -208
  138. package/lib/plugin-config.js +0 -34
  139. package/lib/plugins.js +0 -10
package/lib/appium.js CHANGED
@@ -1,14 +1,19 @@
1
+ /* eslint-disable no-unused-vars */
1
2
  import _ from 'lodash';
2
- import log from './logger';
3
3
  import { getBuildInfo, updateBuildInfo, APPIUM_VER } from './config';
4
- import { findMatchingDriver } from './drivers';
5
- import { BaseDriver, errors, isSessionCommand } from 'appium-base-driver';
6
- import B from 'bluebird';
4
+ import { BaseDriver, DriverCore, errors, isSessionCommand,
5
+ CREATE_SESSION_COMMAND, DELETE_SESSION_COMMAND, GET_STATUS_COMMAND
6
+ } from '@appium/base-driver';
7
7
  import AsyncLock from 'async-lock';
8
8
  import { parseCapsForInnerDriver, pullSettings } from './utils';
9
- import { util } from 'appium-support';
10
-
11
- const desiredCapabilityConstraints = {
9
+ import { util, node, logger } from '@appium/support';
10
+ import { getDefaultsForExtension } from './schema';
11
+
12
+ /**
13
+ * Invariant set of base constraints
14
+ * @type {Readonly<Constraints>}
15
+ */
16
+ const desiredCapabilityConstraints = Object.freeze({
12
17
  automationName: {
13
18
  presence: true,
14
19
  isString: true,
@@ -17,44 +22,97 @@ const desiredCapabilityConstraints = {
17
22
  presence: true,
18
23
  isString: true,
19
24
  },
20
- };
25
+ });
21
26
 
22
27
  const sessionsListGuard = new AsyncLock();
23
28
  const pendingDriversGuard = new AsyncLock();
24
29
 
25
- class AppiumDriver extends BaseDriver {
26
- constructor (args) {
30
+ /**
31
+ * @implements {SessionHandler}
32
+ */
33
+ class AppiumDriver extends DriverCore {
34
+ /**
35
+ * Access to sessions list must be guarded with a Semaphore, because
36
+ * it might be changed by other async calls at any time
37
+ * It is not recommended to access this property directly from the outside
38
+ * @type {Record<string,ExternalDriver>}
39
+ */
40
+ sessions = {};
41
+
42
+ /**
43
+ * Access to pending drivers list must be guarded with a Semaphore, because
44
+ * it might be changed by other async calls at any time
45
+ * It is not recommended to access this property directly from the outside
46
+ * @type {Record<string,ExternalDriver[]>}
47
+ */
48
+ pendingDrivers = {};
49
+
50
+ /**
51
+ * Note that {@linkcode AppiumDriver} has no `newCommandTimeout` method.
52
+ * `AppiumDriver` does not set and observe its own timeouts; individual
53
+ * sessions (managed drivers) do instead.
54
+ */
55
+ newCommandTimeoutMs = 0;
56
+
57
+ /**
58
+ * List of active plugins
59
+ * @type {PluginClass[]}
60
+ */
61
+ pluginClasses = [];
62
+
63
+ /**
64
+ * map of sessions to actual plugin instances per session
65
+ * @type {Record<string,InstanceType<PluginClass>[]>}
66
+ */
67
+ sessionPlugins = {};
68
+
69
+ /**
70
+ * some commands are sessionless, so we need a set of plugins for them
71
+ * @type {InstanceType<PluginClass>[]}
72
+ */
73
+ sessionlessPlugins = [];;
74
+
75
+ /** @type {DriverConfig} */
76
+ driverConfig;
77
+
78
+ /** @type {AppiumServer} */
79
+ server;
80
+
81
+ /**
82
+ * @param {DriverOpts} opts
83
+ */
84
+ constructor (opts) {
27
85
  // It is necessary to set `--tmp` here since it should be set to
28
86
  // process.env.APPIUM_TMP_DIR once at an initial point in the Appium lifecycle.
29
87
  // The process argument will be referenced by BaseDriver.
30
- // Please call appium-support.tempDir module to apply this benefit.
31
- if (args.tmpDir) {
32
- process.env.APPIUM_TMP_DIR = args.tmpDir;
88
+ // Please call @appium/support.tempDir module to apply this benefit.
89
+ if (opts.tmpDir) {
90
+ process.env.APPIUM_TMP_DIR = opts.tmpDir;
33
91
  }
34
92
 
35
- super(args);
93
+ super(opts);
36
94
 
37
95
  this.desiredCapConstraints = desiredCapabilityConstraints;
38
96
 
39
- // the main Appium Driver has no new command timeout
40
- this.newCommandTimeoutMs = 0;
41
-
42
- this.args = Object.assign({}, args);
43
-
44
- // Access to sessions list must be guarded with a Semaphore, because
45
- // it might be changed by other async calls at any time
46
- // It is not recommended to access this property directly from the outside
47
- this.sessions = {};
48
-
49
- // Access to pending drivers list must be guarded with a Semaphore, because
50
- // it might be changed by other async calls at any time
51
- // It is not recommended to access this property directly from the outside
52
- this.pendingDrivers = {};
53
-
54
- this.plugins = [];
97
+ this.args = {...opts};
55
98
 
56
99
  // allow this to happen in the background, so no `await`
57
- updateBuildInfo();
100
+ // catch this to avoid an unhandled rejection
101
+ // eslint-disable-next-line promise/prefer-await-to-then,promise/prefer-await-to-callbacks
102
+ updateBuildInfo().catch((err) => {
103
+ this.log.debug(err);
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Retrieves logger instance for the current umbrella driver instance
109
+ */
110
+ get log () {
111
+ if (!this._log) {
112
+ const instanceName = `${this.constructor.name}@${node.getObjectId(this).substring(0, 4)}`;
113
+ this._log = logger.getLogger(instanceName);
114
+ }
115
+ return this._log;
58
116
  }
59
117
 
60
118
  /**
@@ -79,47 +137,72 @@ class AppiumDriver extends BaseDriver {
79
137
  };
80
138
  }
81
139
 
82
- async getSessions () {
83
- const sessions = await sessionsListGuard.acquire(AppiumDriver.name, () => this.sessions);
84
- return _.toPairs(sessions)
140
+ async getSessions () { // eslint-disable-line require-await
141
+ return _.toPairs(this.sessions)
85
142
  .map(([id, driver]) => ({id, capabilities: driver.caps}));
86
143
  }
87
144
 
88
- printNewSessionAnnouncement (driverName, driverVersion) {
89
- const introString = driverVersion
145
+ printNewSessionAnnouncement (driverName, driverVersion, driverBaseVersion) {
146
+ this.log.info(driverVersion
90
147
  ? `Appium v${APPIUM_VER} creating new ${driverName} (v${driverVersion}) session`
91
- : `Appium v${APPIUM_VER} creating new ${driverName} session`;
92
- log.info(introString);
148
+ : `Appium v${APPIUM_VER} creating new ${driverName} session`
149
+ );
150
+ this.log.info(`Checking BaseDriver versions for Appium and ${driverName}`);
151
+ this.log.info(AppiumDriver.baseVersion
152
+ ? `Appium's BaseDriver version is ${AppiumDriver.baseVersion}`
153
+ : `Could not determine Appium's BaseDriver version`
154
+ );
155
+ this.log.info(driverBaseVersion
156
+ ? `${driverName}'s BaseDriver version is ${driverBaseVersion}`
157
+ : `Could not determine ${driverName}'s BaseDriver version`
158
+ );
93
159
  }
94
160
 
95
161
  /**
96
- * This is just an alias for driver.js's method, which is necessary for
97
- * mocking in the test suite
162
+ * Validate and assign CLI args for a driver or plugin
163
+ *
164
+ * If the extension has provided a schema, validation has already happened.
165
+ *
166
+ * Any arg which is equal to its default value will not be assigned to the extension.
167
+ * @param {ExtensionType} extType 'driver' or 'plugin'
168
+ * @param {string} extName the name of the extension
169
+ * @param {Object} extInstance the driver or plugin instance
98
170
  */
99
- _findMatchingDriver (...args) {
100
- return findMatchingDriver(...args);
171
+ assignCliArgsToExtension (extType, extName, extInstance) {
172
+ const allCliArgsForExt = /** @type {Record<string,unknown>|undefined} */(this.args[extType]?.[extName]);
173
+ if (!_.isEmpty(allCliArgsForExt)) {
174
+ const defaults = getDefaultsForExtension(extType, extName);
175
+ const cliArgs = _.isEmpty(defaults)
176
+ ? allCliArgsForExt
177
+ : _.omitBy(allCliArgsForExt, (value, key) => _.isEqual(defaults[key], value));
178
+ if (!_.isEmpty(cliArgs)) {
179
+ extInstance.cliArgs = cliArgs;
180
+ }
181
+ }
101
182
  }
102
183
 
103
184
  /**
104
185
  * Create a new session
105
- * @param {Object} jsonwpCaps JSONWP formatted desired capabilities
106
- * @param {Object} reqCaps Required capabilities (JSONWP standard)
107
- * @param {Object} w3cCapabilities W3C capabilities
108
- * @return {Array} Unique session ID and capabilities
186
+ * @param {W3CCapabilities} jsonwpCaps JSONWP formatted desired capabilities
187
+ * @param {W3CCapabilities} reqCaps Required capabilities (JSONWP standard)
188
+ * @param {W3CCapabilities} w3cCapabilities W3C capabilities
189
+ * @param {import('@appium/types').DriverData[]} [driverData]
109
190
  */
110
- async createSession (jsonwpCaps, reqCaps, w3cCapabilities) {
191
+ async createSession (jsonwpCaps, reqCaps, w3cCapabilities, driverData) {
111
192
  const defaultCapabilities = _.cloneDeep(this.args.defaultCapabilities);
112
193
  const defaultSettings = pullSettings(defaultCapabilities);
113
194
  jsonwpCaps = _.cloneDeep(jsonwpCaps);
114
- const jwpSettings = Object.assign({}, defaultSettings, pullSettings(jsonwpCaps));
195
+ const jwpSettings = {...defaultSettings, ...pullSettings(jsonwpCaps)};
115
196
  w3cCapabilities = _.cloneDeep(w3cCapabilities);
116
197
  // It is possible that the client only provides caps using JSONWP standard,
117
198
  // although firstMatch/alwaysMatch properties are still present.
118
199
  // In such case we assume the client understands W3C protocol and merge the given
119
200
  // JSONWP caps to W3C caps
120
- const w3cSettings = Object.assign({}, jwpSettings);
121
- Object.assign(w3cSettings, pullSettings((w3cCapabilities || {}).alwaysMatch || {}));
122
- for (const firstMatchEntry of ((w3cCapabilities || {}).firstMatch || [])) {
201
+ const w3cSettings = {
202
+ ...jwpSettings,
203
+ ...pullSettings((w3cCapabilities ?? {}).alwaysMatch ?? {})
204
+ };
205
+ for (const firstMatchEntry of ((w3cCapabilities ?? {}).firstMatch ?? [])) {
123
206
  Object.assign(w3cSettings, pullSettings(firstMatchEntry));
124
207
  }
125
208
 
@@ -134,9 +217,9 @@ class AppiumDriver extends BaseDriver {
134
217
  defaultCapabilities
135
218
  );
136
219
 
137
- const {desiredCaps, processedJsonwpCapabilities, processedW3CCapabilities, error} = parsedCaps;
220
+ const {desiredCaps, processedJsonwpCapabilities, processedW3CCapabilities} = /** @type {import('./utils').ParsedDriverCaps} */(parsedCaps);
138
221
  protocol = parsedCaps.protocol;
139
-
222
+ const error = /** @type {import('./utils').InvalidCaps} */(parsedCaps).error;
140
223
  // If the parsing of the caps produced an error, throw it in here
141
224
  if (error) {
142
225
  throw error;
@@ -144,87 +227,101 @@ class AppiumDriver extends BaseDriver {
144
227
 
145
228
  const {
146
229
  driver: InnerDriver,
147
- version: driverVersion
148
- } = this._findMatchingDriver(this.driverConfig, desiredCaps);
149
- this.printNewSessionAnnouncement(InnerDriver.name, driverVersion);
230
+ version: driverVersion,
231
+ driverName
232
+ } = this.driverConfig.findMatchingDriver(desiredCaps);
233
+ this.printNewSessionAnnouncement(InnerDriver.name, driverVersion, InnerDriver.baseVersion);
150
234
 
151
235
  if (this.args.sessionOverride) {
152
236
  await this.deleteAllSessions();
153
237
  }
154
238
 
155
- let runningDriversData, otherPendingDriversData;
156
- const d = new InnerDriver(this.args);
239
+ /**
240
+ * @type {DriverData[]}
241
+ */
242
+ let runningDriversData = [];
243
+ /**
244
+ * @type {DriverData[]}
245
+ */
246
+ let otherPendingDriversData = [];
247
+
248
+ const driverInstance = new InnerDriver(this.args, true);
157
249
 
158
250
  // We want to assign security values directly on the driver. The driver
159
251
  // should not read security values from `this.opts` because those values
160
252
  // could have been set by a malicious user via capabilities, whereas we
161
253
  // want a guarantee the values were set by the appium server admin
162
254
  if (this.args.relaxedSecurityEnabled) {
163
- log.info(`Applying relaxed security to '${InnerDriver.name}' as per ` +
164
- `server command line argument. All insecure features will be ` +
165
- `enabled unless explicitly disabled by --deny-insecure`);
166
- d.relaxedSecurityEnabled = true;
255
+ this.log.info(`Applying relaxed security to '${InnerDriver.name}' as per ` +
256
+ `server command line argument. All insecure features will be ` +
257
+ `enabled unless explicitly disabled by --deny-insecure`);
258
+ driverInstance.relaxedSecurityEnabled = true;
167
259
  }
168
260
 
169
261
  if (!_.isEmpty(this.args.denyInsecure)) {
170
- log.info('Explicitly preventing use of insecure features:');
171
- this.args.denyInsecure.map((a) => log.info(` ${a}`));
172
- d.denyInsecure = this.args.denyInsecure;
262
+ this.log.info('Explicitly preventing use of insecure features:');
263
+ this.args.denyInsecure.map((a) => this.log.info(` ${a}`));
264
+ driverInstance.denyInsecure = this.args.denyInsecure;
173
265
  }
174
266
 
175
267
  if (!_.isEmpty(this.args.allowInsecure)) {
176
- log.info('Explicitly enabling use of insecure features:');
177
- this.args.allowInsecure.map((a) => log.info(` ${a}`));
178
- d.allowInsecure = this.args.allowInsecure;
268
+ this.log.info('Explicitly enabling use of insecure features:');
269
+ this.args.allowInsecure.map((a) => this.log.info(` ${a}`));
270
+ driverInstance.allowInsecure = this.args.allowInsecure;
179
271
  }
180
272
 
273
+ // Likewise, any driver-specific CLI args that were passed in should be assigned directly to
274
+ // the driver so that they cannot be mimicked by a malicious user sending in capabilities
275
+ this.assignCliArgsToExtension('driver', driverName, driverInstance);
276
+
277
+
181
278
  // This assignment is required for correct web sockets functionality inside the driver
182
- d.server = this.server;
279
+ // Drivers/plugins might also want to know where they are hosted
280
+ driverInstance.assignServer(this.server, this.args.address, this.args.port, this.args.basePath);
281
+
183
282
  try {
184
- runningDriversData = await this.curSessionDataForDriver(InnerDriver);
283
+ runningDriversData = await this.curSessionDataForDriver(InnerDriver) ?? [];
185
284
  } catch (e) {
186
285
  throw new errors.SessionNotCreatedError(e.message);
187
286
  }
188
287
  await pendingDriversGuard.acquire(AppiumDriver.name, () => {
189
288
  this.pendingDrivers[InnerDriver.name] = this.pendingDrivers[InnerDriver.name] || [];
190
- otherPendingDriversData = this.pendingDrivers[InnerDriver.name].map((drv) => drv.driverData);
191
- this.pendingDrivers[InnerDriver.name].push(d);
289
+ otherPendingDriversData = _.compact(this.pendingDrivers[InnerDriver.name].map((drv) => drv.driverData));
290
+ this.pendingDrivers[InnerDriver.name].push(driverInstance);
192
291
  });
193
292
 
194
293
  try {
195
- [innerSessionId, dCaps] = await d.createSession(
294
+ [innerSessionId, dCaps] = await driverInstance.createSession(
196
295
  processedJsonwpCapabilities,
197
296
  reqCaps,
198
297
  processedW3CCapabilities,
199
298
  [...runningDriversData, ...otherPendingDriversData]
200
299
  );
201
- protocol = d.protocol;
202
- await sessionsListGuard.acquire(AppiumDriver.name, () => {
203
- this.sessions[innerSessionId] = d;
204
- });
300
+ protocol = driverInstance.protocol;
301
+ this.sessions[innerSessionId] = driverInstance;
205
302
  } finally {
206
303
  await pendingDriversGuard.acquire(AppiumDriver.name, () => {
207
- _.pull(this.pendingDrivers[InnerDriver.name], d);
304
+ _.pull(this.pendingDrivers[InnerDriver.name], driverInstance);
208
305
  });
209
306
  }
210
307
 
211
- this.attachUnexpectedShutdownHandler(d, innerSessionId);
308
+ this.attachUnexpectedShutdownHandler(driverInstance, innerSessionId);
212
309
 
213
- log.info(`New ${InnerDriver.name} session created successfully, session ` +
214
- `${innerSessionId} added to master session list`);
310
+ this.log.info(`New ${InnerDriver.name} session created successfully, session ` +
311
+ `${innerSessionId} added to master session list`);
215
312
 
216
313
  // set the New Command Timeout for the inner driver
217
- d.startNewCommandTimeout();
314
+ driverInstance.startNewCommandTimeout();
218
315
 
219
316
  // apply initial values to Appium settings (if provided)
220
- if (d.isW3CProtocol() && !_.isEmpty(w3cSettings)) {
221
- log.info(`Applying the initial values to Appium settings parsed from W3C caps: ` +
317
+ if (driverInstance.isW3CProtocol() && !_.isEmpty(w3cSettings)) {
318
+ this.log.info(`Applying the initial values to Appium settings parsed from W3C caps: ` +
222
319
  JSON.stringify(w3cSettings));
223
- await d.updateSettings(w3cSettings);
224
- } else if (d.isMjsonwpProtocol() && !_.isEmpty(jwpSettings)) {
225
- log.info(`Applying the initial values to Appium settings parsed from MJSONWP caps: ` +
320
+ await driverInstance.updateSettings(w3cSettings);
321
+ } else if (driverInstance.isMjsonwpProtocol() && !_.isEmpty(jwpSettings)) {
322
+ this.log.info(`Applying the initial values to Appium settings parsed from MJSONWP caps: ` +
226
323
  JSON.stringify(jwpSettings));
227
- await d.updateSettings(jwpSettings);
324
+ await driverInstance.updateSettings(jwpSettings);
228
325
  }
229
326
  } catch (error) {
230
327
  return {
@@ -240,60 +337,63 @@ class AppiumDriver extends BaseDriver {
240
337
  }
241
338
 
242
339
  attachUnexpectedShutdownHandler (driver, innerSessionId) {
243
- const removeSessionFromMasterList = (cause = new Error('Unknown error')) => {
244
- log.warn(`Closing session, cause was '${cause.message}'`);
245
- log.info(`Removing session '${innerSessionId}' from our master session list`);
340
+ const onShutdown = (cause = new Error('Unknown error')) => {
341
+ this.log.warn(`Ending session, cause was '${cause.message}'`);
342
+
343
+ if (this.sessionPlugins[innerSessionId]) {
344
+ for (const plugin of this.sessionPlugins[innerSessionId]) {
345
+ if (_.isFunction(plugin.onUnexpectedShutdown)) {
346
+ this.log.debug(`Plugin ${plugin.name} defines an unexpected shutdown handler; calling it now`);
347
+ try {
348
+ plugin.onUnexpectedShutdown(driver, cause);
349
+ } catch (e) {
350
+ this.log.warn(`Got an error when running plugin ${plugin.name} shutdown handler: ${e}`);
351
+ }
352
+ } else {
353
+ this.log.debug(`Plugin ${plugin.name} does not define an unexpected shutdown handler`);
354
+ }
355
+ }
356
+ }
357
+
358
+ this.log.info(`Removing session '${innerSessionId}' from our master session list`);
246
359
  delete this.sessions[innerSessionId];
360
+ delete this.sessionPlugins[innerSessionId];
247
361
  };
248
362
 
249
- // eslint-disable-next-line promise/prefer-await-to-then
250
- if (_.isFunction((driver.onUnexpectedShutdown || {}).then)) {
251
- // TODO: Remove this block after all the drivers use base driver above v 5.0.0
252
- // Remove the session on unexpected shutdown, so that we are in a position
253
- // to open another session later on.
254
- driver.onUnexpectedShutdown
255
- // eslint-disable-next-line promise/prefer-await-to-then
256
- .then(() => {
257
- // if we get here, we've had an unexpected shutdown, so error
258
- throw new Error('Unexpected shutdown');
259
- })
260
- .catch((e) => {
261
- // if we cancelled the unexpected shutdown promise, that means we
262
- // no longer care about it, and can safely ignore it
263
- if (!(e instanceof B.CancellationError)) {
264
- removeSessionFromMasterList(e);
265
- }
266
- }); // this is a cancellable promise
267
- } else if (_.isFunction(driver.onUnexpectedShutdown)) {
268
- // since base driver v 5.0.0
269
- driver.onUnexpectedShutdown(removeSessionFromMasterList);
363
+ if (_.isFunction(driver.onUnexpectedShutdown)) {
364
+ driver.onUnexpectedShutdown(onShutdown);
270
365
  } else {
271
- log.warn(`Failed to attach the unexpected shutdown listener. ` +
366
+ this.log.warn(`Failed to attach the unexpected shutdown listener. ` +
272
367
  `Is 'onUnexpectedShutdown' method available for '${driver.constructor.name}'?`);
273
368
  }
274
369
  }
275
370
 
276
- async curSessionDataForDriver (InnerDriver) {
277
- const sessions = await sessionsListGuard.acquire(AppiumDriver.name, () => this.sessions);
278
- const data = _.values(sessions)
279
- .filter((s) => s.constructor.name === InnerDriver.name)
280
- .map((s) => s.driverData);
281
- for (let datum of data) {
371
+ /**
372
+ *
373
+ * @param {import('../types/extension').DriverClass} InnerDriver
374
+ * @returns {Promise<DriverData[]>}}
375
+ */
376
+ async curSessionDataForDriver (InnerDriver) { // eslint-disable-line require-await
377
+ const data = _.compact(_.values(this.sessions)
378
+ .filter((s) => s.constructor.name === InnerDriver.name)
379
+ .map((s) => s.driverData));
380
+ for (const datum of data) {
282
381
  if (!datum) {
283
382
  throw new Error(`Problem getting session data for driver type ` +
284
- `${InnerDriver.name}; does it implement 'get ` +
285
- `driverData'?`);
383
+ `${InnerDriver.name}; does it implement 'get driverData'?`);
286
384
  }
287
385
  }
288
386
  return data;
289
387
  }
290
388
 
389
+ /**
390
+ * @param {string} sessionId
391
+ */
291
392
  async deleteSession (sessionId) {
292
393
  let protocol;
293
394
  try {
294
- let otherSessionsData = null;
295
- let dstSession = null;
296
- await sessionsListGuard.acquire(AppiumDriver.name, () => {
395
+ let otherSessionsData;
396
+ const dstSession = await sessionsListGuard.acquire(AppiumDriver.name, () => {
297
397
  if (!this.sessions[sessionId]) {
298
398
  return;
299
399
  }
@@ -301,20 +401,27 @@ class AppiumDriver extends BaseDriver {
301
401
  otherSessionsData = _.toPairs(this.sessions)
302
402
  .filter(([key, value]) => value.constructor.name === curConstructorName && key !== sessionId)
303
403
  .map(([, value]) => value.driverData);
304
- dstSession = this.sessions[sessionId];
404
+ const dstSession = this.sessions[sessionId];
305
405
  protocol = dstSession.protocol;
306
- log.info(`Removing session ${sessionId} from our master session list`);
406
+ this.log.info(`Removing session ${sessionId} from our master session list`);
307
407
  // regardless of whether the deleteSession completes successfully or not
308
408
  // make the session unavailable, because who knows what state it might
309
409
  // be in otherwise
310
410
  delete this.sessions[sessionId];
411
+ delete this.sessionPlugins[sessionId];
412
+ return dstSession;
311
413
  });
414
+ // this may not be correct, but if `dstSession` was falsy, the call to `deleteSession()` would
415
+ // throw anyway.
416
+ if (!dstSession) {
417
+ throw new Error('Session not found');
418
+ }
312
419
  return {
313
420
  protocol,
314
421
  value: await dstSession.deleteSession(sessionId, otherSessionsData),
315
422
  };
316
423
  } catch (e) {
317
- log.error(`Had trouble ending session ${sessionId}: ${e.message}`);
424
+ this.log.error(`Had trouble ending session ${sessionId}: ${e.message}`);
318
425
  return {
319
426
  protocol,
320
427
  error: e,
@@ -325,7 +432,7 @@ class AppiumDriver extends BaseDriver {
325
432
  async deleteAllSessions (opts = {}) {
326
433
  const sessionsCount = _.size(this.sessions);
327
434
  if (0 === sessionsCount) {
328
- log.debug('There are no active sessions for cleanup');
435
+ this.log.debug('There are no active sessions for cleanup');
329
436
  return;
330
437
  }
331
438
 
@@ -333,7 +440,7 @@ class AppiumDriver extends BaseDriver {
333
440
  force = false,
334
441
  reason,
335
442
  } = opts;
336
- log.debug(`Cleaning up ${util.pluralize('active session', sessionsCount, true)}`);
443
+ this.log.debug(`Cleaning up ${util.pluralize('active session', sessionsCount, true)}`);
337
444
  const cleanupPromises = force
338
445
  ? _.values(this.sessions).map((drv) => drv.startUnexpectedShutdown(reason && new Error(reason)))
339
446
  : _.keys(this.sessions).map((id) => this.deleteSession(id));
@@ -341,18 +448,63 @@ class AppiumDriver extends BaseDriver {
341
448
  try {
342
449
  await cleanupPromise;
343
450
  } catch (e) {
344
- log.debug(e);
451
+ this.log.debug(e);
345
452
  }
346
453
  }
347
454
  }
348
455
 
349
- pluginsToHandleCmd (cmd) {
350
- return this.plugins.filter((p) =>
351
- p.commands === true ||
352
- (_.isArray(p.commands) && _.includes(p.commands, cmd))
353
- );
456
+ /**
457
+ * Get the appropriate plugins for a session (or sessionless plugins)
458
+ *
459
+ * @param {?string} sessionId - the sessionId (or null) to use to find plugins
460
+ * @returns {Array} - array of plugin instances
461
+ */
462
+ pluginsForSession (sessionId = null) {
463
+ if (sessionId) {
464
+ if (!this.sessionPlugins[sessionId]) {
465
+ this.sessionPlugins[sessionId] = this.createPluginInstances();
466
+ }
467
+ return this.sessionPlugins[sessionId];
468
+ }
469
+
470
+ if (_.isEmpty(this.sessionlessPlugins)) {
471
+ this.sessionlessPlugins = this.createPluginInstances();
472
+ }
473
+ return this.sessionlessPlugins;
354
474
  }
355
475
 
476
+ /**
477
+ * To get plugins for a command, we either get the plugin instances associated with the
478
+ * particular command's session, or in the case of sessionless plugins, pull from the set of
479
+ * plugin instances reserved for sessionless commands (and we lazily create plugin instances on
480
+ * first use)
481
+ *
482
+ * @param {string} cmd - the name of the command to find a plugin to handle
483
+ * @param {?string} sessionId - the particular session for which to find a plugin, or null if
484
+ * sessionless
485
+ */
486
+ pluginsToHandleCmd (cmd, sessionId = null) {
487
+ // to handle a given command, a plugin should either implement that command as a plugin
488
+ // instance method or it should implement a generic 'handle' method
489
+ return this.pluginsForSession(sessionId)
490
+ .filter((p) => _.isFunction(p[cmd]) || _.isFunction(p.handle));
491
+ }
492
+
493
+ createPluginInstances () {
494
+ return this.pluginClasses.map((PluginClass) => {
495
+ const name = PluginClass.pluginName;
496
+ const plugin = new PluginClass(name);
497
+ this.assignCliArgsToExtension('plugin', name, plugin);
498
+ return plugin;
499
+ });
500
+ }
501
+
502
+ /**
503
+ *
504
+ * @param {string} cmd
505
+ * @param {...any} args
506
+ * @returns {Promise<{value: any, error?: Error, protocol: string} | import('type-fest').AsyncReturnType<import('@appium/types').Driver['executeCommand']>>}
507
+ */
356
508
  async executeCommand (cmd, ...args) {
357
509
  // We have basically three cases for how to handle commands:
358
510
  // 1. handle getStatus (we do this as a special out of band case so it doesn't get added to an
@@ -363,28 +515,42 @@ class AppiumDriver extends BaseDriver {
363
515
  // The tricky part is that because we support command plugins, we need to wrap any of these
364
516
  // cases with plugin handling.
365
517
 
366
- const isGetStatus = cmd === 'getStatus';
367
- const isUmbrellaCmd = !isGetStatus && isAppiumDriverCommand(cmd);
368
- const isSessionCmd = !isGetStatus && !isUmbrellaCmd;
518
+ const isGetStatus = cmd === GET_STATUS_COMMAND;
519
+ const isUmbrellaCmd = isAppiumDriverCommand(cmd);
520
+ const isSessionCmd = isSessionCommand(cmd);
521
+
522
+ // if a plugin override proxying for this command and that is why we are here instead of just
523
+ // letting the protocol proxy the command entirely, determine that, get the request object for
524
+ // use later on, then clean up the args
525
+ const reqForProxy = _.last(args)?.reqForProxy;
526
+ if (reqForProxy) {
527
+ args.pop();
528
+ }
369
529
 
370
- // get any plugins which are registered as handling this command
371
- const plugins = this.pluginsToHandleCmd(cmd);
372
530
 
373
531
  // first do some error checking. If we're requesting a session command execution, then make
374
532
  // sure that session actually exists on the session driver, and set the session driver itself
375
533
  let sessionId = null;
376
534
  let dstSession = null;
377
535
  let protocol = null;
536
+ /** @type {this | ExternalDriver} */
537
+ let driver = this;
378
538
  if (isSessionCmd) {
379
539
  sessionId = _.last(args);
380
- dstSession = await sessionsListGuard.acquire(AppiumDriver.name, () => this.sessions[sessionId]);
540
+ dstSession = this.sessions[sessionId];
381
541
  if (!dstSession) {
382
542
  throw new Error(`The session with id '${sessionId}' does not exist`);
383
543
  }
384
544
  // now save the response protocol given that the session driver's protocol might differ
385
545
  protocol = dstSession.protocol;
546
+ if (!isUmbrellaCmd) {
547
+ driver = dstSession;
548
+ }
386
549
  }
387
550
 
551
+ // get any plugins which are registered as handling this command
552
+ const plugins = this.pluginsToHandleCmd(cmd, sessionId);
553
+
388
554
  // now we define a 'cmdHandledBy' object which will keep track of which plugins have handled this
389
555
  // command. we care about this because (a) multiple plugins can handle the same command, and
390
556
  // (b) there's no guarantee that a plugin will actually call the next() method which runs the
@@ -401,11 +567,23 @@ class AppiumDriver extends BaseDriver {
401
567
  // if we're running with plugins, make sure we log that the default behavior is actually
402
568
  // happening so we can tell when the plugin call chain is unwrapping to the default behavior
403
569
  // if that's what happens
404
- plugins.length && log.info(`Executing default handling behavior for command '${cmd}'`);
570
+ plugins.length && this.log.info(`Executing default handling behavior for command '${cmd}'`);
405
571
 
406
572
  // if we make it here, we know that the default behavior is handled
407
573
  cmdHandledBy.default = true;
408
574
 
575
+ if (reqForProxy) {
576
+ // we would have proxied this command had a plugin not handled it, so the default behavior
577
+ // is to do the proxy and retrieve the result internally so it can be passed to the plugin
578
+ // in case it calls 'await next()'. This requires that the driver have defined
579
+ // 'proxyCommand' and not just 'proxyReqRes'.
580
+ if (!dstSession.proxyCommand) {
581
+ throw new NoDriverProxyCommandError();
582
+ }
583
+ return await dstSession.proxyCommand(reqForProxy.originalUrl, reqForProxy.method,
584
+ reqForProxy.body);
585
+ }
586
+
409
587
  if (isGetStatus) {
410
588
  return await this.getStatus();
411
589
  }
@@ -413,7 +591,7 @@ class AppiumDriver extends BaseDriver {
413
591
  if (isUmbrellaCmd) {
414
592
  // some commands, like deleteSession, we want to make sure to handle on *this* driver,
415
593
  // not the platform driver
416
- return await super.executeCommand(cmd, ...args);
594
+ return await BaseDriver.prototype.executeCommand.call(this, cmd, ...args);
417
595
  }
418
596
 
419
597
  // here we know that we are executing a session command, and have a valid session driver
@@ -421,18 +599,31 @@ class AppiumDriver extends BaseDriver {
421
599
  };
422
600
 
423
601
  // now take our default behavior, wrap it with any number of plugin behaviors, and run it
424
- const wrappedCmd = this.wrapCommandWithPlugins({cmd, args, plugins, cmdHandledBy, next: defaultBehavior});
602
+ const wrappedCmd = this.wrapCommandWithPlugins({
603
+ driver, cmd, args, plugins, cmdHandledBy, next: defaultBehavior
604
+ });
425
605
  const res = await this.executeWrappedCommand({wrappedCmd, protocol});
426
606
 
427
607
  // if we had plugins, make sure to log out the helpful report about which plugins ended up
428
608
  // handling the command and which didn't
429
- plugins.length && this.logPluginHandlerReport({cmd, cmdHandledBy});
609
+ this.logPluginHandlerReport(plugins, {cmd, cmdHandledBy});
610
+
611
+ // And finally, if the command was createSession, we want to migrate any plugins which were
612
+ // previously sessionless to use the new sessionId, so that plugins can share state between
613
+ // their createSession method and other instance methods
614
+ if (cmd === CREATE_SESSION_COMMAND && this.sessionlessPlugins.length && !res.error) {
615
+ const sessionId = _.first(res.value);
616
+ this.log.info(`Promoting ${this.sessionlessPlugins.length} sessionless plugins to be attached ` +
617
+ `to session ID ${sessionId}`);
618
+ this.sessionPlugins[sessionId] = this.sessionlessPlugins;
619
+ this.sessionlessPlugins = [];
620
+ }
430
621
 
431
622
  return res;
432
623
  }
433
624
 
434
- wrapCommandWithPlugins ({cmd, args, next, cmdHandledBy, plugins}) {
435
- plugins.length && log.info(`Plugins which can handle cmd '${cmd}': ${plugins.map((p) => p.name)}`);
625
+ wrapCommandWithPlugins ({driver, cmd, args, next, cmdHandledBy, plugins}) {
626
+ plugins.length && this.log.info(`Plugins which can handle cmd '${cmd}': ${plugins.map((p) => p.name)}`);
436
627
 
437
628
  // now we can go through each plugin and wrap `next` around its own handler, passing the *old*
438
629
  // next in so that it can call it if it wants to
@@ -442,16 +633,25 @@ class AppiumDriver extends BaseDriver {
442
633
  // evaluated, otherwise we end up with infinite recursion of the last `next` to be defined.
443
634
  cmdHandledBy[plugin.name] = false; // we see a new plugin, so add it to the 'cmdHandledBy' object
444
635
  next = ((_next) => async () => {
445
- log.info(`Plugin ${plugin.name} is now handling cmd '${cmd}'`);
636
+ this.log.info(`Plugin ${plugin.name} is now handling cmd '${cmd}'`);
446
637
  cmdHandledBy[plugin.name] = true; // if we make it here, this plugin has attempted to handle cmd
447
- return await plugin.handle(_next, this, cmd, ...args);
638
+ // first attempt to handle the command via a command-specific handler on the plugin
639
+ if (plugin[cmd]) {
640
+ return await plugin[cmd](_next, driver, ...args);
641
+ }
642
+ // otherwise, call the generic 'handle' method
643
+ return await plugin.handle(_next, driver, cmd, ...args);
448
644
  })(next);
449
645
  }
450
646
 
451
647
  return next;
452
648
  }
453
649
 
454
- logPluginHandlerReport ({cmd, cmdHandledBy}) {
650
+ logPluginHandlerReport (plugins, {cmd, cmdHandledBy}) {
651
+ if (!plugins.length) {
652
+ return;
653
+ }
654
+
455
655
  // at the end of the day, we have an object representing which plugins ended up getting
456
656
  // their code run as part of handling this command. Because plugins can choose *not* to
457
657
  // pass control to other plugins or to the default driver behavior, this is information
@@ -461,9 +661,9 @@ class AppiumDriver extends BaseDriver {
461
661
  const didHandle = Object.keys(cmdHandledBy).filter((k) => cmdHandledBy[k]);
462
662
  const didntHandle = Object.keys(cmdHandledBy).filter((k) => !cmdHandledBy[k]);
463
663
  if (didntHandle.length > 0) {
464
- log.info(`Command '${cmd}' was not handled by the following beahviors or plugins, even ` +
465
- `though they were registered to handle it: ${JSON.stringify(didntHandle)}. The ` +
466
- `command *was* handled by these: ${JSON.stringify(didHandle)}.`);
664
+ this.log.info(`Command '${cmd}' was *not* handled by the following behaviours or plugins, even ` +
665
+ `though they were registered to handle it: ${JSON.stringify(didntHandle)}. The ` +
666
+ `command *was* handled by these: ${JSON.stringify(didHandle)}.`);
467
667
  }
468
668
  }
469
669
 
@@ -491,7 +691,6 @@ class AppiumDriver extends BaseDriver {
491
691
  return res;
492
692
  }
493
693
 
494
-
495
694
  proxyActive (sessionId) {
496
695
  const dstSession = this.sessions[sessionId];
497
696
  return dstSession && _.isFunction(dstSession.proxyActive) && dstSession.proxyActive(sessionId);
@@ -511,7 +710,51 @@ class AppiumDriver extends BaseDriver {
511
710
  // help decide which commands should be proxied to sub-drivers and which
512
711
  // should be handled by this, our umbrella driver
513
712
  function isAppiumDriverCommand (cmd) {
514
- return !isSessionCommand(cmd) || cmd === 'deleteSession';
713
+ return !isSessionCommand(cmd) || cmd === DELETE_SESSION_COMMAND;
714
+ }
715
+
716
+ /**
717
+ * Thrown when Appium tried to proxy a command using a driver's `proxyCommand` method but the
718
+ * method did not exist
719
+ */
720
+ export class NoDriverProxyCommandError extends Error {
721
+ /**
722
+ * @type {Readonly<string>}
723
+ */
724
+ code = 'APPIUMERR_NO_DRIVER_PROXYCOMMAND';
725
+
726
+ constructor () {
727
+ super(`The default behavior for this command was to proxy, but the driver ` +
728
+ `did not have the 'proxyCommand' method defined. To fully support ` +
729
+ `plugins, drivers should have 'proxyCommand' set to a jwpProxy object's ` +
730
+ `'command()' method, in addition to the normal 'proxyReqRes'`);
731
+ }
515
732
  }
516
733
 
517
734
  export { AppiumDriver };
735
+
736
+ /**
737
+ * @typedef {import('@appium/types').ExternalDriver} ExternalDriver
738
+ * @typedef {import('@appium/types').W3CCapabilities} W3CCapabilities
739
+ * @typedef {import('@appium/types').DriverData} DriverData
740
+ * @typedef {import('@appium/types').DriverOpts} DriverOpts
741
+ * @typedef {import('@appium/types').Constraints} Constraints
742
+ * @typedef {import('@appium/types').AppiumServer} AppiumServer
743
+ * @typedef {import('../types').ExtensionType} ExtensionType
744
+ * @typedef {import('../types/extension').PluginClass} PluginClass
745
+ * @typedef {import('./extension/driver-config').DriverConfig} DriverConfig
746
+ */
747
+
748
+ /**
749
+ * Used by {@linkcode AppiumDriver.createSession} and {@linkcode AppiumDriver.deleteSession} to describe
750
+ * result.
751
+ * @template V
752
+ * @typedef SessionHandlerResult
753
+ * @property {V} [value]
754
+ * @property {Error} [error]
755
+ * @property {string} [protocol]
756
+ */
757
+
758
+ /**
759
+ * @typedef {import('@appium/types').SessionHandler<SessionHandlerResult<any[]>,SessionHandlerResult<void>>} SessionHandler
760
+ */