@appium/base-driver 9.6.0 → 9.8.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/helpers.d.ts.map +1 -1
- package/build/lib/basedriver/helpers.js +207 -119
- package/build/lib/basedriver/helpers.js.map +1 -1
- package/build/lib/express/middleware.d.ts +46 -7
- package/build/lib/express/middleware.d.ts.map +1 -1
- package/build/lib/express/middleware.js +65 -2
- package/build/lib/express/middleware.js.map +1 -1
- package/build/lib/express/server.d.ts +2 -1
- package/build/lib/express/server.d.ts.map +1 -1
- package/build/lib/express/server.js +5 -2
- package/build/lib/express/server.js.map +1 -1
- package/build/lib/express/websocket.d.ts +0 -3
- package/build/lib/express/websocket.d.ts.map +1 -1
- package/build/lib/express/websocket.js +0 -27
- package/build/lib/express/websocket.js.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.js +6 -6
- package/build/lib/jsonwp-proxy/protocol-converter.js.map +1 -1
- package/lib/basedriver/helpers.js +215 -125
- package/lib/express/middleware.js +70 -16
- package/lib/express/server.js +6 -1
- package/lib/express/websocket.js +0 -27
- package/lib/jsonwp-proxy/protocol-converter.js +10 -6
- package/package.json +8 -8
|
@@ -13,8 +13,8 @@ export const {version: BASEDRIVER_VER} = fs.readPackageJsonFrom(__dirname);
|
|
|
13
13
|
const IPA_EXT = '.ipa';
|
|
14
14
|
const ZIP_EXTS = new Set(['.zip', IPA_EXT]);
|
|
15
15
|
const ZIP_MIME_TYPES = ['application/zip', 'application/x-zip-compressed', 'multipart/x-zip'];
|
|
16
|
-
const
|
|
17
|
-
const MAX_CACHED_APPS = 1024;
|
|
16
|
+
const CACHED_APPS_MAX_AGE_MS = 1000 * 60 * toNaturalNumber(60 * 24, 'APPIUM_APPS_CACHE_MAX_AGE');
|
|
17
|
+
const MAX_CACHED_APPS = toNaturalNumber(1024, 'APPIUM_APPS_CACHE_MAX_ITEMS');
|
|
18
18
|
const HTTP_STATUS_NOT_MODIFIED = 304;
|
|
19
19
|
const DEFAULT_REQ_HEADERS = Object.freeze({
|
|
20
20
|
'user-agent': `Appium (BaseDriver v${BASEDRIVER_VER})`,
|
|
@@ -23,12 +23,12 @@ const AVG_DOWNLOAD_SPEED_MEASUREMENT_THRESHOLD_SEC = 2;
|
|
|
23
23
|
/** @type {LRUCache<string, import('@appium/types').CachedAppInfo>} */
|
|
24
24
|
const APPLICATIONS_CACHE = new LRUCache({
|
|
25
25
|
max: MAX_CACHED_APPS,
|
|
26
|
-
ttl:
|
|
26
|
+
ttl: CACHED_APPS_MAX_AGE_MS, // expire after 24 hours
|
|
27
27
|
updateAgeOnGet: true,
|
|
28
28
|
dispose: ({fullPath}, app) => {
|
|
29
29
|
logger.info(
|
|
30
30
|
`The application '${app}' cached at '${fullPath}' has ` +
|
|
31
|
-
`expired after ${
|
|
31
|
+
`expired after ${CACHED_APPS_MAX_AGE_MS}ms`
|
|
32
32
|
);
|
|
33
33
|
if (fullPath) {
|
|
34
34
|
fs.rimraf(fullPath);
|
|
@@ -61,60 +61,6 @@ process.on('exit', () => {
|
|
|
61
61
|
}
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
/**
|
|
65
|
-
* @param {string} app
|
|
66
|
-
* @param {string[]} supportedAppExtensions
|
|
67
|
-
* @returns {string}
|
|
68
|
-
*/
|
|
69
|
-
function verifyAppExtension(app, supportedAppExtensions) {
|
|
70
|
-
if (supportedAppExtensions.map(_.toLower).includes(_.toLower(path.extname(app)))) {
|
|
71
|
-
return app;
|
|
72
|
-
}
|
|
73
|
-
throw new Error(
|
|
74
|
-
`New app path '${app}' did not have ` +
|
|
75
|
-
`${util.pluralize('extension', supportedAppExtensions.length, false)}: ` +
|
|
76
|
-
supportedAppExtensions
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* @param {string} folderPath
|
|
82
|
-
* @returns {Promise<number>}
|
|
83
|
-
*/
|
|
84
|
-
async function calculateFolderIntegrity(folderPath) {
|
|
85
|
-
return (await fs.glob('**/*', {cwd: folderPath})).length;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* @param {string} filePath
|
|
90
|
-
* @returns {Promise<string>}
|
|
91
|
-
*/
|
|
92
|
-
async function calculateFileIntegrity(filePath) {
|
|
93
|
-
return await fs.hash(filePath);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* @param {string} currentPath
|
|
98
|
-
* @param {import('@appium/types').StringRecord} expectedIntegrity
|
|
99
|
-
* @returns {Promise<boolean>}
|
|
100
|
-
*/
|
|
101
|
-
async function isAppIntegrityOk(currentPath, expectedIntegrity = {}) {
|
|
102
|
-
if (!(await fs.exists(currentPath))) {
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Folder integrity check is simple:
|
|
107
|
-
// Verify the previous amount of files is not greater than the current one.
|
|
108
|
-
// We don't want to use equality comparison because of an assumption that the OS might
|
|
109
|
-
// create some unwanted service files/cached inside of that folder or its subfolders.
|
|
110
|
-
// Ofc, validating the hash sum of each file (or at least of file path) would be much
|
|
111
|
-
// more precise, but we don't need to be very precise here and also don't want to
|
|
112
|
-
// overuse RAM and have a performance drop.
|
|
113
|
-
return (await fs.stat(currentPath)).isDirectory()
|
|
114
|
-
? (await calculateFolderIntegrity(currentPath)) >= expectedIntegrity?.folder
|
|
115
|
-
: (await calculateFileIntegrity(currentPath)) === expectedIntegrity?.file;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
64
|
/**
|
|
119
65
|
*
|
|
120
66
|
* @param {string} app
|
|
@@ -145,6 +91,7 @@ export async function configureApp(
|
|
|
145
91
|
}
|
|
146
92
|
|
|
147
93
|
let newApp = app;
|
|
94
|
+
const originalAppLink = app;
|
|
148
95
|
let shouldUnzipApp = false;
|
|
149
96
|
let packageHash = null;
|
|
150
97
|
/** @type {import('axios').AxiosResponse['headers']|undefined} */
|
|
@@ -156,15 +103,16 @@ export async function configureApp(
|
|
|
156
103
|
maxAge: null,
|
|
157
104
|
etag: null,
|
|
158
105
|
};
|
|
159
|
-
const {protocol, pathname} =
|
|
160
|
-
const isUrl =
|
|
106
|
+
const {protocol, pathname} = parseAppLink(app);
|
|
107
|
+
const isUrl = isSupportedUrl(app);
|
|
108
|
+
const appCacheKey = toCacheKey(app);
|
|
161
109
|
|
|
162
|
-
const cachedAppInfo = APPLICATIONS_CACHE.get(
|
|
110
|
+
const cachedAppInfo = APPLICATIONS_CACHE.get(appCacheKey);
|
|
163
111
|
if (cachedAppInfo) {
|
|
164
112
|
logger.debug(`Cached app data: ${JSON.stringify(cachedAppInfo, null, 2)}`);
|
|
165
113
|
}
|
|
166
114
|
|
|
167
|
-
return await APPLICATIONS_CACHE_GUARD.acquire(
|
|
115
|
+
return await APPLICATIONS_CACHE_GUARD.acquire(appCacheKey, async () => {
|
|
168
116
|
if (isUrl) {
|
|
169
117
|
// Use the app from remote URL
|
|
170
118
|
logger.info(`Using downloadable app '${newApp}'`);
|
|
@@ -208,7 +156,7 @@ export async function configureApp(
|
|
|
208
156
|
`The application at '${cachedAppInfo.fullPath}' does not exist anymore ` +
|
|
209
157
|
`or its integrity has been damaged. Deleting it from the internal cache`
|
|
210
158
|
);
|
|
211
|
-
APPLICATIONS_CACHE.delete(
|
|
159
|
+
APPLICATIONS_CACHE.delete(appCacheKey);
|
|
212
160
|
|
|
213
161
|
if (!stream.closed) {
|
|
214
162
|
stream.destroy();
|
|
@@ -269,6 +217,7 @@ export async function configureApp(
|
|
|
269
217
|
}
|
|
270
218
|
newApp = onDownload
|
|
271
219
|
? await onDownload({
|
|
220
|
+
url: originalAppLink,
|
|
272
221
|
headers: /** @type {import('@appium/types').HTTPHeaders} */ (_.clone(headers)),
|
|
273
222
|
stream,
|
|
274
223
|
})
|
|
@@ -316,7 +265,7 @@ export async function configureApp(
|
|
|
316
265
|
`The application at '${fullPath}' does not exist anymore ` +
|
|
317
266
|
`or its integrity has been damaged. Deleting it from the cache`
|
|
318
267
|
);
|
|
319
|
-
APPLICATIONS_CACHE.delete(
|
|
268
|
+
APPLICATIONS_CACHE.delete(appCacheKey);
|
|
320
269
|
}
|
|
321
270
|
const tmpRoot = await tempDir.openDir();
|
|
322
271
|
try {
|
|
@@ -347,7 +296,7 @@ export async function configureApp(
|
|
|
347
296
|
} else {
|
|
348
297
|
integrity.file = await calculateFileIntegrity(appPathToCache);
|
|
349
298
|
}
|
|
350
|
-
APPLICATIONS_CACHE.set(
|
|
299
|
+
APPLICATIONS_CACHE.set(appCacheKey, {
|
|
351
300
|
...remoteAppProps,
|
|
352
301
|
timestamp: Date.now(),
|
|
353
302
|
packageHash,
|
|
@@ -362,6 +311,7 @@ export async function configureApp(
|
|
|
362
311
|
/** @type {import('@appium/types').PostProcessOptions<import('axios').AxiosResponseHeaders>} */ ({
|
|
363
312
|
cachedAppInfo: _.clone(cachedAppInfo),
|
|
364
313
|
isUrl,
|
|
314
|
+
originalAppLink,
|
|
365
315
|
headers: _.clone(headers),
|
|
366
316
|
appPath: newApp,
|
|
367
317
|
})
|
|
@@ -372,12 +322,94 @@ export async function configureApp(
|
|
|
372
322
|
}
|
|
373
323
|
|
|
374
324
|
verifyAppExtension(newApp, supportedAppExtensions);
|
|
375
|
-
return
|
|
325
|
+
return appCacheKey !== toCacheKey(newApp) && (packageHash || _.values(remoteAppProps).some(Boolean))
|
|
376
326
|
? await storeAppInCache(newApp)
|
|
377
327
|
: newApp;
|
|
378
328
|
});
|
|
379
329
|
}
|
|
380
330
|
|
|
331
|
+
/**
|
|
332
|
+
* @param {string} app
|
|
333
|
+
* @returns {boolean}
|
|
334
|
+
*/
|
|
335
|
+
export function isPackageOrBundle(app) {
|
|
336
|
+
return /^([a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+)+$/.test(app);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Finds all instances 'firstKey' and create a duplicate with the key 'secondKey',
|
|
341
|
+
* Do the same thing in reverse. If we find 'secondKey', create a duplicate with the key 'firstKey'.
|
|
342
|
+
*
|
|
343
|
+
* This will cause keys to be overwritten if the object contains 'firstKey' and 'secondKey'.
|
|
344
|
+
|
|
345
|
+
* @param {*} input Any type of input
|
|
346
|
+
* @param {String} firstKey The first key to duplicate
|
|
347
|
+
* @param {String} secondKey The second key to duplicate
|
|
348
|
+
*/
|
|
349
|
+
export function duplicateKeys(input, firstKey, secondKey) {
|
|
350
|
+
// If array provided, recursively call on all elements
|
|
351
|
+
if (_.isArray(input)) {
|
|
352
|
+
return input.map((item) => duplicateKeys(item, firstKey, secondKey));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// If object, create duplicates for keys and then recursively call on values
|
|
356
|
+
if (_.isPlainObject(input)) {
|
|
357
|
+
const resultObj = {};
|
|
358
|
+
for (let [key, value] of _.toPairs(input)) {
|
|
359
|
+
const recursivelyCalledValue = duplicateKeys(value, firstKey, secondKey);
|
|
360
|
+
if (key === firstKey) {
|
|
361
|
+
resultObj[secondKey] = recursivelyCalledValue;
|
|
362
|
+
} else if (key === secondKey) {
|
|
363
|
+
resultObj[firstKey] = recursivelyCalledValue;
|
|
364
|
+
}
|
|
365
|
+
resultObj[key] = recursivelyCalledValue;
|
|
366
|
+
}
|
|
367
|
+
return resultObj;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Base case. Return primitives without doing anything.
|
|
371
|
+
return input;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Takes a desired capability and tries to JSON.parse it as an array,
|
|
376
|
+
* and either returns the parsed array or a singleton array.
|
|
377
|
+
*
|
|
378
|
+
* @param {string|Array<String>} cap A desired capability
|
|
379
|
+
*/
|
|
380
|
+
export function parseCapsArray(cap) {
|
|
381
|
+
if (_.isArray(cap)) {
|
|
382
|
+
return cap;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let parsedCaps;
|
|
386
|
+
try {
|
|
387
|
+
parsedCaps = JSON.parse(cap);
|
|
388
|
+
if (_.isArray(parsedCaps)) {
|
|
389
|
+
return parsedCaps;
|
|
390
|
+
}
|
|
391
|
+
} catch (ign) {
|
|
392
|
+
logger.warn(`Failed to parse capability as JSON array`);
|
|
393
|
+
}
|
|
394
|
+
if (_.isString(cap)) {
|
|
395
|
+
return [cap];
|
|
396
|
+
}
|
|
397
|
+
throw new Error(`must provide a string or JSON Array; received ${cap}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Generate a string that uniquely describes driver instance
|
|
402
|
+
*
|
|
403
|
+
* @param {import('@appium/types').Core} obj driver instance
|
|
404
|
+
* @param {string?} sessionId session identifier (if exists)
|
|
405
|
+
* @returns {string}
|
|
406
|
+
*/
|
|
407
|
+
export function generateDriverLogPrefix(obj, sessionId = null) {
|
|
408
|
+
const instanceName = `${obj.constructor.name}@${node.getObjectId(obj).substring(0, 4)}`;
|
|
409
|
+
return sessionId ? `${instanceName} (${sessionId.substring(0, 8)})` : instanceName;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
|
|
381
413
|
/**
|
|
382
414
|
* Sends a HTTP GET query to fetch the app with caching enabled.
|
|
383
415
|
* Follows https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
|
|
@@ -483,9 +515,7 @@ async function unzipApp(zipPath, dstRoot, supportedAppExtensions) {
|
|
|
483
515
|
try {
|
|
484
516
|
logger.debug(`Unzipping '${zipPath}'`);
|
|
485
517
|
const timer = new timing.Timer().start();
|
|
486
|
-
const
|
|
487
|
-
const useSystemUnzip =
|
|
488
|
-
_.isEmpty(useSystemUnzipEnv) || !['0', 'false'].includes(_.toLower(useSystemUnzipEnv));
|
|
518
|
+
const useSystemUnzip = isEnvOptionEnabled('APPIUM_PREFER_SYSTEM_UNZIP', true);
|
|
489
519
|
/**
|
|
490
520
|
* Attempt to use use the system `unzip` (e.g., `/usr/bin/unzip`) due
|
|
491
521
|
* to the significant performance improvement it provides over the native
|
|
@@ -539,84 +569,144 @@ async function unzipApp(zipPath, dstRoot, supportedAppExtensions) {
|
|
|
539
569
|
}
|
|
540
570
|
|
|
541
571
|
/**
|
|
542
|
-
*
|
|
543
|
-
*
|
|
572
|
+
* Transforms the given app link to the cache key.
|
|
573
|
+
* This is necessary to properly cache apps
|
|
574
|
+
* having the same address, but different query strings,
|
|
575
|
+
* for example ones stored in S3 using presigned URLs.
|
|
576
|
+
*
|
|
577
|
+
* @param {string} app App link.
|
|
578
|
+
* @returns {string} Transformed app link or the original arg if
|
|
579
|
+
* no transfromation is needed.
|
|
544
580
|
*/
|
|
545
|
-
|
|
546
|
-
|
|
581
|
+
function toCacheKey(app) {
|
|
582
|
+
if (!isEnvOptionEnabled('APPIUM_APPS_CACHE_IGNORE_URL_QUERY') || !isSupportedUrl(app)) {
|
|
583
|
+
return app;
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
const {href, search} = parseAppLink(app);
|
|
587
|
+
if (href && search) {
|
|
588
|
+
return href.replace(search, '');
|
|
589
|
+
}
|
|
590
|
+
if (href) {
|
|
591
|
+
return href;
|
|
592
|
+
}
|
|
593
|
+
} catch {}
|
|
594
|
+
return app;
|
|
547
595
|
}
|
|
548
596
|
|
|
549
597
|
/**
|
|
550
|
-
*
|
|
551
|
-
* Do the same thing in reverse. If we find 'secondKey', create a duplicate with the key 'firstKey'.
|
|
598
|
+
* Safely parses the given app link to a URL object
|
|
552
599
|
*
|
|
553
|
-
*
|
|
554
|
-
|
|
555
|
-
*
|
|
556
|
-
* @param {String} firstKey The first key to duplicate
|
|
557
|
-
* @param {String} secondKey The second key to duplicate
|
|
600
|
+
* @param {string} appLink
|
|
601
|
+
* @returns {URL|import('@appium/types').StringRecord} Parsed URL object
|
|
602
|
+
* or an empty object if the parsing has failed
|
|
558
603
|
*/
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
604
|
+
function parseAppLink(appLink) {
|
|
605
|
+
try {
|
|
606
|
+
return new URL(appLink);
|
|
607
|
+
} catch {
|
|
608
|
+
return {};
|
|
563
609
|
}
|
|
610
|
+
}
|
|
564
611
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
return
|
|
612
|
+
/**
|
|
613
|
+
* Checks whether we can threat the given app link
|
|
614
|
+
* as a URL,
|
|
615
|
+
*
|
|
616
|
+
* @param {string} app
|
|
617
|
+
* @returns {boolean} True if app is a supported URL
|
|
618
|
+
*/
|
|
619
|
+
function isSupportedUrl(app) {
|
|
620
|
+
try {
|
|
621
|
+
const {protocol} = parseAppLink(app);
|
|
622
|
+
return ['http:', 'https:'].includes(protocol);
|
|
623
|
+
} catch {
|
|
624
|
+
return false;
|
|
578
625
|
}
|
|
579
|
-
|
|
580
|
-
// Base case. Return primitives without doing anything.
|
|
581
|
-
return input;
|
|
582
626
|
}
|
|
583
627
|
|
|
584
628
|
/**
|
|
585
|
-
*
|
|
586
|
-
* and either returns the parsed array or a singleton array.
|
|
629
|
+
* Check if the given environment option is enabled
|
|
587
630
|
*
|
|
588
|
-
* @param {string
|
|
631
|
+
* @param {string} optionName Option name
|
|
632
|
+
* @param {boolean|null} [defaultValue=null] The value to return if the given env value
|
|
633
|
+
* is not set explcitly
|
|
634
|
+
* @returns {boolean} True if the option is enabled
|
|
589
635
|
*/
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
636
|
+
function isEnvOptionEnabled(optionName, defaultValue = null) {
|
|
637
|
+
const value = process.env[optionName];
|
|
638
|
+
if (!_.isNull(defaultValue) && _.isEmpty(value)) {
|
|
639
|
+
return defaultValue;
|
|
593
640
|
}
|
|
641
|
+
return !_.isEmpty(value) && !['0', 'false', 'no'].includes(_.toLower(value));
|
|
642
|
+
}
|
|
594
643
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
if (_.isString(cap)) {
|
|
605
|
-
return [cap];
|
|
644
|
+
/**
|
|
645
|
+
*
|
|
646
|
+
* @param {string} [envVarName]
|
|
647
|
+
* @param {number} defaultValue
|
|
648
|
+
* @returns {number}
|
|
649
|
+
*/
|
|
650
|
+
function toNaturalNumber(defaultValue, envVarName) {
|
|
651
|
+
if (!envVarName || _.isUndefined(process.env[envVarName])) {
|
|
652
|
+
return defaultValue;
|
|
606
653
|
}
|
|
607
|
-
|
|
654
|
+
const num = parseInt(`${process.env[envVarName]}`, 10);
|
|
655
|
+
return num > 0 ? num : defaultValue;
|
|
608
656
|
}
|
|
609
657
|
|
|
610
658
|
/**
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
* @param {import('@appium/types').Core} obj driver instance
|
|
614
|
-
* @param {string?} sessionId session identifier (if exists)
|
|
659
|
+
* @param {string} app
|
|
660
|
+
* @param {string[]} supportedAppExtensions
|
|
615
661
|
* @returns {string}
|
|
616
662
|
*/
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
663
|
+
function verifyAppExtension(app, supportedAppExtensions) {
|
|
664
|
+
if (supportedAppExtensions.map(_.toLower).includes(_.toLower(path.extname(app)))) {
|
|
665
|
+
return app;
|
|
666
|
+
}
|
|
667
|
+
throw new Error(
|
|
668
|
+
`New app path '${app}' did not have ` +
|
|
669
|
+
`${util.pluralize('extension', supportedAppExtensions.length, false)}: ` +
|
|
670
|
+
supportedAppExtensions
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* @param {string} folderPath
|
|
676
|
+
* @returns {Promise<number>}
|
|
677
|
+
*/
|
|
678
|
+
async function calculateFolderIntegrity(folderPath) {
|
|
679
|
+
return (await fs.glob('**/*', {cwd: folderPath})).length;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* @param {string} filePath
|
|
684
|
+
* @returns {Promise<string>}
|
|
685
|
+
*/
|
|
686
|
+
async function calculateFileIntegrity(filePath) {
|
|
687
|
+
return await fs.hash(filePath);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* @param {string} currentPath
|
|
692
|
+
* @param {import('@appium/types').StringRecord} expectedIntegrity
|
|
693
|
+
* @returns {Promise<boolean>}
|
|
694
|
+
*/
|
|
695
|
+
async function isAppIntegrityOk(currentPath, expectedIntegrity = {}) {
|
|
696
|
+
if (!(await fs.exists(currentPath))) {
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Folder integrity check is simple:
|
|
701
|
+
// Verify the previous amount of files is not greater than the current one.
|
|
702
|
+
// We don't want to use equality comparison because of an assumption that the OS might
|
|
703
|
+
// create some unwanted service files/cached inside of that folder or its subfolders.
|
|
704
|
+
// Ofc, validating the hash sum of each file (or at least of file path) would be much
|
|
705
|
+
// more precise, but we don't need to be very precise here and also don't want to
|
|
706
|
+
// overuse RAM and have a performance drop.
|
|
707
|
+
return (await fs.stat(currentPath)).isDirectory()
|
|
708
|
+
? (await calculateFolderIntegrity(currentPath)) >= expectedIntegrity?.folder
|
|
709
|
+
: (await calculateFileIntegrity(currentPath)) === expectedIntegrity?.file;
|
|
620
710
|
}
|
|
621
711
|
|
|
622
712
|
/** @type {import('@appium/types').DriverHelpers} */
|
|
@@ -2,8 +2,16 @@ import _ from 'lodash';
|
|
|
2
2
|
import log from './logger';
|
|
3
3
|
import {errors} from '../protocol';
|
|
4
4
|
import {handleIdempotency} from './idempotency';
|
|
5
|
+
import {pathToRegexp} from 'path-to-regexp';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
* @param {import('express').Request} req
|
|
10
|
+
* @param {import('express').Response} res
|
|
11
|
+
* @param {import('express').NextFunction} next
|
|
12
|
+
* @returns {any}
|
|
13
|
+
*/
|
|
14
|
+
export function allowCrossDomain(req, res, next) {
|
|
7
15
|
try {
|
|
8
16
|
res.header('Access-Control-Allow-Origin', '*');
|
|
9
17
|
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS, DELETE');
|
|
@@ -22,7 +30,11 @@ function allowCrossDomain(req, res, next) {
|
|
|
22
30
|
next();
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} basePath
|
|
35
|
+
* @returns {import('express').RequestHandler}
|
|
36
|
+
*/
|
|
37
|
+
export function allowCrossDomainAsyncExecute(basePath) {
|
|
26
38
|
return (req, res, next) => {
|
|
27
39
|
// there are two paths for async responses, so cover both
|
|
28
40
|
// https://regex101.com/r/txYiEz/1
|
|
@@ -36,12 +48,17 @@ function allowCrossDomainAsyncExecute(basePath) {
|
|
|
36
48
|
};
|
|
37
49
|
}
|
|
38
50
|
|
|
39
|
-
|
|
51
|
+
/**
|
|
52
|
+
*
|
|
53
|
+
* @param {string} basePath
|
|
54
|
+
* @returns {import('express').RequestHandler}
|
|
55
|
+
*/
|
|
56
|
+
export function fixPythonContentType(basePath) {
|
|
40
57
|
return (req, res, next) => {
|
|
41
58
|
// hack because python client library gives us wrong content-type
|
|
42
59
|
if (
|
|
43
60
|
new RegExp(`^${_.escapeRegExp(basePath)}`).test(req.path) &&
|
|
44
|
-
/^Python/.test(req.headers['user-agent'])
|
|
61
|
+
/^Python/.test(req.headers['user-agent'] ?? '')
|
|
45
62
|
) {
|
|
46
63
|
if (req.headers['content-type'] === 'application/x-www-form-urlencoded') {
|
|
47
64
|
req.headers['content-type'] = 'application/json; charset=utf-8';
|
|
@@ -51,14 +68,55 @@ function fixPythonContentType(basePath) {
|
|
|
51
68
|
};
|
|
52
69
|
}
|
|
53
70
|
|
|
54
|
-
|
|
71
|
+
/**
|
|
72
|
+
*
|
|
73
|
+
* @param {import('express').Request} req
|
|
74
|
+
* @param {import('express').Response} res
|
|
75
|
+
* @param {import('express').NextFunction} next
|
|
76
|
+
* @returns {any}
|
|
77
|
+
*/
|
|
78
|
+
export function defaultToJSONContentType(req, res, next) {
|
|
55
79
|
if (!req.headers['content-type']) {
|
|
56
80
|
req.headers['content-type'] = 'application/json; charset=utf-8';
|
|
57
81
|
}
|
|
58
82
|
next();
|
|
59
83
|
}
|
|
60
84
|
|
|
61
|
-
|
|
85
|
+
/**
|
|
86
|
+
*
|
|
87
|
+
* @param {import('@appium/types').StringRecord<import('@appium/types').WSServer>} webSocketsMapping
|
|
88
|
+
* @returns {import('express').RequestHandler}
|
|
89
|
+
*/
|
|
90
|
+
export function handleUpgrade(webSocketsMapping) {
|
|
91
|
+
return (req, res, next) => {
|
|
92
|
+
if (!req.headers?.upgrade || _.toLower(req.headers.upgrade) !== 'websocket') {
|
|
93
|
+
return next();
|
|
94
|
+
}
|
|
95
|
+
let currentPathname;
|
|
96
|
+
try {
|
|
97
|
+
currentPathname = new URL(req.url ?? '').pathname;
|
|
98
|
+
} catch {
|
|
99
|
+
currentPathname = req.url ?? '';
|
|
100
|
+
}
|
|
101
|
+
for (const [pathname, wsServer] of _.toPairs(webSocketsMapping)) {
|
|
102
|
+
if (pathToRegexp(pathname).test(currentPathname)) {
|
|
103
|
+
return wsServer.handleUpgrade(req, req.socket, Buffer.from(''), (ws) => {
|
|
104
|
+
wsServer.emit('connection', ws, req);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
log.info(`Did not match the websocket upgrade request at ${currentPathname} to any known route`);
|
|
109
|
+
next();
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {Error} err
|
|
115
|
+
* @param {import('express').Request} req
|
|
116
|
+
* @param {import('express').Response} res
|
|
117
|
+
* @param {import('express').NextFunction} next
|
|
118
|
+
*/
|
|
119
|
+
export function catchAllHandler(err, req, res, next) {
|
|
62
120
|
if (res.headersSent) {
|
|
63
121
|
return next(err);
|
|
64
122
|
}
|
|
@@ -79,7 +137,11 @@ function catchAllHandler(err, req, res, next) {
|
|
|
79
137
|
log.error(err);
|
|
80
138
|
}
|
|
81
139
|
|
|
82
|
-
|
|
140
|
+
/**
|
|
141
|
+
* @param {import('express').Request} req
|
|
142
|
+
* @param {import('express').Response} res
|
|
143
|
+
*/
|
|
144
|
+
export function catch404Handler(req, res) {
|
|
83
145
|
log.debug(`No route found for ${req.url}`);
|
|
84
146
|
const error = errors.UnknownCommandError;
|
|
85
147
|
res.status(error.w3cStatus()).json(
|
|
@@ -107,12 +169,4 @@ function patchWithSessionId(req, body) {
|
|
|
107
169
|
return body;
|
|
108
170
|
}
|
|
109
171
|
|
|
110
|
-
export {
|
|
111
|
-
allowCrossDomain,
|
|
112
|
-
fixPythonContentType,
|
|
113
|
-
defaultToJSONContentType,
|
|
114
|
-
catchAllHandler,
|
|
115
|
-
allowCrossDomainAsyncExecute,
|
|
116
|
-
handleIdempotency,
|
|
117
|
-
catch404Handler,
|
|
118
|
-
};
|
|
172
|
+
export { handleIdempotency };
|
package/lib/express/server.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
catchAllHandler,
|
|
15
15
|
allowCrossDomainAsyncExecute,
|
|
16
16
|
handleIdempotency,
|
|
17
|
+
handleUpgrade,
|
|
17
18
|
catch404Handler,
|
|
18
19
|
} from './middleware';
|
|
19
20
|
import {guineaPig, guineaPigScrollable, guineaPigAppBanner, welcome, STATIC_DIR} from './static';
|
|
@@ -109,6 +110,7 @@ async function server(opts) {
|
|
|
109
110
|
allowCors,
|
|
110
111
|
basePath,
|
|
111
112
|
extraMethodMap,
|
|
113
|
+
webSocketsMapping: appiumServer.webSocketsMapping,
|
|
112
114
|
});
|
|
113
115
|
// allow extensions to update the app and http server objects
|
|
114
116
|
for (const updater of serverUpdaters) {
|
|
@@ -139,6 +141,7 @@ function configureServer({
|
|
|
139
141
|
allowCors = true,
|
|
140
142
|
basePath = DEFAULT_BASE_PATH,
|
|
141
143
|
extraMethodMap = {},
|
|
144
|
+
webSocketsMapping = {},
|
|
142
145
|
}) {
|
|
143
146
|
basePath = normalizeBasePath(basePath);
|
|
144
147
|
|
|
@@ -152,7 +155,7 @@ function configureServer({
|
|
|
152
155
|
app.use(`${basePath}/produce_error`, produceError);
|
|
153
156
|
app.use(`${basePath}/crash`, produceCrash);
|
|
154
157
|
|
|
155
|
-
|
|
158
|
+
app.use(handleUpgrade(webSocketsMapping));
|
|
156
159
|
if (allowCors) {
|
|
157
160
|
app.use(allowCrossDomain);
|
|
158
161
|
} else {
|
|
@@ -195,6 +198,7 @@ function configureHttp({httpServer, reject, keepAliveTimeout}) {
|
|
|
195
198
|
* @type {AppiumServer}
|
|
196
199
|
*/
|
|
197
200
|
const appiumServer = /** @type {any} */ (httpServer);
|
|
201
|
+
appiumServer.webSocketsMapping = {};
|
|
198
202
|
appiumServer.addWebSocketHandler = addWebSocketHandler;
|
|
199
203
|
appiumServer.removeWebSocketHandler = removeWebSocketHandler;
|
|
200
204
|
appiumServer.removeAllWebSocketHandlers = removeAllWebSocketHandlers;
|
|
@@ -370,4 +374,5 @@ export {server, configureServer, normalizeBasePath};
|
|
|
370
374
|
* @property {boolean} [allowCors]
|
|
371
375
|
* @property {string} [basePath]
|
|
372
376
|
* @property {MethodMap} [extraMethodMap]
|
|
377
|
+
* @property {import('@appium/types').StringRecord} [webSocketsMapping={}]
|
|
373
378
|
*/
|
package/lib/express/websocket.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/* eslint-disable require-await */
|
|
2
2
|
import _ from 'lodash';
|
|
3
|
-
import {URL} from 'url';
|
|
4
3
|
import B from 'bluebird';
|
|
5
|
-
import { pathToRegexp } from 'path-to-regexp';
|
|
6
4
|
|
|
7
5
|
const DEFAULT_WS_PATHNAME_PREFIX = '/ws';
|
|
8
6
|
|
|
@@ -11,27 +9,6 @@ const DEFAULT_WS_PATHNAME_PREFIX = '/ws';
|
|
|
11
9
|
* @type {AppiumServer['addWebSocketHandler']}
|
|
12
10
|
*/
|
|
13
11
|
async function addWebSocketHandler(handlerPathname, handlerServer) {
|
|
14
|
-
if (_.isUndefined(this.webSocketsMapping)) {
|
|
15
|
-
this.webSocketsMapping = {};
|
|
16
|
-
// https://github.com/websockets/ws/pull/885
|
|
17
|
-
this.on('upgrade', (request, socket, head) => {
|
|
18
|
-
let currentPathname;
|
|
19
|
-
try {
|
|
20
|
-
currentPathname = new URL(request.url ?? '').pathname;
|
|
21
|
-
} catch {
|
|
22
|
-
currentPathname = request.url ?? '';
|
|
23
|
-
}
|
|
24
|
-
for (const [pathname, wsServer] of _.toPairs(this.webSocketsMapping)) {
|
|
25
|
-
if (pathToRegexp(pathname).test(currentPathname)) {
|
|
26
|
-
wsServer.handleUpgrade(request, socket, head, (ws) => {
|
|
27
|
-
wsServer.emit('connection', ws, request);
|
|
28
|
-
});
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
socket.destroy();
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
12
|
this.webSocketsMapping[handlerPathname] = handlerServer;
|
|
36
13
|
}
|
|
37
14
|
|
|
@@ -40,10 +17,6 @@ async function addWebSocketHandler(handlerPathname, handlerServer) {
|
|
|
40
17
|
* @type {AppiumServer['getWebSocketHandlers']}
|
|
41
18
|
*/
|
|
42
19
|
async function getWebSocketHandlers(keysFilter = null) {
|
|
43
|
-
if (_.isEmpty(this.webSocketsMapping)) {
|
|
44
|
-
return {};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
20
|
return _.toPairs(this.webSocketsMapping).reduce((acc, [pathname, wsServer]) => {
|
|
48
21
|
if (!_.isString(keysFilter) || pathname.includes(keysFilter)) {
|
|
49
22
|
acc[pathname] = wsServer;
|