@appium/base-driver 8.2.2 → 8.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/lib/basedriver/capabilities.js +3 -1
- package/build/lib/basedriver/commands/find.js +4 -11
- package/build/lib/basedriver/commands/log.js +3 -6
- package/build/lib/basedriver/commands/session.js +18 -27
- package/build/lib/basedriver/commands/settings.js +4 -8
- package/build/lib/basedriver/commands/timeout.js +10 -15
- package/build/lib/basedriver/device-settings.js +14 -2
- package/build/lib/basedriver/driver.js +25 -23
- package/build/lib/basedriver/helpers.js +140 -84
- package/build/lib/express/express-logging.js +2 -2
- package/build/lib/express/idempotency.js +2 -2
- package/build/lib/helpers/capabilities.js +39 -0
- package/build/lib/index.js +7 -7
- package/build/lib/jsonwp-proxy/protocol-converter.js +19 -16
- package/build/lib/jsonwp-proxy/proxy.js +20 -16
- package/build/lib/protocol/errors.js +4 -2
- package/build/lib/protocol/helpers.js +3 -20
- package/build/lib/protocol/protocol.js +44 -45
- package/build/lib/protocol/routes.js +67 -1
- package/build/test/basedriver/capabilities-specs.js +43 -1
- package/build/test/basedriver/capability-specs.js +126 -167
- package/build/test/basedriver/commands/log-specs.js +12 -5
- package/build/test/basedriver/driver-tests.js +11 -14
- package/build/test/basedriver/helpers-specs.js +5 -1
- package/build/test/basedriver/timeout-specs.js +7 -9
- package/build/test/express/server-e2e-specs.js +10 -5
- package/build/test/express/server-specs.js +22 -16
- package/build/test/express/static-specs.js +10 -5
- 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/fake-driver.js +12 -15
- package/build/test/protocol/protocol-e2e-specs.js +49 -103
- package/build/test/protocol/routes-specs.js +2 -2
- package/lib/basedriver/capabilities.js +3 -0
- package/lib/basedriver/commands/find.js +3 -6
- package/lib/basedriver/commands/log.js +2 -4
- package/lib/basedriver/commands/session.js +21 -22
- package/lib/basedriver/commands/settings.js +3 -5
- package/lib/basedriver/commands/timeout.js +9 -10
- package/lib/basedriver/device-settings.js +10 -1
- package/lib/basedriver/driver.js +29 -16
- package/lib/basedriver/helpers.js +201 -83
- package/lib/express/express-logging.js +1 -1
- package/lib/express/idempotency.js +1 -1
- package/lib/helpers/capabilities.js +25 -0
- package/lib/index.js +6 -4
- package/lib/jsonwp-proxy/protocol-converter.js +15 -18
- package/lib/jsonwp-proxy/proxy.js +17 -15
- package/lib/protocol/errors.js +1 -1
- package/lib/protocol/helpers.js +5 -25
- package/lib/protocol/protocol.js +43 -54
- package/lib/protocol/routes.js +60 -1
- package/package.json +29 -22
- package/test/basedriver/capabilities-specs.js +34 -2
- package/test/basedriver/capability-specs.js +120 -146
- package/test/basedriver/commands/log-specs.js +12 -3
- package/test/basedriver/driver-tests.js +12 -7
- package/test/basedriver/helpers-specs.js +4 -0
- package/test/basedriver/timeout-specs.js +6 -11
- package/build/lib/protocol/sessions-cache.js +0 -88
- package/lib/protocol/sessions-cache.js +0 -74
|
@@ -15,16 +15,17 @@ const ZIP_MIME_TYPES = [
|
|
|
15
15
|
'multipart/x-zip',
|
|
16
16
|
];
|
|
17
17
|
const CACHED_APPS_MAX_AGE = 1000 * 60 * 60 * 24; // ms
|
|
18
|
+
const MAX_CACHED_APPS = 1024;
|
|
18
19
|
const APPLICATIONS_CACHE = new LRU({
|
|
19
|
-
|
|
20
|
+
max: MAX_CACHED_APPS,
|
|
21
|
+
ttl: CACHED_APPS_MAX_AGE, // expire after 24 hours
|
|
20
22
|
updateAgeOnGet: true,
|
|
21
|
-
dispose:
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
dispose: (app, {fullPath}) => {
|
|
24
|
+
logger.info(`The application '${app}' cached at '${fullPath}' has ` +
|
|
25
|
+
`expired after ${CACHED_APPS_MAX_AGE}ms`);
|
|
26
|
+
if (fullPath) {
|
|
27
|
+
fs.rimraf(fullPath);
|
|
24
28
|
}
|
|
25
|
-
|
|
26
|
-
logger.info(`The application '${app}' cached at '${fullPath}' has expired`);
|
|
27
|
-
await fs.rimraf(fullPath);
|
|
28
29
|
},
|
|
29
30
|
noDisposeOnSet: true,
|
|
30
31
|
});
|
|
@@ -34,11 +35,11 @@ const DEFAULT_BASENAME = 'appium-app';
|
|
|
34
35
|
const APP_DOWNLOAD_TIMEOUT_MS = 120 * 1000;
|
|
35
36
|
|
|
36
37
|
process.on('exit', () => {
|
|
37
|
-
if (APPLICATIONS_CACHE.
|
|
38
|
+
if (APPLICATIONS_CACHE.size === 0) {
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
const appPaths = APPLICATIONS_CACHE.values()
|
|
42
|
+
const appPaths = [...APPLICATIONS_CACHE.values()]
|
|
42
43
|
.map(({fullPath}) => fullPath);
|
|
43
44
|
logger.debug(`Performing cleanup of ${appPaths.length} cached ` +
|
|
44
45
|
util.pluralize('application', appPaths.length));
|
|
@@ -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,106 @@ 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 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 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 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
|
+
* If the downloaded app has `.zip` extension, this method will unzip it.
|
|
197
|
+
* The unzip does not work when `onPostProcess` is provided.
|
|
198
|
+
*
|
|
199
|
+
* @param {string} app Either a full path to the app or a remote URL
|
|
200
|
+
* @param {string|string[]|ConfigureAppOptions} options
|
|
201
|
+
* @returns The full path to the resulting application bundle
|
|
202
|
+
*/
|
|
203
|
+
async function configureApp (app, options = {}) {
|
|
125
204
|
if (!_.isString(app)) {
|
|
126
205
|
// immediately shortcircuit if not given an app
|
|
127
206
|
return;
|
|
128
207
|
}
|
|
129
|
-
|
|
130
|
-
|
|
208
|
+
|
|
209
|
+
let supportedAppExtensions;
|
|
210
|
+
const {
|
|
211
|
+
onPostProcess,
|
|
212
|
+
} = _.isPlainObject(options) ? options : {};
|
|
213
|
+
if (_.isString(options)) {
|
|
214
|
+
supportedAppExtensions = [options];
|
|
215
|
+
} else if (_.isArray(options)) {
|
|
216
|
+
supportedAppExtensions = options;
|
|
217
|
+
} else if (_.isPlainObject(options)) {
|
|
218
|
+
supportedAppExtensions = options.supportedExtensions;
|
|
219
|
+
}
|
|
220
|
+
if (_.isEmpty(supportedAppExtensions)) {
|
|
221
|
+
throw new Error(`One or more supported app extensions must be provided`);
|
|
131
222
|
}
|
|
132
223
|
|
|
133
224
|
let newApp = app;
|
|
134
225
|
let shouldUnzipApp = false;
|
|
135
|
-
let
|
|
226
|
+
let packageHash = null;
|
|
227
|
+
let headers = null;
|
|
136
228
|
const remoteAppProps = {
|
|
137
229
|
lastModified: null,
|
|
138
230
|
immutable: false,
|
|
@@ -141,11 +233,13 @@ async function configureApp (app, supportedAppExtensions) {
|
|
|
141
233
|
const {protocol, pathname} = url.parse(newApp);
|
|
142
234
|
const isUrl = ['http:', 'https:'].includes(protocol);
|
|
143
235
|
|
|
236
|
+
const cachedAppInfo = APPLICATIONS_CACHE.get(app);
|
|
237
|
+
|
|
144
238
|
return await APPLICATIONS_CACHE_GUARD.acquire(app, async () => {
|
|
145
239
|
if (isUrl) {
|
|
146
240
|
// Use the app from remote URL
|
|
147
241
|
logger.info(`Using downloadable app '${newApp}'`);
|
|
148
|
-
|
|
242
|
+
headers = await retrieveHeaders(newApp);
|
|
149
243
|
if (!_.isEmpty(headers)) {
|
|
150
244
|
if (headers['last-modified']) {
|
|
151
245
|
remoteAppProps.lastModified = new Date(headers['last-modified']);
|
|
@@ -160,13 +254,14 @@ async function configureApp (app, supportedAppExtensions) {
|
|
|
160
254
|
}
|
|
161
255
|
logger.debug(`Cache-Control: ${headers['cache-control']}`);
|
|
162
256
|
}
|
|
163
|
-
const cachedPath = getCachedApplicationPath(app, remoteAppProps);
|
|
257
|
+
const cachedPath = getCachedApplicationPath(app, remoteAppProps, cachedAppInfo);
|
|
164
258
|
if (cachedPath) {
|
|
165
|
-
if (await
|
|
259
|
+
if (await isAppIntegrityOk(cachedPath, cachedAppInfo?.integrity)) {
|
|
166
260
|
logger.info(`Reusing previously downloaded application at '${cachedPath}'`);
|
|
167
261
|
return verifyAppExtension(cachedPath, supportedAppExtensions);
|
|
168
262
|
}
|
|
169
|
-
logger.info(`The application at '${cachedPath}' does not exist anymore
|
|
263
|
+
logger.info(`The application at '${cachedPath}' does not exist anymore ` +
|
|
264
|
+
`or its integrity has been damaged. Deleting it from the internal cache`);
|
|
170
265
|
APPLICATIONS_CACHE.del(app);
|
|
171
266
|
}
|
|
172
267
|
|
|
@@ -234,19 +329,24 @@ async function configureApp (app, supportedAppExtensions) {
|
|
|
234
329
|
throw new Error(errorMessage);
|
|
235
330
|
}
|
|
236
331
|
|
|
237
|
-
|
|
332
|
+
const isPackageAFile = (await fs.stat(newApp)).isFile();
|
|
333
|
+
if (isPackageAFile) {
|
|
334
|
+
packageHash = await calculateFileIntegrity(newApp);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (isPackageAFile && shouldUnzipApp && !_.isFunction(onPostProcess)) {
|
|
238
338
|
const archivePath = newApp;
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (await fs.exists(fullPath)) {
|
|
339
|
+
if (packageHash === cachedAppInfo?.packageHash) {
|
|
340
|
+
const {fullPath} = cachedAppInfo;
|
|
341
|
+
if (await isAppIntegrityOk(fullPath, cachedAppInfo?.integrity)) {
|
|
243
342
|
if (archivePath !== app) {
|
|
244
343
|
await fs.rimraf(archivePath);
|
|
245
344
|
}
|
|
246
345
|
logger.info(`Will reuse previously cached application at '${fullPath}'`);
|
|
247
346
|
return verifyAppExtension(fullPath, supportedAppExtensions);
|
|
248
347
|
}
|
|
249
|
-
logger.info(`The application at '${fullPath}' does not exist anymore
|
|
348
|
+
logger.info(`The application at '${fullPath}' does not exist anymore ` +
|
|
349
|
+
`or its integrity has been damaged. Deleting it from the cache`);
|
|
250
350
|
APPLICATIONS_CACHE.del(app);
|
|
251
351
|
}
|
|
252
352
|
const tmpRoot = await tempDir.openDir();
|
|
@@ -265,24 +365,43 @@ async function configureApp (app, supportedAppExtensions) {
|
|
|
265
365
|
app = newApp;
|
|
266
366
|
}
|
|
267
367
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
368
|
+
const storeAppInCache = async (appPathToCache) => {
|
|
369
|
+
const cachedFullPath = cachedAppInfo?.fullPath;
|
|
370
|
+
if (cachedFullPath && cachedFullPath !== appPathToCache) {
|
|
371
|
+
await fs.rimraf(cachedFullPath);
|
|
372
|
+
}
|
|
373
|
+
const integrity = {};
|
|
374
|
+
if ((await fs.stat(appPathToCache)).isDirectory()) {
|
|
375
|
+
integrity.folder = await calculateFolderIntegrity(appPathToCache);
|
|
376
|
+
} else {
|
|
377
|
+
integrity.file = await calculateFileIntegrity(appPathToCache);
|
|
277
378
|
}
|
|
278
379
|
APPLICATIONS_CACHE.set(app, {
|
|
279
380
|
...remoteAppProps,
|
|
280
381
|
timestamp: Date.now(),
|
|
281
|
-
|
|
282
|
-
|
|
382
|
+
packageHash,
|
|
383
|
+
integrity,
|
|
384
|
+
fullPath: appPathToCache,
|
|
385
|
+
});
|
|
386
|
+
return appPathToCache;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
if (_.isFunction(onPostProcess)) {
|
|
390
|
+
const result = await onPostProcess({
|
|
391
|
+
cachedAppInfo: _.clone(cachedAppInfo),
|
|
392
|
+
isUrl,
|
|
393
|
+
headers: _.clone(headers),
|
|
394
|
+
appPath: newApp,
|
|
283
395
|
});
|
|
396
|
+
return (!result?.appPath || app === result?.appPath || !await fs.exists(result?.appPath))
|
|
397
|
+
? newApp
|
|
398
|
+
: await storeAppInCache(result.appPath);
|
|
284
399
|
}
|
|
285
|
-
|
|
400
|
+
|
|
401
|
+
verifyAppExtension(newApp, supportedAppExtensions);
|
|
402
|
+
return (app !== newApp && (packageHash || _.values(remoteAppProps).some(Boolean)))
|
|
403
|
+
? await storeAppInCache(newApp)
|
|
404
|
+
: newApp;
|
|
286
405
|
});
|
|
287
406
|
}
|
|
288
407
|
|
|
@@ -338,23 +457,22 @@ async function unzipApp (zipPath, dstRoot, supportedAppExtensions) {
|
|
|
338
457
|
extractionOpts.fileNamesEncoding = 'utf8';
|
|
339
458
|
}
|
|
340
459
|
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}' ` +
|
|
460
|
+
const globPattern = `**/*.+(${supportedAppExtensions.map((ext) => ext.replace(/^\./, '')).join('|')})`;
|
|
461
|
+
const sortedBundleItems = (await fs.glob(globPattern, {
|
|
462
|
+
cwd: tmpRoot,
|
|
463
|
+
strict: false,
|
|
464
|
+
// Get the top level match
|
|
465
|
+
})).sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
|
|
466
|
+
if (_.isEmpty(sortedBundleItems)) {
|
|
467
|
+
logger.errorAndThrow(`App unzipped OK, but we could not find any '${supportedAppExtensions}' ` +
|
|
351
468
|
util.pluralize('bundle', supportedAppExtensions.length, false) +
|
|
352
469
|
` in it. Make sure your archive contains at least one package having ` +
|
|
353
470
|
`'${supportedAppExtensions}' ${util.pluralize('extension', supportedAppExtensions.length, false)}`);
|
|
354
471
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
472
|
+
logger.debug(`Extracted ${util.pluralize('bundle item', sortedBundleItems.length, true)} ` +
|
|
473
|
+
`from '${zipPath}' in ${Math.round(timer.getDuration().asMilliSeconds)}ms: ${sortedBundleItems}`);
|
|
474
|
+
const matchedBundle = _.first(sortedBundleItems);
|
|
475
|
+
logger.info(`Assuming '${matchedBundle}' is the correct bundle`);
|
|
358
476
|
const dstPath = path.resolve(dstRoot, path.basename(matchedBundle));
|
|
359
477
|
await fs.mv(path.resolve(tmpRoot, matchedBundle), dstPath, {mkdirp: true});
|
|
360
478
|
return dstPath;
|
|
@@ -20,7 +20,7 @@ const MONITORED_METHODS = ['POST', 'PATCH'];
|
|
|
20
20
|
const IDEMPOTENCY_KEY_HEADER = 'x-idempotency-key';
|
|
21
21
|
|
|
22
22
|
process.on('exit', () => {
|
|
23
|
-
const resPaths = IDEMPOTENT_RESPONSES.values()
|
|
23
|
+
const resPaths = [...IDEMPOTENT_RESPONSES.values()]
|
|
24
24
|
.map(({response}) => response)
|
|
25
25
|
.filter(Boolean);
|
|
26
26
|
for (const resPath of resPaths) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
|
|
3
|
+
function isW3cCaps (caps) {
|
|
4
|
+
if (!_.isPlainObject(caps)) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const isFirstMatchValid = () => _.isArray(caps.firstMatch)
|
|
9
|
+
&& !_.isEmpty(caps.firstMatch) && _.every(caps.firstMatch, _.isPlainObject);
|
|
10
|
+
const isAlwaysMatchValid = () => _.isPlainObject(caps.alwaysMatch);
|
|
11
|
+
if (_.has(caps, 'firstMatch') && _.has(caps, 'alwaysMatch')) {
|
|
12
|
+
return isFirstMatchValid() && isAlwaysMatchValid();
|
|
13
|
+
}
|
|
14
|
+
if (_.has(caps, 'firstMatch')) {
|
|
15
|
+
return isFirstMatchValid();
|
|
16
|
+
}
|
|
17
|
+
if (_.has(caps, 'alwaysMatch')) {
|
|
18
|
+
return isAlwaysMatchValid();
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
isW3cCaps,
|
|
25
|
+
};
|
package/lib/index.js
CHANGED
|
@@ -5,9 +5,9 @@ import * as driver from './basedriver/driver';
|
|
|
5
5
|
import * as deviceSettings from './basedriver/device-settings';
|
|
6
6
|
|
|
7
7
|
const { BaseDriver } = driver;
|
|
8
|
-
const { DeviceSettings
|
|
8
|
+
const { DeviceSettings } = deviceSettings;
|
|
9
9
|
|
|
10
|
-
export { BaseDriver, DeviceSettings
|
|
10
|
+
export { BaseDriver, DeviceSettings };
|
|
11
11
|
export default BaseDriver;
|
|
12
12
|
|
|
13
13
|
|
|
@@ -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,10 +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
|
-
|
|
9
|
-
const log = logger.getLogger('Protocol Converter');
|
|
10
|
-
|
|
11
7
|
|
|
12
8
|
export const COMMAND_URLS_CONFLICTS = [
|
|
13
9
|
{
|
|
@@ -42,20 +38,24 @@ export const COMMAND_URLS_CONFLICTS = [
|
|
|
42
38
|
jsonwpConverter: (w3cUrl) => {
|
|
43
39
|
const w3cPropertyRegex = /\/element\/([^/]+)\/property\/([^/]+)/;
|
|
44
40
|
const jsonwpUrl = w3cUrl.replace(w3cPropertyRegex, '/element/$1/attribute/$2');
|
|
45
|
-
log.info(`Converting W3C '${w3cUrl}' to '${jsonwpUrl}'`);
|
|
46
41
|
return jsonwpUrl;
|
|
47
42
|
},
|
|
48
43
|
w3cConverter: (jsonwpUrl) => jsonwpUrl // Don't convert JSONWP URL to W3C. W3C accepts /attribute and /property
|
|
49
44
|
}
|
|
50
45
|
];
|
|
51
|
-
|
|
52
46
|
const {MJSONWP, W3C} = PROTOCOLS;
|
|
47
|
+
const DEFAULT_LOG = logger.getLogger('Protocol Converter');
|
|
53
48
|
|
|
54
49
|
|
|
55
50
|
class ProtocolConverter {
|
|
56
|
-
constructor (proxyFunc) {
|
|
51
|
+
constructor (proxyFunc, log = null) {
|
|
57
52
|
this.proxyFunc = proxyFunc;
|
|
58
53
|
this._downstreamProtocol = null;
|
|
54
|
+
this._log = log;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get log () {
|
|
58
|
+
return this._log ?? DEFAULT_LOG;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
set downstreamProtocol (value) {
|
|
@@ -108,7 +108,7 @@ class ProtocolConverter {
|
|
|
108
108
|
let response, resBody;
|
|
109
109
|
|
|
110
110
|
const timeoutRequestObjects = this.getTimeoutRequestObjects(body);
|
|
111
|
-
log.debug(`Will send the following request bodies to /timeouts: ${JSON.stringify(timeoutRequestObjects)}`);
|
|
111
|
+
this.log.debug(`Will send the following request bodies to /timeouts: ${JSON.stringify(timeoutRequestObjects)}`);
|
|
112
112
|
for (const timeoutObj of timeoutRequestObjects) {
|
|
113
113
|
[response, resBody] = await this.proxyFunc(url, method, timeoutObj);
|
|
114
114
|
|
|
@@ -131,14 +131,14 @@ class ProtocolConverter {
|
|
|
131
131
|
const bodyObj = util.safeJsonParse(body);
|
|
132
132
|
if (_.isPlainObject(bodyObj)) {
|
|
133
133
|
if (this.downstreamProtocol === W3C && _.has(bodyObj, 'name') && !_.has(bodyObj, 'handle')) {
|
|
134
|
-
log.debug(`Copied 'name' value '${bodyObj.name}' to 'handle' as per W3C spec`);
|
|
134
|
+
this.log.debug(`Copied 'name' value '${bodyObj.name}' to 'handle' as per W3C spec`);
|
|
135
135
|
return await this.proxyFunc(url, method, {
|
|
136
136
|
...bodyObj,
|
|
137
137
|
handle: bodyObj.name,
|
|
138
138
|
});
|
|
139
139
|
}
|
|
140
140
|
if (this.downstreamProtocol === MJSONWP && _.has(bodyObj, 'handle') && !_.has(bodyObj, 'name')) {
|
|
141
|
-
log.debug(`Copied 'handle' value '${bodyObj.handle}' to 'name' as per JSONWP spec`);
|
|
141
|
+
this.log.debug(`Copied 'handle' value '${bodyObj.handle}' to 'name' as per JSONWP spec`);
|
|
142
142
|
return await this.proxyFunc(url, method, {
|
|
143
143
|
...bodyObj,
|
|
144
144
|
name: bodyObj.handle,
|
|
@@ -157,12 +157,12 @@ class ProtocolConverter {
|
|
|
157
157
|
value = _.isString(text)
|
|
158
158
|
? [...text]
|
|
159
159
|
: (_.isArray(text) ? text : []);
|
|
160
|
-
log.debug(`Added 'value' property ${JSON.stringify(value)} to 'setValue' request body`);
|
|
160
|
+
this.log.debug(`Added 'value' property ${JSON.stringify(value)} to 'setValue' request body`);
|
|
161
161
|
} else if (!util.hasValue(text) && util.hasValue(value)) {
|
|
162
162
|
text = _.isArray(value)
|
|
163
163
|
? value.join('')
|
|
164
164
|
: (_.isString(value) ? value : '');
|
|
165
|
-
log.debug(`Added 'text' property ${JSON.stringify(text)} to 'setValue' request body`);
|
|
165
|
+
this.log.debug(`Added 'text' property ${JSON.stringify(text)} to 'setValue' request body`);
|
|
166
166
|
}
|
|
167
167
|
return await this.proxyFunc(url, method, Object.assign({}, bodyObj, {
|
|
168
168
|
text,
|
|
@@ -206,10 +206,7 @@ class ProtocolConverter {
|
|
|
206
206
|
*/
|
|
207
207
|
async convertAndProxy (commandName, url, method, body) {
|
|
208
208
|
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)];
|
|
209
|
+
return await this.proxyFunc(url, method, body);
|
|
213
210
|
}
|
|
214
211
|
|
|
215
212
|
// Same url, but different arguments
|
|
@@ -240,11 +237,11 @@ class ProtocolConverter {
|
|
|
240
237
|
? jsonwpConverter(url)
|
|
241
238
|
: w3cConverter(url);
|
|
242
239
|
if (rewrittenUrl === url) {
|
|
243
|
-
log.debug(`Did not know how to rewrite the original URL '${url}' ` +
|
|
240
|
+
this.log.debug(`Did not know how to rewrite the original URL '${url}' ` +
|
|
244
241
|
`for ${this.downstreamProtocol} protocol`);
|
|
245
242
|
break;
|
|
246
243
|
}
|
|
247
|
-
log.info(`Rewrote the original URL '${url}' to '${rewrittenUrl}' ` +
|
|
244
|
+
this.log.info(`Rewrote the original URL '${url}' to '${rewrittenUrl}' ` +
|
|
248
245
|
`for ${this.downstreamProtocol} protocol`);
|
|
249
246
|
return await this.proxyFunc(rewrittenUrl, method, body);
|
|
250
247
|
}
|