@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.
- package/build/lib/basedriver/capabilities.d.ts +1 -1
- package/build/lib/basedriver/capabilities.d.ts.map +1 -1
- package/build/lib/basedriver/capabilities.js +58 -50
- package/build/lib/basedriver/capabilities.js.map +1 -1
- package/build/lib/basedriver/commands/bidi.d.ts.map +1 -1
- package/build/lib/basedriver/commands/bidi.js +10 -14
- package/build/lib/basedriver/commands/bidi.js.map +1 -1
- package/build/lib/basedriver/commands/event.d.ts.map +1 -1
- package/build/lib/basedriver/commands/event.js +4 -7
- package/build/lib/basedriver/commands/event.js.map +1 -1
- package/build/lib/basedriver/commands/execute.js +3 -6
- package/build/lib/basedriver/commands/execute.js.map +1 -1
- package/build/lib/basedriver/commands/find.d.ts.map +1 -1
- package/build/lib/basedriver/commands/find.js +2 -1
- package/build/lib/basedriver/commands/find.js.map +1 -1
- package/build/lib/basedriver/commands/log.d.ts.map +1 -1
- package/build/lib/basedriver/commands/log.js +1 -5
- package/build/lib/basedriver/commands/log.js.map +1 -1
- package/build/lib/basedriver/commands/timeout.d.ts.map +1 -1
- package/build/lib/basedriver/commands/timeout.js +9 -13
- package/build/lib/basedriver/commands/timeout.js.map +1 -1
- package/build/lib/basedriver/core.d.ts.map +1 -1
- package/build/lib/basedriver/core.js +17 -14
- package/build/lib/basedriver/core.js.map +1 -1
- package/build/lib/basedriver/device-settings.d.ts.map +1 -1
- package/build/lib/basedriver/device-settings.js +3 -7
- package/build/lib/basedriver/device-settings.js.map +1 -1
- package/build/lib/basedriver/driver.d.ts.map +1 -1
- package/build/lib/basedriver/driver.js +34 -38
- package/build/lib/basedriver/driver.js.map +1 -1
- package/build/lib/basedriver/extension-core.d.ts +4 -1
- package/build/lib/basedriver/extension-core.d.ts.map +1 -1
- package/build/lib/basedriver/extension-core.js +37 -13
- package/build/lib/basedriver/extension-core.js.map +1 -1
- package/build/lib/basedriver/helpers.d.ts.map +1 -1
- package/build/lib/basedriver/helpers.js +47 -33
- package/build/lib/basedriver/helpers.js.map +1 -1
- package/build/lib/basedriver/ipc.d.ts +36 -0
- package/build/lib/basedriver/ipc.d.ts.map +1 -0
- package/build/lib/basedriver/ipc.js +157 -0
- package/build/lib/basedriver/ipc.js.map +1 -0
- package/build/lib/basedriver/validation.d.ts.map +1 -1
- package/build/lib/basedriver/validation.js +27 -29
- package/build/lib/basedriver/validation.js.map +1 -1
- package/build/lib/express/express-logging.d.ts +0 -1
- package/build/lib/express/express-logging.d.ts.map +1 -1
- package/build/lib/express/express-logging.js +11 -11
- package/build/lib/express/express-logging.js.map +1 -1
- package/build/lib/express/idempotency.js +3 -6
- package/build/lib/express/idempotency.js.map +1 -1
- package/build/lib/express/middleware.d.ts.map +1 -1
- package/build/lib/express/middleware.js +6 -10
- package/build/lib/express/middleware.js.map +1 -1
- package/build/lib/express/server.d.ts +1 -1
- package/build/lib/express/server.d.ts.map +1 -1
- package/build/lib/express/server.js +82 -73
- package/build/lib/express/server.js.map +1 -1
- package/build/lib/express/websocket.d.ts.map +1 -1
- package/build/lib/express/websocket.js +6 -9
- package/build/lib/express/websocket.js.map +1 -1
- package/build/lib/helpers/capabilities.d.ts.map +1 -1
- package/build/lib/helpers/capabilities.js +14 -17
- package/build/lib/helpers/capabilities.js.map +1 -1
- package/build/lib/helpers/extension-command-name.js +2 -5
- package/build/lib/helpers/extension-command-name.js.map +1 -1
- package/build/lib/helpers/levenshtein-match.d.ts.map +1 -1
- package/build/lib/helpers/levenshtein-match.js +6 -7
- package/build/lib/helpers/levenshtein-match.js.map +1 -1
- package/build/lib/index.d.ts +2 -1
- package/build/lib/index.d.ts.map +1 -1
- package/build/lib/index.js +6 -16
- package/build/lib/index.js.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.js +21 -18
- package/build/lib/jsonwp-proxy/protocol-converter.js.map +1 -1
- package/build/lib/jsonwp-proxy/proxy-request.d.ts +2 -2
- package/build/lib/jsonwp-proxy/proxy-request.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/proxy-request.js +25 -21
- package/build/lib/jsonwp-proxy/proxy-request.js.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.js +45 -36
- package/build/lib/jsonwp-proxy/proxy.js.map +1 -1
- package/build/lib/protocol/errors.d.ts.map +1 -1
- package/build/lib/protocol/errors.js +33 -37
- package/build/lib/protocol/errors.js.map +1 -1
- package/build/lib/protocol/helpers.d.ts.map +1 -1
- package/build/lib/protocol/helpers.js +9 -8
- package/build/lib/protocol/helpers.js.map +1 -1
- package/build/lib/protocol/protocol.d.ts +1 -1
- package/build/lib/protocol/protocol.d.ts.map +1 -1
- package/build/lib/protocol/protocol.js +73 -61
- package/build/lib/protocol/protocol.js.map +1 -1
- package/build/lib/protocol/routes.d.ts +1 -1
- package/build/lib/protocol/routes.d.ts.map +1 -1
- package/build/lib/protocol/routes.js +16 -17
- package/build/lib/protocol/routes.js.map +1 -1
- package/build/lib/protocol/validators.d.ts.map +1 -1
- package/build/lib/protocol/validators.js +1 -5
- package/build/lib/protocol/validators.js.map +1 -1
- package/build/lib/test-pages/crash.d.ts.map +1 -0
- package/build/lib/test-pages/crash.js.map +1 -0
- package/build/lib/test-pages/env.d.ts +5 -0
- package/build/lib/test-pages/env.d.ts.map +1 -0
- package/build/lib/test-pages/env.js +12 -0
- package/build/lib/test-pages/env.js.map +1 -0
- package/build/lib/{express/static.d.ts → test-pages/handlers.d.ts} +1 -2
- package/build/lib/test-pages/handlers.d.ts.map +1 -0
- package/build/lib/{express/static.js → test-pages/handlers.js} +9 -12
- package/build/lib/test-pages/handlers.js.map +1 -0
- package/build/lib/test-pages/index.d.ts +6 -0
- package/build/lib/test-pages/index.d.ts.map +1 -0
- package/build/lib/test-pages/index.js +35 -0
- package/build/lib/test-pages/index.js.map +1 -0
- package/build/lib/test-pages/static-dir.d.ts +8 -0
- package/build/lib/test-pages/static-dir.d.ts.map +1 -0
- package/build/lib/test-pages/static-dir.js +24 -0
- package/build/lib/test-pages/static-dir.js.map +1 -0
- package/build/lib/test-pages/template.d.ts +3 -0
- package/build/lib/test-pages/template.d.ts.map +1 -0
- package/build/lib/test-pages/template.js +19 -0
- package/build/lib/test-pages/template.js.map +1 -0
- package/build/lib/utils.d.ts +14 -0
- package/build/lib/utils.d.ts.map +1 -0
- package/build/lib/utils.js +55 -0
- package/build/lib/utils.js.map +1 -0
- package/lib/basedriver/capabilities.ts +126 -115
- package/lib/basedriver/commands/bidi.ts +11 -11
- package/lib/basedriver/commands/event.ts +17 -11
- package/lib/basedriver/commands/execute.ts +15 -12
- package/lib/basedriver/commands/find.ts +20 -12
- package/lib/basedriver/commands/log.ts +4 -3
- package/lib/basedriver/commands/timeout.ts +22 -14
- package/lib/basedriver/core.ts +26 -26
- package/lib/basedriver/device-settings.ts +7 -12
- package/lib/basedriver/driver.ts +62 -50
- package/lib/basedriver/extension-core.ts +60 -18
- package/lib/basedriver/helpers.ts +81 -52
- package/lib/basedriver/ipc.ts +198 -0
- package/lib/basedriver/validation.ts +37 -30
- package/lib/express/express-logging.ts +16 -20
- package/lib/express/idempotency.ts +9 -9
- package/lib/express/middleware.ts +14 -18
- package/lib/express/server.ts +118 -120
- package/lib/express/websocket.ts +11 -15
- package/lib/helpers/capabilities.ts +21 -16
- package/lib/helpers/extension-command-name.ts +3 -3
- package/lib/helpers/levenshtein-match.ts +20 -14
- package/lib/index.js +3 -12
- package/lib/jsonwp-proxy/protocol-converter.ts +58 -35
- package/lib/jsonwp-proxy/proxy-request.ts +26 -26
- package/lib/jsonwp-proxy/proxy.ts +74 -75
- package/lib/protocol/errors.ts +69 -88
- package/lib/protocol/helpers.ts +9 -5
- package/lib/protocol/protocol.ts +149 -107
- package/lib/protocol/routes.ts +17 -17
- package/lib/protocol/validators.ts +1 -3
- package/lib/test-pages/env.ts +9 -0
- package/lib/{express/static.ts → test-pages/handlers.ts} +10 -22
- package/lib/test-pages/index.ts +34 -0
- package/lib/test-pages/static-dir.ts +19 -0
- package/lib/test-pages/template.ts +17 -0
- package/lib/utils.ts +65 -0
- package/package.json +10 -13
- package/tsconfig.json +1 -0
- package/build/lib/express/crash.d.ts.map +0 -1
- package/build/lib/express/crash.js.map +0 -1
- package/build/lib/express/static.d.ts.map +0 -1
- package/build/lib/express/static.js.map +0 -1
- /package/build/lib/{express → test-pages}/crash.d.ts +0 -0
- /package/build/lib/{express → test-pages}/crash.js +0 -0
- /package/lib/{express → test-pages}/crash.ts +0 -0
- /package/{static → test-fixtures/static}/appium.png +0 -0
- /package/{static → test-fixtures/static}/favicon.ico +0 -0
- /package/{static → test-fixtures/static}/js/jquery.min.js +0 -0
- /package/{static → test-fixtures/static}/test/frameset.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig-app-banner.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig-scrollable.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig2.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig3.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig4.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig5.html +0 -0
- /package/{static → test-fixtures/static}/test/iframes.html +0 -0
- /package/{static → test-fixtures/static}/test/shadow-dom.html +0 -0
- /package/{static → test-fixtures/static}/test/subframe1.html +0 -0
- /package/{static → test-fixtures/static}/test/subframe2.html +0 -0
- /package/{static → test-fixtures/static}/test/subframe3.html +0 -0
- /package/{static → test-fixtures/static}/test/touch.html +0 -0
- /package/{static → test-fixtures/static}/test/welcome.html +0 -0
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import
|
|
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
|
|
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 (
|
|
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 =
|
|
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 (
|
|
115
|
+
if (typeof options === 'string') {
|
|
116
116
|
supportedAppExtensions = [options];
|
|
117
|
-
} else if (
|
|
117
|
+
} else if (Array.isArray(options)) {
|
|
118
118
|
supportedAppExtensions = options;
|
|
119
|
-
} else if (
|
|
119
|
+
} else if (util.isPlainObject(options)) {
|
|
120
120
|
supportedAppExtensions = options.supportedExtensions ?? [];
|
|
121
121
|
} else {
|
|
122
122
|
supportedAppExtensions = [];
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
if (
|
|
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
|
-
|
|
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 (!
|
|
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:
|
|
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(
|
|
221
|
-
|
|
222
|
-
|
|
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 (
|
|
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 (
|
|
273
|
+
if (typeof onPostProcess === 'function') {
|
|
271
274
|
const postProcessArg: PostProcessOptions = {
|
|
272
|
-
cachedAppInfo:
|
|
275
|
+
cachedAppInfo: structuredClone(cachedAppInfo) as CachedAppInfo | undefined,
|
|
273
276
|
isUrl,
|
|
274
277
|
originalAppLink,
|
|
275
|
-
headers:
|
|
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) &&
|
|
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 (
|
|
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 (
|
|
322
|
+
if (util.isPlainObject(input)) {
|
|
319
323
|
const resultObj: Record<string, unknown> = {};
|
|
320
|
-
for (const [key, value] of
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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(
|
|
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 (
|
|
401
|
+
if (defaultValue !== null && util.isEmpty(value)) {
|
|
396
402
|
return defaultValue;
|
|
397
403
|
}
|
|
398
|
-
return !
|
|
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(
|
|
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
|
-
|
|
458
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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 '${
|
|
534
|
+
`Defaulting to '${supportedAppExtensions[0]}'`,
|
|
522
535
|
);
|
|
523
|
-
resultingExt =
|
|
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 (
|
|
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 ||
|
|
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
|
+
}
|