@appium/base-driver 8.1.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/commands/index.js +2 -4
- package/build/lib/basedriver/driver.js +8 -10
- package/build/lib/basedriver/helpers.js +140 -82
- package/build/lib/express/express-logging.js +2 -2
- package/build/lib/index.js +126 -0
- 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/index.js +13 -1
- package/build/lib/protocol/protocol.js +26 -25
- package/build/lib/protocol/routes.js +1 -10
- package/build/test/basedriver/capability-specs.js +10 -10
- package/build/test/basedriver/commands/event-specs.js +10 -10
- package/build/test/basedriver/driver-e2e-specs.js +3 -3
- package/build/test/basedriver/driver-e2e-tests.js +6 -223
- package/build/test/basedriver/driver-specs.js +3 -3
- package/build/test/basedriver/driver-tests.js +6 -6
- package/build/test/basedriver/helpers-specs.js +5 -1
- package/build/test/basedriver/timeout-specs.js +7 -7
- package/build/test/basedriver/websockets-e2e-specs.js +5 -5
- package/build/test/express/server-e2e-specs.js +156 -0
- package/build/test/express/server-specs.js +151 -0
- package/build/test/express/static-specs.js +23 -0
- package/build/test/helpers.js +57 -0
- package/build/test/jsonwp-proxy/mock-request.js +93 -0
- package/build/test/jsonwp-proxy/protocol-converter-specs.js +173 -0
- package/build/test/jsonwp-proxy/proxy-e2e-specs.js +61 -0
- package/build/test/jsonwp-proxy/proxy-specs.js +294 -0
- package/build/test/jsonwp-proxy/url-specs.js +167 -0
- package/build/test/jsonwp-status/status-specs.js +36 -0
- package/build/test/protocol/errors-specs.js +388 -0
- package/build/test/protocol/fake-driver.js +168 -0
- package/build/test/protocol/helpers.js +27 -0
- package/build/test/protocol/protocol-e2e-specs.js +1182 -0
- package/build/test/protocol/routes-specs.js +82 -0
- package/build/test/protocol/validator-specs.js +151 -0
- package/index.d.ts +5 -3
- package/index.js +1 -62
- package/lib/basedriver/capabilities.js +3 -0
- package/lib/basedriver/commands/index.js +0 -2
- package/lib/basedriver/driver.js +6 -26
- package/lib/basedriver/helpers.js +202 -85
- package/lib/express/express-logging.js +1 -1
- package/lib/index.js +64 -0
- 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/index.js +3 -1
- package/lib/protocol/protocol.js +26 -31
- package/lib/protocol/routes.js +0 -3
- package/package.json +8 -16
- package/test/basedriver/capability-specs.js +1 -1
- package/test/basedriver/commands/event-specs.js +1 -1
- package/test/basedriver/driver-e2e-specs.js +1 -1
- package/test/basedriver/driver-e2e-tests.js +1 -179
- package/test/basedriver/driver-specs.js +1 -1
- package/test/basedriver/driver-tests.js +3 -3
- package/test/basedriver/helpers-specs.js +4 -0
- package/test/basedriver/timeout-specs.js +1 -1
- package/test/basedriver/websockets-e2e-specs.js +1 -1
- package/build/index.js +0 -120
- package/build/lib/basedriver/commands/execute-child.js +0 -137
- package/build/lib/basedriver/commands/execute.js +0 -119
- package/build/test/basedriver/fixtures/custom-element-finder-bad.js +0 -12
- package/build/test/basedriver/fixtures/custom-element-finder.js +0 -36
- package/lib/basedriver/commands/execute-child.js +0 -132
- package/lib/basedriver/commands/execute.js +0 -126
|
@@ -2,7 +2,7 @@ import _ from 'lodash';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import url from 'url';
|
|
4
4
|
import logger from './logger';
|
|
5
|
-
import {
|
|
5
|
+
import { tempDir, fs, util, zip, net, timing } from '@appium/support';
|
|
6
6
|
import LRU from 'lru-cache';
|
|
7
7
|
import AsyncLock from 'async-lock';
|
|
8
8
|
import axios from 'axios';
|
|
@@ -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,
|
|
283
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,
|
|
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
|
|
|
@@ -322,38 +439,38 @@ async function unzipApp (zipPath, dstRoot, supportedAppExtensions) {
|
|
|
322
439
|
try {
|
|
323
440
|
logger.debug(`Unzipping '${zipPath}'`);
|
|
324
441
|
const timer = new timing.Timer().start();
|
|
442
|
+
const useSystemUnzipEnv = process.env.APPIUM_PREFER_SYSTEM_UNZIP;
|
|
443
|
+
const useSystemUnzip = _.isEmpty(useSystemUnzipEnv)
|
|
444
|
+
|| !['0', 'false'].includes(_.toLower(useSystemUnzipEnv));
|
|
325
445
|
/**
|
|
326
446
|
* Attempt to use use the system `unzip` (e.g., `/usr/bin/unzip`) due
|
|
327
447
|
* to the significant performance improvement it provides over the native
|
|
328
448
|
* JS "unzip" implementation.
|
|
329
449
|
* @type {import('@appium/support/lib/zip').ExtractAllOptions}
|
|
330
450
|
*/
|
|
331
|
-
const extractionOpts = {
|
|
332
|
-
useSystemUnzip: !system.isWindows(),
|
|
333
|
-
};
|
|
451
|
+
const extractionOpts = {useSystemUnzip};
|
|
334
452
|
// https://github.com/appium/appium/issues/14100
|
|
335
453
|
if (path.extname(zipPath) === IPA_EXT) {
|
|
336
454
|
logger.debug(`Enforcing UTF-8 encoding on the extracted file names for '${path.basename(zipPath)}'`);
|
|
337
455
|
extractionOpts.fileNamesEncoding = 'utf8';
|
|
338
456
|
}
|
|
339
457
|
await zip.extractAllTo(zipPath, tmpRoot, extractionOpts);
|
|
340
|
-
const
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
.
|
|
348
|
-
if (_.isEmpty(allBundleItems)) {
|
|
349
|
-
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}' ` +
|
|
350
466
|
util.pluralize('bundle', supportedAppExtensions.length, false) +
|
|
351
467
|
` in it. Make sure your archive contains at least one package having ` +
|
|
352
468
|
`'${supportedAppExtensions}' ${util.pluralize('extension', supportedAppExtensions.length, false)}`);
|
|
353
469
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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`);
|
|
357
474
|
const dstPath = path.resolve(dstRoot, path.basename(matchedBundle));
|
|
358
475
|
await fs.mv(path.resolve(tmpRoot, matchedBundle), dstPath, {mkdirp: true});
|
|
359
476
|
return dstPath;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// transpile:main
|
|
2
|
+
|
|
3
|
+
// BaseDriver exports
|
|
4
|
+
import * as driver from './basedriver/driver';
|
|
5
|
+
import * as deviceSettings from './basedriver/device-settings';
|
|
6
|
+
|
|
7
|
+
const { BaseDriver } = driver;
|
|
8
|
+
const { DeviceSettings, BASEDRIVER_HANDLED_SETTINGS } = deviceSettings;
|
|
9
|
+
|
|
10
|
+
export { BaseDriver, DeviceSettings, BASEDRIVER_HANDLED_SETTINGS };
|
|
11
|
+
export default BaseDriver;
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
// MJSONWP exports
|
|
15
|
+
import * as protocol from './protocol';
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_BASE_PATH, PROTOCOLS
|
|
18
|
+
} from './constants';
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
Protocol, routeConfiguringFunction, errors, isErrorType,
|
|
22
|
+
errorFromMJSONWPStatusCode, errorFromW3CJsonCode, ALL_COMMANDS, METHOD_MAP,
|
|
23
|
+
routeToCommandName, NO_SESSION_ID_COMMANDS, isSessionCommand,
|
|
24
|
+
normalizeBasePath, determineProtocol, CREATE_SESSION_COMMAND,
|
|
25
|
+
DELETE_SESSION_COMMAND, GET_STATUS_COMMAND,
|
|
26
|
+
} = protocol;
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
Protocol, routeConfiguringFunction, errors, isErrorType, PROTOCOLS,
|
|
30
|
+
errorFromMJSONWPStatusCode, errorFromW3CJsonCode, determineProtocol,
|
|
31
|
+
errorFromMJSONWPStatusCode as errorFromCode, ALL_COMMANDS, METHOD_MAP,
|
|
32
|
+
routeToCommandName, NO_SESSION_ID_COMMANDS, isSessionCommand,
|
|
33
|
+
DEFAULT_BASE_PATH, normalizeBasePath, CREATE_SESSION_COMMAND,
|
|
34
|
+
DELETE_SESSION_COMMAND, GET_STATUS_COMMAND,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Express exports
|
|
38
|
+
import * as staticIndex from './express/static';
|
|
39
|
+
const { STATIC_DIR } = staticIndex;
|
|
40
|
+
export { STATIC_DIR };
|
|
41
|
+
|
|
42
|
+
import * as serverIndex from './express/server';
|
|
43
|
+
const { server } = serverIndex;
|
|
44
|
+
export { server };
|
|
45
|
+
|
|
46
|
+
// jsonwp-proxy exports
|
|
47
|
+
import * as proxyIndex from './jsonwp-proxy/proxy';
|
|
48
|
+
const { JWProxy } = proxyIndex;
|
|
49
|
+
export { JWProxy };
|
|
50
|
+
|
|
51
|
+
// jsonwp-status exports
|
|
52
|
+
import * as statusIndex from './jsonwp-status/status';
|
|
53
|
+
const { codes: statusCodes, getSummaryByCode } = statusIndex;
|
|
54
|
+
export { statusCodes, getSummaryByCode };
|
|
55
|
+
|
|
56
|
+
// W3C capabilities parser
|
|
57
|
+
import * as caps from './basedriver/capabilities';
|
|
58
|
+
const { processCapabilities, isStandardCap, validateCaps } = caps;
|
|
59
|
+
export { processCapabilities, isStandardCap, validateCaps };
|
|
60
|
+
|
|
61
|
+
// Web socket helpers
|
|
62
|
+
import * as ws from './express/websocket';
|
|
63
|
+
const { DEFAULT_WS_PATHNAME_PREFIX } = ws;
|
|
64
|
+
export { DEFAULT_WS_PATHNAME_PREFIX };
|
|
@@ -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/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// transpile:main
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
Protocol, isSessionCommand, routeConfiguringFunction, determineProtocol
|
|
4
|
+
Protocol, isSessionCommand, routeConfiguringFunction, determineProtocol,
|
|
5
|
+
CREATE_SESSION_COMMAND, DELETE_SESSION_COMMAND,
|
|
5
6
|
} from './protocol';
|
|
6
7
|
import {
|
|
7
8
|
NO_SESSION_ID_COMMANDS, ALL_COMMANDS, METHOD_MAP,
|
|
@@ -15,4 +16,5 @@ export {
|
|
|
15
16
|
Protocol, routeConfiguringFunction, errors, isErrorType,
|
|
16
17
|
errorFromMJSONWPStatusCode, errorFromW3CJsonCode, ALL_COMMANDS, METHOD_MAP,
|
|
17
18
|
routeToCommandName, NO_SESSION_ID_COMMANDS, isSessionCommand, determineProtocol,
|
|
19
|
+
CREATE_SESSION_COMMAND, DELETE_SESSION_COMMAND,
|
|
18
20
|
};
|