@appium/base-driver 8.5.3 → 8.5.4
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.d.ts.map +1 -1
- package/build/lib/basedriver/capabilities.js +1 -1
- package/build/lib/basedriver/commands/event.js +1 -1
- package/build/lib/basedriver/commands/find.d.ts.map +1 -1
- package/build/lib/basedriver/commands/find.js +1 -1
- package/build/lib/basedriver/commands/index.js +1 -1
- package/build/lib/basedriver/commands/log.d.ts.map +1 -1
- package/build/lib/basedriver/commands/log.js +1 -1
- package/build/lib/basedriver/commands/session.js +1 -1
- package/build/lib/basedriver/commands/settings.d.ts.map +1 -1
- package/build/lib/basedriver/commands/settings.js +1 -1
- package/build/lib/basedriver/commands/timeout.d.ts.map +1 -1
- package/build/lib/basedriver/commands/timeout.js +1 -1
- package/build/lib/basedriver/core.d.ts +120 -139
- package/build/lib/basedriver/core.d.ts.map +1 -1
- package/build/lib/basedriver/core.js +1 -49
- package/build/lib/basedriver/desired-caps.d.ts +5 -2
- package/build/lib/basedriver/desired-caps.d.ts.map +1 -1
- package/build/lib/basedriver/desired-caps.js +14 -18
- package/build/lib/basedriver/device-settings.d.ts +9 -9
- package/build/lib/basedriver/device-settings.d.ts.map +1 -1
- package/build/lib/basedriver/device-settings.js +4 -4
- package/build/lib/basedriver/driver.d.ts +43 -38
- package/build/lib/basedriver/driver.d.ts.map +1 -1
- package/build/lib/basedriver/driver.js +58 -11
- package/build/lib/basedriver/helpers.d.ts +8 -3
- package/build/lib/basedriver/helpers.d.ts.map +1 -1
- package/build/lib/basedriver/helpers.js +4 -6
- package/build/lib/basedriver/logger.d.ts +1 -1
- package/build/lib/basedriver/logger.d.ts.map +1 -1
- package/build/lib/basedriver/logger.js +1 -1
- package/build/lib/constants.js +1 -1
- package/build/lib/express/crash.d.ts.map +1 -1
- package/build/lib/express/crash.js +1 -1
- package/build/lib/express/express-logging.d.ts.map +1 -1
- package/build/lib/express/express-logging.js +1 -1
- package/build/lib/express/idempotency.js +1 -1
- package/build/lib/express/logger.d.ts +1 -1
- package/build/lib/express/logger.d.ts.map +1 -1
- package/build/lib/express/logger.js +1 -1
- package/build/lib/express/middleware.d.ts.map +1 -1
- package/build/lib/express/middleware.js +1 -1
- package/build/lib/express/server.d.ts +21 -0
- package/build/lib/express/server.d.ts.map +1 -1
- package/build/lib/express/server.js +4 -9
- package/build/lib/express/static.d.ts.map +1 -1
- package/build/lib/express/static.js +2 -2
- package/build/lib/express/websocket.d.ts +14 -11
- package/build/lib/express/websocket.d.ts.map +1 -1
- package/build/lib/express/websocket.js +2 -2
- package/build/lib/helpers/capabilities.d.ts.map +1 -1
- package/build/lib/helpers/capabilities.js +1 -1
- package/build/lib/index.d.ts +2 -1
- package/build/lib/index.js +7 -1
- package/build/lib/jsonwp-proxy/protocol-converter.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.js +2 -2
- package/build/lib/jsonwp-proxy/proxy.d.ts +30 -5
- package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.js +20 -4
- package/build/lib/jsonwp-status/status.d.ts.map +1 -1
- package/build/lib/jsonwp-status/status.js +2 -2
- package/build/lib/protocol/errors.d.ts +17 -8
- package/build/lib/protocol/errors.d.ts.map +1 -1
- package/build/lib/protocol/errors.js +9 -5
- package/build/lib/protocol/helpers.js +1 -1
- package/build/lib/protocol/index.js +1 -1
- package/build/lib/protocol/protocol.d.ts.map +1 -1
- package/build/lib/protocol/protocol.js +1 -1
- package/build/lib/protocol/routes.d.ts +17 -3
- package/build/lib/protocol/routes.d.ts.map +1 -1
- package/build/lib/protocol/routes.js +1 -1
- package/build/lib/protocol/validators.js +1 -1
- package/build/test/basedriver/driver-e2e-tests.js +1 -1
- package/build/test/basedriver/driver-tests.js +1 -1
- package/build/test/basedriver/index.js +1 -1
- package/build/test/e2e/basedriver/driver.e2e.spec.js +1 -1
- package/build/test/e2e/basedriver/helpers.e2e.spec.js +1 -1
- package/build/test/e2e/basedriver/websockets.e2e.spec.js +1 -1
- package/build/test/e2e/express/server.e2e.spec.js +1 -1
- package/build/test/e2e/jsonwp-proxy/proxy.e2e.spec.js +1 -1
- package/build/test/e2e/protocol/fake-driver.js +1 -1
- package/build/test/e2e/protocol/helpers.js +1 -1
- package/build/test/e2e/protocol/protocol.e2e.spec.js +13 -13
- package/build/test/helpers.js +1 -1
- package/build/test/unit/basedriver/capabilities.spec.js +12 -12
- package/build/test/unit/basedriver/capability.spec.js +15 -15
- package/build/test/unit/basedriver/commands/event.spec.js +1 -1
- package/build/test/unit/basedriver/commands/log.spec.js +1 -1
- package/build/test/unit/basedriver/device-settings.spec.js +1 -1
- package/build/test/unit/basedriver/driver.spec.js +1 -1
- package/build/test/unit/basedriver/helpers.spec.js +33 -33
- package/build/test/unit/basedriver/timeout.spec.js +1 -1
- package/build/test/unit/express/server.spec.js +1 -1
- package/build/test/unit/express/static.spec.js +2 -2
- package/build/test/unit/jsonwp-proxy/mock-request.js +1 -1
- package/build/test/unit/jsonwp-proxy/protocol-converter.spec.js +1 -1
- package/build/test/unit/jsonwp-proxy/proxy.spec.js +2 -2
- package/build/test/unit/jsonwp-proxy/url.spec.js +1 -1
- package/build/test/unit/jsonwp-status/status.spec.js +1 -1
- package/build/test/unit/protocol/errors.spec.js +1 -1
- package/build/test/unit/protocol/routes.spec.js +1 -1
- package/build/test/unit/protocol/validator.spec.js +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/lib/basedriver/capabilities.js +95 -47
- package/lib/basedriver/commands/event.js +4 -4
- package/lib/basedriver/commands/find.js +12 -26
- package/lib/basedriver/commands/index.js +7 -7
- package/lib/basedriver/commands/log.js +5 -7
- package/lib/basedriver/commands/session.js +3 -3
- package/lib/basedriver/commands/settings.js +3 -5
- package/lib/basedriver/commands/timeout.js +18 -23
- package/lib/basedriver/core.js +150 -229
- package/lib/basedriver/desired-caps.js +30 -29
- package/lib/basedriver/device-settings.js +21 -20
- package/lib/basedriver/driver.js +131 -96
- package/lib/basedriver/helpers.js +124 -81
- package/lib/basedriver/logger.js +1 -1
- package/lib/constants.js +2 -6
- package/lib/express/crash.js +4 -6
- package/lib/express/express-logging.js +26 -24
- package/lib/express/idempotency.js +16 -16
- package/lib/express/logger.js +1 -1
- package/lib/express/middleware.js +49 -33
- package/lib/express/server.js +68 -44
- package/lib/express/static.js +11 -12
- package/lib/express/websocket.js +26 -16
- package/lib/helpers/capabilities.js +11 -16
- package/lib/index.js +50 -33
- package/lib/jsonwp-proxy/protocol-converter.js +85 -69
- package/lib/jsonwp-proxy/proxy.js +116 -53
- package/lib/jsonwp-status/status.js +36 -29
- package/lib/protocol/errors.js +469 -292
- package/lib/protocol/helpers.js +5 -8
- package/lib/protocol/index.js +22 -15
- package/lib/protocol/protocol.js +103 -55
- package/lib/protocol/routes.js +430 -273
- package/lib/protocol/validators.js +5 -5
- package/package.json +7 -6
- package/test/basedriver/driver-e2e-tests.js +92 -66
- package/test/basedriver/driver-tests.js +90 -33
- package/test/basedriver/index.js +1 -1
|
@@ -2,18 +2,14 @@ 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, node} from '@appium/support';
|
|
6
6
|
import LRU from 'lru-cache';
|
|
7
7
|
import AsyncLock from 'async-lock';
|
|
8
8
|
import axios from 'axios';
|
|
9
9
|
|
|
10
10
|
const IPA_EXT = '.ipa';
|
|
11
11
|
const ZIP_EXTS = ['.zip', IPA_EXT];
|
|
12
|
-
const ZIP_MIME_TYPES = [
|
|
13
|
-
'application/zip',
|
|
14
|
-
'application/x-zip-compressed',
|
|
15
|
-
'multipart/x-zip',
|
|
16
|
-
];
|
|
12
|
+
const ZIP_MIME_TYPES = ['application/zip', 'application/x-zip-compressed', 'multipart/x-zip'];
|
|
17
13
|
const CACHED_APPS_MAX_AGE = 1000 * 60 * 60 * 24; // ms
|
|
18
14
|
const MAX_CACHED_APPS = 1024;
|
|
19
15
|
const APPLICATIONS_CACHE = new LRU({
|
|
@@ -21,8 +17,10 @@ const APPLICATIONS_CACHE = new LRU({
|
|
|
21
17
|
ttl: CACHED_APPS_MAX_AGE, // expire after 24 hours
|
|
22
18
|
updateAgeOnGet: true,
|
|
23
19
|
dispose: (app, {fullPath}) => {
|
|
24
|
-
logger.info(
|
|
25
|
-
`
|
|
20
|
+
logger.info(
|
|
21
|
+
`The application '${app}' cached at '${fullPath}' has ` +
|
|
22
|
+
`expired after ${CACHED_APPS_MAX_AGE}ms`
|
|
23
|
+
);
|
|
26
24
|
if (fullPath) {
|
|
27
25
|
fs.rimraf(fullPath);
|
|
28
26
|
}
|
|
@@ -39,10 +37,11 @@ process.on('exit', () => {
|
|
|
39
37
|
return;
|
|
40
38
|
}
|
|
41
39
|
|
|
42
|
-
const appPaths = [...APPLICATIONS_CACHE.values()]
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
const appPaths = [...APPLICATIONS_CACHE.values()].map(({fullPath}) => fullPath);
|
|
41
|
+
logger.debug(
|
|
42
|
+
`Performing cleanup of ${appPaths.length} cached ` +
|
|
43
|
+
util.pluralize('application', appPaths.length)
|
|
44
|
+
);
|
|
46
45
|
for (const appPath of appPaths) {
|
|
47
46
|
try {
|
|
48
47
|
// Asynchronous calls are not supported in onExit handler
|
|
@@ -53,21 +52,22 @@ process.on('exit', () => {
|
|
|
53
52
|
}
|
|
54
53
|
});
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
async function retrieveHeaders (link) {
|
|
55
|
+
async function retrieveHeaders(link) {
|
|
58
56
|
try {
|
|
59
|
-
return (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
return (
|
|
58
|
+
await axios({
|
|
59
|
+
url: link,
|
|
60
|
+
method: 'HEAD',
|
|
61
|
+
timeout: 5000,
|
|
62
|
+
})
|
|
63
|
+
).headers;
|
|
64
64
|
} catch (e) {
|
|
65
65
|
logger.info(`Cannot send HEAD request to '${link}'. Original error: ${e.message}`);
|
|
66
66
|
}
|
|
67
67
|
return {};
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
function getCachedApplicationPath
|
|
70
|
+
function getCachedApplicationPath(link, currentAppProps = {}, cachedAppInfo = {}) {
|
|
71
71
|
const refresh = () => {
|
|
72
72
|
logger.debug(`A fresh copy of the application is going to be downloaded from ${link}`);
|
|
73
73
|
return null;
|
|
@@ -108,7 +108,9 @@ function getCachedApplicationPath (link, currentAppProps = {}, cachedAppInfo = {
|
|
|
108
108
|
if (currentMaxAge && timestamp) {
|
|
109
109
|
const msLeft = timestamp + currentMaxAge * 1000 - Date.now();
|
|
110
110
|
if (msLeft > 0) {
|
|
111
|
-
logger.debug(
|
|
111
|
+
logger.debug(
|
|
112
|
+
`The cached application '${path.basename(fullPath)}' will expire in ${msLeft / 1000}s`
|
|
113
|
+
);
|
|
112
114
|
return fullPath;
|
|
113
115
|
}
|
|
114
116
|
logger.debug(`The cached application '${path.basename(fullPath)}' has expired`);
|
|
@@ -116,25 +118,27 @@ function getCachedApplicationPath (link, currentAppProps = {}, cachedAppInfo = {
|
|
|
116
118
|
return refresh();
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
function verifyAppExtension
|
|
121
|
+
function verifyAppExtension(app, supportedAppExtensions) {
|
|
120
122
|
if (supportedAppExtensions.map(_.toLower).includes(_.toLower(path.extname(app)))) {
|
|
121
123
|
return app;
|
|
122
124
|
}
|
|
123
|
-
throw new Error(
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
throw new Error(
|
|
126
|
+
`New app path '${app}' did not have ` +
|
|
127
|
+
`${util.pluralize('extension', supportedAppExtensions.length, false)}: ` +
|
|
128
|
+
supportedAppExtensions
|
|
129
|
+
);
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
async function calculateFolderIntegrity
|
|
132
|
+
async function calculateFolderIntegrity(folderPath) {
|
|
129
133
|
return (await fs.glob('**/*', {cwd: folderPath, strict: false, nosort: true})).length;
|
|
130
134
|
}
|
|
131
135
|
|
|
132
|
-
async function calculateFileIntegrity
|
|
136
|
+
async function calculateFileIntegrity(filePath) {
|
|
133
137
|
return await fs.hash(filePath);
|
|
134
138
|
}
|
|
135
139
|
|
|
136
|
-
async function isAppIntegrityOk
|
|
137
|
-
if (!await fs.exists(currentPath)) {
|
|
140
|
+
async function isAppIntegrityOk(currentPath, expectedIntegrity = {}) {
|
|
141
|
+
if (!(await fs.exists(currentPath))) {
|
|
138
142
|
return false;
|
|
139
143
|
}
|
|
140
144
|
|
|
@@ -146,8 +150,8 @@ async function isAppIntegrityOk (currentPath, expectedIntegrity = {}) {
|
|
|
146
150
|
// more precise, but we don't need to be very precise here and also don't want to
|
|
147
151
|
// overuse RAM and have a performance drop.
|
|
148
152
|
return (await fs.stat(currentPath)).isDirectory()
|
|
149
|
-
? await calculateFolderIntegrity(currentPath) >= expectedIntegrity?.folder
|
|
150
|
-
: await calculateFileIntegrity(currentPath) === expectedIntegrity?.file;
|
|
153
|
+
? (await calculateFolderIntegrity(currentPath)) >= expectedIntegrity?.folder
|
|
154
|
+
: (await calculateFileIntegrity(currentPath)) === expectedIntegrity?.file;
|
|
151
155
|
}
|
|
152
156
|
|
|
153
157
|
/**
|
|
@@ -178,7 +182,7 @@ async function isAppIntegrityOk (currentPath, expectedIntegrity = {}) {
|
|
|
178
182
|
|
|
179
183
|
/**
|
|
180
184
|
* @typedef ConfigureAppOptions
|
|
181
|
-
* @property {(obj: PostProcessOptions) => (Promise<PostProcessResult|undefined>|PostProcessResult|undefined)} onPostProcess
|
|
185
|
+
* @property {(obj: PostProcessOptions) => (Promise<PostProcessResult|undefined>|PostProcessResult|undefined)} [onPostProcess]
|
|
182
186
|
* Optional function, which should be applied
|
|
183
187
|
* to the application after it is downloaded/preprocessed. This function may be async
|
|
184
188
|
* and is expected to accept single object parameter.
|
|
@@ -200,16 +204,16 @@ async function isAppIntegrityOk (currentPath, expectedIntegrity = {}) {
|
|
|
200
204
|
* @param {string|string[]|ConfigureAppOptions} options
|
|
201
205
|
* @returns The full path to the resulting application bundle
|
|
202
206
|
*/
|
|
203
|
-
async function configureApp
|
|
207
|
+
async function configureApp(app, options = /** @type {ConfigureAppOptions} */ ({})) {
|
|
204
208
|
if (!_.isString(app)) {
|
|
205
209
|
// immediately shortcircuit if not given an app
|
|
206
210
|
return;
|
|
207
211
|
}
|
|
208
212
|
|
|
209
213
|
let supportedAppExtensions;
|
|
210
|
-
const
|
|
211
|
-
onPostProcess
|
|
212
|
-
|
|
214
|
+
const onPostProcess =
|
|
215
|
+
!_.isString(options) && !_.isArray(options) ? options.onPostProcess : undefined;
|
|
216
|
+
|
|
213
217
|
if (_.isString(options)) {
|
|
214
218
|
supportedAppExtensions = [options];
|
|
215
219
|
} else if (_.isArray(options)) {
|
|
@@ -225,13 +229,14 @@ async function configureApp (app, options = {}) {
|
|
|
225
229
|
let shouldUnzipApp = false;
|
|
226
230
|
let packageHash = null;
|
|
227
231
|
let headers = null;
|
|
232
|
+
/** @type {RemoteAppProps} */
|
|
228
233
|
const remoteAppProps = {
|
|
229
234
|
lastModified: null,
|
|
230
235
|
immutable: false,
|
|
231
236
|
maxAge: null,
|
|
232
237
|
};
|
|
233
238
|
const {protocol, pathname} = url.parse(newApp);
|
|
234
|
-
const isUrl = ['http:', 'https:'].includes(protocol);
|
|
239
|
+
const isUrl = protocol === null ? false : ['http:', 'https:'].includes(protocol);
|
|
235
240
|
|
|
236
241
|
const cachedAppInfo = APPLICATIONS_CACHE.get(app);
|
|
237
242
|
|
|
@@ -260,14 +265,16 @@ async function configureApp (app, options = {}) {
|
|
|
260
265
|
logger.info(`Reusing previously downloaded application at '${cachedPath}'`);
|
|
261
266
|
return verifyAppExtension(cachedPath, supportedAppExtensions);
|
|
262
267
|
}
|
|
263
|
-
logger.info(
|
|
264
|
-
`
|
|
268
|
+
logger.info(
|
|
269
|
+
`The application at '${cachedPath}' does not exist anymore ` +
|
|
270
|
+
`or its integrity has been damaged. Deleting it from the internal cache`
|
|
271
|
+
);
|
|
265
272
|
APPLICATIONS_CACHE.delete(app);
|
|
266
273
|
}
|
|
267
274
|
|
|
268
275
|
let fileName = null;
|
|
269
|
-
const basename = fs.sanitizeName(path.basename(decodeURIComponent(pathname)), {
|
|
270
|
-
replacement: SANITIZE_REPLACEMENT
|
|
276
|
+
const basename = fs.sanitizeName(path.basename(decodeURIComponent(pathname ?? '')), {
|
|
277
|
+
replacement: SANITIZE_REPLACEMENT,
|
|
271
278
|
});
|
|
272
279
|
const extname = path.extname(basename);
|
|
273
280
|
// to determine if we need to unzip the app, we have a number of places
|
|
@@ -280,7 +287,11 @@ async function configureApp (app, options = {}) {
|
|
|
280
287
|
const ct = headers['content-type'];
|
|
281
288
|
logger.debug(`Content-Type: ${ct}`);
|
|
282
289
|
// the filetype may not be obvious for certain urls, so check the mime type too
|
|
283
|
-
if (
|
|
290
|
+
if (
|
|
291
|
+
ZIP_MIME_TYPES.some((mimeType) =>
|
|
292
|
+
new RegExp(`\\b${_.escapeRegExp(mimeType)}\\b`).test(ct)
|
|
293
|
+
)
|
|
294
|
+
) {
|
|
284
295
|
if (!fileName) {
|
|
285
296
|
fileName = `${DEFAULT_BASENAME}.zip`;
|
|
286
297
|
}
|
|
@@ -292,7 +303,7 @@ async function configureApp (app, options = {}) {
|
|
|
292
303
|
const match = /filename="([^"]+)/i.exec(headers['content-disposition']);
|
|
293
304
|
if (match) {
|
|
294
305
|
fileName = fs.sanitizeName(match[1], {
|
|
295
|
-
replacement: SANITIZE_REPLACEMENT
|
|
306
|
+
replacement: SANITIZE_REPLACEMENT,
|
|
296
307
|
});
|
|
297
308
|
shouldUnzipApp = shouldUnzipApp || ZIP_EXTS.includes(path.extname(fileName));
|
|
298
309
|
}
|
|
@@ -304,9 +315,11 @@ async function configureApp (app, options = {}) {
|
|
|
304
315
|
: DEFAULT_BASENAME;
|
|
305
316
|
let resultingExt = extname;
|
|
306
317
|
if (!supportedAppExtensions.includes(resultingExt)) {
|
|
307
|
-
logger.info(
|
|
308
|
-
`
|
|
309
|
-
|
|
318
|
+
logger.info(
|
|
319
|
+
`The current file extension '${resultingExt}' is not supported. ` +
|
|
320
|
+
`Defaulting to '${_.first(supportedAppExtensions)}'`
|
|
321
|
+
);
|
|
322
|
+
resultingExt = /** @type {string} */ (_.first(supportedAppExtensions));
|
|
310
323
|
}
|
|
311
324
|
fileName = `${resultingName}${resultingExt}`;
|
|
312
325
|
}
|
|
@@ -323,7 +336,8 @@ async function configureApp (app, options = {}) {
|
|
|
323
336
|
let errorMessage = `The application at '${newApp}' does not exist or is not accessible`;
|
|
324
337
|
// protocol value for 'C:\\temp' is 'c:', so we check the length as well
|
|
325
338
|
if (_.isString(protocol) && protocol.length > 2) {
|
|
326
|
-
errorMessage =
|
|
339
|
+
errorMessage =
|
|
340
|
+
`The protocol '${protocol}' used in '${newApp}' is not supported. ` +
|
|
327
341
|
`Only http: and https: protocols are supported`;
|
|
328
342
|
}
|
|
329
343
|
throw new Error(errorMessage);
|
|
@@ -345,8 +359,10 @@ async function configureApp (app, options = {}) {
|
|
|
345
359
|
logger.info(`Will reuse previously cached application at '${fullPath}'`);
|
|
346
360
|
return verifyAppExtension(fullPath, supportedAppExtensions);
|
|
347
361
|
}
|
|
348
|
-
logger.info(
|
|
349
|
-
`
|
|
362
|
+
logger.info(
|
|
363
|
+
`The application at '${fullPath}' does not exist anymore ` +
|
|
364
|
+
`or its integrity has been damaged. Deleting it from the cache`
|
|
365
|
+
);
|
|
350
366
|
APPLICATIONS_CACHE.delete(app);
|
|
351
367
|
}
|
|
352
368
|
const tmpRoot = await tempDir.openDir();
|
|
@@ -360,8 +376,10 @@ async function configureApp (app, options = {}) {
|
|
|
360
376
|
logger.info(`Unzipped local app to '${newApp}'`);
|
|
361
377
|
} else if (!path.isAbsolute(newApp)) {
|
|
362
378
|
newApp = path.resolve(process.cwd(), newApp);
|
|
363
|
-
logger.warn(
|
|
364
|
-
`
|
|
379
|
+
logger.warn(
|
|
380
|
+
`The current application path '${app}' is not absolute ` +
|
|
381
|
+
`and has been rewritten to '${newApp}'. Consider using absolute paths rather than relative`
|
|
382
|
+
);
|
|
365
383
|
app = newApp;
|
|
366
384
|
}
|
|
367
385
|
|
|
@@ -393,19 +411,19 @@ async function configureApp (app, options = {}) {
|
|
|
393
411
|
headers: _.clone(headers),
|
|
394
412
|
appPath: newApp,
|
|
395
413
|
});
|
|
396
|
-
return
|
|
414
|
+
return !result?.appPath || app === result?.appPath || !(await fs.exists(result?.appPath))
|
|
397
415
|
? newApp
|
|
398
416
|
: await storeAppInCache(result.appPath);
|
|
399
417
|
}
|
|
400
418
|
|
|
401
419
|
verifyAppExtension(newApp, supportedAppExtensions);
|
|
402
|
-
return
|
|
420
|
+
return app !== newApp && (packageHash || _.values(remoteAppProps).some(Boolean))
|
|
403
421
|
? await storeAppInCache(newApp)
|
|
404
422
|
: newApp;
|
|
405
423
|
});
|
|
406
424
|
}
|
|
407
425
|
|
|
408
|
-
async function downloadApp
|
|
426
|
+
async function downloadApp(app, targetPath) {
|
|
409
427
|
const {href} = url.parse(app);
|
|
410
428
|
try {
|
|
411
429
|
await net.downloadFile(href, targetPath, {
|
|
@@ -426,11 +444,11 @@ async function downloadApp (app, targetPath) {
|
|
|
426
444
|
* @param {Array<string>|string} supportedAppExtensions The list of extensions
|
|
427
445
|
* the target application bundle supports, for example ['.apk', '.apks'] for
|
|
428
446
|
* Android packages
|
|
429
|
-
* @returns {string} Full path to the bundle in the destination folder
|
|
447
|
+
* @returns {Promise<string>} Full path to the bundle in the destination folder
|
|
430
448
|
* @throws {Error} If the given archive is invalid or no application bundles
|
|
431
449
|
* have been found inside
|
|
432
450
|
*/
|
|
433
|
-
async function unzipApp
|
|
451
|
+
async function unzipApp(zipPath, dstRoot, supportedAppExtensions) {
|
|
434
452
|
await zip.assertValidZip(zipPath);
|
|
435
453
|
|
|
436
454
|
if (!_.isArray(supportedAppExtensions)) {
|
|
@@ -442,8 +460,8 @@ async function unzipApp (zipPath, dstRoot, supportedAppExtensions) {
|
|
|
442
460
|
logger.debug(`Unzipping '${zipPath}'`);
|
|
443
461
|
const timer = new timing.Timer().start();
|
|
444
462
|
const useSystemUnzipEnv = process.env.APPIUM_PREFER_SYSTEM_UNZIP;
|
|
445
|
-
const useSystemUnzip =
|
|
446
|
-
|| !['0', 'false'].includes(_.toLower(useSystemUnzipEnv));
|
|
463
|
+
const useSystemUnzip =
|
|
464
|
+
_.isEmpty(useSystemUnzipEnv) || !['0', 'false'].includes(_.toLower(useSystemUnzipEnv));
|
|
447
465
|
/**
|
|
448
466
|
* Attempt to use use the system `unzip` (e.g., `/usr/bin/unzip`) due
|
|
449
467
|
* to the significant performance improvement it provides over the native
|
|
@@ -453,25 +471,41 @@ async function unzipApp (zipPath, dstRoot, supportedAppExtensions) {
|
|
|
453
471
|
const extractionOpts = {useSystemUnzip};
|
|
454
472
|
// https://github.com/appium/appium/issues/14100
|
|
455
473
|
if (path.extname(zipPath) === IPA_EXT) {
|
|
456
|
-
logger.debug(
|
|
474
|
+
logger.debug(
|
|
475
|
+
`Enforcing UTF-8 encoding on the extracted file names for '${path.basename(zipPath)}'`
|
|
476
|
+
);
|
|
457
477
|
extractionOpts.fileNamesEncoding = 'utf8';
|
|
458
478
|
}
|
|
459
479
|
await zip.extractAllTo(zipPath, tmpRoot, extractionOpts);
|
|
460
|
-
const globPattern = `**/*.+(${supportedAppExtensions
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
480
|
+
const globPattern = `**/*.+(${supportedAppExtensions
|
|
481
|
+
.map((ext) => ext.replace(/^\./, ''))
|
|
482
|
+
.join('|')})`;
|
|
483
|
+
const sortedBundleItems = (
|
|
484
|
+
await fs.glob(globPattern, {
|
|
485
|
+
cwd: tmpRoot,
|
|
486
|
+
strict: false,
|
|
487
|
+
// Get the top level match
|
|
488
|
+
})
|
|
489
|
+
).sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
|
|
466
490
|
if (_.isEmpty(sortedBundleItems)) {
|
|
467
|
-
logger.errorAndThrow(
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
491
|
+
logger.errorAndThrow(
|
|
492
|
+
`App unzipped OK, but we could not find any '${supportedAppExtensions}' ` +
|
|
493
|
+
util.pluralize('bundle', supportedAppExtensions.length, false) +
|
|
494
|
+
` in it. Make sure your archive contains at least one package having ` +
|
|
495
|
+
`'${supportedAppExtensions}' ${util.pluralize(
|
|
496
|
+
'extension',
|
|
497
|
+
supportedAppExtensions.length,
|
|
498
|
+
false
|
|
499
|
+
)}`
|
|
500
|
+
);
|
|
471
501
|
}
|
|
472
|
-
logger.debug(
|
|
473
|
-
`
|
|
474
|
-
|
|
502
|
+
logger.debug(
|
|
503
|
+
`Extracted ${util.pluralize('bundle item', sortedBundleItems.length, true)} ` +
|
|
504
|
+
`from '${zipPath}' in ${Math.round(
|
|
505
|
+
timer.getDuration().asMilliSeconds
|
|
506
|
+
)}ms: ${sortedBundleItems}`
|
|
507
|
+
);
|
|
508
|
+
const matchedBundle = /** @type {string} */ (_.first(sortedBundleItems));
|
|
475
509
|
logger.info(`Assuming '${matchedBundle}' is the correct bundle`);
|
|
476
510
|
const dstPath = path.resolve(dstRoot, path.basename(matchedBundle));
|
|
477
511
|
await fs.mv(path.resolve(tmpRoot, matchedBundle), dstPath, {mkdirp: true});
|
|
@@ -481,8 +515,8 @@ async function unzipApp (zipPath, dstRoot, supportedAppExtensions) {
|
|
|
481
515
|
}
|
|
482
516
|
}
|
|
483
517
|
|
|
484
|
-
function isPackageOrBundle
|
|
485
|
-
return
|
|
518
|
+
function isPackageOrBundle(app) {
|
|
519
|
+
return /^([a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+)+$/.test(app);
|
|
486
520
|
}
|
|
487
521
|
|
|
488
522
|
/**
|
|
@@ -495,7 +529,7 @@ function isPackageOrBundle (app) {
|
|
|
495
529
|
* @param {String} firstKey The first key to duplicate
|
|
496
530
|
* @param {String} secondKey The second key to duplicate
|
|
497
531
|
*/
|
|
498
|
-
function duplicateKeys
|
|
532
|
+
function duplicateKeys(input, firstKey, secondKey) {
|
|
499
533
|
// If array provided, recursively call on all elements
|
|
500
534
|
if (_.isArray(input)) {
|
|
501
535
|
return input.map((item) => duplicateKeys(item, firstKey, secondKey));
|
|
@@ -526,7 +560,7 @@ function duplicateKeys (input, firstKey, secondKey) {
|
|
|
526
560
|
*
|
|
527
561
|
* @param {string|Array<String>} cap A desired capability
|
|
528
562
|
*/
|
|
529
|
-
function parseCapsArray
|
|
563
|
+
function parseCapsArray(cap) {
|
|
530
564
|
if (_.isArray(cap)) {
|
|
531
565
|
return cap;
|
|
532
566
|
}
|
|
@@ -553,15 +587,24 @@ function parseCapsArray (cap) {
|
|
|
553
587
|
* @param {string?} sessionId session identifier (if exists)
|
|
554
588
|
* @returns {string}
|
|
555
589
|
*/
|
|
556
|
-
function generateDriverLogPrefix
|
|
590
|
+
function generateDriverLogPrefix(obj, sessionId = null) {
|
|
557
591
|
const instanceName = `${obj.constructor.name}@${node.getObjectId(obj).substring(0, 4)}`;
|
|
558
592
|
return sessionId ? `${instanceName} (${sessionId.substring(0, 8)})` : instanceName;
|
|
559
593
|
}
|
|
560
594
|
|
|
561
595
|
/** @type {import('@appium/types').DriverHelpers} */
|
|
562
596
|
export default {
|
|
563
|
-
configureApp,
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
597
|
+
configureApp,
|
|
598
|
+
isPackageOrBundle,
|
|
599
|
+
duplicateKeys,
|
|
600
|
+
parseCapsArray,
|
|
601
|
+
generateDriverLogPrefix,
|
|
567
602
|
};
|
|
603
|
+
export {configureApp, isPackageOrBundle, duplicateKeys, parseCapsArray, generateDriverLogPrefix};
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* @typedef RemoteAppProps
|
|
607
|
+
* @property {Date?} lastModified
|
|
608
|
+
* @property {boolean} immutable
|
|
609
|
+
* @property {number?} maxAge
|
|
610
|
+
*/
|
package/lib/basedriver/logger.js
CHANGED
package/lib/constants.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {util} from '@appium/support';
|
|
2
2
|
|
|
3
3
|
// The default maximum length of a single log record
|
|
4
4
|
// containing http request/response body
|
|
@@ -17,8 +17,4 @@ const PROTOCOLS = {
|
|
|
17
17
|
// Before Appium 2.0, this default value was '/wd/hub' by historical reasons.
|
|
18
18
|
const DEFAULT_BASE_PATH = '';
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
export {
|
|
22
|
-
MAX_LOG_BODY_LENGTH, MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY,
|
|
23
|
-
PROTOCOLS, DEFAULT_BASE_PATH
|
|
24
|
-
};
|
|
20
|
+
export {MAX_LOG_BODY_LENGTH, MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY, PROTOCOLS, DEFAULT_BASE_PATH};
|
package/lib/express/crash.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {errors} from '../protocol';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
function produceError () {
|
|
3
|
+
function produceError() {
|
|
5
4
|
throw new errors.UnknownCommandError('Produced generic error for testing');
|
|
6
5
|
}
|
|
7
6
|
|
|
8
|
-
function produceCrash
|
|
7
|
+
function produceCrash() {
|
|
9
8
|
throw new Error('We just tried to crash Appium!');
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
export { produceError, produceCrash };
|
|
11
|
+
export {produceError, produceCrash};
|
|
@@ -2,23 +2,21 @@ import _ from 'lodash';
|
|
|
2
2
|
import '@colors/colors';
|
|
3
3
|
import morgan from 'morgan';
|
|
4
4
|
import log from './logger';
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import {MAX_LOG_BODY_LENGTH} from '../constants';
|
|
7
6
|
|
|
8
7
|
// Copied the morgan compile function over so that cooler formats
|
|
9
8
|
// may be configured
|
|
10
|
-
function compile
|
|
9
|
+
function compile(fmt) {
|
|
11
10
|
// escape quotes
|
|
12
11
|
fmt = fmt.replace(/"/g, '\\"');
|
|
13
|
-
fmt = fmt.replace(/:([-\w]{2,})(?:\[([^\]]+)\])?/g,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
});
|
|
12
|
+
fmt = fmt.replace(/:([-\w]{2,})(?:\[([^\]]+)\])?/g, function replace(_, name, arg) {
|
|
13
|
+
return `"\n + (tokens["${name}"](req, res, "${arg}") || "-") + "`;
|
|
14
|
+
});
|
|
17
15
|
let js = ` return "${fmt}";`;
|
|
18
16
|
return new Function('tokens, req, res', js);
|
|
19
17
|
}
|
|
20
18
|
|
|
21
|
-
function requestEndLoggingFormat
|
|
19
|
+
function requestEndLoggingFormat(tokens, req, res) {
|
|
22
20
|
let status = res.statusCode;
|
|
23
21
|
let statusStr = ':status';
|
|
24
22
|
if (status >= 500) {
|
|
@@ -30,28 +28,32 @@ function requestEndLoggingFormat (tokens, req, res) {
|
|
|
30
28
|
} else {
|
|
31
29
|
statusStr = statusStr.green;
|
|
32
30
|
}
|
|
33
|
-
let fn = compile(
|
|
31
|
+
let fn = compile(
|
|
32
|
+
`${'<-- :method :url '.white}${statusStr} ${':response-time ms - :res[content-length]'.grey}`
|
|
33
|
+
);
|
|
34
34
|
return fn(tokens, req, res);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const endLogFormatter = morgan((tokens, req, res) => {
|
|
38
|
-
log.info(requestEndLoggingFormat(tokens, req, res),
|
|
39
|
-
(res.jsonResp || '').grey);
|
|
38
|
+
log.info(requestEndLoggingFormat(tokens, req, res), (res.jsonResp || '').grey);
|
|
40
39
|
});
|
|
41
40
|
|
|
42
41
|
const requestStartLoggingFormat = compile(`${'-->'.white} ${':method'.white} ${':url'.white}`);
|
|
43
42
|
|
|
44
|
-
const startLogFormatter = morgan(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
43
|
+
const startLogFormatter = morgan(
|
|
44
|
+
(tokens, req, res) => {
|
|
45
|
+
// morgan output is redirected straight to winston
|
|
46
|
+
let reqBody = '';
|
|
47
|
+
if (req.body) {
|
|
48
|
+
try {
|
|
49
|
+
reqBody = _.truncate(_.isString(req.body) ? req.body : JSON.stringify(req.body), {
|
|
50
|
+
length: MAX_LOG_BODY_LENGTH,
|
|
51
|
+
});
|
|
52
|
+
} catch (ign) {}
|
|
53
|
+
}
|
|
54
|
+
log.info(requestStartLoggingFormat(tokens, req, res), reqBody.grey);
|
|
55
|
+
},
|
|
56
|
+
{immediate: true}
|
|
57
|
+
);
|
|
56
58
|
|
|
57
|
-
export {
|
|
59
|
+
export {endLogFormatter, startLogFormatter};
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import log from './logger';
|
|
2
2
|
import LRU from 'lru-cache';
|
|
3
|
-
import {
|
|
3
|
+
import {fs, util} from '@appium/support';
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import path from 'path';
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {EventEmitter} from 'events';
|
|
8
7
|
|
|
9
8
|
const CACHE_SIZE = 1024;
|
|
10
9
|
const IDEMPOTENT_RESPONSES = new LRU({
|
|
11
10
|
max: CACHE_SIZE,
|
|
12
11
|
updateAgeOnGet: true,
|
|
13
|
-
dispose
|
|
12
|
+
dispose(key, {response}) {
|
|
14
13
|
if (response) {
|
|
15
14
|
fs.rimrafSync(response);
|
|
16
15
|
}
|
|
@@ -20,9 +19,7 @@ const MONITORED_METHODS = ['POST', 'PATCH'];
|
|
|
20
19
|
const IDEMPOTENCY_KEY_HEADER = 'x-idempotency-key';
|
|
21
20
|
|
|
22
21
|
process.on('exit', () => {
|
|
23
|
-
const resPaths = [...IDEMPOTENT_RESPONSES.values()]
|
|
24
|
-
.map(({response}) => response)
|
|
25
|
-
.filter(Boolean);
|
|
22
|
+
const resPaths = [...IDEMPOTENT_RESPONSES.values()].map(({response}) => response).filter(Boolean);
|
|
26
23
|
for (const resPath of resPaths) {
|
|
27
24
|
try {
|
|
28
25
|
// Asynchronous calls are not supported in onExit handler
|
|
@@ -31,8 +28,7 @@ process.on('exit', () => {
|
|
|
31
28
|
}
|
|
32
29
|
});
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
function cacheResponse (key, req, res) {
|
|
31
|
+
function cacheResponse(key, req, res) {
|
|
36
32
|
const responseStateListener = new EventEmitter();
|
|
37
33
|
IDEMPOTENT_RESPONSES.set(key, {
|
|
38
34
|
method: req.method,
|
|
@@ -72,8 +68,10 @@ function cacheResponse (key, req, res) {
|
|
|
72
68
|
}
|
|
73
69
|
|
|
74
70
|
if (!IDEMPOTENT_RESPONSES.has(key)) {
|
|
75
|
-
log.info(
|
|
76
|
-
`
|
|
71
|
+
log.info(
|
|
72
|
+
`Could not cache the response identified by '${key}'. ` +
|
|
73
|
+
`Cache consistency has been damaged`
|
|
74
|
+
);
|
|
77
75
|
return responseStateListener.emit('ready', null);
|
|
78
76
|
}
|
|
79
77
|
if (writeError) {
|
|
@@ -82,8 +80,10 @@ function cacheResponse (key, req, res) {
|
|
|
82
80
|
return responseStateListener.emit('ready', null);
|
|
83
81
|
}
|
|
84
82
|
if (!isResponseFullySent) {
|
|
85
|
-
log.info(
|
|
86
|
-
`
|
|
83
|
+
log.info(
|
|
84
|
+
`Could not cache the response identified by '${key}', ` +
|
|
85
|
+
`because it has not been completed`
|
|
86
|
+
);
|
|
87
87
|
log.info('Does the client terminate connections too early?');
|
|
88
88
|
IDEMPOTENT_RESPONSES.delete(key);
|
|
89
89
|
return responseStateListener.emit('ready', null);
|
|
@@ -94,7 +94,7 @@ function cacheResponse (key, req, res) {
|
|
|
94
94
|
});
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
async function handleIdempotency
|
|
97
|
+
async function handleIdempotency(req, res, next) {
|
|
98
98
|
const key = req.headers[IDEMPOTENCY_KEY_HEADER];
|
|
99
99
|
if (!key) {
|
|
100
100
|
return next();
|
|
@@ -124,7 +124,7 @@ async function handleIdempotency (req, res, next) {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
const rerouteCachedResponse = async (cachedResPath) => {
|
|
127
|
-
if (!await fs.exists(cachedResPath)) {
|
|
127
|
+
if (!(await fs.exists(cachedResPath))) {
|
|
128
128
|
IDEMPOTENT_RESPONSES.delete(key);
|
|
129
129
|
log.warn(`Could not read the cached response identified by key '${key}'`);
|
|
130
130
|
log.warn('The temporary storage is not accessible anymore');
|
|
@@ -149,4 +149,4 @@ async function handleIdempotency (req, res, next) {
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
export {
|
|
152
|
+
export {handleIdempotency};
|
package/lib/express/logger.js
CHANGED