@appium/base-driver 8.2.2 → 8.2.3
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.js +3 -1
- package/build/lib/basedriver/driver.js +4 -4
- package/build/lib/basedriver/helpers.js +137 -81
- package/build/lib/express/express-logging.js +2 -2
- package/build/lib/index.js +5 -3
- package/build/lib/jsonwp-proxy/protocol-converter.js +2 -5
- package/build/lib/jsonwp-proxy/proxy.js +2 -5
- package/build/lib/protocol/errors.js +4 -2
- package/build/lib/protocol/helpers.js +3 -20
- package/build/lib/protocol/protocol.js +9 -19
- package/build/test/basedriver/helpers-specs.js +5 -1
- package/build/test/jsonwp-proxy/proxy-e2e-specs.js +1 -2
- package/build/test/jsonwp-proxy/proxy-specs.js +1 -6
- package/build/test/protocol/protocol-e2e-specs.js +34 -94
- package/lib/basedriver/capabilities.js +3 -0
- package/lib/basedriver/driver.js +4 -4
- package/lib/basedriver/helpers.js +197 -81
- package/lib/express/express-logging.js +1 -1
- package/lib/index.js +4 -2
- package/lib/jsonwp-proxy/protocol-converter.js +1 -5
- package/lib/jsonwp-proxy/proxy.js +1 -3
- package/lib/protocol/errors.js +1 -1
- package/lib/protocol/helpers.js +5 -25
- package/lib/protocol/protocol.js +9 -25
- package/package.json +5 -5
- package/test/basedriver/helpers-specs.js +4 -0
|
@@ -145,7 +145,10 @@ function parseCaps (caps, constraints = {}, shouldValidateCaps = true) {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
// If an empty array as provided, we'll be forgiving and make it an array of one empty object
|
|
148
|
+
// In the future, reject 'firstMatch' argument if its array did not have one or more entries (#3.2)
|
|
148
149
|
if (allFirstMatchCaps.length === 0) {
|
|
150
|
+
log.warn(`The firstMatch array in the given capabilities has no entries. Adding an empty entry fo rnow, ` +
|
|
151
|
+
`but it will require one or more entries as W3C spec.`);
|
|
149
152
|
allFirstMatchCaps.push({});
|
|
150
153
|
}
|
|
151
154
|
|
package/lib/basedriver/driver.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Protocol, errors, determineProtocol
|
|
2
|
+
Protocol, errors, determineProtocol, DELETE_SESSION_COMMAND,
|
|
3
3
|
} from '../protocol';
|
|
4
4
|
import { fs } from '@appium/support';
|
|
5
5
|
import { PROTOCOLS, DEFAULT_BASE_PATH } from '../constants';
|
|
@@ -311,7 +311,7 @@ class BaseDriver extends Protocol {
|
|
|
311
311
|
// If creating a session determine if W3C or MJSONWP protocol was requested and remember the choice
|
|
312
312
|
this.protocol = determineProtocol(...args);
|
|
313
313
|
this.logEvent(EVENT_SESSION_INIT);
|
|
314
|
-
} else if (cmd ===
|
|
314
|
+
} else if (cmd === DELETE_SESSION_COMMAND) {
|
|
315
315
|
this.logEvent(EVENT_SESSION_QUIT_START);
|
|
316
316
|
}
|
|
317
317
|
|
|
@@ -352,7 +352,7 @@ class BaseDriver extends Protocol {
|
|
|
352
352
|
// automatic session deletion in this.onCommandTimeout. Of course we don't
|
|
353
353
|
// want to trigger the timer when the user is shutting down the session
|
|
354
354
|
// intentionally
|
|
355
|
-
if (this.isCommandsQueueEnabled && cmd !==
|
|
355
|
+
if (this.isCommandsQueueEnabled && cmd !== DELETE_SESSION_COMMAND) {
|
|
356
356
|
// resetting existing timeout
|
|
357
357
|
this.startNewCommandTimeout();
|
|
358
358
|
}
|
|
@@ -362,7 +362,7 @@ class BaseDriver extends Protocol {
|
|
|
362
362
|
this._eventHistory.commands.push({cmd, startTime, endTime});
|
|
363
363
|
if (cmd === 'createSession') {
|
|
364
364
|
this.logEvent(EVENT_SESSION_START);
|
|
365
|
-
} else if (cmd ===
|
|
365
|
+
} else if (cmd === DELETE_SESSION_COMMAND) {
|
|
366
366
|
this.logEvent(EVENT_SESSION_QUIT_DONE);
|
|
367
367
|
}
|
|
368
368
|
|
|
@@ -18,13 +18,14 @@ const CACHED_APPS_MAX_AGE = 1000 * 60 * 60 * 24; // ms
|
|
|
18
18
|
const APPLICATIONS_CACHE = new LRU({
|
|
19
19
|
maxAge: CACHED_APPS_MAX_AGE, // expire after 24 hours
|
|
20
20
|
updateAgeOnGet: true,
|
|
21
|
-
dispose:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
dispose: (app, {fullPath}) => {
|
|
22
|
+
logger.info(`The application '${app}' cached at '${fullPath}' has ` +
|
|
23
|
+
`expired after ${CACHED_APPS_MAX_AGE}ms`);
|
|
24
|
+
setTimeout(async () => {
|
|
25
|
+
if (fullPath) {
|
|
26
|
+
await fs.rimraf(fullPath);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
28
29
|
},
|
|
29
30
|
noDisposeOnSet: true,
|
|
30
31
|
});
|
|
@@ -66,54 +67,57 @@ async function retrieveHeaders (link) {
|
|
|
66
67
|
return {};
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
function getCachedApplicationPath (link, currentAppProps = {}) {
|
|
70
|
+
function getCachedApplicationPath (link, currentAppProps = {}, cachedAppInfo = {}) {
|
|
70
71
|
const refresh = () => {
|
|
71
72
|
logger.debug(`A fresh copy of the application is going to be downloaded from ${link}`);
|
|
72
73
|
return null;
|
|
73
74
|
};
|
|
74
75
|
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
if (immutable && currentImmutable) {
|
|
100
|
-
logger.debug(`The application at ${link} is immutable`);
|
|
76
|
+
if (!_.isPlainObject(cachedAppInfo) || !_.isPlainObject(currentAppProps)) {
|
|
77
|
+
// if an invalid arg is passed then assume cache miss
|
|
78
|
+
return refresh();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const {
|
|
82
|
+
lastModified: currentModified,
|
|
83
|
+
immutable: currentImmutable,
|
|
84
|
+
// maxAge is in seconds
|
|
85
|
+
maxAge: currentMaxAge,
|
|
86
|
+
} = currentAppProps;
|
|
87
|
+
const {
|
|
88
|
+
// Date instance
|
|
89
|
+
lastModified,
|
|
90
|
+
// boolean
|
|
91
|
+
immutable,
|
|
92
|
+
// Unix time in milliseconds
|
|
93
|
+
timestamp,
|
|
94
|
+
fullPath,
|
|
95
|
+
} = cachedAppInfo;
|
|
96
|
+
if (lastModified && currentModified) {
|
|
97
|
+
if (currentModified.getTime() <= lastModified.getTime()) {
|
|
98
|
+
logger.debug(`The application at ${link} has not been modified since ${lastModified}`);
|
|
101
99
|
return fullPath;
|
|
102
100
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
101
|
+
logger.debug(`The application at ${link} has been modified since ${lastModified}`);
|
|
102
|
+
return refresh();
|
|
103
|
+
}
|
|
104
|
+
if (immutable && currentImmutable) {
|
|
105
|
+
logger.debug(`The application at ${link} is immutable`);
|
|
106
|
+
return fullPath;
|
|
107
|
+
}
|
|
108
|
+
if (currentMaxAge && timestamp) {
|
|
109
|
+
const msLeft = timestamp + currentMaxAge * 1000 - Date.now();
|
|
110
|
+
if (msLeft > 0) {
|
|
111
|
+
logger.debug(`The cached application '${path.basename(fullPath)}' will expire in ${msLeft / 1000}s`);
|
|
112
|
+
return fullPath;
|
|
110
113
|
}
|
|
114
|
+
logger.debug(`The cached application '${path.basename(fullPath)}' has expired`);
|
|
111
115
|
}
|
|
112
116
|
return refresh();
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
function verifyAppExtension (app, supportedAppExtensions) {
|
|
116
|
-
if (supportedAppExtensions.includes(path.extname(app))) {
|
|
120
|
+
if (supportedAppExtensions.map(_.toLower).includes(_.toLower(path.extname(app)))) {
|
|
117
121
|
return app;
|
|
118
122
|
}
|
|
119
123
|
throw new Error(`New app path '${app}' did not have ` +
|
|
@@ -121,18 +125,104 @@ function verifyAppExtension (app, supportedAppExtensions) {
|
|
|
121
125
|
supportedAppExtensions);
|
|
122
126
|
}
|
|
123
127
|
|
|
124
|
-
async function
|
|
128
|
+
async function calculateFolderIntegrity (folderPath) {
|
|
129
|
+
return (await fs.glob('**/*', {cwd: folderPath, strict: false, nosort: true})).length;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function calculateFileIntegrity (filePath) {
|
|
133
|
+
return await fs.hash(filePath);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function isAppIntegrityOk (currentPath, expectedIntegrity = {}) {
|
|
137
|
+
if (!await fs.exists(currentPath)) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Folder integrity check is simple:
|
|
142
|
+
// Verify the previous amount of files is not greater than the current one.
|
|
143
|
+
// We don't want to use equality comparison because of an assumption that the OS might
|
|
144
|
+
// create some unwanted service files/cached inside of that folder or its subfolders.
|
|
145
|
+
// Ofc, validating the hash sum of each file (or at least of file path) would be much
|
|
146
|
+
// more precise, but we don't need to be very precise here and also don't want to
|
|
147
|
+
// overuse RAM and have a performance drop.
|
|
148
|
+
return (await fs.stat(currentPath)).isDirectory()
|
|
149
|
+
? await calculateFolderIntegrity(currentPath) >= expectedIntegrity?.folder
|
|
150
|
+
: await calculateFileIntegrity(currentPath) === expectedIntegrity?.file;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @typedef {Object} PostProcessOptions
|
|
155
|
+
* @property {?Object} cachedAppInfo The information about the previously cached app instance (if exists):
|
|
156
|
+
* - packageHash: SHA1 hash of the package if it is a file and not a folder
|
|
157
|
+
* - lastModified: Optional Date instance, the value of file's `Last-Modified` header
|
|
158
|
+
* - immutable: Optional boolean value. Contains true if the file has an `immutable` mark
|
|
159
|
+
* in `Cache-control` header
|
|
160
|
+
* - maxAge: Optional integer representation of `maxAge` parameter in `Cache-control` header
|
|
161
|
+
* - timestamp: The timestamp this item has been added to the cache (measured in Unix epoch
|
|
162
|
+
* milliseconds)
|
|
163
|
+
* - integrity: An object containing either `file` property with SHA1 hash of the file
|
|
164
|
+
* or `folder` property with total amount of cached files and subfolders
|
|
165
|
+
* - fullPath: the full path to the cached app
|
|
166
|
+
* @property {boolean} isUrl Whether the app has been downloaded from a remote URL
|
|
167
|
+
* @property {?Object} headers Optional headers object. Only present if `isUrl` is true and if the server
|
|
168
|
+
* responds to HEAD requests. All header names are normalized to lowercase.
|
|
169
|
+
* @property {string} appPath A string containing full path to the preprocessed application package (either
|
|
170
|
+
* downloaded or a local one)
|
|
171
|
+
*/
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @typedef {Object} PostProcessResult
|
|
175
|
+
* @property {string} appPath The full past to the post-processed application package on the
|
|
176
|
+
* local file system (might be a file or a folder path)
|
|
177
|
+
*/
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @typedef {Object} ConfigureAppOptions
|
|
181
|
+
* @property {(obj: PostProcessOptions) => (Promise<PostProcessResult|undefined>|PostProcessResult|undefined)} onPostProcess
|
|
182
|
+
* Optional function, which should be applied
|
|
183
|
+
* to the application after it is downloaded/preprocessed. This function may be async
|
|
184
|
+
* and is expected to accept single object parameter.
|
|
185
|
+
* The function is expected to either return a falsy value, which means the app must not be
|
|
186
|
+
* cached and a fresh copy of it is downloaded each time. If this function returns an object
|
|
187
|
+
* containing `appPath` property then the integrity of it will be verified and stored into
|
|
188
|
+
* the cache.
|
|
189
|
+
* @property {string[]} supportedExtensions List of supported application extensions (
|
|
190
|
+
* including starting dots). This property is mandatory and must not be empty.
|
|
191
|
+
*/
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Prepares an app to be used in an automated test. The app gets cached automatically
|
|
195
|
+
* if it is an archive or if it is downloaded from an URL.
|
|
196
|
+
*
|
|
197
|
+
* @param {string} app Either a full path to the app or a remote URL
|
|
198
|
+
* @param {string|string[]|ConfigureAppOptions} options
|
|
199
|
+
* @returns The full path to the resulting application bundle
|
|
200
|
+
*/
|
|
201
|
+
async function configureApp (app, options = {}) {
|
|
125
202
|
if (!_.isString(app)) {
|
|
126
203
|
// immediately shortcircuit if not given an app
|
|
127
204
|
return;
|
|
128
205
|
}
|
|
129
|
-
|
|
130
|
-
|
|
206
|
+
|
|
207
|
+
let supportedAppExtensions;
|
|
208
|
+
const {
|
|
209
|
+
onPostProcess,
|
|
210
|
+
} = _.isPlainObject(options) ? options : {};
|
|
211
|
+
if (_.isString(options)) {
|
|
212
|
+
supportedAppExtensions = [options];
|
|
213
|
+
} else if (_.isArray(options)) {
|
|
214
|
+
supportedAppExtensions = options;
|
|
215
|
+
} else if (_.isPlainObject(options)) {
|
|
216
|
+
supportedAppExtensions = options.supportedExtensions;
|
|
217
|
+
}
|
|
218
|
+
if (_.isEmpty(supportedAppExtensions)) {
|
|
219
|
+
throw new Error(`One or more supported app extensions must be provided`);
|
|
131
220
|
}
|
|
132
221
|
|
|
133
222
|
let newApp = app;
|
|
134
223
|
let shouldUnzipApp = false;
|
|
135
|
-
let
|
|
224
|
+
let packageHash = null;
|
|
225
|
+
let headers = null;
|
|
136
226
|
const remoteAppProps = {
|
|
137
227
|
lastModified: null,
|
|
138
228
|
immutable: false,
|
|
@@ -141,11 +231,13 @@ async function configureApp (app, supportedAppExtensions) {
|
|
|
141
231
|
const {protocol, pathname} = url.parse(newApp);
|
|
142
232
|
const isUrl = ['http:', 'https:'].includes(protocol);
|
|
143
233
|
|
|
234
|
+
const cachedAppInfo = APPLICATIONS_CACHE.get(app);
|
|
235
|
+
|
|
144
236
|
return await APPLICATIONS_CACHE_GUARD.acquire(app, async () => {
|
|
145
237
|
if (isUrl) {
|
|
146
238
|
// Use the app from remote URL
|
|
147
239
|
logger.info(`Using downloadable app '${newApp}'`);
|
|
148
|
-
|
|
240
|
+
headers = await retrieveHeaders(newApp);
|
|
149
241
|
if (!_.isEmpty(headers)) {
|
|
150
242
|
if (headers['last-modified']) {
|
|
151
243
|
remoteAppProps.lastModified = new Date(headers['last-modified']);
|
|
@@ -160,13 +252,14 @@ async function configureApp (app, supportedAppExtensions) {
|
|
|
160
252
|
}
|
|
161
253
|
logger.debug(`Cache-Control: ${headers['cache-control']}`);
|
|
162
254
|
}
|
|
163
|
-
const cachedPath = getCachedApplicationPath(app, remoteAppProps);
|
|
255
|
+
const cachedPath = getCachedApplicationPath(app, remoteAppProps, cachedAppInfo);
|
|
164
256
|
if (cachedPath) {
|
|
165
|
-
if (await
|
|
257
|
+
if (await isAppIntegrityOk(cachedPath, cachedAppInfo?.integrity)) {
|
|
166
258
|
logger.info(`Reusing previously downloaded application at '${cachedPath}'`);
|
|
167
259
|
return verifyAppExtension(cachedPath, supportedAppExtensions);
|
|
168
260
|
}
|
|
169
|
-
logger.info(`The application at '${cachedPath}' does not exist anymore
|
|
261
|
+
logger.info(`The application at '${cachedPath}' does not exist anymore ` +
|
|
262
|
+
`or its integrity has been damaged. Deleting it from the internal cache`);
|
|
170
263
|
APPLICATIONS_CACHE.del(app);
|
|
171
264
|
}
|
|
172
265
|
|
|
@@ -234,19 +327,24 @@ async function configureApp (app, supportedAppExtensions) {
|
|
|
234
327
|
throw new Error(errorMessage);
|
|
235
328
|
}
|
|
236
329
|
|
|
237
|
-
|
|
330
|
+
const isPackageAFile = (await fs.stat(newApp)).isFile();
|
|
331
|
+
if (isPackageAFile) {
|
|
332
|
+
packageHash = await calculateFileIntegrity(newApp);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (isPackageAFile && shouldUnzipApp && !_.isFunction(onPostProcess)) {
|
|
238
336
|
const archivePath = newApp;
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (await fs.exists(fullPath)) {
|
|
337
|
+
if (packageHash === cachedAppInfo?.packageHash) {
|
|
338
|
+
const {fullPath} = cachedAppInfo;
|
|
339
|
+
if (await isAppIntegrityOk(fullPath, cachedAppInfo?.integrity)) {
|
|
243
340
|
if (archivePath !== app) {
|
|
244
341
|
await fs.rimraf(archivePath);
|
|
245
342
|
}
|
|
246
343
|
logger.info(`Will reuse previously cached application at '${fullPath}'`);
|
|
247
344
|
return verifyAppExtension(fullPath, supportedAppExtensions);
|
|
248
345
|
}
|
|
249
|
-
logger.info(`The application at '${fullPath}' does not exist anymore
|
|
346
|
+
logger.info(`The application at '${fullPath}' does not exist anymore ` +
|
|
347
|
+
`or its integrity has been damaged. Deleting it from the cache`);
|
|
250
348
|
APPLICATIONS_CACHE.del(app);
|
|
251
349
|
}
|
|
252
350
|
const tmpRoot = await tempDir.openDir();
|
|
@@ -265,24 +363,43 @@ async function configureApp (app, supportedAppExtensions) {
|
|
|
265
363
|
app = newApp;
|
|
266
364
|
}
|
|
267
365
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
366
|
+
const storeAppInCache = async (appPathToCache) => {
|
|
367
|
+
const cachedFullPath = cachedAppInfo?.fullPath;
|
|
368
|
+
if (cachedFullPath && cachedFullPath !== appPathToCache) {
|
|
369
|
+
await fs.rimraf(cachedFullPath);
|
|
370
|
+
}
|
|
371
|
+
const integrity = {};
|
|
372
|
+
if ((await fs.stat(appPathToCache)).isDirectory()) {
|
|
373
|
+
integrity.folder = await calculateFolderIntegrity(appPathToCache);
|
|
374
|
+
} else {
|
|
375
|
+
integrity.file = await calculateFileIntegrity(appPathToCache);
|
|
277
376
|
}
|
|
278
377
|
APPLICATIONS_CACHE.set(app, {
|
|
279
378
|
...remoteAppProps,
|
|
280
379
|
timestamp: Date.now(),
|
|
281
|
-
|
|
282
|
-
|
|
380
|
+
packageHash,
|
|
381
|
+
integrity,
|
|
382
|
+
fullPath: appPathToCache,
|
|
383
|
+
});
|
|
384
|
+
return appPathToCache;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
if (_.isFunction(onPostProcess)) {
|
|
388
|
+
const result = await onPostProcess({
|
|
389
|
+
cachedAppInfo: _.clone(cachedAppInfo),
|
|
390
|
+
isUrl,
|
|
391
|
+
headers: _.clone(headers),
|
|
392
|
+
appPath: newApp,
|
|
283
393
|
});
|
|
394
|
+
return (!result?.appPath || app === result?.appPath || !await fs.exists(result?.appPath))
|
|
395
|
+
? newApp
|
|
396
|
+
: await storeAppInCache(result.appPath);
|
|
284
397
|
}
|
|
285
|
-
|
|
398
|
+
|
|
399
|
+
verifyAppExtension(newApp, supportedAppExtensions);
|
|
400
|
+
return (app !== newApp && (packageHash || _.values(remoteAppProps).some(Boolean)))
|
|
401
|
+
? await storeAppInCache(newApp)
|
|
402
|
+
: newApp;
|
|
286
403
|
});
|
|
287
404
|
}
|
|
288
405
|
|
|
@@ -338,23 +455,22 @@ async function unzipApp (zipPath, dstRoot, supportedAppExtensions) {
|
|
|
338
455
|
extractionOpts.fileNamesEncoding = 'utf8';
|
|
339
456
|
}
|
|
340
457
|
await zip.extractAllTo(zipPath, tmpRoot, extractionOpts);
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
.
|
|
349
|
-
if (_.isEmpty(allBundleItems)) {
|
|
350
|
-
throw new Error(`App zip unzipped OK, but we could not find '${supportedAppExtensions}' ` +
|
|
458
|
+
const globPattern = `**/*.+(${supportedAppExtensions.map((ext) => ext.replace(/^\./, '')).join('|')})`;
|
|
459
|
+
const sortedBundleItems = (await fs.glob(globPattern, {
|
|
460
|
+
cwd: tmpRoot,
|
|
461
|
+
strict: false,
|
|
462
|
+
// Get the top level match
|
|
463
|
+
})).sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
|
|
464
|
+
if (_.isEmpty(sortedBundleItems)) {
|
|
465
|
+
logger.errorAndThrow(`App unzipped OK, but we could not find any '${supportedAppExtensions}' ` +
|
|
351
466
|
util.pluralize('bundle', supportedAppExtensions.length, false) +
|
|
352
467
|
` in it. Make sure your archive contains at least one package having ` +
|
|
353
468
|
`'${supportedAppExtensions}' ${util.pluralize('extension', supportedAppExtensions.length, false)}`);
|
|
354
469
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
470
|
+
logger.debug(`Extracted ${util.pluralize('bundle item', sortedBundleItems.length, true)} ` +
|
|
471
|
+
`from '${zipPath}' in ${Math.round(timer.getDuration().asMilliSeconds)}ms: ${sortedBundleItems}`);
|
|
472
|
+
const matchedBundle = _.first(sortedBundleItems);
|
|
473
|
+
logger.info(`Assuming '${matchedBundle}' is the correct bundle`);
|
|
358
474
|
const dstPath = path.resolve(dstRoot, path.basename(matchedBundle));
|
|
359
475
|
await fs.mv(path.resolve(tmpRoot, matchedBundle), dstPath, {mkdirp: true});
|
|
360
476
|
return dstPath;
|
package/lib/index.js
CHANGED
|
@@ -21,7 +21,8 @@ const {
|
|
|
21
21
|
Protocol, routeConfiguringFunction, errors, isErrorType,
|
|
22
22
|
errorFromMJSONWPStatusCode, errorFromW3CJsonCode, ALL_COMMANDS, METHOD_MAP,
|
|
23
23
|
routeToCommandName, NO_SESSION_ID_COMMANDS, isSessionCommand,
|
|
24
|
-
normalizeBasePath, determineProtocol, CREATE_SESSION_COMMAND,
|
|
24
|
+
normalizeBasePath, determineProtocol, CREATE_SESSION_COMMAND,
|
|
25
|
+
DELETE_SESSION_COMMAND, GET_STATUS_COMMAND,
|
|
25
26
|
} = protocol;
|
|
26
27
|
|
|
27
28
|
export {
|
|
@@ -29,7 +30,8 @@ export {
|
|
|
29
30
|
errorFromMJSONWPStatusCode, errorFromW3CJsonCode, determineProtocol,
|
|
30
31
|
errorFromMJSONWPStatusCode as errorFromCode, ALL_COMMANDS, METHOD_MAP,
|
|
31
32
|
routeToCommandName, NO_SESSION_ID_COMMANDS, isSessionCommand,
|
|
32
|
-
DEFAULT_BASE_PATH, normalizeBasePath, CREATE_SESSION_COMMAND,
|
|
33
|
+
DEFAULT_BASE_PATH, normalizeBasePath, CREATE_SESSION_COMMAND,
|
|
34
|
+
DELETE_SESSION_COMMAND, GET_STATUS_COMMAND,
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
// Express exports
|
|
@@ -4,7 +4,6 @@ import { duplicateKeys } from '../basedriver/helpers';
|
|
|
4
4
|
import {
|
|
5
5
|
MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY, PROTOCOLS
|
|
6
6
|
} from '../constants';
|
|
7
|
-
import { formatStatus } from '../protocol/helpers';
|
|
8
7
|
|
|
9
8
|
const log = logger.getLogger('Protocol Converter');
|
|
10
9
|
|
|
@@ -206,10 +205,7 @@ class ProtocolConverter {
|
|
|
206
205
|
*/
|
|
207
206
|
async convertAndProxy (commandName, url, method, body) {
|
|
208
207
|
if (!this.downstreamProtocol) {
|
|
209
|
-
|
|
210
|
-
// to preserve the backward compatibility
|
|
211
|
-
const [res, resBodyObj] = await this.proxyFunc(url, method, body);
|
|
212
|
-
return [res, formatStatus(resBodyObj, res.statusCode)];
|
|
208
|
+
return await this.proxyFunc(url, method, body);
|
|
213
209
|
}
|
|
214
210
|
|
|
215
211
|
// Same url, but different arguments
|
|
@@ -9,7 +9,6 @@ import { routeToCommandName } from '../protocol';
|
|
|
9
9
|
import { MAX_LOG_BODY_LENGTH, DEFAULT_BASE_PATH, PROTOCOLS } from '../constants';
|
|
10
10
|
import ProtocolConverter from './protocol-converter';
|
|
11
11
|
import { formatResponseValue, formatStatus } from '../protocol/helpers';
|
|
12
|
-
import SESSIONS_CACHE from '../protocol/sessions-cache';
|
|
13
12
|
import http from 'http';
|
|
14
13
|
import https from 'https';
|
|
15
14
|
|
|
@@ -327,8 +326,7 @@ class JWProxy {
|
|
|
327
326
|
}
|
|
328
327
|
}
|
|
329
328
|
resBodyObj.value = formatResponseValue(resBodyObj.value);
|
|
330
|
-
|
|
331
|
-
res.status(response.statusCode).send(JSON.stringify(resBodyObj));
|
|
329
|
+
res.status(response.statusCode).send(JSON.stringify(formatStatus(resBodyObj)));
|
|
332
330
|
}
|
|
333
331
|
}
|
|
334
332
|
|
package/lib/protocol/errors.js
CHANGED
|
@@ -657,7 +657,7 @@ class ProxyRequestError extends ES6Error {
|
|
|
657
657
|
|
|
658
658
|
getActualError () {
|
|
659
659
|
// If it's MJSONWP error, returns actual error cause for request failure based on `jsonwp.status`
|
|
660
|
-
if (util.hasValue(this.jsonwp
|
|
660
|
+
if (util.hasValue(this.jsonwp?.status) && util.hasValue(this.jsonwp?.value)) {
|
|
661
661
|
return errorFromMJSONWPStatusCode(this.jsonwp.status, this.jsonwp.value);
|
|
662
662
|
} else if (util.hasValue(this.w3c) && _.isNumber(this.w3cStatus) && this.w3cStatus >= 300) {
|
|
663
663
|
return errorFromW3CJsonCode(this.w3c.error, this.w3c.message || this.message, this.w3c.stacktrace);
|
package/lib/protocol/helpers.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
2
|
import { duplicateKeys } from '../basedriver/helpers';
|
|
3
|
-
import { MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY
|
|
4
|
-
|
|
5
|
-
const JSONWP_SUCCESS_STATUS_CODE = 0;
|
|
6
|
-
const JSONWP_UNKNOWN_ERROR_STATUS_CODE = 13;
|
|
3
|
+
import { MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY } from '../constants';
|
|
7
4
|
|
|
8
5
|
/**
|
|
9
6
|
* Preprocesses the resulting value for API responses,
|
|
@@ -26,33 +23,16 @@ function formatResponseValue (resValue) {
|
|
|
26
23
|
|
|
27
24
|
/**
|
|
28
25
|
* Properly formats the status for API responses,
|
|
29
|
-
* so they are correct for
|
|
30
|
-
* This method DOES mutate the `responseBody` argument if needed
|
|
26
|
+
* so they are correct for the W3C protocol.
|
|
31
27
|
*
|
|
32
28
|
* @param {Object} responseBody
|
|
33
|
-
* @param {number} responseCode the HTTP response code
|
|
34
|
-
* @param {?string} protocol The name of the protocol, either
|
|
35
|
-
* `PROTOCOLS.W3C` or `PROTOCOLS.MJSONWP`
|
|
36
29
|
* @returns {Object} The fixed response body
|
|
37
30
|
*/
|
|
38
|
-
function formatStatus (responseBody
|
|
39
|
-
|
|
40
|
-
return responseBody;
|
|
41
|
-
}
|
|
42
|
-
const isError = _.has(responseBody.value, 'error') || responseCode >= 400;
|
|
43
|
-
if ((protocol === PROTOCOLS.MJSONWP && !_.isInteger(responseBody.status))
|
|
44
|
-
|| (!protocol && !_.has(responseBody, 'status'))) {
|
|
45
|
-
responseBody.status = isError
|
|
46
|
-
? JSONWP_UNKNOWN_ERROR_STATUS_CODE
|
|
47
|
-
: JSONWP_SUCCESS_STATUS_CODE;
|
|
48
|
-
} else if (protocol === PROTOCOLS.W3C && _.has(responseBody, 'status')) {
|
|
49
|
-
delete responseBody.status;
|
|
50
|
-
}
|
|
51
|
-
return responseBody;
|
|
31
|
+
function formatStatus (responseBody) {
|
|
32
|
+
return _.isPlainObject(responseBody) ? _.omit(responseBody, ['status']) : responseBody;
|
|
52
33
|
}
|
|
53
34
|
|
|
54
35
|
|
|
55
36
|
export {
|
|
56
|
-
MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY, formatResponseValue,
|
|
57
|
-
JSONWP_SUCCESS_STATUS_CODE, formatStatus,
|
|
37
|
+
MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY, formatResponseValue, formatStatus
|
|
58
38
|
};
|
package/lib/protocol/protocol.js
CHANGED
|
@@ -2,7 +2,7 @@ import _ from 'lodash';
|
|
|
2
2
|
import { util } from '@appium/support';
|
|
3
3
|
import { validators } from './validators';
|
|
4
4
|
import {
|
|
5
|
-
errors, isErrorType, getResponseForW3CError,
|
|
5
|
+
errors, isErrorType, getResponseForW3CError,
|
|
6
6
|
errorFromMJSONWPStatusCode, errorFromW3CJsonCode,
|
|
7
7
|
} from './errors';
|
|
8
8
|
import { METHOD_MAP, NO_SESSION_ID_COMMANDS } from './routes';
|
|
@@ -16,6 +16,7 @@ import SESSIONS_CACHE from './sessions-cache';
|
|
|
16
16
|
|
|
17
17
|
const CREATE_SESSION_COMMAND = 'createSession';
|
|
18
18
|
const DELETE_SESSION_COMMAND = 'deleteSession';
|
|
19
|
+
const GET_STATUS_COMMAND = 'getStatus';
|
|
19
20
|
|
|
20
21
|
class Protocol {}
|
|
21
22
|
|
|
@@ -377,24 +378,7 @@ function buildHandler (app, method, path, spec, driver, isSessCmd) {
|
|
|
377
378
|
.debug(`Encountered internal error running command: ${errMsg}`);
|
|
378
379
|
}
|
|
379
380
|
|
|
380
|
-
|
|
381
|
-
[httpStatus, httpResBody] = getResponseForW3CError(actualErr);
|
|
382
|
-
} else if (currentProtocol === PROTOCOLS.MJSONWP) {
|
|
383
|
-
[httpStatus, httpResBody] = getResponseForJsonwpError(actualErr);
|
|
384
|
-
} else {
|
|
385
|
-
// If it's unknown what the protocol is (like if it's `getStatus` prior to `createSession`), merge the responses
|
|
386
|
-
// together to be protocol-agnostic
|
|
387
|
-
let jsonwpRes = getResponseForJsonwpError(actualErr);
|
|
388
|
-
let w3cRes = getResponseForW3CError(actualErr);
|
|
389
|
-
|
|
390
|
-
httpResBody = {
|
|
391
|
-
...jsonwpRes[1],
|
|
392
|
-
...w3cRes[1],
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
// Use the JSONWP status code (which is usually 500)
|
|
396
|
-
httpStatus = jsonwpRes[0];
|
|
397
|
-
}
|
|
381
|
+
[httpStatus, httpResBody] = getResponseForW3CError(actualErr);
|
|
398
382
|
}
|
|
399
383
|
|
|
400
384
|
// decode the response, which is either a string or json
|
|
@@ -415,7 +399,7 @@ function buildHandler (app, method, path, spec, driver, isSessCmd) {
|
|
|
415
399
|
delete httpResBody.sessionId;
|
|
416
400
|
}
|
|
417
401
|
|
|
418
|
-
httpResBody = formatStatus(httpResBody
|
|
402
|
+
httpResBody = formatStatus(httpResBody);
|
|
419
403
|
res.status(httpStatus).json(httpResBody);
|
|
420
404
|
}
|
|
421
405
|
};
|
|
@@ -433,7 +417,7 @@ function driverShouldDoJwpProxy (driver, req, command) {
|
|
|
433
417
|
|
|
434
418
|
// we should never proxy deleteSession because we need to give the containing
|
|
435
419
|
// driver an opportunity to clean itself up
|
|
436
|
-
if (command ===
|
|
420
|
+
if (command === DELETE_SESSION_COMMAND) {
|
|
437
421
|
return false;
|
|
438
422
|
}
|
|
439
423
|
|
|
@@ -452,11 +436,10 @@ async function doJwpProxy (driver, req, res) {
|
|
|
452
436
|
|
|
453
437
|
// check that the inner driver has a proxy function
|
|
454
438
|
if (!driver.canProxy(req.params.sessionId)) {
|
|
455
|
-
throw new Error('Trying to proxy to a
|
|
439
|
+
throw new Error('Trying to proxy to a server but the driver is unable to proxy');
|
|
456
440
|
}
|
|
457
441
|
try {
|
|
458
|
-
|
|
459
|
-
if (proxiedRes && proxiedRes.error) throw proxiedRes.error; // eslint-disable-line curly
|
|
442
|
+
await driver.executeCommand('proxyReqRes', req, res, req.params.sessionId);
|
|
460
443
|
} catch (err) {
|
|
461
444
|
if (isErrorType(err, errors.ProxyRequestError)) {
|
|
462
445
|
throw err;
|
|
@@ -469,5 +452,6 @@ async function doJwpProxy (driver, req, res) {
|
|
|
469
452
|
|
|
470
453
|
export {
|
|
471
454
|
Protocol, routeConfiguringFunction, isSessionCommand,
|
|
472
|
-
driverShouldDoJwpProxy, determineProtocol, CREATE_SESSION_COMMAND,
|
|
455
|
+
driverShouldDoJwpProxy, determineProtocol, CREATE_SESSION_COMMAND,
|
|
456
|
+
DELETE_SESSION_COMMAND, GET_STATUS_COMMAND,
|
|
473
457
|
};
|