@appium/base-driver 10.5.1 → 10.6.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.d.ts.map +1 -1
- package/build/lib/basedriver/capabilities.js +45 -45
- package/build/lib/basedriver/capabilities.js.map +1 -1
- package/build/lib/basedriver/commands/bidi.d.ts.map +1 -1
- package/build/lib/basedriver/commands/bidi.js +9 -13
- package/build/lib/basedriver/commands/bidi.js.map +1 -1
- package/build/lib/basedriver/commands/event.d.ts.map +1 -1
- package/build/lib/basedriver/commands/event.js +4 -7
- package/build/lib/basedriver/commands/event.js.map +1 -1
- package/build/lib/basedriver/commands/execute.js +3 -6
- package/build/lib/basedriver/commands/execute.js.map +1 -1
- package/build/lib/basedriver/commands/log.d.ts.map +1 -1
- package/build/lib/basedriver/commands/log.js +1 -5
- package/build/lib/basedriver/commands/log.js.map +1 -1
- package/build/lib/basedriver/commands/timeout.d.ts.map +1 -1
- package/build/lib/basedriver/commands/timeout.js +5 -9
- package/build/lib/basedriver/commands/timeout.js.map +1 -1
- package/build/lib/basedriver/core.js +12 -12
- package/build/lib/basedriver/core.js.map +1 -1
- package/build/lib/basedriver/device-settings.d.ts.map +1 -1
- package/build/lib/basedriver/device-settings.js +3 -7
- package/build/lib/basedriver/device-settings.js.map +1 -1
- package/build/lib/basedriver/driver.d.ts.map +1 -1
- package/build/lib/basedriver/driver.js +13 -16
- package/build/lib/basedriver/driver.js.map +1 -1
- package/build/lib/basedriver/extension-core.d.ts +4 -1
- package/build/lib/basedriver/extension-core.d.ts.map +1 -1
- package/build/lib/basedriver/extension-core.js +27 -9
- package/build/lib/basedriver/extension-core.js.map +1 -1
- package/build/lib/basedriver/helpers.d.ts.map +1 -1
- package/build/lib/basedriver/helpers.js +28 -30
- package/build/lib/basedriver/helpers.js.map +1 -1
- package/build/lib/basedriver/ipc.d.ts +36 -0
- package/build/lib/basedriver/ipc.d.ts.map +1 -0
- package/build/lib/basedriver/ipc.js +155 -0
- package/build/lib/basedriver/ipc.js.map +1 -0
- package/build/lib/basedriver/validation.js +25 -28
- package/build/lib/basedriver/validation.js.map +1 -1
- package/build/lib/express/express-logging.d.ts.map +1 -1
- package/build/lib/express/express-logging.js +2 -3
- package/build/lib/express/express-logging.js.map +1 -1
- package/build/lib/express/idempotency.js +3 -6
- package/build/lib/express/idempotency.js.map +1 -1
- package/build/lib/express/middleware.d.ts.map +1 -1
- package/build/lib/express/middleware.js +6 -10
- package/build/lib/express/middleware.js.map +1 -1
- package/build/lib/express/server.d.ts.map +1 -1
- package/build/lib/express/server.js +64 -54
- package/build/lib/express/server.js.map +1 -1
- package/build/lib/express/static.d.ts.map +1 -1
- package/build/lib/express/static.js +14 -7
- package/build/lib/express/static.js.map +1 -1
- package/build/lib/express/websocket.d.ts.map +1 -1
- package/build/lib/express/websocket.js +6 -9
- package/build/lib/express/websocket.js.map +1 -1
- package/build/lib/helpers/capabilities.d.ts.map +1 -1
- package/build/lib/helpers/capabilities.js +14 -17
- package/build/lib/helpers/capabilities.js.map +1 -1
- package/build/lib/helpers/extension-command-name.js +2 -5
- package/build/lib/helpers/extension-command-name.js.map +1 -1
- package/build/lib/helpers/levenshtein-match.d.ts.map +1 -1
- package/build/lib/helpers/levenshtein-match.js +2 -6
- package/build/lib/helpers/levenshtein-match.js.map +1 -1
- package/build/lib/index.d.ts +1 -0
- package/build/lib/index.d.ts.map +1 -1
- package/build/lib/index.js +3 -14
- package/build/lib/index.js.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.js +13 -17
- package/build/lib/jsonwp-proxy/protocol-converter.js.map +1 -1
- package/build/lib/jsonwp-proxy/proxy-request.d.ts +2 -2
- package/build/lib/jsonwp-proxy/proxy-request.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/proxy-request.js +25 -21
- package/build/lib/jsonwp-proxy/proxy-request.js.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.js +29 -26
- package/build/lib/jsonwp-proxy/proxy.js.map +1 -1
- package/build/lib/protocol/errors.d.ts.map +1 -1
- package/build/lib/protocol/errors.js +25 -29
- package/build/lib/protocol/errors.js.map +1 -1
- package/build/lib/protocol/helpers.d.ts.map +1 -1
- package/build/lib/protocol/helpers.js +9 -8
- package/build/lib/protocol/helpers.js.map +1 -1
- package/build/lib/protocol/protocol.d.ts.map +1 -1
- package/build/lib/protocol/protocol.js +43 -48
- package/build/lib/protocol/protocol.js.map +1 -1
- package/build/lib/protocol/routes.d.ts +1 -1
- package/build/lib/protocol/routes.d.ts.map +1 -1
- package/build/lib/protocol/routes.js +9 -12
- package/build/lib/protocol/routes.js.map +1 -1
- package/build/lib/protocol/validators.d.ts.map +1 -1
- package/build/lib/protocol/validators.js +1 -5
- package/build/lib/protocol/validators.js.map +1 -1
- package/build/lib/utils.d.ts +16 -0
- package/build/lib/utils.d.ts.map +1 -0
- package/build/lib/utils.js +71 -0
- package/build/lib/utils.js.map +1 -0
- package/lib/basedriver/capabilities.ts +60 -55
- package/lib/basedriver/commands/bidi.ts +10 -10
- package/lib/basedriver/commands/event.ts +11 -10
- package/lib/basedriver/commands/execute.ts +3 -3
- package/lib/basedriver/commands/log.ts +3 -2
- package/lib/basedriver/commands/timeout.ts +5 -6
- package/lib/basedriver/core.ts +12 -12
- package/lib/basedriver/device-settings.ts +3 -4
- package/lib/basedriver/driver.ts +15 -13
- package/lib/basedriver/extension-core.ts +33 -7
- package/lib/basedriver/helpers.ts +28 -30
- package/lib/basedriver/ipc.ts +179 -0
- package/lib/basedriver/validation.ts +26 -26
- package/lib/express/express-logging.ts +3 -4
- package/lib/express/idempotency.ts +3 -3
- package/lib/express/middleware.ts +6 -8
- package/lib/express/server.ts +67 -61
- package/lib/express/static.ts +15 -7
- package/lib/express/websocket.ts +8 -10
- package/lib/helpers/capabilities.ts +18 -14
- package/lib/helpers/extension-command-name.ts +2 -2
- package/lib/helpers/levenshtein-match.ts +2 -5
- package/lib/index.js +1 -11
- package/lib/jsonwp-proxy/protocol-converter.ts +14 -15
- package/lib/jsonwp-proxy/proxy-request.ts +26 -26
- package/lib/jsonwp-proxy/proxy.ts +36 -37
- package/lib/protocol/errors.ts +29 -28
- package/lib/protocol/helpers.ts +9 -5
- package/lib/protocol/protocol.ts +44 -46
- package/lib/protocol/routes.ts +9 -9
- package/lib/protocol/validators.ts +1 -3
- package/lib/utils.ts +85 -0
- package/package.json +7 -9
package/lib/basedriver/driver.ts
CHANGED
|
@@ -17,8 +17,6 @@ import {
|
|
|
17
17
|
type SingularSessionData,
|
|
18
18
|
type SessionCapabilities,
|
|
19
19
|
} from '@appium/types';
|
|
20
|
-
import B from 'bluebird';
|
|
21
|
-
import _ from 'lodash';
|
|
22
20
|
import {fixCaps, isW3cCaps} from '../helpers/capabilities';
|
|
23
21
|
import {getLevenshteinSuggestion} from '../helpers/levenshtein-match';
|
|
24
22
|
import {calcSignature} from '../helpers/session';
|
|
@@ -27,6 +25,7 @@ import {processCapabilities, validateCaps} from './capabilities';
|
|
|
27
25
|
import {DriverCore} from './core';
|
|
28
26
|
import * as helpers from './helpers';
|
|
29
27
|
import {resolveExecuteExtensionName} from '../helpers/extension-command-name';
|
|
28
|
+
import {mergePlainObjects} from '../utils';
|
|
30
29
|
|
|
31
30
|
const EVENT_SESSION_INIT = 'newSessionRequested';
|
|
32
31
|
const EVENT_SESSION_START = 'newSessionStarted';
|
|
@@ -69,7 +68,9 @@ export class BaseDriver<
|
|
|
69
68
|
* @see {@link https://github.com/appium/appium/issues/new}
|
|
70
69
|
*/
|
|
71
70
|
protected get _desiredCapConstraints(): Readonly<BaseDriverCapConstraints & C> {
|
|
72
|
-
return Object.freeze(
|
|
71
|
+
return Object.freeze(
|
|
72
|
+
mergePlainObjects({}, BASE_DESIRED_CAP_CONSTRAINTS, this.desiredCapConstraints) as BaseDriverCapConstraints & C
|
|
73
|
+
);
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
/**
|
|
@@ -113,11 +114,11 @@ export class BaseDriver<
|
|
|
113
114
|
unexpectedShutdownRejecter?.(e);
|
|
114
115
|
};
|
|
115
116
|
try {
|
|
116
|
-
return await
|
|
117
|
+
return await Promise.race([
|
|
117
118
|
this[cmd](...args),
|
|
118
119
|
// This promise is needed to monitor if the session has been
|
|
119
120
|
// shut down unexpectedly while the command was running
|
|
120
|
-
new
|
|
121
|
+
new Promise((resolve, reject) => {
|
|
121
122
|
unexpectedShutdownResolver = resolve;
|
|
122
123
|
unexpectedShutdownRejecter = reject;
|
|
123
124
|
this.eventEmitter.once(ON_UNEXPECTED_SHUTDOWN_EVENT, onUnexpectedShutdown);
|
|
@@ -180,7 +181,7 @@ export class BaseDriver<
|
|
|
180
181
|
clarifyCommandName(cmd: string, args: string[]): string {
|
|
181
182
|
if (cmd === 'execute') {
|
|
182
183
|
const firstArg = args?.[0];
|
|
183
|
-
if (
|
|
184
|
+
if (typeof firstArg === 'string' && firstArg.trim().length > 0) {
|
|
184
185
|
return resolveExecuteExtensionName.call(this, firstArg);
|
|
185
186
|
}
|
|
186
187
|
}
|
|
@@ -257,7 +258,7 @@ export class BaseDriver<
|
|
|
257
258
|
await this.createSession(this.originalCaps);
|
|
258
259
|
} finally {
|
|
259
260
|
// always restore state.
|
|
260
|
-
for (const [key, value] of
|
|
261
|
+
for (const [key, value] of Object.entries(currentConfig)) {
|
|
261
262
|
this[key] = value;
|
|
262
263
|
}
|
|
263
264
|
}
|
|
@@ -286,7 +287,7 @@ export class BaseDriver<
|
|
|
286
287
|
|
|
287
288
|
this.log.debug();
|
|
288
289
|
|
|
289
|
-
const originalCaps =
|
|
290
|
+
const originalCaps = structuredClone(
|
|
290
291
|
[w3cCapabilities, w3cCapabilities1, w3cCapabilities2].find(isW3cCaps),
|
|
291
292
|
);
|
|
292
293
|
if (!originalCaps) {
|
|
@@ -321,7 +322,7 @@ export class BaseDriver<
|
|
|
321
322
|
this.sessionCreationTimestampMs = Date.now();
|
|
322
323
|
this.caps = caps;
|
|
323
324
|
// merge caps onto opts so we don't need to worry about what's where
|
|
324
|
-
this.opts = {...
|
|
325
|
+
this.opts = {...structuredClone(this.initialOpts), ...this.caps};
|
|
325
326
|
|
|
326
327
|
// deal with resets
|
|
327
328
|
// some people like to do weird things by setting noReset and fullReset
|
|
@@ -347,7 +348,7 @@ export class BaseDriver<
|
|
|
347
348
|
delete this.opts.app;
|
|
348
349
|
}
|
|
349
350
|
|
|
350
|
-
if (
|
|
351
|
+
if (this.caps.newCommandTimeout !== undefined) {
|
|
351
352
|
this.newCommandTimeoutMs = (this.caps.newCommandTimeout as number) * 1000;
|
|
352
353
|
}
|
|
353
354
|
|
|
@@ -388,7 +389,7 @@ export class BaseDriver<
|
|
|
388
389
|
// simple hack to release pending commands if they exist
|
|
389
390
|
// @ts-expect-error private API
|
|
390
391
|
const queues = this.commandsQueueGuard.queues;
|
|
391
|
-
for (const key of
|
|
392
|
+
for (const key of Object.keys(queues)) {
|
|
392
393
|
queues[key] = [];
|
|
393
394
|
}
|
|
394
395
|
}
|
|
@@ -396,8 +397,9 @@ export class BaseDriver<
|
|
|
396
397
|
}
|
|
397
398
|
|
|
398
399
|
logExtraCaps(caps: Capabilities<C>) {
|
|
399
|
-
const knownCaps =
|
|
400
|
-
const
|
|
400
|
+
const knownCaps = Object.keys(this._desiredCapConstraints);
|
|
401
|
+
const knownCapsSet = new Set(knownCaps);
|
|
402
|
+
const extraCaps = Object.keys(caps).filter((cap) => !knownCapsSet.has(cap));
|
|
401
403
|
if (extraCaps.length) {
|
|
402
404
|
this.log.warn(`The following provided capabilities were not recognized by this driver:`);
|
|
403
405
|
for (const cap of extraCaps) {
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {logger} from '@appium/support';
|
|
1
|
+
import {logger, util} from '@appium/support';
|
|
2
2
|
import {EventEmitter} from 'node:events';
|
|
3
3
|
import type {
|
|
4
4
|
AppiumLogger,
|
|
5
5
|
BidiModuleMap,
|
|
6
6
|
BiDiResultData,
|
|
7
|
+
IAppiumIpc,
|
|
8
|
+
IIpcSubscription,
|
|
9
|
+
IpcData,
|
|
7
10
|
StringRecord,
|
|
8
11
|
} from '@appium/types';
|
|
9
12
|
import {
|
|
@@ -11,7 +14,6 @@ import {
|
|
|
11
14
|
} from '../constants';
|
|
12
15
|
import {errors} from '../protocol';
|
|
13
16
|
import {BIDI_COMMANDS} from '../protocol/bidi-commands';
|
|
14
|
-
import _ from 'lodash';
|
|
15
17
|
import {generateDriverLogPrefix} from './helpers';
|
|
16
18
|
|
|
17
19
|
export class ExtensionCore {
|
|
@@ -21,6 +23,7 @@ export class ExtensionCore {
|
|
|
21
23
|
// used to handle driver events
|
|
22
24
|
readonly eventEmitter: NodeJS.EventEmitter;
|
|
23
25
|
protected _log: AppiumLogger;
|
|
26
|
+
private ipc?: IAppiumIpc;
|
|
24
27
|
|
|
25
28
|
|
|
26
29
|
constructor(logPrefix?: string) {
|
|
@@ -41,7 +44,7 @@ export class ExtensionCore {
|
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
updateBidiCommands(cmds: BidiModuleMap): void {
|
|
44
|
-
const overlappingKeys =
|
|
47
|
+
const overlappingKeys = Object.keys(cmds).filter((key) => key in this.bidiCommands);
|
|
45
48
|
if (overlappingKeys.length) {
|
|
46
49
|
this.log.warn(`Overwriting existing bidi modules: ${JSON.stringify(overlappingKeys)}. This may not be intended!`);
|
|
47
50
|
}
|
|
@@ -98,7 +101,7 @@ export class ExtensionCore {
|
|
|
98
101
|
const args: any[] = [];
|
|
99
102
|
if (params?.required?.length) {
|
|
100
103
|
for (const requiredParam of params.required) {
|
|
101
|
-
if (
|
|
104
|
+
if (bidiParams[requiredParam] === undefined) {
|
|
102
105
|
throw new errors.InvalidArgumentError(
|
|
103
106
|
`The ${requiredParam} parameter was required but you omitted it`,
|
|
104
107
|
);
|
|
@@ -111,18 +114,41 @@ export class ExtensionCore {
|
|
|
111
114
|
args.push(bidiParams[optionalParam]);
|
|
112
115
|
}
|
|
113
116
|
}
|
|
114
|
-
const logParams =
|
|
117
|
+
const logParams = util.truncateString(JSON.stringify(bidiParams), {length: MAX_LOG_BODY_LENGTH});
|
|
115
118
|
this.log.debug(
|
|
116
119
|
`Executing bidi command '${bidiCmd}' with params ${logParams} by passing to ${handlerType} ` +
|
|
117
120
|
`method '${command}'`,
|
|
118
121
|
);
|
|
119
122
|
// call the handler with the signature appropriate to extension type (plugin or driver)
|
|
120
123
|
const response = (next && driver) ? await this[command](next, driver, ...args) : await this[command](...args);
|
|
121
|
-
const finalResponse =
|
|
124
|
+
const finalResponse = response === undefined ? {} : response;
|
|
122
125
|
this.log.debug(
|
|
123
126
|
`Responding to bidi command '${bidiCmd}' with ` +
|
|
124
|
-
`${
|
|
127
|
+
`${util.truncateString(JSON.stringify(finalResponse), {length: MAX_LOG_BODY_LENGTH})}`
|
|
125
128
|
);
|
|
126
129
|
return finalResponse;
|
|
127
130
|
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @internal Used by AppiumDriver to wire session IPC; extension authors should use {@link onIpcInit} instead.
|
|
134
|
+
*/
|
|
135
|
+
async assignIpc(ipc: IAppiumIpc): Promise<void> {
|
|
136
|
+
this.ipc = ipc;
|
|
137
|
+
try {
|
|
138
|
+
await this.onIpcInit();
|
|
139
|
+
} catch (e) {
|
|
140
|
+
this.log.error(`Error running onIpcInit: `, e);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async onIpcInit(): Promise<void> {}
|
|
145
|
+
|
|
146
|
+
ipcSubscribe<T extends IpcData>(topic: string): IIpcSubscription<T> {
|
|
147
|
+
if (!this.ipc) {
|
|
148
|
+
throw new Error(`Cannot subscribe to an IPC topic without an IPC object assigned. ` +
|
|
149
|
+
`This is likely a programming error. ipcSubscribe should be called in the ` +
|
|
150
|
+
`onIpcInit handler or after you are certain that createSession has completed successfully.`);
|
|
151
|
+
}
|
|
152
|
+
return this.ipc.subscribe<T>(topic, generateDriverLogPrefix(this));
|
|
153
|
+
}
|
|
128
154
|
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import _ from 'lodash';
|
|
2
1
|
import path from 'node:path';
|
|
3
2
|
import {log as logger} from './logger';
|
|
4
3
|
import {tempDir, fs, util, timing, node} from '@appium/support';
|
|
5
4
|
import {LRUCache} from 'lru-cache';
|
|
6
5
|
import AsyncLock from 'async-lock';
|
|
7
6
|
import axios from 'axios';
|
|
8
|
-
import B from 'bluebird';
|
|
9
7
|
import type {
|
|
10
8
|
ConfigureAppOptions,
|
|
11
9
|
CachedAppInfo,
|
|
@@ -102,27 +100,27 @@ export async function configureApp(
|
|
|
102
100
|
app: string,
|
|
103
101
|
options: string | string[] | ConfigureAppOptions = {} as ConfigureAppOptions
|
|
104
102
|
): Promise<string> {
|
|
105
|
-
if (
|
|
103
|
+
if (typeof app !== 'string') {
|
|
106
104
|
// immediately shortcircuit if not given an app
|
|
107
105
|
return '';
|
|
108
106
|
}
|
|
109
107
|
|
|
110
108
|
let supportedAppExtensions: string[];
|
|
111
|
-
const opts =
|
|
109
|
+
const opts = typeof options !== 'string' && !Array.isArray(options) ? options : undefined;
|
|
112
110
|
const onPostProcess = opts?.onPostProcess;
|
|
113
111
|
const onDownload = opts?.onDownload;
|
|
114
112
|
|
|
115
|
-
if (
|
|
113
|
+
if (typeof options === 'string') {
|
|
116
114
|
supportedAppExtensions = [options];
|
|
117
|
-
} else if (
|
|
115
|
+
} else if (Array.isArray(options)) {
|
|
118
116
|
supportedAppExtensions = options;
|
|
119
|
-
} else if (
|
|
117
|
+
} else if (util.isPlainObject(options)) {
|
|
120
118
|
supportedAppExtensions = options.supportedExtensions ?? [];
|
|
121
119
|
} else {
|
|
122
120
|
supportedAppExtensions = [];
|
|
123
121
|
}
|
|
124
122
|
|
|
125
|
-
if (
|
|
123
|
+
if (util.isEmpty(supportedAppExtensions)) {
|
|
126
124
|
throw new Error(`One or more supported app extensions must be provided`);
|
|
127
125
|
}
|
|
128
126
|
|
|
@@ -170,7 +168,7 @@ export async function configureApp(
|
|
|
170
168
|
let {stream, status} = result;
|
|
171
169
|
logger.debug(`Response status: ${status}`);
|
|
172
170
|
try {
|
|
173
|
-
if (!
|
|
171
|
+
if (!util.isEmpty(headers)) {
|
|
174
172
|
if (headers.etag) {
|
|
175
173
|
logger.debug(`Etag: ${headers.etag}`);
|
|
176
174
|
remoteAppProps.etag = headers.etag;
|
|
@@ -212,7 +210,7 @@ export async function configureApp(
|
|
|
212
210
|
if (onDownload) {
|
|
213
211
|
newApp = await onDownload({
|
|
214
212
|
url: originalAppLink,
|
|
215
|
-
headers:
|
|
213
|
+
headers: structuredClone(headers) as HTTPHeaders,
|
|
216
214
|
stream,
|
|
217
215
|
});
|
|
218
216
|
} else {
|
|
@@ -233,7 +231,7 @@ export async function configureApp(
|
|
|
233
231
|
} else {
|
|
234
232
|
let errorMessage = `The application at '${newApp}' does not exist or is not accessible`;
|
|
235
233
|
// protocol value for 'C:\\temp' is 'c:', so we check the length as well
|
|
236
|
-
if (
|
|
234
|
+
if (typeof protocol === 'string' && protocol.length > 2) {
|
|
237
235
|
errorMessage =
|
|
238
236
|
`The protocol '${protocol}' used in '${newApp}' is not supported. ` +
|
|
239
237
|
`Only http: and https: protocols are supported`;
|
|
@@ -267,12 +265,12 @@ export async function configureApp(
|
|
|
267
265
|
return appPathToCache;
|
|
268
266
|
};
|
|
269
267
|
|
|
270
|
-
if (
|
|
268
|
+
if (typeof onPostProcess === 'function') {
|
|
271
269
|
const postProcessArg: PostProcessOptions = {
|
|
272
|
-
cachedAppInfo:
|
|
270
|
+
cachedAppInfo: structuredClone(cachedAppInfo) as CachedAppInfo | undefined,
|
|
273
271
|
isUrl,
|
|
274
272
|
originalAppLink,
|
|
275
|
-
headers:
|
|
273
|
+
headers: structuredClone(headers) as HTTPHeaders,
|
|
276
274
|
appPath: newApp,
|
|
277
275
|
};
|
|
278
276
|
const result = await onPostProcess(postProcessArg);
|
|
@@ -282,7 +280,7 @@ export async function configureApp(
|
|
|
282
280
|
}
|
|
283
281
|
|
|
284
282
|
verifyAppExtension(newApp, supportedAppExtensions);
|
|
285
|
-
return appCacheKey !== toCacheKey(newApp) && (packageHash ||
|
|
283
|
+
return appCacheKey !== toCacheKey(newApp) && (packageHash || Object.values(remoteAppProps).some(Boolean))
|
|
286
284
|
? await storeAppInCache(newApp)
|
|
287
285
|
: newApp;
|
|
288
286
|
});
|
|
@@ -310,14 +308,14 @@ export function isPackageOrBundle(app: string): boolean {
|
|
|
310
308
|
*/
|
|
311
309
|
export function duplicateKeys<T>(input: T, firstKey: string, secondKey: string): T {
|
|
312
310
|
// If array provided, recursively call on all elements
|
|
313
|
-
if (
|
|
311
|
+
if (Array.isArray(input)) {
|
|
314
312
|
return input.map((item) => duplicateKeys(item, firstKey, secondKey)) as T;
|
|
315
313
|
}
|
|
316
314
|
|
|
317
315
|
// If object, create duplicates for keys and then recursively call on values
|
|
318
|
-
if (
|
|
316
|
+
if (util.isPlainObject(input)) {
|
|
319
317
|
const resultObj: Record<string, unknown> = {};
|
|
320
|
-
for (const [key, value] of
|
|
318
|
+
for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
|
|
321
319
|
const recursivelyCalledValue = duplicateKeys(value, firstKey, secondKey);
|
|
322
320
|
if (key === firstKey) {
|
|
323
321
|
resultObj[secondKey] = recursivelyCalledValue;
|
|
@@ -342,23 +340,23 @@ export function duplicateKeys<T>(input: T, firstKey: string, secondKey: string):
|
|
|
342
340
|
* @throws {TypeError} If value is not a string/array or JSON parsing fails for array-like input.
|
|
343
341
|
*/
|
|
344
342
|
export function parseCapsArray(capValue: string | string[]): string[] {
|
|
345
|
-
if (
|
|
343
|
+
if (Array.isArray(capValue)) {
|
|
346
344
|
return capValue;
|
|
347
345
|
}
|
|
348
346
|
|
|
349
347
|
try {
|
|
350
348
|
const parsed = JSON.parse(capValue);
|
|
351
|
-
if (
|
|
349
|
+
if (Array.isArray(parsed)) {
|
|
352
350
|
return parsed;
|
|
353
351
|
}
|
|
354
352
|
} catch (e) {
|
|
355
353
|
const message = `Failed to parse capability as JSON array: ${(e as Error).message}`;
|
|
356
|
-
if (
|
|
354
|
+
if (typeof capValue === 'string' && capValue.trimStart().startsWith('[')) {
|
|
357
355
|
throw new TypeError(message, {cause: e});
|
|
358
356
|
}
|
|
359
357
|
logger.warn(message);
|
|
360
358
|
}
|
|
361
|
-
if (
|
|
359
|
+
if (typeof capValue === 'string') {
|
|
362
360
|
return [capValue];
|
|
363
361
|
}
|
|
364
362
|
throw new TypeError(`Expected a string or a valid JSON array; received '${capValue}'`);
|
|
@@ -392,10 +390,10 @@ function parseAppLink(appLink: string): URL | {protocol?: string; pathname?: str
|
|
|
392
390
|
|
|
393
391
|
function isEnvOptionEnabled(optionName: string, defaultValue: boolean | null = null): boolean {
|
|
394
392
|
const value = process.env[optionName];
|
|
395
|
-
if (
|
|
393
|
+
if (defaultValue !== null && util.isEmpty(value)) {
|
|
396
394
|
return defaultValue;
|
|
397
395
|
}
|
|
398
|
-
return !
|
|
396
|
+
return !util.isEmpty(value) && !['0', 'false', 'no'].includes(String(value).toLowerCase());
|
|
399
397
|
}
|
|
400
398
|
|
|
401
399
|
function isSupportedUrl(app: string): boolean {
|
|
@@ -466,7 +464,7 @@ async function fetchApp(srcStream: Readable, dstPath: string): Promise<string> {
|
|
|
466
464
|
const writer = fs.createWriteStream(dstPath);
|
|
467
465
|
srcStream.pipe(writer);
|
|
468
466
|
|
|
469
|
-
await new
|
|
467
|
+
await new Promise<void>((resolve, reject) => {
|
|
470
468
|
srcStream.once('error', reject);
|
|
471
469
|
writer.once('finish', () => resolve());
|
|
472
470
|
writer.once('error', (e: Error) => {
|
|
@@ -515,18 +513,18 @@ function determineFilename(
|
|
|
515
513
|
? basename.substring(0, basename.length - extname.length)
|
|
516
514
|
: DEFAULT_BASENAME;
|
|
517
515
|
let resultingExt = extname;
|
|
518
|
-
if (!supportedAppExtensions.map(
|
|
516
|
+
if (!supportedAppExtensions.map((ext) => ext.toLowerCase()).includes(resultingExt.toLowerCase())) {
|
|
519
517
|
logger.info(
|
|
520
518
|
`The current file extension '${resultingExt}' is not supported. ` +
|
|
521
|
-
`Defaulting to '${
|
|
519
|
+
`Defaulting to '${supportedAppExtensions[0]}'`
|
|
522
520
|
);
|
|
523
|
-
resultingExt =
|
|
521
|
+
resultingExt = supportedAppExtensions[0] as string;
|
|
524
522
|
}
|
|
525
523
|
return `${resultingName}${resultingExt}`;
|
|
526
524
|
}
|
|
527
525
|
|
|
528
526
|
function verifyAppExtension(app: string, supportedAppExtensions: string[]): string {
|
|
529
|
-
if (supportedAppExtensions.map(
|
|
527
|
+
if (supportedAppExtensions.map((ext) => ext.toLowerCase()).includes(path.extname(app).toLowerCase())) {
|
|
530
528
|
return app;
|
|
531
529
|
}
|
|
532
530
|
throw new Error(
|
|
@@ -565,7 +563,7 @@ async function isAppIntegrityOk(
|
|
|
565
563
|
}
|
|
566
564
|
|
|
567
565
|
function toNaturalNumber(defaultValue: number, envVarName?: string): number {
|
|
568
|
-
if (!envVarName ||
|
|
566
|
+
if (!envVarName || process.env[envVarName] === undefined) {
|
|
569
567
|
return defaultValue;
|
|
570
568
|
}
|
|
571
569
|
const num = parseInt(`${process.env[envVarName]}`, 10);
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import {log} from './logger';
|
|
2
|
+
import type {StringRecord, IIpcSubscription, IAppiumIpc, IpcMessage, IpcEvent, AppiumLogger, IpcData} from '@appium/types';
|
|
3
|
+
import EventEmitter from 'node:events';
|
|
4
|
+
import {sleep} from 'asyncbox';
|
|
5
|
+
import {node} from '@appium/support';
|
|
6
|
+
|
|
7
|
+
const DEF_MAX_OBJ_SIZE_BYTES = 1024 * 1024; // 1mb seems like plenty for any plugin to pass a message
|
|
8
|
+
const DEF_MAX_TOPICS = 1000;
|
|
9
|
+
|
|
10
|
+
export const EVT_MESSAGE = 'message';
|
|
11
|
+
export const EVT_UNSUBSCRIBED = 'unsubscribed';
|
|
12
|
+
|
|
13
|
+
export type AppiumIpcOpts = {
|
|
14
|
+
maxObjSize?: number;
|
|
15
|
+
maxTopics?: number;
|
|
16
|
+
log?: AppiumLogger;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const ASYNC_ITERATOR_STOP = Symbol('asyncIteratorStop');
|
|
20
|
+
|
|
21
|
+
export class IpcSubscription<T extends IpcData> extends EventEmitter<IpcEvent<T>> implements IIpcSubscription<T> {
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
public readonly subscriber: string,
|
|
25
|
+
public readonly topic: string,
|
|
26
|
+
private readonly ipc: AppiumIpc
|
|
27
|
+
) {
|
|
28
|
+
super();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get isActive() {
|
|
32
|
+
return this.ipc.subscriptionExists(this.topic, this.subscriber);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getMessage(): IpcMessage<T> | undefined {
|
|
36
|
+
if (!this.isActive) {
|
|
37
|
+
throw new Error('Cannot get message from subscription after unsubscribing');
|
|
38
|
+
}
|
|
39
|
+
return this.ipc.getMessage<T>(this.topic);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async publish(data: T): Promise<void> {
|
|
43
|
+
if (!this.isActive) {
|
|
44
|
+
throw new Error('Cannot publish data to topic from subscription after unsubscribing');
|
|
45
|
+
}
|
|
46
|
+
return await this.ipc.publish<T>(this.topic, this.subscriber, data);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
unsubscribe(): boolean {
|
|
50
|
+
if (!this.isActive) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const unsubscribeRes = this.ipc.unsubscribe(this.topic, this.subscriber);
|
|
54
|
+
this.emit('unsubscribed');
|
|
55
|
+
this.removeAllListeners(EVT_MESSAGE);
|
|
56
|
+
return unsubscribeRes;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async *[Symbol.asyncIterator](): AsyncGenerator<IpcMessage<T>> {
|
|
60
|
+
// yield any messages that are emitted, but keep an eye out for an unsubscribed that happens
|
|
61
|
+
// while a caller is waiting on the loop, because we want to exit the loop in case of
|
|
62
|
+
// unsubscription, even if we were already waiting on the next message.
|
|
63
|
+
while (this.isActive) {
|
|
64
|
+
const val = await new Promise<IpcMessage<T> | typeof ASYNC_ITERATOR_STOP>((resolve) => {
|
|
65
|
+
this.once(EVT_MESSAGE, (message: IpcMessage<T>) => {
|
|
66
|
+
this.removeAllListeners(EVT_UNSUBSCRIBED);
|
|
67
|
+
resolve(message);
|
|
68
|
+
});
|
|
69
|
+
this.once(EVT_UNSUBSCRIBED, () => {
|
|
70
|
+
// EVT_MESSAGE listeners are already removed in unsubscribe()
|
|
71
|
+
resolve(ASYNC_ITERATOR_STOP);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
if (val === ASYNC_ITERATOR_STOP) {
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
yield val;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class AppiumIpc implements IAppiumIpc {
|
|
83
|
+
protected readonly messageByTopic: StringRecord<IpcMessage<any>> = {};
|
|
84
|
+
protected readonly subs: StringRecord<Array<IpcSubscription<any>>> = {};
|
|
85
|
+
protected readonly topics = new Set<string>();
|
|
86
|
+
protected readonly maxObjSize: number;
|
|
87
|
+
protected readonly maxTopics: number;
|
|
88
|
+
protected readonly log: AppiumLogger;
|
|
89
|
+
|
|
90
|
+
constructor (opts: AppiumIpcOpts = {}) {
|
|
91
|
+
this.maxObjSize = opts.maxObjSize ?? DEF_MAX_OBJ_SIZE_BYTES;
|
|
92
|
+
this.maxTopics = opts.maxTopics ?? DEF_MAX_TOPICS;
|
|
93
|
+
this.log = opts.log ?? log;
|
|
94
|
+
this.log.debug(`Initialized new IPC object with max object size of ${this.maxObjSize} bytes ` +
|
|
95
|
+
`and max topics of ${this.maxTopics}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
subscribe<T extends IpcData>(topic: string, subscriber: string): IpcSubscription<T> {
|
|
99
|
+
this.log.info(`Subscribing ${subscriber} to topic '${topic}'`);
|
|
100
|
+
if (this.subscriptionExists(topic, subscriber)) {
|
|
101
|
+
throw new Error(`Subscription already exists for topic "${topic}" and subscriber "${subscriber}"`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.ensureTopic(topic);
|
|
105
|
+
this.subs[topic] ??= [];
|
|
106
|
+
const sub = new IpcSubscription<T>(subscriber, topic, this);
|
|
107
|
+
this.subs[topic].push(sub);
|
|
108
|
+
return sub;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
unsubscribe(topic: string, subscriber: string): boolean {
|
|
112
|
+
this.log.info(`Unsubscribing ${subscriber} from topic '${topic}'`);
|
|
113
|
+
if (this.subscriptionExists(topic, subscriber)) {
|
|
114
|
+
this.subs[topic] = this.subs[topic].filter((sub) => sub.subscriber !== subscriber);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async publish<T extends IpcData>(topic: string, publisher: string, data: T): Promise<void> {
|
|
121
|
+
this.log.debug(`${publisher} is publishing a message to topic ${topic}`);
|
|
122
|
+
|
|
123
|
+
this.ensureTopic(topic);
|
|
124
|
+
|
|
125
|
+
const messageSize = node.getObjectSize(data);
|
|
126
|
+
if (messageSize > this.maxObjSize) {
|
|
127
|
+
throw new Error(`Error when ${publisher} is publishing to topic '${topic}': ` +
|
|
128
|
+
`Message with size ${messageSize} bytes is bigger than max size of ${this.maxObjSize} bytes`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let clonedData: T;
|
|
132
|
+
try {
|
|
133
|
+
clonedData = structuredClone(data);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
throw new Error(`Could not clone data for IPC publish from ${publisher} on topic ${topic}`, {cause: e});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const message: IpcMessage<T> = {publisher, data: clonedData, topic, timestampMs: Date.now()};
|
|
139
|
+
|
|
140
|
+
this.messageByTopic[topic] = message;
|
|
141
|
+
|
|
142
|
+
const subs = this.subs[topic] ?
|
|
143
|
+
this.subs[topic].filter((sub) => sub.subscriber !== publisher) :
|
|
144
|
+
[];
|
|
145
|
+
|
|
146
|
+
for (const sub of subs) {
|
|
147
|
+
sub.emit(EVT_MESSAGE, structuredClone(message));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// we don't want to return from publish until the async iterators on subscriptions have had
|
|
151
|
+
// a chance to observe the emitted value, otherwise some might get lost
|
|
152
|
+
await sleep(0);
|
|
153
|
+
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
getMessage<T extends IpcData>(topic: string): IpcMessage<T> | undefined {
|
|
157
|
+
if (!this.messageByTopic[topic]) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return structuredClone(this.messageByTopic[topic] as IpcMessage<T>);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
subscriptionExists(topic: string, subscriber: string): boolean {
|
|
165
|
+
return !!this.subs[topic]?.some((sub) => sub.subscriber === subscriber);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
protected ensureTopic(topic: string): void {
|
|
169
|
+
if (this.topics.has(topic)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (this.topics.size >= this.maxTopics) {
|
|
173
|
+
throw new Error(`Cannot create new IPC topic '${topic}': ` +
|
|
174
|
+
`maximum of ${this.maxTopics} topics per session reached. ` +
|
|
175
|
+
`Adjust with the --max-ipc-topics server arg.`);
|
|
176
|
+
}
|
|
177
|
+
this.topics.add(topic);
|
|
178
|
+
}
|
|
179
|
+
}
|