@appium/base-driver 10.2.0 → 10.2.2
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/LICENSE +201 -0
- package/build/lib/basedriver/capabilities.js +7 -7
- package/build/lib/basedriver/capabilities.js.map +1 -1
- package/build/lib/basedriver/commands/event.d.ts +1 -1
- package/build/lib/basedriver/commands/event.d.ts.map +1 -1
- package/build/lib/basedriver/commands/execute.d.ts +1 -1
- package/build/lib/basedriver/commands/execute.d.ts.map +1 -1
- package/build/lib/basedriver/commands/find.d.ts +1 -1
- package/build/lib/basedriver/commands/find.d.ts.map +1 -1
- package/build/lib/basedriver/commands/mixin.d.ts +1 -1
- package/build/lib/basedriver/commands/mixin.d.ts.map +1 -1
- package/build/lib/basedriver/commands/timeout.d.ts +1 -1
- package/build/lib/basedriver/commands/timeout.d.ts.map +1 -1
- package/build/lib/basedriver/device-settings.d.ts +14 -23
- package/build/lib/basedriver/device-settings.d.ts.map +1 -1
- package/build/lib/basedriver/device-settings.js +11 -26
- package/build/lib/basedriver/device-settings.js.map +1 -1
- package/build/lib/basedriver/helpers.d.ts +36 -57
- package/build/lib/basedriver/helpers.d.ts.map +1 -1
- package/build/lib/basedriver/helpers.js +148 -239
- package/build/lib/basedriver/helpers.js.map +1 -1
- package/build/lib/basedriver/logger.d.ts +1 -2
- package/build/lib/basedriver/logger.d.ts.map +1 -1
- package/build/lib/basedriver/logger.js +2 -2
- package/build/lib/basedriver/logger.js.map +1 -1
- package/build/lib/basedriver/validation.d.ts.map +1 -1
- package/build/lib/basedriver/validation.js +3 -3
- package/build/lib/basedriver/validation.js.map +1 -1
- package/build/lib/constants.d.ts +1 -1
- package/build/lib/constants.d.ts.map +1 -1
- package/build/lib/express/crash.d.ts +8 -2
- package/build/lib/express/crash.d.ts.map +1 -1
- package/build/lib/express/crash.js +6 -0
- package/build/lib/express/crash.js.map +1 -1
- package/build/lib/express/express-logging.d.ts +12 -2
- package/build/lib/express/express-logging.d.ts.map +1 -1
- package/build/lib/express/express-logging.js +34 -26
- package/build/lib/express/express-logging.js.map +1 -1
- package/build/lib/express/idempotency.d.ts +4 -10
- package/build/lib/express/idempotency.d.ts.map +1 -1
- package/build/lib/express/idempotency.js +69 -73
- package/build/lib/express/idempotency.js.map +1 -1
- package/build/lib/express/logger.d.ts +1 -2
- package/build/lib/express/logger.d.ts.map +1 -1
- package/build/lib/express/logger.js +2 -2
- package/build/lib/express/logger.js.map +1 -1
- package/build/lib/express/middleware.d.ts +37 -41
- package/build/lib/express/middleware.d.ts.map +1 -1
- package/build/lib/express/middleware.js +48 -60
- package/build/lib/express/middleware.js.map +1 -1
- package/build/lib/express/server.d.ts +57 -101
- package/build/lib/express/server.d.ts.map +1 -1
- package/build/lib/express/server.js +51 -128
- package/build/lib/express/server.js.map +1 -1
- package/build/lib/express/static.d.ts +10 -5
- package/build/lib/express/static.d.ts.map +1 -1
- package/build/lib/express/static.js +32 -42
- package/build/lib/express/static.js.map +1 -1
- package/build/lib/express/websocket.d.ts +22 -6
- package/build/lib/express/websocket.d.ts.map +1 -1
- package/build/lib/express/websocket.js +10 -15
- package/build/lib/express/websocket.js.map +1 -1
- package/build/lib/helpers/capabilities.d.ts +4 -16
- package/build/lib/helpers/capabilities.d.ts.map +1 -1
- package/build/lib/helpers/capabilities.js +36 -48
- package/build/lib/helpers/capabilities.js.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.d.ts +42 -78
- package/build/lib/jsonwp-proxy/protocol-converter.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.js +87 -139
- package/build/lib/jsonwp-proxy/protocol-converter.js.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.d.ts +1 -1
- package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.js +2 -2
- package/build/lib/jsonwp-proxy/proxy.js.map +1 -1
- package/build/lib/jsonwp-status/status.d.ts +113 -158
- package/build/lib/jsonwp-status/status.d.ts.map +1 -1
- package/build/lib/jsonwp-status/status.js +10 -14
- package/build/lib/jsonwp-status/status.js.map +1 -1
- package/build/lib/protocol/bidi-commands.d.ts +31 -36
- package/build/lib/protocol/bidi-commands.d.ts.map +1 -1
- package/build/lib/protocol/bidi-commands.js +5 -5
- package/build/lib/protocol/bidi-commands.js.map +1 -1
- package/build/lib/protocol/errors.d.ts.map +1 -1
- package/build/lib/protocol/helpers.d.ts +7 -11
- package/build/lib/protocol/helpers.d.ts.map +1 -1
- package/build/lib/protocol/helpers.js +5 -9
- package/build/lib/protocol/helpers.js.map +1 -1
- package/build/lib/protocol/index.d.ts +4 -21
- package/build/lib/protocol/index.d.ts.map +1 -1
- package/build/lib/protocol/index.js.map +1 -1
- package/build/lib/protocol/protocol.d.ts +15 -1
- package/build/lib/protocol/protocol.d.ts.map +1 -1
- package/build/lib/protocol/protocol.js +50 -20
- package/build/lib/protocol/protocol.js.map +1 -1
- package/build/lib/protocol/routes.d.ts +8 -15
- package/build/lib/protocol/routes.d.ts.map +1 -1
- package/build/lib/protocol/routes.js +18 -33
- package/build/lib/protocol/routes.js.map +1 -1
- package/lib/basedriver/capabilities.ts +1 -1
- package/lib/basedriver/commands/event.ts +2 -2
- package/lib/basedriver/commands/execute.ts +2 -2
- package/lib/basedriver/commands/find.ts +2 -2
- package/lib/basedriver/commands/mixin.ts +1 -1
- package/lib/basedriver/commands/timeout.ts +2 -2
- package/lib/basedriver/{device-settings.js → device-settings.ts} +24 -35
- package/lib/basedriver/{helpers.js → helpers.ts} +208 -266
- package/lib/basedriver/logger.ts +3 -0
- package/lib/basedriver/validation.ts +2 -2
- package/lib/constants.ts +1 -1
- package/lib/express/crash.ts +15 -0
- package/lib/express/express-logging.ts +84 -0
- package/lib/express/{idempotency.js → idempotency.ts} +105 -89
- package/lib/express/logger.ts +3 -0
- package/lib/express/middleware.ts +187 -0
- package/lib/express/{server.js → server.ts} +175 -167
- package/lib/express/static.ts +77 -0
- package/lib/express/websocket.ts +81 -0
- package/lib/helpers/capabilities.ts +83 -0
- package/lib/jsonwp-proxy/protocol-converter.ts +284 -0
- package/lib/jsonwp-proxy/proxy.js +1 -1
- package/lib/jsonwp-status/{status.js → status.ts} +12 -15
- package/lib/protocol/{bidi-commands.js → bidi-commands.ts} +7 -5
- package/lib/protocol/errors.ts +1 -1
- package/lib/protocol/{helpers.js → helpers.ts} +8 -11
- package/lib/protocol/protocol.ts +57 -26
- package/lib/protocol/{routes.js → routes.ts} +29 -40
- package/package.json +11 -11
- package/tsconfig.json +3 -1
- package/lib/basedriver/logger.js +0 -4
- package/lib/express/crash.js +0 -11
- package/lib/express/express-logging.js +0 -60
- package/lib/express/logger.js +0 -4
- package/lib/express/middleware.js +0 -171
- package/lib/express/static.js +0 -76
- package/lib/express/websocket.js +0 -79
- package/lib/helpers/capabilities.js +0 -93
- package/lib/jsonwp-proxy/protocol-converter.js +0 -317
- /package/lib/protocol/{index.js → index.ts} +0 -0
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import logger from './logger';
|
|
3
|
+
import {log as logger} from './logger';
|
|
4
4
|
import {tempDir, fs, util, timing, node} from '@appium/support';
|
|
5
|
-
import {
|
|
5
|
+
import {LRUCache} from 'lru-cache';
|
|
6
6
|
import AsyncLock from 'async-lock';
|
|
7
7
|
import axios from 'axios';
|
|
8
8
|
import B from 'bluebird';
|
|
9
|
+
import type {
|
|
10
|
+
ConfigureAppOptions,
|
|
11
|
+
CachedAppInfo,
|
|
12
|
+
PostProcessOptions,
|
|
13
|
+
HTTPHeaders,
|
|
14
|
+
DriverHelpers,
|
|
15
|
+
} from '@appium/types';
|
|
16
|
+
import type {AxiosResponseHeaders, RawAxiosRequestHeaders} from 'axios';
|
|
17
|
+
import type {Readable} from 'node:stream';
|
|
9
18
|
|
|
10
19
|
// for compat with running tests transpiled and in-place
|
|
11
20
|
export const {version: BASEDRIVER_VER} = fs.readPackageJsonFrom(__dirname);
|
|
21
|
+
|
|
12
22
|
const CACHED_APPS_MAX_AGE_MS = 1000 * 60 * toNaturalNumber(60 * 24, 'APPIUM_APPS_CACHE_MAX_AGE');
|
|
13
23
|
const MAX_CACHED_APPS = toNaturalNumber(1024, 'APPIUM_APPS_CACHE_MAX_ITEMS');
|
|
14
24
|
const HTTP_STATUS_NOT_MODIFIED = 304;
|
|
@@ -16,8 +26,7 @@ const DEFAULT_REQ_HEADERS = Object.freeze({
|
|
|
16
26
|
'user-agent': `Appium (BaseDriver v${BASEDRIVER_VER})`,
|
|
17
27
|
});
|
|
18
28
|
const AVG_DOWNLOAD_SPEED_MEASUREMENT_THRESHOLD_SEC = 2;
|
|
19
|
-
|
|
20
|
-
const APPLICATIONS_CACHE = new LRUCache({
|
|
29
|
+
const APPLICATIONS_CACHE = new LRUCache<string, CachedAppInfoEntry>({
|
|
21
30
|
max: MAX_CACHED_APPS,
|
|
22
31
|
ttl: CACHED_APPS_MAX_AGE_MS, // expire after 24 hours
|
|
23
32
|
updateAgeOnGet: true,
|
|
@@ -44,65 +53,65 @@ process.on('exit', () => {
|
|
|
44
53
|
|
|
45
54
|
const appPaths = [...APPLICATIONS_CACHE.values()].map(({fullPath}) => fullPath);
|
|
46
55
|
logger.debug(
|
|
47
|
-
`Performing cleanup of ${appPaths.length}
|
|
48
|
-
util.pluralize('application', appPaths.length)
|
|
56
|
+
`Performing cleanup of ${util.pluralize('cached application', appPaths.length, true)}`
|
|
49
57
|
);
|
|
50
58
|
for (const appPath of appPaths) {
|
|
59
|
+
if (!appPath) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
51
62
|
try {
|
|
52
|
-
// @ts-ignore it's defined
|
|
53
63
|
fs.rimrafSync(appPath);
|
|
54
64
|
} catch (e) {
|
|
55
|
-
logger.warn(e.message);
|
|
65
|
+
logger.warn((e as Error).message);
|
|
56
66
|
}
|
|
57
67
|
}
|
|
58
68
|
});
|
|
59
69
|
|
|
60
70
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* - Manages caching logic
|
|
65
|
-
* - Downloads the app from a remote URL to the local filesystem
|
|
66
|
-
* - Determines package name
|
|
67
|
-
* - Checks basic requirements on the application package
|
|
71
|
+
* Performs initial application package configuration so the app is ready for driver use.
|
|
72
|
+
* Resolves local paths, downloads remote apps (http/https) with optional caching, and
|
|
73
|
+
* runs optional post-process or custom download hooks.
|
|
68
74
|
*
|
|
69
|
-
* @param
|
|
70
|
-
* @param
|
|
71
|
-
* @
|
|
75
|
+
* @param app - Path to a local app or URL of a downloadable app (http/https).
|
|
76
|
+
* @param options - Supported extensions and optional hooks. Either a single extension
|
|
77
|
+
* string, an array of extension strings, or {@link ConfigureAppOptions} (e.g.
|
|
78
|
+
* `supportedExtensions`, `onPostProcess`, `onDownload`).
|
|
79
|
+
* @returns Resolved path to the application (local path or path to downloaded/cached app).
|
|
80
|
+
* @throws {Error} If supported extensions are missing, the app path/URL is invalid, or download fails.
|
|
72
81
|
*/
|
|
73
82
|
export async function configureApp(
|
|
74
|
-
app,
|
|
75
|
-
options
|
|
76
|
-
) {
|
|
83
|
+
app: string,
|
|
84
|
+
options: string | string[] | ConfigureAppOptions = {} as ConfigureAppOptions
|
|
85
|
+
): Promise<string> {
|
|
77
86
|
if (!_.isString(app)) {
|
|
78
87
|
// immediately shortcircuit if not given an app
|
|
79
88
|
return '';
|
|
80
89
|
}
|
|
81
90
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const onPostProcess =
|
|
85
|
-
const onDownload =
|
|
91
|
+
let supportedAppExtensions: string[];
|
|
92
|
+
const opts = !_.isString(options) && !_.isArray(options) ? options : undefined;
|
|
93
|
+
const onPostProcess = opts?.onPostProcess;
|
|
94
|
+
const onDownload = opts?.onDownload;
|
|
86
95
|
|
|
87
96
|
if (_.isString(options)) {
|
|
88
97
|
supportedAppExtensions = [options];
|
|
89
98
|
} else if (_.isArray(options)) {
|
|
90
99
|
supportedAppExtensions = options;
|
|
91
100
|
} else if (_.isPlainObject(options)) {
|
|
92
|
-
supportedAppExtensions = options.supportedExtensions;
|
|
101
|
+
supportedAppExtensions = options.supportedExtensions ?? [];
|
|
102
|
+
} else {
|
|
103
|
+
supportedAppExtensions = [];
|
|
93
104
|
}
|
|
94
|
-
|
|
105
|
+
|
|
95
106
|
if (_.isEmpty(supportedAppExtensions)) {
|
|
96
107
|
throw new Error(`One or more supported app extensions must be provided`);
|
|
97
108
|
}
|
|
98
109
|
|
|
99
110
|
let newApp = app;
|
|
100
111
|
const originalAppLink = app;
|
|
101
|
-
let packageHash = null;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
/** @type {RemoteAppProps} */
|
|
105
|
-
const remoteAppProps = {
|
|
112
|
+
let packageHash: string | null = null;
|
|
113
|
+
let headers: AxiosResponseHeaders | RawAxiosRequestHeaders | undefined;
|
|
114
|
+
const remoteAppProps: RemoteAppProps = {
|
|
106
115
|
lastModified: null,
|
|
107
116
|
immutable: false,
|
|
108
117
|
maxAge: null,
|
|
@@ -129,9 +138,7 @@ export async function configureApp(
|
|
|
129
138
|
if (isUrl) {
|
|
130
139
|
// Use the app from remote URL
|
|
131
140
|
logger.info(`Using downloadable app '${newApp}'`);
|
|
132
|
-
const reqHeaders = {
|
|
133
|
-
...DEFAULT_REQ_HEADERS,
|
|
134
|
-
};
|
|
141
|
+
const reqHeaders = {...DEFAULT_REQ_HEADERS};
|
|
135
142
|
if (cachedAppInfo?.etag) {
|
|
136
143
|
reqHeaders['if-none-match'] = cachedAppInfo.etag;
|
|
137
144
|
} else if (cachedAppInfo?.lastModified) {
|
|
@@ -139,7 +146,9 @@ export async function configureApp(
|
|
|
139
146
|
}
|
|
140
147
|
logger.debug(`Request headers: ${JSON.stringify(reqHeaders)}`);
|
|
141
148
|
|
|
142
|
-
let
|
|
149
|
+
let result = await queryAppLink(newApp, reqHeaders);
|
|
150
|
+
headers = result.headers;
|
|
151
|
+
let {stream, status} = result;
|
|
143
152
|
logger.debug(`Response status: ${status}`);
|
|
144
153
|
try {
|
|
145
154
|
if (!_.isEmpty(headers)) {
|
|
@@ -149,21 +158,22 @@ export async function configureApp(
|
|
|
149
158
|
}
|
|
150
159
|
if (headers['last-modified']) {
|
|
151
160
|
logger.debug(`Last-Modified: ${headers['last-modified']}`);
|
|
152
|
-
remoteAppProps.lastModified = new Date(headers['last-modified']);
|
|
161
|
+
remoteAppProps.lastModified = new Date(headers['last-modified'] as string);
|
|
153
162
|
}
|
|
154
163
|
if (headers['cache-control']) {
|
|
155
164
|
logger.debug(`Cache-Control: ${headers['cache-control']}`);
|
|
156
|
-
remoteAppProps.immutable = /\bimmutable\b/i.test(headers['cache-control']);
|
|
157
|
-
const maxAgeMatch = /\bmax-age=(\d+)\b/i.exec(headers['cache-control']);
|
|
165
|
+
remoteAppProps.immutable = /\bimmutable\b/i.test(String(headers['cache-control']));
|
|
166
|
+
const maxAgeMatch = /\bmax-age=(\d+)\b/i.exec(String(headers['cache-control']));
|
|
158
167
|
if (maxAgeMatch) {
|
|
159
168
|
remoteAppProps.maxAge = parseInt(maxAgeMatch[1], 10);
|
|
160
169
|
}
|
|
161
170
|
}
|
|
162
171
|
}
|
|
163
172
|
if (cachedAppInfo && status === HTTP_STATUS_NOT_MODIFIED) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
173
|
+
const cachedPath = cachedAppInfo.fullPath ?? '';
|
|
174
|
+
if (cachedPath && (await isAppIntegrityOk(cachedPath, cachedAppInfo.integrity))) {
|
|
175
|
+
logger.info(`Reusing previously downloaded application at '${cachedPath}'`);
|
|
176
|
+
return verifyAppExtension(cachedPath, supportedAppExtensions);
|
|
167
177
|
}
|
|
168
178
|
logger.info(
|
|
169
179
|
`The application at '${cachedAppInfo.fullPath}' does not exist anymore ` +
|
|
@@ -174,17 +184,20 @@ export async function configureApp(
|
|
|
174
184
|
if (!stream.closed) {
|
|
175
185
|
stream.destroy();
|
|
176
186
|
}
|
|
177
|
-
|
|
187
|
+
result = await queryAppLink(newApp, {...DEFAULT_REQ_HEADERS});
|
|
188
|
+
stream = result.stream;
|
|
189
|
+
headers = result.headers;
|
|
190
|
+
status = result.status;
|
|
178
191
|
}
|
|
179
192
|
|
|
180
193
|
if (onDownload) {
|
|
181
194
|
newApp = await onDownload({
|
|
182
195
|
url: originalAppLink,
|
|
183
|
-
headers:
|
|
196
|
+
headers: _.clone(headers) as HTTPHeaders,
|
|
184
197
|
stream,
|
|
185
198
|
});
|
|
186
199
|
} else {
|
|
187
|
-
const fileName = determineFilename(headers, pathname, supportedAppExtensions);
|
|
200
|
+
const fileName = determineFilename(headers, pathname ?? '', supportedAppExtensions);
|
|
188
201
|
newApp = await fetchApp(stream, await tempDir.path({
|
|
189
202
|
prefix: fileName,
|
|
190
203
|
suffix: '',
|
|
@@ -214,13 +227,12 @@ export async function configureApp(
|
|
|
214
227
|
packageHash = await calculateFileIntegrity(newApp);
|
|
215
228
|
}
|
|
216
229
|
|
|
217
|
-
|
|
218
|
-
const storeAppInCache = async (appPathToCache) => {
|
|
230
|
+
const storeAppInCache = async (appPathToCache: string): Promise<string> => {
|
|
219
231
|
const cachedFullPath = cachedAppInfo?.fullPath;
|
|
220
232
|
if (cachedFullPath && cachedFullPath !== appPathToCache) {
|
|
221
233
|
await fs.rimraf(cachedFullPath);
|
|
222
234
|
}
|
|
223
|
-
const integrity = {};
|
|
235
|
+
const integrity: {file?: string; folder?: number} = {};
|
|
224
236
|
if ((await fs.stat(appPathToCache)).isDirectory()) {
|
|
225
237
|
integrity.folder = await calculateFolderIntegrity(appPathToCache);
|
|
226
238
|
} else {
|
|
@@ -237,15 +249,14 @@ export async function configureApp(
|
|
|
237
249
|
};
|
|
238
250
|
|
|
239
251
|
if (_.isFunction(onPostProcess)) {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
);
|
|
252
|
+
const postProcessArg: PostProcessOptions = {
|
|
253
|
+
cachedAppInfo: _.clone(cachedAppInfo) as CachedAppInfo | undefined,
|
|
254
|
+
isUrl,
|
|
255
|
+
originalAppLink,
|
|
256
|
+
headers: _.clone(headers) as HTTPHeaders,
|
|
257
|
+
appPath: newApp,
|
|
258
|
+
};
|
|
259
|
+
const result = await onPostProcess(postProcessArg);
|
|
249
260
|
return !result?.appPath || app === result?.appPath || !(await fs.exists(result?.appPath))
|
|
250
261
|
? newApp
|
|
251
262
|
: await storeAppInCache(result.appPath);
|
|
@@ -259,33 +270,35 @@ export async function configureApp(
|
|
|
259
270
|
}
|
|
260
271
|
|
|
261
272
|
/**
|
|
262
|
-
*
|
|
263
|
-
*
|
|
273
|
+
* Returns whether the given string looks like a package or bundle identifier
|
|
274
|
+
* (e.g. `com.example.app` or `org.company.AnotherApp`).
|
|
275
|
+
*
|
|
276
|
+
* @param app - Value to check (e.g. app path or bundle id).
|
|
277
|
+
* @returns `true` if the value matches a dot-separated identifier pattern.
|
|
264
278
|
*/
|
|
265
|
-
export function isPackageOrBundle(app) {
|
|
279
|
+
export function isPackageOrBundle(app: string): boolean {
|
|
266
280
|
return /^([a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+)+$/.test(app);
|
|
267
281
|
}
|
|
268
282
|
|
|
269
283
|
/**
|
|
270
|
-
*
|
|
271
|
-
*
|
|
284
|
+
* Recursively ensures both keys exist with the same value in objects and arrays.
|
|
285
|
+
* For each object, if `firstKey` exists its value is also set at `secondKey`, and vice versa.
|
|
272
286
|
*
|
|
273
|
-
*
|
|
274
|
-
|
|
275
|
-
* @param
|
|
276
|
-
* @
|
|
277
|
-
* @param {String} secondKey The second key to duplicate
|
|
287
|
+
* @param input - Object, array, or primitive to process (arrays/objects traversed recursively).
|
|
288
|
+
* @param firstKey - First key name to mirror.
|
|
289
|
+
* @param secondKey - Second key name to mirror.
|
|
290
|
+
* @returns A deep copy of `input` with both keys present where objects had either key.
|
|
278
291
|
*/
|
|
279
|
-
export function duplicateKeys(input, firstKey, secondKey) {
|
|
292
|
+
export function duplicateKeys<T>(input: T, firstKey: string, secondKey: string): T {
|
|
280
293
|
// If array provided, recursively call on all elements
|
|
281
294
|
if (_.isArray(input)) {
|
|
282
|
-
return input.map((item) => duplicateKeys(item, firstKey, secondKey));
|
|
295
|
+
return input.map((item) => duplicateKeys(item, firstKey, secondKey)) as T;
|
|
283
296
|
}
|
|
284
297
|
|
|
285
298
|
// If object, create duplicates for keys and then recursively call on values
|
|
286
299
|
if (_.isPlainObject(input)) {
|
|
287
|
-
const resultObj = {};
|
|
288
|
-
for (
|
|
300
|
+
const resultObj: Record<string, unknown> = {};
|
|
301
|
+
for (const [key, value] of _.toPairs(input as Record<string, unknown>)) {
|
|
289
302
|
const recursivelyCalledValue = duplicateKeys(value, firstKey, secondKey);
|
|
290
303
|
if (key === firstKey) {
|
|
291
304
|
resultObj[secondKey] = recursivelyCalledValue;
|
|
@@ -294,7 +307,7 @@ export function duplicateKeys(input, firstKey, secondKey) {
|
|
|
294
307
|
}
|
|
295
308
|
resultObj[key] = recursivelyCalledValue;
|
|
296
309
|
}
|
|
297
|
-
return resultObj;
|
|
310
|
+
return resultObj as T;
|
|
298
311
|
}
|
|
299
312
|
|
|
300
313
|
// Base case. Return primitives without doing anything.
|
|
@@ -302,13 +315,14 @@ export function duplicateKeys(input, firstKey, secondKey) {
|
|
|
302
315
|
}
|
|
303
316
|
|
|
304
317
|
/**
|
|
305
|
-
*
|
|
306
|
-
*
|
|
318
|
+
* Normalizes a capability value to a string array. If already an array, returns it;
|
|
319
|
+
* if a string, parses as JSON array when possible, otherwise returns a single-element array.
|
|
307
320
|
*
|
|
308
|
-
* @param
|
|
309
|
-
* @returns
|
|
321
|
+
* @param capValue - Capability value: string (including JSON array like `"[\"a\",\"b\"]"`) or string[].
|
|
322
|
+
* @returns Array of strings.
|
|
323
|
+
* @throws {TypeError} If value is not a string/array or JSON parsing fails for array-like input.
|
|
310
324
|
*/
|
|
311
|
-
export function parseCapsArray(capValue) {
|
|
325
|
+
export function parseCapsArray(capValue: string | string[]): string[] {
|
|
312
326
|
if (_.isArray(capValue)) {
|
|
313
327
|
return capValue;
|
|
314
328
|
}
|
|
@@ -319,7 +333,7 @@ export function parseCapsArray(capValue) {
|
|
|
319
333
|
return parsed;
|
|
320
334
|
}
|
|
321
335
|
} catch (e) {
|
|
322
|
-
const message = `Failed to parse capability as JSON array: ${e.message}`;
|
|
336
|
+
const message = `Failed to parse capability as JSON array: ${(e as Error).message}`;
|
|
323
337
|
if (_.isString(capValue) && _.startsWith(_.trimStart(capValue), '[')) {
|
|
324
338
|
throw new TypeError(message);
|
|
325
339
|
}
|
|
@@ -332,86 +346,126 @@ export function parseCapsArray(capValue) {
|
|
|
332
346
|
}
|
|
333
347
|
|
|
334
348
|
/**
|
|
335
|
-
*
|
|
349
|
+
* Builds a short log prefix for a driver instance (e.g. `UiAutomator2@a1b2`).
|
|
336
350
|
*
|
|
337
|
-
* @param
|
|
338
|
-
* @param
|
|
339
|
-
*
|
|
340
|
-
* @returns {string}
|
|
351
|
+
* @param obj - Driver or other object; its constructor name and a short id are used.
|
|
352
|
+
* @param _sessionId - Deprecated and unused; kept for {@link DriverHelpers} interface compatibility.
|
|
353
|
+
* @returns Prefix string like `DriverName@xxxx`, or `UnknownDriver@????` if `obj` is null.
|
|
341
354
|
*/
|
|
342
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
343
|
-
export function generateDriverLogPrefix(obj,
|
|
355
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- DriverHelpers interface
|
|
356
|
+
export function generateDriverLogPrefix(obj: object | null, _sessionId?: string | null): string {
|
|
357
|
+
if (!obj) {
|
|
358
|
+
// This should not happen
|
|
359
|
+
return 'UnknownDriver@????';
|
|
360
|
+
}
|
|
344
361
|
return `${obj.constructor.name}@${node.getObjectId(obj).substring(0, 4)}`;
|
|
345
362
|
}
|
|
346
363
|
|
|
364
|
+
// #region Private types and helpers
|
|
365
|
+
interface RemoteAppProps {
|
|
366
|
+
lastModified: Date | null;
|
|
367
|
+
immutable: boolean;
|
|
368
|
+
maxAge: number | null;
|
|
369
|
+
etag: string | null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
interface RemoteAppData {
|
|
373
|
+
status: number;
|
|
374
|
+
stream: Readable;
|
|
375
|
+
headers: AxiosResponseHeaders | RawAxiosRequestHeaders;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function parseAppLink(appLink: string): URL | {protocol?: string; pathname?: string; href?: string; search?: string} {
|
|
379
|
+
try {
|
|
380
|
+
return new URL(appLink);
|
|
381
|
+
} catch {
|
|
382
|
+
return {};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function isEnvOptionEnabled(optionName: string, defaultValue: boolean | null = null): boolean {
|
|
387
|
+
const value = process.env[optionName];
|
|
388
|
+
if (!_.isNull(defaultValue) && _.isEmpty(value)) {
|
|
389
|
+
return defaultValue;
|
|
390
|
+
}
|
|
391
|
+
return !_.isEmpty(value) && !['0', 'false', 'no'].includes(_.toLower(value));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function isSupportedUrl(app: string): boolean {
|
|
395
|
+
try {
|
|
396
|
+
const {protocol} = parseAppLink(app);
|
|
397
|
+
return ['http:', 'https:'].includes(protocol ?? '');
|
|
398
|
+
} catch {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
347
403
|
/**
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
* @param {string} appLink The URL to download an app from
|
|
352
|
-
* @param {import('axios').RawAxiosRequestHeaders} reqHeaders Additional HTTP request headers
|
|
353
|
-
* @returns {Promise<RemoteAppData>}
|
|
404
|
+
* Transforms the given app link to the cache key.
|
|
405
|
+
* Necessary to properly cache apps having the same address but different query strings,
|
|
406
|
+
* e.g. ones stored in S3 using presigned URLs.
|
|
354
407
|
*/
|
|
355
|
-
|
|
408
|
+
function toCacheKey(app: string): string {
|
|
409
|
+
if (!isEnvOptionEnabled('APPIUM_APPS_CACHE_IGNORE_URL_QUERY') || !isSupportedUrl(app)) {
|
|
410
|
+
return app;
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
const parsed = parseAppLink(app);
|
|
414
|
+
const href = 'href' in parsed ? parsed.href : undefined;
|
|
415
|
+
const search = 'search' in parsed ? parsed.search : undefined;
|
|
416
|
+
if (href && search) {
|
|
417
|
+
return href.replace(search, '');
|
|
418
|
+
}
|
|
419
|
+
if (href) {
|
|
420
|
+
return href;
|
|
421
|
+
}
|
|
422
|
+
} catch {
|
|
423
|
+
// ignore
|
|
424
|
+
}
|
|
425
|
+
return app;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function queryAppLink(appLink: string, reqHeaders: RawAxiosRequestHeaders): Promise<RemoteAppData> {
|
|
356
429
|
const url = new URL(appLink);
|
|
357
430
|
// Extract credentials, then remove them from the URL for axios
|
|
358
431
|
const {username, password} = url;
|
|
359
432
|
url.username = '';
|
|
360
433
|
url.password = '';
|
|
361
434
|
const axiosUrl = url.href;
|
|
362
|
-
|
|
363
|
-
const axiosAuth = username ? {
|
|
364
|
-
username,
|
|
365
|
-
password,
|
|
366
|
-
} : undefined;
|
|
367
|
-
/**
|
|
368
|
-
* @type {import('axios').RawAxiosRequestConfig}
|
|
369
|
-
*/
|
|
435
|
+
const axiosAuth = username ? {username, password} : undefined;
|
|
370
436
|
const requestOpts = {
|
|
371
437
|
url: axiosUrl,
|
|
372
438
|
auth: axiosAuth,
|
|
373
|
-
responseType: 'stream',
|
|
439
|
+
responseType: 'stream' as const,
|
|
374
440
|
timeout: APP_DOWNLOAD_TIMEOUT_MS,
|
|
375
|
-
validateStatus: (status) =>
|
|
441
|
+
validateStatus: (status: number) =>
|
|
376
442
|
(status >= 200 && status < 300) || status === HTTP_STATUS_NOT_MODIFIED,
|
|
377
443
|
headers: reqHeaders,
|
|
378
444
|
};
|
|
379
445
|
try {
|
|
380
446
|
const {data: stream, headers, status} = await axios(requestOpts);
|
|
381
|
-
return {
|
|
382
|
-
stream,
|
|
383
|
-
headers,
|
|
384
|
-
status,
|
|
385
|
-
};
|
|
447
|
+
return {stream, headers, status};
|
|
386
448
|
} catch (err) {
|
|
387
|
-
throw new Error(`Cannot download the app from ${axiosUrl}: ${err.message}`);
|
|
449
|
+
throw new Error(`Cannot download the app from ${axiosUrl}: ${(err as Error).message}`);
|
|
388
450
|
}
|
|
389
451
|
}
|
|
390
452
|
|
|
391
|
-
|
|
392
|
-
* Retrieves app payload from the given stream. Also meters the download performance.
|
|
393
|
-
*
|
|
394
|
-
* @param {import('stream').Readable} srcStream The incoming stream
|
|
395
|
-
* @param {string} dstPath The target file path to be written
|
|
396
|
-
* @returns {Promise<string>} The same dstPath
|
|
397
|
-
* @throws {Error} If there was a failure while downloading the file
|
|
398
|
-
*/
|
|
399
|
-
async function fetchApp(srcStream, dstPath) {
|
|
453
|
+
async function fetchApp(srcStream: Readable, dstPath: string): Promise<string> {
|
|
400
454
|
const timer = new timing.Timer().start();
|
|
401
455
|
try {
|
|
402
456
|
const writer = fs.createWriteStream(dstPath);
|
|
403
457
|
srcStream.pipe(writer);
|
|
404
458
|
|
|
405
|
-
await new B((resolve, reject) => {
|
|
459
|
+
await new B<void>((resolve, reject) => {
|
|
406
460
|
srcStream.once('error', reject);
|
|
407
|
-
writer.once('finish', resolve);
|
|
408
|
-
writer.once('error', (e) => {
|
|
461
|
+
writer.once('finish', () => resolve());
|
|
462
|
+
writer.once('error', (e: Error) => {
|
|
409
463
|
srcStream.unpipe(writer);
|
|
410
464
|
reject(e);
|
|
411
465
|
});
|
|
412
466
|
});
|
|
413
467
|
} catch (err) {
|
|
414
|
-
throw new Error(`Cannot fetch the application: ${err.message}`);
|
|
468
|
+
throw new Error(`Cannot fetch the application: ${(err as Error).message}`);
|
|
415
469
|
}
|
|
416
470
|
|
|
417
471
|
const secondsElapsed = timer.getDuration().asSeconds;
|
|
@@ -429,70 +483,20 @@ async function fetchApp(srcStream, dstPath) {
|
|
|
429
483
|
return dstPath;
|
|
430
484
|
}
|
|
431
485
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
*
|
|
438
|
-
* @param {string} app App link.
|
|
439
|
-
* @returns {string} Transformed app link or the original arg if
|
|
440
|
-
* no transformation is needed.
|
|
441
|
-
*/
|
|
442
|
-
function toCacheKey(app) {
|
|
443
|
-
if (!isEnvOptionEnabled('APPIUM_APPS_CACHE_IGNORE_URL_QUERY') || !isSupportedUrl(app)) {
|
|
444
|
-
return app;
|
|
445
|
-
}
|
|
446
|
-
try {
|
|
447
|
-
const {href, search} = parseAppLink(app);
|
|
448
|
-
if (href && search) {
|
|
449
|
-
return href.replace(search, '');
|
|
450
|
-
}
|
|
451
|
-
if (href) {
|
|
452
|
-
return href;
|
|
453
|
-
}
|
|
454
|
-
} catch {}
|
|
455
|
-
return app;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Safely parses the given app link to a URL object
|
|
460
|
-
*
|
|
461
|
-
* @param {string} appLink
|
|
462
|
-
* @returns {URL|import('@appium/types').StringRecord} Parsed URL object
|
|
463
|
-
* or an empty object if the parsing has failed
|
|
464
|
-
*/
|
|
465
|
-
function parseAppLink(appLink) {
|
|
466
|
-
try {
|
|
467
|
-
return new URL(appLink);
|
|
468
|
-
} catch {
|
|
469
|
-
return {};
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* Tries to determine the file name of the payload that is going
|
|
475
|
-
* to be downloaded from an URL
|
|
476
|
-
*
|
|
477
|
-
* @param {import('axios').RawAxiosRequestHeaders} headers
|
|
478
|
-
* @param {string} pathname
|
|
479
|
-
* @param {string[]} supportedAppExtensions
|
|
480
|
-
* @returns {string}
|
|
481
|
-
*/
|
|
482
|
-
function determineFilename(headers, pathname, supportedAppExtensions) {
|
|
486
|
+
function determineFilename(
|
|
487
|
+
headers: AxiosResponseHeaders | RawAxiosRequestHeaders,
|
|
488
|
+
pathname: string,
|
|
489
|
+
supportedAppExtensions: string[]
|
|
490
|
+
): string {
|
|
483
491
|
const basename = fs.sanitizeName(path.basename(decodeURIComponent(pathname ?? '')), {
|
|
484
492
|
replacement: SANITIZE_REPLACEMENT,
|
|
485
493
|
});
|
|
486
494
|
const extname = path.extname(basename);
|
|
487
|
-
if (headers['content-disposition'] && /^attachment/i.test(
|
|
488
|
-
/** @type {string} */ (headers['content-disposition']
|
|
489
|
-
))) {
|
|
495
|
+
if (headers['content-disposition'] && /^attachment/i.test(String(headers['content-disposition']))) {
|
|
490
496
|
logger.debug(`Content-Disposition: ${headers['content-disposition']}`);
|
|
491
|
-
const match = /filename="([^"]+)/i.exec(
|
|
497
|
+
const match = /filename="([^"]+)/i.exec(String(headers['content-disposition']));
|
|
492
498
|
if (match) {
|
|
493
|
-
return fs.sanitizeName(match[1], {
|
|
494
|
-
replacement: SANITIZE_REPLACEMENT,
|
|
495
|
-
});
|
|
499
|
+
return fs.sanitizeName(match[1], {replacement: SANITIZE_REPLACEMENT});
|
|
496
500
|
}
|
|
497
501
|
}
|
|
498
502
|
|
|
@@ -506,63 +510,12 @@ function determineFilename(headers, pathname, supportedAppExtensions) {
|
|
|
506
510
|
`The current file extension '${resultingExt}' is not supported. ` +
|
|
507
511
|
`Defaulting to '${_.first(supportedAppExtensions)}'`
|
|
508
512
|
);
|
|
509
|
-
resultingExt =
|
|
513
|
+
resultingExt = _.first(supportedAppExtensions) as string;
|
|
510
514
|
}
|
|
511
515
|
return `${resultingName}${resultingExt}`;
|
|
512
516
|
}
|
|
513
517
|
|
|
514
|
-
|
|
515
|
-
* Checks whether we can threat the given app link
|
|
516
|
-
* as a URL,
|
|
517
|
-
*
|
|
518
|
-
* @param {string} app
|
|
519
|
-
* @returns {boolean} True if app is a supported URL
|
|
520
|
-
*/
|
|
521
|
-
function isSupportedUrl(app) {
|
|
522
|
-
try {
|
|
523
|
-
const {protocol} = parseAppLink(app);
|
|
524
|
-
return ['http:', 'https:'].includes(protocol);
|
|
525
|
-
} catch {
|
|
526
|
-
return false;
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* Check if the given environment option is enabled
|
|
532
|
-
*
|
|
533
|
-
* @param {string} optionName Option name
|
|
534
|
-
* @param {boolean|null} [defaultValue=null] The value to return if the given env value
|
|
535
|
-
* is not set explicitly
|
|
536
|
-
* @returns {boolean} True if the option is enabled
|
|
537
|
-
*/
|
|
538
|
-
function isEnvOptionEnabled(optionName, defaultValue = null) {
|
|
539
|
-
const value = process.env[optionName];
|
|
540
|
-
if (!_.isNull(defaultValue) && _.isEmpty(value)) {
|
|
541
|
-
return defaultValue;
|
|
542
|
-
}
|
|
543
|
-
return !_.isEmpty(value) && !['0', 'false', 'no'].includes(_.toLower(value));
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
/**
|
|
547
|
-
*
|
|
548
|
-
* @param {string} [envVarName]
|
|
549
|
-
* @param {number} defaultValue
|
|
550
|
-
* @returns {number}
|
|
551
|
-
*/
|
|
552
|
-
function toNaturalNumber(defaultValue, envVarName) {
|
|
553
|
-
if (!envVarName || _.isUndefined(process.env[envVarName])) {
|
|
554
|
-
return defaultValue;
|
|
555
|
-
}
|
|
556
|
-
const num = parseInt(`${process.env[envVarName]}`, 10);
|
|
557
|
-
return num > 0 ? num : defaultValue;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* @param {string} app
|
|
562
|
-
* @param {string[]} supportedAppExtensions
|
|
563
|
-
* @returns {string}
|
|
564
|
-
*/
|
|
565
|
-
function verifyAppExtension(app, supportedAppExtensions) {
|
|
518
|
+
function verifyAppExtension(app: string, supportedAppExtensions: string[]): string {
|
|
566
519
|
if (supportedAppExtensions.map(_.toLower).includes(_.toLower(path.extname(app)))) {
|
|
567
520
|
return app;
|
|
568
521
|
}
|
|
@@ -573,28 +526,18 @@ function verifyAppExtension(app, supportedAppExtensions) {
|
|
|
573
526
|
);
|
|
574
527
|
}
|
|
575
528
|
|
|
576
|
-
|
|
577
|
-
* @param {string} folderPath
|
|
578
|
-
* @returns {Promise<number>}
|
|
579
|
-
*/
|
|
580
|
-
async function calculateFolderIntegrity(folderPath) {
|
|
529
|
+
async function calculateFolderIntegrity(folderPath: string): Promise<number> {
|
|
581
530
|
return (await fs.glob('**/*', {cwd: folderPath})).length;
|
|
582
531
|
}
|
|
583
532
|
|
|
584
|
-
|
|
585
|
-
* @param {string} filePath
|
|
586
|
-
* @returns {Promise<string>}
|
|
587
|
-
*/
|
|
588
|
-
async function calculateFileIntegrity(filePath) {
|
|
533
|
+
async function calculateFileIntegrity(filePath: string): Promise<string> {
|
|
589
534
|
return await fs.hash(filePath);
|
|
590
535
|
}
|
|
591
536
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
*/
|
|
597
|
-
async function isAppIntegrityOk(currentPath, expectedIntegrity = {}) {
|
|
537
|
+
async function isAppIntegrityOk(
|
|
538
|
+
currentPath: string,
|
|
539
|
+
expectedIntegrity: {file?: string; folder?: number} = {}
|
|
540
|
+
): Promise<boolean> {
|
|
598
541
|
if (!(await fs.exists(currentPath))) {
|
|
599
542
|
return false;
|
|
600
543
|
}
|
|
@@ -607,30 +550,29 @@ async function isAppIntegrityOk(currentPath, expectedIntegrity = {}) {
|
|
|
607
550
|
// more precise, but we don't need to be very precise here and also don't want to
|
|
608
551
|
// overuse RAM and have a performance drop.
|
|
609
552
|
return (await fs.stat(currentPath)).isDirectory()
|
|
610
|
-
? (await calculateFolderIntegrity(currentPath)) >= expectedIntegrity?.folder
|
|
553
|
+
? (await calculateFolderIntegrity(currentPath)) >= (expectedIntegrity?.folder ?? 0)
|
|
611
554
|
: (await calculateFileIntegrity(currentPath)) === expectedIntegrity?.file;
|
|
612
555
|
}
|
|
613
556
|
|
|
614
|
-
|
|
557
|
+
function toNaturalNumber(defaultValue: number, envVarName?: string): number {
|
|
558
|
+
if (!envVarName || _.isUndefined(process.env[envVarName])) {
|
|
559
|
+
return defaultValue;
|
|
560
|
+
}
|
|
561
|
+
const num = parseInt(`${process.env[envVarName]}`, 10);
|
|
562
|
+
return num > 0 ? num : defaultValue;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** Cache value we store (extends CachedAppInfo with optional packageHash) */
|
|
566
|
+
interface CachedAppInfoEntry extends Omit<CachedAppInfo, 'packageHash'> {
|
|
567
|
+
packageHash?: string | null;
|
|
568
|
+
fullPath?: string;
|
|
569
|
+
}
|
|
570
|
+
// #endregion
|
|
571
|
+
|
|
615
572
|
export default {
|
|
616
573
|
configureApp,
|
|
617
574
|
isPackageOrBundle,
|
|
618
575
|
duplicateKeys,
|
|
619
576
|
parseCapsArray,
|
|
620
577
|
generateDriverLogPrefix,
|
|
621
|
-
};
|
|
622
|
-
|
|
623
|
-
/**
|
|
624
|
-
* @typedef RemoteAppProps
|
|
625
|
-
* @property {Date?} lastModified
|
|
626
|
-
* @property {boolean} immutable
|
|
627
|
-
* @property {number?} maxAge
|
|
628
|
-
* @property {string?} etag
|
|
629
|
-
*/
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* @typedef RemoteAppData Properties of the remote application (e.g. GET HTTP response) to be downloaded.
|
|
633
|
-
* @property {number} status The HTTP status of the response
|
|
634
|
-
* @property {import('stream').Readable} stream The HTTP response body represented as readable stream
|
|
635
|
-
* @property {import('axios').RawAxiosResponseHeaders | import('axios').AxiosResponseHeaders} headers HTTP response headers
|
|
636
|
-
*/
|
|
578
|
+
} satisfies DriverHelpers;
|