@appium/base-driver 10.5.2 → 10.7.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 (189) hide show
  1. package/build/lib/basedriver/capabilities.d.ts +1 -1
  2. package/build/lib/basedriver/capabilities.d.ts.map +1 -1
  3. package/build/lib/basedriver/capabilities.js +58 -50
  4. package/build/lib/basedriver/capabilities.js.map +1 -1
  5. package/build/lib/basedriver/commands/bidi.d.ts.map +1 -1
  6. package/build/lib/basedriver/commands/bidi.js +10 -14
  7. package/build/lib/basedriver/commands/bidi.js.map +1 -1
  8. package/build/lib/basedriver/commands/event.d.ts.map +1 -1
  9. package/build/lib/basedriver/commands/event.js +4 -7
  10. package/build/lib/basedriver/commands/event.js.map +1 -1
  11. package/build/lib/basedriver/commands/execute.js +3 -6
  12. package/build/lib/basedriver/commands/execute.js.map +1 -1
  13. package/build/lib/basedriver/commands/find.d.ts.map +1 -1
  14. package/build/lib/basedriver/commands/find.js +2 -1
  15. package/build/lib/basedriver/commands/find.js.map +1 -1
  16. package/build/lib/basedriver/commands/log.d.ts.map +1 -1
  17. package/build/lib/basedriver/commands/log.js +1 -5
  18. package/build/lib/basedriver/commands/log.js.map +1 -1
  19. package/build/lib/basedriver/commands/timeout.d.ts.map +1 -1
  20. package/build/lib/basedriver/commands/timeout.js +9 -13
  21. package/build/lib/basedriver/commands/timeout.js.map +1 -1
  22. package/build/lib/basedriver/core.d.ts.map +1 -1
  23. package/build/lib/basedriver/core.js +17 -14
  24. package/build/lib/basedriver/core.js.map +1 -1
  25. package/build/lib/basedriver/device-settings.d.ts.map +1 -1
  26. package/build/lib/basedriver/device-settings.js +3 -7
  27. package/build/lib/basedriver/device-settings.js.map +1 -1
  28. package/build/lib/basedriver/driver.d.ts.map +1 -1
  29. package/build/lib/basedriver/driver.js +34 -38
  30. package/build/lib/basedriver/driver.js.map +1 -1
  31. package/build/lib/basedriver/extension-core.d.ts +4 -1
  32. package/build/lib/basedriver/extension-core.d.ts.map +1 -1
  33. package/build/lib/basedriver/extension-core.js +37 -13
  34. package/build/lib/basedriver/extension-core.js.map +1 -1
  35. package/build/lib/basedriver/helpers.d.ts.map +1 -1
  36. package/build/lib/basedriver/helpers.js +47 -33
  37. package/build/lib/basedriver/helpers.js.map +1 -1
  38. package/build/lib/basedriver/ipc.d.ts +36 -0
  39. package/build/lib/basedriver/ipc.d.ts.map +1 -0
  40. package/build/lib/basedriver/ipc.js +157 -0
  41. package/build/lib/basedriver/ipc.js.map +1 -0
  42. package/build/lib/basedriver/validation.d.ts.map +1 -1
  43. package/build/lib/basedriver/validation.js +27 -29
  44. package/build/lib/basedriver/validation.js.map +1 -1
  45. package/build/lib/express/express-logging.d.ts +0 -1
  46. package/build/lib/express/express-logging.d.ts.map +1 -1
  47. package/build/lib/express/express-logging.js +11 -11
  48. package/build/lib/express/express-logging.js.map +1 -1
  49. package/build/lib/express/idempotency.js +3 -6
  50. package/build/lib/express/idempotency.js.map +1 -1
  51. package/build/lib/express/middleware.d.ts.map +1 -1
  52. package/build/lib/express/middleware.js +6 -10
  53. package/build/lib/express/middleware.js.map +1 -1
  54. package/build/lib/express/server.d.ts +1 -1
  55. package/build/lib/express/server.d.ts.map +1 -1
  56. package/build/lib/express/server.js +82 -73
  57. package/build/lib/express/server.js.map +1 -1
  58. package/build/lib/express/websocket.d.ts.map +1 -1
  59. package/build/lib/express/websocket.js +6 -9
  60. package/build/lib/express/websocket.js.map +1 -1
  61. package/build/lib/helpers/capabilities.d.ts.map +1 -1
  62. package/build/lib/helpers/capabilities.js +14 -17
  63. package/build/lib/helpers/capabilities.js.map +1 -1
  64. package/build/lib/helpers/extension-command-name.js +2 -5
  65. package/build/lib/helpers/extension-command-name.js.map +1 -1
  66. package/build/lib/helpers/levenshtein-match.d.ts.map +1 -1
  67. package/build/lib/helpers/levenshtein-match.js +6 -7
  68. package/build/lib/helpers/levenshtein-match.js.map +1 -1
  69. package/build/lib/index.d.ts +2 -1
  70. package/build/lib/index.d.ts.map +1 -1
  71. package/build/lib/index.js +6 -16
  72. package/build/lib/index.js.map +1 -1
  73. package/build/lib/jsonwp-proxy/protocol-converter.d.ts.map +1 -1
  74. package/build/lib/jsonwp-proxy/protocol-converter.js +21 -18
  75. package/build/lib/jsonwp-proxy/protocol-converter.js.map +1 -1
  76. package/build/lib/jsonwp-proxy/proxy-request.d.ts +2 -2
  77. package/build/lib/jsonwp-proxy/proxy-request.d.ts.map +1 -1
  78. package/build/lib/jsonwp-proxy/proxy-request.js +25 -21
  79. package/build/lib/jsonwp-proxy/proxy-request.js.map +1 -1
  80. package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
  81. package/build/lib/jsonwp-proxy/proxy.js +45 -36
  82. package/build/lib/jsonwp-proxy/proxy.js.map +1 -1
  83. package/build/lib/protocol/errors.d.ts.map +1 -1
  84. package/build/lib/protocol/errors.js +33 -37
  85. package/build/lib/protocol/errors.js.map +1 -1
  86. package/build/lib/protocol/helpers.d.ts.map +1 -1
  87. package/build/lib/protocol/helpers.js +9 -8
  88. package/build/lib/protocol/helpers.js.map +1 -1
  89. package/build/lib/protocol/protocol.d.ts +1 -1
  90. package/build/lib/protocol/protocol.d.ts.map +1 -1
  91. package/build/lib/protocol/protocol.js +73 -61
  92. package/build/lib/protocol/protocol.js.map +1 -1
  93. package/build/lib/protocol/routes.d.ts +1 -1
  94. package/build/lib/protocol/routes.d.ts.map +1 -1
  95. package/build/lib/protocol/routes.js +16 -17
  96. package/build/lib/protocol/routes.js.map +1 -1
  97. package/build/lib/protocol/validators.d.ts.map +1 -1
  98. package/build/lib/protocol/validators.js +1 -5
  99. package/build/lib/protocol/validators.js.map +1 -1
  100. package/build/lib/test-pages/crash.d.ts.map +1 -0
  101. package/build/lib/test-pages/crash.js.map +1 -0
  102. package/build/lib/test-pages/env.d.ts +5 -0
  103. package/build/lib/test-pages/env.d.ts.map +1 -0
  104. package/build/lib/test-pages/env.js +12 -0
  105. package/build/lib/test-pages/env.js.map +1 -0
  106. package/build/lib/{express/static.d.ts → test-pages/handlers.d.ts} +1 -2
  107. package/build/lib/test-pages/handlers.d.ts.map +1 -0
  108. package/build/lib/{express/static.js → test-pages/handlers.js} +9 -12
  109. package/build/lib/test-pages/handlers.js.map +1 -0
  110. package/build/lib/test-pages/index.d.ts +6 -0
  111. package/build/lib/test-pages/index.d.ts.map +1 -0
  112. package/build/lib/test-pages/index.js +35 -0
  113. package/build/lib/test-pages/index.js.map +1 -0
  114. package/build/lib/test-pages/static-dir.d.ts +8 -0
  115. package/build/lib/test-pages/static-dir.d.ts.map +1 -0
  116. package/build/lib/test-pages/static-dir.js +24 -0
  117. package/build/lib/test-pages/static-dir.js.map +1 -0
  118. package/build/lib/test-pages/template.d.ts +3 -0
  119. package/build/lib/test-pages/template.d.ts.map +1 -0
  120. package/build/lib/test-pages/template.js +19 -0
  121. package/build/lib/test-pages/template.js.map +1 -0
  122. package/build/lib/utils.d.ts +14 -0
  123. package/build/lib/utils.d.ts.map +1 -0
  124. package/build/lib/utils.js +55 -0
  125. package/build/lib/utils.js.map +1 -0
  126. package/lib/basedriver/capabilities.ts +126 -115
  127. package/lib/basedriver/commands/bidi.ts +11 -11
  128. package/lib/basedriver/commands/event.ts +17 -11
  129. package/lib/basedriver/commands/execute.ts +15 -12
  130. package/lib/basedriver/commands/find.ts +20 -12
  131. package/lib/basedriver/commands/log.ts +4 -3
  132. package/lib/basedriver/commands/timeout.ts +22 -14
  133. package/lib/basedriver/core.ts +26 -26
  134. package/lib/basedriver/device-settings.ts +7 -12
  135. package/lib/basedriver/driver.ts +62 -50
  136. package/lib/basedriver/extension-core.ts +60 -18
  137. package/lib/basedriver/helpers.ts +81 -52
  138. package/lib/basedriver/ipc.ts +198 -0
  139. package/lib/basedriver/validation.ts +37 -30
  140. package/lib/express/express-logging.ts +16 -20
  141. package/lib/express/idempotency.ts +9 -9
  142. package/lib/express/middleware.ts +14 -18
  143. package/lib/express/server.ts +118 -120
  144. package/lib/express/websocket.ts +11 -15
  145. package/lib/helpers/capabilities.ts +21 -16
  146. package/lib/helpers/extension-command-name.ts +3 -3
  147. package/lib/helpers/levenshtein-match.ts +20 -14
  148. package/lib/index.js +3 -12
  149. package/lib/jsonwp-proxy/protocol-converter.ts +58 -35
  150. package/lib/jsonwp-proxy/proxy-request.ts +26 -26
  151. package/lib/jsonwp-proxy/proxy.ts +74 -75
  152. package/lib/protocol/errors.ts +69 -88
  153. package/lib/protocol/helpers.ts +9 -5
  154. package/lib/protocol/protocol.ts +149 -107
  155. package/lib/protocol/routes.ts +17 -17
  156. package/lib/protocol/validators.ts +1 -3
  157. package/lib/test-pages/env.ts +9 -0
  158. package/lib/{express/static.ts → test-pages/handlers.ts} +10 -22
  159. package/lib/test-pages/index.ts +34 -0
  160. package/lib/test-pages/static-dir.ts +19 -0
  161. package/lib/test-pages/template.ts +17 -0
  162. package/lib/utils.ts +65 -0
  163. package/package.json +10 -13
  164. package/tsconfig.json +1 -0
  165. package/build/lib/express/crash.d.ts.map +0 -1
  166. package/build/lib/express/crash.js.map +0 -1
  167. package/build/lib/express/static.d.ts.map +0 -1
  168. package/build/lib/express/static.js.map +0 -1
  169. /package/build/lib/{express → test-pages}/crash.d.ts +0 -0
  170. /package/build/lib/{express → test-pages}/crash.js +0 -0
  171. /package/lib/{express → test-pages}/crash.ts +0 -0
  172. /package/{static → test-fixtures/static}/appium.png +0 -0
  173. /package/{static → test-fixtures/static}/favicon.ico +0 -0
  174. /package/{static → test-fixtures/static}/js/jquery.min.js +0 -0
  175. /package/{static → test-fixtures/static}/test/frameset.html +0 -0
  176. /package/{static → test-fixtures/static}/test/guinea-pig-app-banner.html +0 -0
  177. /package/{static → test-fixtures/static}/test/guinea-pig-scrollable.html +0 -0
  178. /package/{static → test-fixtures/static}/test/guinea-pig.html +0 -0
  179. /package/{static → test-fixtures/static}/test/guinea-pig2.html +0 -0
  180. /package/{static → test-fixtures/static}/test/guinea-pig3.html +0 -0
  181. /package/{static → test-fixtures/static}/test/guinea-pig4.html +0 -0
  182. /package/{static → test-fixtures/static}/test/guinea-pig5.html +0 -0
  183. /package/{static → test-fixtures/static}/test/iframes.html +0 -0
  184. /package/{static → test-fixtures/static}/test/shadow-dom.html +0 -0
  185. /package/{static → test-fixtures/static}/test/subframe1.html +0 -0
  186. /package/{static → test-fixtures/static}/test/subframe2.html +0 -0
  187. /package/{static → test-fixtures/static}/test/subframe3.html +0 -0
  188. /package/{static → test-fixtures/static}/test/touch.html +0 -0
  189. /package/{static → test-fixtures/static}/test/welcome.html +0 -0
@@ -1,11 +1,10 @@
1
- import _ from 'lodash';
1
+ import nodeFs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import {log as logger} from './logger';
4
4
  import {tempDir, fs, util, timing, node} from '@appium/support';
5
5
  import {LRUCache} from 'lru-cache';
6
6
  import AsyncLock from 'async-lock';
7
7
  import axios from 'axios';
8
- import B from 'bluebird';
9
8
  import type {
10
9
  ConfigureAppOptions,
11
10
  CachedAppInfo,
@@ -15,14 +14,15 @@ import type {
15
14
  } from '@appium/types';
16
15
  import type {AxiosResponseHeaders, RawAxiosRequestHeaders} from 'axios';
17
16
  import type {Readable} from 'node:stream';
17
+ import type {PackageJson} from 'type-fest';
18
18
 
19
19
  // for compat with running tests transpiled and in-place
20
- export const {version: BASEDRIVER_VER} = fs.readPackageJsonFrom(__dirname);
20
+ export const BASEDRIVER_VER = readBaseDriverVersion();
21
21
 
22
22
  const CACHED_APPS_MAX_AGE_MS = 1000 * 60 * toNaturalNumber(60 * 24, 'APPIUM_APPS_CACHE_MAX_AGE');
23
23
  const MAX_CACHED_APPS = toNaturalNumber(1024, 'APPIUM_APPS_CACHE_MAX_ITEMS');
24
24
  const HTTP_STATUS_NOT_MODIFIED = 304;
25
- const DEFAULT_REQ_HEADERS = Object.freeze({
25
+ const DEFAULT_REQ_HEADERS: RawAxiosRequestHeaders = Object.freeze({
26
26
  'user-agent': `Appium (BaseDriver v${BASEDRIVER_VER})`,
27
27
  });
28
28
  const AVG_DOWNLOAD_SPEED_MEASUREMENT_THRESHOLD_SEC = 2;
@@ -33,7 +33,7 @@ const APPLICATIONS_CACHE = new LRUCache<string, CachedAppInfoEntry>({
33
33
  dispose: ({fullPath}, app) => {
34
34
  logger.info(
35
35
  `The application '${app}' cached at '${fullPath}' has ` +
36
- `expired after ${CACHED_APPS_MAX_AGE_MS}ms`
36
+ `expired after ${CACHED_APPS_MAX_AGE_MS}ms`,
37
37
  );
38
38
  if (fullPath) {
39
39
  void fs.rimraf(fullPath);
@@ -53,7 +53,7 @@ process.on('exit', () => {
53
53
 
54
54
  const appPaths = [...APPLICATIONS_CACHE.values()].map(({fullPath}) => fullPath);
55
55
  logger.debug(
56
- `Performing cleanup of ${util.pluralize('cached application', appPaths.length, true)}`
56
+ `Performing cleanup of ${util.pluralize('cached application', appPaths.length, true)}`,
57
57
  );
58
58
  for (const appPath of appPaths) {
59
59
  if (!appPath) {
@@ -100,29 +100,29 @@ interface CachedAppInfoEntry extends Omit<CachedAppInfo, 'packageHash'> {
100
100
  */
101
101
  export async function configureApp(
102
102
  app: string,
103
- options: string | string[] | ConfigureAppOptions = {} as ConfigureAppOptions
103
+ options: string | string[] | ConfigureAppOptions = {} as ConfigureAppOptions,
104
104
  ): Promise<string> {
105
- if (!_.isString(app)) {
105
+ if (typeof app !== 'string') {
106
106
  // immediately shortcircuit if not given an app
107
107
  return '';
108
108
  }
109
109
 
110
110
  let supportedAppExtensions: string[];
111
- const opts = !_.isString(options) && !_.isArray(options) ? options : undefined;
111
+ const opts = typeof options !== 'string' && !Array.isArray(options) ? options : undefined;
112
112
  const onPostProcess = opts?.onPostProcess;
113
113
  const onDownload = opts?.onDownload;
114
114
 
115
- if (_.isString(options)) {
115
+ if (typeof options === 'string') {
116
116
  supportedAppExtensions = [options];
117
- } else if (_.isArray(options)) {
117
+ } else if (Array.isArray(options)) {
118
118
  supportedAppExtensions = options;
119
- } else if (_.isPlainObject(options)) {
119
+ } else if (util.isPlainObject(options)) {
120
120
  supportedAppExtensions = options.supportedExtensions ?? [];
121
121
  } else {
122
122
  supportedAppExtensions = [];
123
123
  }
124
124
 
125
- if (_.isEmpty(supportedAppExtensions)) {
125
+ if (util.isEmpty(supportedAppExtensions)) {
126
126
  throw new Error(`One or more supported app extensions must be provided`);
127
127
  }
128
128
 
@@ -142,7 +142,7 @@ export async function configureApp(
142
142
  newApp = path.resolve(process.cwd(), newApp);
143
143
  logger.warn(
144
144
  `The current application path '${app}' is not absolute ` +
145
- `and has been rewritten to '${newApp}'. Consider using absolute paths rather than relative`
145
+ `and has been rewritten to '${newApp}'. Consider using absolute paths rather than relative`,
146
146
  );
147
147
  app = newApp;
148
148
  }
@@ -170,7 +170,7 @@ export async function configureApp(
170
170
  let {stream, status} = result;
171
171
  logger.debug(`Response status: ${status}`);
172
172
  try {
173
- if (!_.isEmpty(headers)) {
173
+ if (!util.isEmpty(headers)) {
174
174
  if (headers.etag) {
175
175
  logger.debug(`Etag: ${headers.etag}`);
176
176
  remoteAppProps.etag = headers.etag;
@@ -196,7 +196,7 @@ export async function configureApp(
196
196
  }
197
197
  logger.info(
198
198
  `The application at '${cachedAppInfo.fullPath}' does not exist anymore ` +
199
- `or its integrity has been damaged. Deleting it from the internal cache`
199
+ `or its integrity has been damaged. Deleting it from the internal cache`,
200
200
  );
201
201
  APPLICATIONS_CACHE.delete(appCacheKey);
202
202
 
@@ -212,15 +212,18 @@ export async function configureApp(
212
212
  if (onDownload) {
213
213
  newApp = await onDownload({
214
214
  url: originalAppLink,
215
- headers: _.clone(headers) as HTTPHeaders,
215
+ headers: structuredClone(headers) as HTTPHeaders,
216
216
  stream,
217
217
  });
218
218
  } else {
219
219
  const fileName = determineFilename(headers, pathname ?? '', supportedAppExtensions);
220
- newApp = await fetchApp(stream, await tempDir.path({
221
- prefix: fileName,
222
- suffix: '',
223
- }));
220
+ newApp = await fetchApp(
221
+ stream,
222
+ await tempDir.path({
223
+ prefix: fileName,
224
+ suffix: '',
225
+ }),
226
+ );
224
227
  }
225
228
  } finally {
226
229
  if (!stream.closed) {
@@ -233,7 +236,7 @@ export async function configureApp(
233
236
  } else {
234
237
  let errorMessage = `The application at '${newApp}' does not exist or is not accessible`;
235
238
  // protocol value for 'C:\\temp' is 'c:', so we check the length as well
236
- if (_.isString(protocol) && protocol.length > 2) {
239
+ if (typeof protocol === 'string' && protocol.length > 2) {
237
240
  errorMessage =
238
241
  `The protocol '${protocol}' used in '${newApp}' is not supported. ` +
239
242
  `Only http: and https: protocols are supported`;
@@ -267,12 +270,12 @@ export async function configureApp(
267
270
  return appPathToCache;
268
271
  };
269
272
 
270
- if (_.isFunction(onPostProcess)) {
273
+ if (typeof onPostProcess === 'function') {
271
274
  const postProcessArg: PostProcessOptions = {
272
- cachedAppInfo: _.clone(cachedAppInfo) as CachedAppInfo | undefined,
275
+ cachedAppInfo: structuredClone(cachedAppInfo) as CachedAppInfo | undefined,
273
276
  isUrl,
274
277
  originalAppLink,
275
- headers: _.clone(headers) as HTTPHeaders,
278
+ headers: structuredClone(headers) as HTTPHeaders,
276
279
  appPath: newApp,
277
280
  };
278
281
  const result = await onPostProcess(postProcessArg);
@@ -282,7 +285,8 @@ export async function configureApp(
282
285
  }
283
286
 
284
287
  verifyAppExtension(newApp, supportedAppExtensions);
285
- return appCacheKey !== toCacheKey(newApp) && (packageHash || _.values(remoteAppProps).some(Boolean))
288
+ return appCacheKey !== toCacheKey(newApp) &&
289
+ (packageHash || Object.values(remoteAppProps).some(Boolean))
286
290
  ? await storeAppInCache(newApp)
287
291
  : newApp;
288
292
  });
@@ -310,14 +314,14 @@ export function isPackageOrBundle(app: string): boolean {
310
314
  */
311
315
  export function duplicateKeys<T>(input: T, firstKey: string, secondKey: string): T {
312
316
  // If array provided, recursively call on all elements
313
- if (_.isArray(input)) {
317
+ if (Array.isArray(input)) {
314
318
  return input.map((item) => duplicateKeys(item, firstKey, secondKey)) as T;
315
319
  }
316
320
 
317
321
  // If object, create duplicates for keys and then recursively call on values
318
- if (_.isPlainObject(input)) {
322
+ if (util.isPlainObject(input)) {
319
323
  const resultObj: Record<string, unknown> = {};
320
- for (const [key, value] of _.toPairs(input as Record<string, unknown>)) {
324
+ for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
321
325
  const recursivelyCalledValue = duplicateKeys(value, firstKey, secondKey);
322
326
  if (key === firstKey) {
323
327
  resultObj[secondKey] = recursivelyCalledValue;
@@ -342,23 +346,23 @@ export function duplicateKeys<T>(input: T, firstKey: string, secondKey: string):
342
346
  * @throws {TypeError} If value is not a string/array or JSON parsing fails for array-like input.
343
347
  */
344
348
  export function parseCapsArray(capValue: string | string[]): string[] {
345
- if (_.isArray(capValue)) {
349
+ if (Array.isArray(capValue)) {
346
350
  return capValue;
347
351
  }
348
352
 
349
353
  try {
350
354
  const parsed = JSON.parse(capValue);
351
- if (_.isArray(parsed)) {
355
+ if (Array.isArray(parsed)) {
352
356
  return parsed;
353
357
  }
354
358
  } catch (e) {
355
359
  const message = `Failed to parse capability as JSON array: ${(e as Error).message}`;
356
- if (_.isString(capValue) && _.startsWith(_.trimStart(capValue), '[')) {
360
+ if (typeof capValue === 'string' && capValue.trimStart().startsWith('[')) {
357
361
  throw new TypeError(message, {cause: e});
358
362
  }
359
363
  logger.warn(message);
360
364
  }
361
- if (_.isString(capValue)) {
365
+ if (typeof capValue === 'string') {
362
366
  return [capValue];
363
367
  }
364
368
  throw new TypeError(`Expected a string or a valid JSON array; received '${capValue}'`);
@@ -382,7 +386,9 @@ export function generateDriverLogPrefix(obj: object | null, _sessionId?: string
382
386
 
383
387
  // #region Private helpers
384
388
 
385
- function parseAppLink(appLink: string): URL | {protocol?: string; pathname?: string; href?: string; search?: string} {
389
+ function parseAppLink(
390
+ appLink: string,
391
+ ): URL | {protocol?: string; pathname?: string; href?: string; search?: string} {
386
392
  try {
387
393
  return new URL(appLink);
388
394
  } catch {
@@ -392,10 +398,10 @@ function parseAppLink(appLink: string): URL | {protocol?: string; pathname?: str
392
398
 
393
399
  function isEnvOptionEnabled(optionName: string, defaultValue: boolean | null = null): boolean {
394
400
  const value = process.env[optionName];
395
- if (!_.isNull(defaultValue) && _.isEmpty(value)) {
401
+ if (defaultValue !== null && util.isEmpty(value)) {
396
402
  return defaultValue;
397
403
  }
398
- return !_.isEmpty(value) && !['0', 'false', 'no'].includes(_.toLower(value));
404
+ return !util.isEmpty(value) && !['0', 'false', 'no'].includes(String(value).toLowerCase());
399
405
  }
400
406
 
401
407
  function isSupportedUrl(app: string): boolean {
@@ -432,7 +438,10 @@ function toCacheKey(app: string): string {
432
438
  return app;
433
439
  }
434
440
 
435
- async function queryAppLink(appLink: string, reqHeaders: RawAxiosRequestHeaders): Promise<RemoteAppData> {
441
+ async function queryAppLink(
442
+ appLink: string,
443
+ reqHeaders: RawAxiosRequestHeaders,
444
+ ): Promise<RemoteAppData> {
436
445
  const url = new URL(appLink);
437
446
  // Extract credentials, then remove them from the URL for axios
438
447
  const {username, password} = url;
@@ -453,10 +462,9 @@ async function queryAppLink(appLink: string, reqHeaders: RawAxiosRequestHeaders)
453
462
  const {data: stream, headers, status} = await axios(requestOpts);
454
463
  return {stream, headers, status};
455
464
  } catch (err) {
456
- throw new Error(
457
- `Cannot download the app from ${axiosUrl}: ${(err as Error).message}`,
458
- {cause: err}
459
- );
465
+ throw new Error(`Cannot download the app from ${axiosUrl}: ${(err as Error).message}`, {
466
+ cause: err,
467
+ });
460
468
  }
461
469
  }
462
470
 
@@ -466,7 +474,7 @@ async function fetchApp(srcStream: Readable, dstPath: string): Promise<string> {
466
474
  const writer = fs.createWriteStream(dstPath);
467
475
  srcStream.pipe(writer);
468
476
 
469
- await new B<void>((resolve, reject) => {
477
+ await new Promise<void>((resolve, reject) => {
470
478
  srcStream.once('error', reject);
471
479
  writer.once('finish', () => resolve());
472
480
  writer.once('error', (e: Error) => {
@@ -482,7 +490,7 @@ async function fetchApp(srcStream: Readable, dstPath: string): Promise<string> {
482
490
  const {size} = await fs.stat(dstPath);
483
491
  logger.debug(
484
492
  `The application (${util.toReadableSizeString(size)}) ` +
485
- `has been downloaded to '${dstPath}' in ${secondsElapsed.toFixed(3)}s`
493
+ `has been downloaded to '${dstPath}' in ${secondsElapsed.toFixed(3)}s`,
486
494
  );
487
495
  // it does not make much sense to approximate the speed for short downloads
488
496
  if (secondsElapsed >= AVG_DOWNLOAD_SPEED_MEASUREMENT_THRESHOLD_SEC) {
@@ -496,13 +504,16 @@ async function fetchApp(srcStream: Readable, dstPath: string): Promise<string> {
496
504
  function determineFilename(
497
505
  headers: AxiosResponseHeaders | RawAxiosRequestHeaders,
498
506
  pathname: string,
499
- supportedAppExtensions: string[]
507
+ supportedAppExtensions: string[],
500
508
  ): string {
501
509
  const basename = fs.sanitizeName(path.basename(decodeURIComponent(pathname ?? '')), {
502
510
  replacement: SANITIZE_REPLACEMENT,
503
511
  });
504
512
  const extname = path.extname(basename);
505
- if (headers['content-disposition'] && /^attachment/i.test(String(headers['content-disposition']))) {
513
+ if (
514
+ headers['content-disposition'] &&
515
+ /^attachment/i.test(String(headers['content-disposition']))
516
+ ) {
506
517
  logger.debug(`Content-Disposition: ${headers['content-disposition']}`);
507
518
  const match = /filename="([^"]+)/i.exec(String(headers['content-disposition']));
508
519
  if (match) {
@@ -515,24 +526,28 @@ function determineFilename(
515
526
  ? basename.substring(0, basename.length - extname.length)
516
527
  : DEFAULT_BASENAME;
517
528
  let resultingExt = extname;
518
- if (!supportedAppExtensions.map(_.toLower).includes(_.toLower(resultingExt))) {
529
+ if (
530
+ !supportedAppExtensions.map((ext) => ext.toLowerCase()).includes(resultingExt.toLowerCase())
531
+ ) {
519
532
  logger.info(
520
533
  `The current file extension '${resultingExt}' is not supported. ` +
521
- `Defaulting to '${_.first(supportedAppExtensions)}'`
534
+ `Defaulting to '${supportedAppExtensions[0]}'`,
522
535
  );
523
- resultingExt = _.first(supportedAppExtensions) as string;
536
+ resultingExt = supportedAppExtensions[0] as string;
524
537
  }
525
538
  return `${resultingName}${resultingExt}`;
526
539
  }
527
540
 
528
541
  function verifyAppExtension(app: string, supportedAppExtensions: string[]): string {
529
- if (supportedAppExtensions.map(_.toLower).includes(_.toLower(path.extname(app)))) {
542
+ if (
543
+ supportedAppExtensions.map((ext) => ext.toLowerCase()).includes(path.extname(app).toLowerCase())
544
+ ) {
530
545
  return app;
531
546
  }
532
547
  throw new Error(
533
548
  `New app path '${app}' did not have ` +
534
549
  `${util.pluralize('extension', supportedAppExtensions.length, false)}: ` +
535
- supportedAppExtensions
550
+ supportedAppExtensions,
536
551
  );
537
552
  }
538
553
 
@@ -546,7 +561,7 @@ async function calculateFileIntegrity(filePath: string): Promise<string> {
546
561
 
547
562
  async function isAppIntegrityOk(
548
563
  currentPath: string,
549
- expectedIntegrity: {file?: string; folder?: number} = {}
564
+ expectedIntegrity: {file?: string; folder?: number} = {},
550
565
  ): Promise<boolean> {
551
566
  if (!(await fs.exists(currentPath))) {
552
567
  return false;
@@ -565,13 +580,27 @@ async function isAppIntegrityOk(
565
580
  }
566
581
 
567
582
  function toNaturalNumber(defaultValue: number, envVarName?: string): number {
568
- if (!envVarName || _.isUndefined(process.env[envVarName])) {
583
+ if (!envVarName || process.env[envVarName] === undefined) {
569
584
  return defaultValue;
570
585
  }
571
586
  const num = parseInt(`${process.env[envVarName]}`, 10);
572
587
  return num > 0 ? num : defaultValue;
573
588
  }
574
589
 
590
+ function readBaseDriverVersion(): string {
591
+ const pkgRoot = node.getModuleRootSync('@appium/base-driver', __filename);
592
+ if (!pkgRoot) {
593
+ throw new Error('Cannot find the @appium/base-driver package root');
594
+ }
595
+ const pkg = JSON.parse(
596
+ nodeFs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8'),
597
+ ) as PackageJson;
598
+ if (typeof pkg.version !== 'string') {
599
+ throw new Error('Invalid `package.json` for @appium/base-driver');
600
+ }
601
+ return pkg.version;
602
+ }
603
+
575
604
  export default {
576
605
  configureApp,
577
606
  isPackageOrBundle,
@@ -0,0 +1,198 @@
1
+ import {log} from './logger';
2
+ import type {
3
+ StringRecord,
4
+ IIpcSubscription,
5
+ IAppiumIpc,
6
+ IpcMessage,
7
+ IpcEvent,
8
+ AppiumLogger,
9
+ IpcData,
10
+ } from '@appium/types';
11
+ import EventEmitter from 'node:events';
12
+ import {sleep} from 'asyncbox';
13
+ import {node} from '@appium/support';
14
+
15
+ const DEF_MAX_OBJ_SIZE_BYTES = 1024 * 1024; // 1mb seems like plenty for any plugin to pass a message
16
+ const DEF_MAX_TOPICS = 1000;
17
+
18
+ export const EVT_MESSAGE = 'message';
19
+ export const EVT_UNSUBSCRIBED = 'unsubscribed';
20
+
21
+ export type AppiumIpcOpts = {
22
+ maxObjSize?: number;
23
+ maxTopics?: number;
24
+ log?: AppiumLogger;
25
+ };
26
+
27
+ const ASYNC_ITERATOR_STOP = Symbol('asyncIteratorStop');
28
+
29
+ export class IpcSubscription<T extends IpcData>
30
+ extends EventEmitter<IpcEvent<T>>
31
+ implements IIpcSubscription<T>
32
+ {
33
+ constructor(
34
+ public readonly subscriber: string,
35
+ public readonly topic: string,
36
+ private readonly ipc: AppiumIpc,
37
+ ) {
38
+ super();
39
+ }
40
+
41
+ get isActive() {
42
+ return this.ipc.subscriptionExists(this.topic, this.subscriber);
43
+ }
44
+
45
+ getMessage(): IpcMessage<T> | undefined {
46
+ if (!this.isActive) {
47
+ throw new Error('Cannot get message from subscription after unsubscribing');
48
+ }
49
+ return this.ipc.getMessage<T>(this.topic);
50
+ }
51
+
52
+ async publish(data: T): Promise<void> {
53
+ if (!this.isActive) {
54
+ throw new Error('Cannot publish data to topic from subscription after unsubscribing');
55
+ }
56
+ return await this.ipc.publish<T>(this.topic, this.subscriber, data);
57
+ }
58
+
59
+ unsubscribe(): boolean {
60
+ if (!this.isActive) {
61
+ return false;
62
+ }
63
+ const unsubscribeRes = this.ipc.unsubscribe(this.topic, this.subscriber);
64
+ this.emit('unsubscribed');
65
+ this.removeAllListeners(EVT_MESSAGE);
66
+ return unsubscribeRes;
67
+ }
68
+
69
+ async *[Symbol.asyncIterator](): AsyncGenerator<IpcMessage<T>> {
70
+ // yield any messages that are emitted, but keep an eye out for an unsubscribed that happens
71
+ // while a caller is waiting on the loop, because we want to exit the loop in case of
72
+ // unsubscription, even if we were already waiting on the next message.
73
+ while (this.isActive) {
74
+ const val = await new Promise<IpcMessage<T> | typeof ASYNC_ITERATOR_STOP>((resolve) => {
75
+ this.once(EVT_MESSAGE, (message: IpcMessage<T>) => {
76
+ this.removeAllListeners(EVT_UNSUBSCRIBED);
77
+ resolve(message);
78
+ });
79
+ this.once(EVT_UNSUBSCRIBED, () => {
80
+ // EVT_MESSAGE listeners are already removed in unsubscribe()
81
+ resolve(ASYNC_ITERATOR_STOP);
82
+ });
83
+ });
84
+ if (val === ASYNC_ITERATOR_STOP) {
85
+ break;
86
+ }
87
+ yield val;
88
+ }
89
+ }
90
+ }
91
+
92
+ export class AppiumIpc implements IAppiumIpc {
93
+ protected readonly messageByTopic: StringRecord<IpcMessage<any>> = {};
94
+ protected readonly subs: StringRecord<Array<IpcSubscription<any>>> = {};
95
+ protected readonly topics = new Set<string>();
96
+ protected readonly maxObjSize: number;
97
+ protected readonly maxTopics: number;
98
+ protected readonly log: AppiumLogger;
99
+
100
+ constructor(opts: AppiumIpcOpts = {}) {
101
+ this.maxObjSize = opts.maxObjSize ?? DEF_MAX_OBJ_SIZE_BYTES;
102
+ this.maxTopics = opts.maxTopics ?? DEF_MAX_TOPICS;
103
+ this.log = opts.log ?? log;
104
+ this.log.debug(
105
+ `Initialized new IPC object with max object size of ${this.maxObjSize} bytes ` +
106
+ `and max topics of ${this.maxTopics}`,
107
+ );
108
+ }
109
+
110
+ subscribe<T extends IpcData>(topic: string, subscriber: string): IpcSubscription<T> {
111
+ this.log.info(`Subscribing ${subscriber} to topic '${topic}'`);
112
+ if (this.subscriptionExists(topic, subscriber)) {
113
+ throw new Error(
114
+ `Subscription already exists for topic "${topic}" and subscriber "${subscriber}"`,
115
+ );
116
+ }
117
+
118
+ this.ensureTopic(topic);
119
+ this.subs[topic] ??= [];
120
+ const sub = new IpcSubscription<T>(subscriber, topic, this);
121
+ this.subs[topic].push(sub);
122
+ return sub;
123
+ }
124
+
125
+ unsubscribe(topic: string, subscriber: string): boolean {
126
+ this.log.info(`Unsubscribing ${subscriber} from topic '${topic}'`);
127
+ if (this.subscriptionExists(topic, subscriber)) {
128
+ this.subs[topic] = this.subs[topic].filter((sub) => sub.subscriber !== subscriber);
129
+ return true;
130
+ }
131
+ return false;
132
+ }
133
+
134
+ async publish<T extends IpcData>(topic: string, publisher: string, data: T): Promise<void> {
135
+ this.log.debug(`${publisher} is publishing a message to topic ${topic}`);
136
+
137
+ this.ensureTopic(topic);
138
+
139
+ const messageSize = node.getObjectSize(data);
140
+ if (messageSize > this.maxObjSize) {
141
+ throw new Error(
142
+ `Error when ${publisher} is publishing to topic '${topic}': ` +
143
+ `Message with size ${messageSize} bytes is bigger than max size of ${this.maxObjSize} bytes`,
144
+ );
145
+ }
146
+
147
+ let clonedData: T;
148
+ try {
149
+ clonedData = structuredClone(data);
150
+ } catch (e) {
151
+ throw new Error(`Could not clone data for IPC publish from ${publisher} on topic ${topic}`, {
152
+ cause: e,
153
+ });
154
+ }
155
+
156
+ const message: IpcMessage<T> = {publisher, data: clonedData, topic, timestampMs: Date.now()};
157
+
158
+ this.messageByTopic[topic] = message;
159
+
160
+ const subs = this.subs[topic]
161
+ ? this.subs[topic].filter((sub) => sub.subscriber !== publisher)
162
+ : [];
163
+
164
+ for (const sub of subs) {
165
+ sub.emit(EVT_MESSAGE, structuredClone(message));
166
+ }
167
+
168
+ // we don't want to return from publish until the async iterators on subscriptions have had
169
+ // a chance to observe the emitted value, otherwise some might get lost
170
+ await sleep(0);
171
+ }
172
+
173
+ getMessage<T extends IpcData>(topic: string): IpcMessage<T> | undefined {
174
+ if (!this.messageByTopic[topic]) {
175
+ return;
176
+ }
177
+
178
+ return structuredClone(this.messageByTopic[topic] as IpcMessage<T>);
179
+ }
180
+
181
+ subscriptionExists(topic: string, subscriber: string): boolean {
182
+ return !!this.subs[topic]?.some((sub) => sub.subscriber === subscriber);
183
+ }
184
+
185
+ protected ensureTopic(topic: string): void {
186
+ if (this.topics.has(topic)) {
187
+ return;
188
+ }
189
+ if (this.topics.size >= this.maxTopics) {
190
+ throw new Error(
191
+ `Cannot create new IPC topic '${topic}': ` +
192
+ `maximum of ${this.maxTopics} topics per session reached. ` +
193
+ `Adjust with the --max-ipc-topics server arg.`,
194
+ );
195
+ }
196
+ this.topics.add(topic);
197
+ }
198
+ }