@appium/base-driver 10.6.0 → 10.7.1
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 +1 -1
- package/build/lib/basedriver/capabilities.d.ts.map +1 -1
- package/build/lib/basedriver/capabilities.js +15 -7
- package/build/lib/basedriver/capabilities.js.map +1 -1
- package/build/lib/basedriver/commands/bidi.js +1 -1
- package/build/lib/basedriver/commands/event.js.map +1 -1
- package/build/lib/basedriver/commands/execute.js.map +1 -1
- package/build/lib/basedriver/commands/find.d.ts.map +1 -1
- package/build/lib/basedriver/commands/find.js +2 -1
- package/build/lib/basedriver/commands/find.js.map +1 -1
- package/build/lib/basedriver/commands/timeout.js +4 -4
- package/build/lib/basedriver/commands/timeout.js.map +1 -1
- package/build/lib/basedriver/core.d.ts.map +1 -1
- package/build/lib/basedriver/core.js +5 -2
- 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.map +1 -1
- package/build/lib/basedriver/driver.d.ts.map +1 -1
- package/build/lib/basedriver/driver.js +23 -24
- package/build/lib/basedriver/driver.js.map +1 -1
- package/build/lib/basedriver/extension-core.d.ts.map +1 -1
- package/build/lib/basedriver/extension-core.js +11 -5
- 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 +20 -4
- package/build/lib/basedriver/helpers.js.map +1 -1
- package/build/lib/basedriver/ipc.d.ts.map +1 -1
- package/build/lib/basedriver/ipc.js +6 -4
- package/build/lib/basedriver/ipc.js.map +1 -1
- package/build/lib/basedriver/validation.d.ts.map +1 -1
- package/build/lib/basedriver/validation.js +3 -2
- package/build/lib/basedriver/validation.js.map +1 -1
- package/build/lib/express/express-logging.d.ts +0 -1
- package/build/lib/express/express-logging.d.ts.map +1 -1
- package/build/lib/express/express-logging.js +9 -8
- package/build/lib/express/express-logging.js.map +1 -1
- 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.map +1 -1
- package/build/lib/express/server.d.ts +1 -1
- package/build/lib/express/server.d.ts.map +1 -1
- package/build/lib/express/server.js +19 -20
- package/build/lib/express/server.js.map +1 -1
- package/build/lib/express/websocket.d.ts.map +1 -1
- 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.map +1 -1
- package/build/lib/helpers/levenshtein-match.d.ts.map +1 -1
- package/build/lib/helpers/levenshtein-match.js +4 -1
- package/build/lib/helpers/levenshtein-match.js.map +1 -1
- package/build/lib/index.d.ts +1 -1
- package/build/lib/index.d.ts.map +1 -1
- package/build/lib/index.js +3 -2
- 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 +14 -7
- package/build/lib/jsonwp-proxy/protocol-converter.js.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.js +17 -11
- 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 +13 -13
- package/build/lib/protocol/errors.js.map +1 -1
- package/build/lib/protocol/protocol.d.ts +1 -1
- package/build/lib/protocol/protocol.d.ts.map +1 -1
- package/build/lib/protocol/protocol.js +35 -18
- package/build/lib/protocol/protocol.js.map +1 -1
- package/build/lib/protocol/routes.d.ts.map +1 -1
- package/build/lib/protocol/routes.js +7 -5
- package/build/lib/protocol/routes.js.map +1 -1
- package/build/lib/test-pages/crash.d.ts.map +1 -0
- package/build/lib/test-pages/crash.js.map +1 -0
- package/build/lib/test-pages/env.d.ts +5 -0
- package/build/lib/test-pages/env.d.ts.map +1 -0
- package/build/lib/test-pages/env.js +12 -0
- package/build/lib/test-pages/env.js.map +1 -0
- package/build/lib/{express/static.d.ts → test-pages/handlers.d.ts} +1 -2
- package/build/lib/test-pages/handlers.d.ts.map +1 -0
- package/build/lib/{express/static.js → test-pages/handlers.js} +7 -17
- package/build/lib/test-pages/handlers.js.map +1 -0
- package/build/lib/test-pages/index.d.ts +6 -0
- package/build/lib/test-pages/index.d.ts.map +1 -0
- package/build/lib/test-pages/index.js +35 -0
- package/build/lib/test-pages/index.js.map +1 -0
- package/build/lib/test-pages/static-dir.d.ts +8 -0
- package/build/lib/test-pages/static-dir.d.ts.map +1 -0
- package/build/lib/test-pages/static-dir.js +24 -0
- package/build/lib/test-pages/static-dir.js.map +1 -0
- package/build/lib/test-pages/template.d.ts +3 -0
- package/build/lib/test-pages/template.d.ts.map +1 -0
- package/build/lib/test-pages/template.js +19 -0
- package/build/lib/test-pages/template.js.map +1 -0
- package/build/lib/utils.d.ts +0 -2
- package/build/lib/utils.d.ts.map +1 -1
- package/build/lib/utils.js +0 -16
- package/build/lib/utils.js.map +1 -1
- package/lib/basedriver/capabilities.ts +72 -66
- package/lib/basedriver/commands/bidi.ts +1 -1
- package/lib/basedriver/commands/event.ts +10 -5
- package/lib/basedriver/commands/execute.ts +12 -9
- package/lib/basedriver/commands/find.ts +20 -12
- package/lib/basedriver/commands/log.ts +2 -2
- package/lib/basedriver/commands/timeout.ts +17 -8
- package/lib/basedriver/core.ts +14 -14
- package/lib/basedriver/device-settings.ts +4 -8
- package/lib/basedriver/driver.ts +50 -40
- package/lib/basedriver/extension-core.ts +33 -17
- package/lib/basedriver/helpers.ts +57 -26
- package/lib/basedriver/ipc.ts +37 -18
- package/lib/basedriver/validation.ts +13 -6
- package/lib/express/express-logging.ts +14 -17
- package/lib/express/idempotency.ts +6 -6
- package/lib/express/middleware.ts +10 -12
- package/lib/express/server.ts +53 -61
- package/lib/express/websocket.ts +5 -7
- package/lib/helpers/capabilities.ts +5 -4
- package/lib/helpers/extension-command-name.ts +1 -1
- package/lib/helpers/levenshtein-match.ts +20 -11
- package/lib/index.js +2 -1
- package/lib/jsonwp-proxy/protocol-converter.ts +51 -27
- package/lib/jsonwp-proxy/proxy.ts +42 -42
- package/lib/protocol/errors.ts +47 -67
- package/lib/protocol/protocol.ts +116 -72
- package/lib/protocol/routes.ts +9 -9
- package/lib/test-pages/env.ts +9 -0
- package/lib/{express/static.ts → test-pages/handlers.ts} +7 -27
- package/lib/test-pages/index.ts +34 -0
- package/lib/test-pages/static-dir.ts +19 -0
- package/lib/test-pages/template.ts +17 -0
- package/lib/utils.ts +3 -23
- package/package.json +9 -10
- package/tsconfig.json +1 -0
- package/build/lib/express/crash.d.ts.map +0 -1
- package/build/lib/express/crash.js.map +0 -1
- package/build/lib/express/static.d.ts.map +0 -1
- package/build/lib/express/static.js.map +0 -1
- /package/build/lib/{express → test-pages}/crash.d.ts +0 -0
- /package/build/lib/{express → test-pages}/crash.js +0 -0
- /package/lib/{express → test-pages}/crash.ts +0 -0
- /package/{static → test-fixtures/static}/appium.png +0 -0
- /package/{static → test-fixtures/static}/favicon.ico +0 -0
- /package/{static → test-fixtures/static}/js/jquery.min.js +0 -0
- /package/{static → test-fixtures/static}/test/frameset.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig-app-banner.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig-scrollable.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig2.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig3.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig4.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig5.html +0 -0
- /package/{static → test-fixtures/static}/test/iframes.html +0 -0
- /package/{static → test-fixtures/static}/test/shadow-dom.html +0 -0
- /package/{static → test-fixtures/static}/test/subframe1.html +0 -0
- /package/{static → test-fixtures/static}/test/subframe2.html +0 -0
- /package/{static → test-fixtures/static}/test/subframe3.html +0 -0
- /package/{static → test-fixtures/static}/test/touch.html +0 -0
- /package/{static → test-fixtures/static}/test/welcome.html +0 -0
package/lib/basedriver/ipc.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import {log} from './logger';
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
StringRecord,
|
|
4
|
+
IIpcSubscription,
|
|
5
|
+
IAppiumIpc,
|
|
6
|
+
IpcMessage,
|
|
7
|
+
IpcEvent,
|
|
8
|
+
AppiumLogger,
|
|
9
|
+
IpcData,
|
|
10
|
+
} from '@appium/types';
|
|
3
11
|
import EventEmitter from 'node:events';
|
|
4
12
|
import {sleep} from 'asyncbox';
|
|
5
13
|
import {node} from '@appium/support';
|
|
@@ -18,12 +26,14 @@ export type AppiumIpcOpts = {
|
|
|
18
26
|
|
|
19
27
|
const ASYNC_ITERATOR_STOP = Symbol('asyncIteratorStop');
|
|
20
28
|
|
|
21
|
-
export class IpcSubscription<T extends IpcData>
|
|
22
|
-
|
|
29
|
+
export class IpcSubscription<T extends IpcData>
|
|
30
|
+
extends EventEmitter<IpcEvent<T>>
|
|
31
|
+
implements IIpcSubscription<T>
|
|
32
|
+
{
|
|
23
33
|
constructor(
|
|
24
34
|
public readonly subscriber: string,
|
|
25
35
|
public readonly topic: string,
|
|
26
|
-
private readonly ipc: AppiumIpc
|
|
36
|
+
private readonly ipc: AppiumIpc,
|
|
27
37
|
) {
|
|
28
38
|
super();
|
|
29
39
|
}
|
|
@@ -87,18 +97,22 @@ export class AppiumIpc implements IAppiumIpc {
|
|
|
87
97
|
protected readonly maxTopics: number;
|
|
88
98
|
protected readonly log: AppiumLogger;
|
|
89
99
|
|
|
90
|
-
constructor
|
|
100
|
+
constructor(opts: AppiumIpcOpts = {}) {
|
|
91
101
|
this.maxObjSize = opts.maxObjSize ?? DEF_MAX_OBJ_SIZE_BYTES;
|
|
92
102
|
this.maxTopics = opts.maxTopics ?? DEF_MAX_TOPICS;
|
|
93
103
|
this.log = opts.log ?? log;
|
|
94
|
-
this.log.debug(
|
|
95
|
-
`
|
|
104
|
+
this.log.debug(
|
|
105
|
+
`Initialized new IPC object with max object size of ${this.maxObjSize} bytes ` +
|
|
106
|
+
`and max topics of ${this.maxTopics}`,
|
|
107
|
+
);
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
subscribe<T extends IpcData>(topic: string, subscriber: string): IpcSubscription<T> {
|
|
99
111
|
this.log.info(`Subscribing ${subscriber} to topic '${topic}'`);
|
|
100
112
|
if (this.subscriptionExists(topic, subscriber)) {
|
|
101
|
-
throw new Error(
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Subscription already exists for topic "${topic}" and subscriber "${subscriber}"`,
|
|
115
|
+
);
|
|
102
116
|
}
|
|
103
117
|
|
|
104
118
|
this.ensureTopic(topic);
|
|
@@ -124,24 +138,28 @@ export class AppiumIpc implements IAppiumIpc {
|
|
|
124
138
|
|
|
125
139
|
const messageSize = node.getObjectSize(data);
|
|
126
140
|
if (messageSize > this.maxObjSize) {
|
|
127
|
-
throw new Error(
|
|
128
|
-
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Error when ${publisher} is publishing to topic '${topic}': ` +
|
|
143
|
+
`Message with size ${messageSize} bytes is bigger than max size of ${this.maxObjSize} bytes`,
|
|
144
|
+
);
|
|
129
145
|
}
|
|
130
146
|
|
|
131
147
|
let clonedData: T;
|
|
132
148
|
try {
|
|
133
149
|
clonedData = structuredClone(data);
|
|
134
150
|
} catch (e) {
|
|
135
|
-
throw new Error(`Could not clone data for IPC publish from ${publisher} on topic ${topic}`, {
|
|
151
|
+
throw new Error(`Could not clone data for IPC publish from ${publisher} on topic ${topic}`, {
|
|
152
|
+
cause: e,
|
|
153
|
+
});
|
|
136
154
|
}
|
|
137
155
|
|
|
138
156
|
const message: IpcMessage<T> = {publisher, data: clonedData, topic, timestampMs: Date.now()};
|
|
139
157
|
|
|
140
158
|
this.messageByTopic[topic] = message;
|
|
141
159
|
|
|
142
|
-
const subs = this.subs[topic]
|
|
143
|
-
this.subs[topic].filter((sub) => sub.subscriber !== publisher)
|
|
144
|
-
[];
|
|
160
|
+
const subs = this.subs[topic]
|
|
161
|
+
? this.subs[topic].filter((sub) => sub.subscriber !== publisher)
|
|
162
|
+
: [];
|
|
145
163
|
|
|
146
164
|
for (const sub of subs) {
|
|
147
165
|
sub.emit(EVT_MESSAGE, structuredClone(message));
|
|
@@ -150,7 +168,6 @@ export class AppiumIpc implements IAppiumIpc {
|
|
|
150
168
|
// we don't want to return from publish until the async iterators on subscriptions have had
|
|
151
169
|
// a chance to observe the emitted value, otherwise some might get lost
|
|
152
170
|
await sleep(0);
|
|
153
|
-
|
|
154
171
|
}
|
|
155
172
|
|
|
156
173
|
getMessage<T extends IpcData>(topic: string): IpcMessage<T> | undefined {
|
|
@@ -170,9 +187,11 @@ export class AppiumIpc implements IAppiumIpc {
|
|
|
170
187
|
return;
|
|
171
188
|
}
|
|
172
189
|
if (this.topics.size >= this.maxTopics) {
|
|
173
|
-
throw new Error(
|
|
174
|
-
`
|
|
175
|
-
|
|
190
|
+
throw new Error(
|
|
191
|
+
`Cannot create new IPC topic '${topic}': ` +
|
|
192
|
+
`maximum of ${this.maxTopics} topics per session reached. ` +
|
|
193
|
+
`Adjust with the --max-ipc-topics server arg.`,
|
|
194
|
+
);
|
|
176
195
|
}
|
|
177
196
|
this.topics.add(topic);
|
|
178
197
|
}
|
|
@@ -77,7 +77,7 @@ export class Validator {
|
|
|
77
77
|
if (value !== undefined && options) {
|
|
78
78
|
log.warn(
|
|
79
79
|
`The '${key}' capability has been deprecated and must not be used anymore. ` +
|
|
80
|
-
|
|
80
|
+
`Please check the driver documentation for possible alternatives.`,
|
|
81
81
|
);
|
|
82
82
|
}
|
|
83
83
|
return null;
|
|
@@ -108,15 +108,19 @@ export class Validator {
|
|
|
108
108
|
}
|
|
109
109
|
if (
|
|
110
110
|
!options?.allowEmpty &&
|
|
111
|
-
((value !== undefined && util.isEmpty(value)) ||
|
|
111
|
+
((value !== undefined && util.isEmpty(value)) ||
|
|
112
|
+
(typeof value === 'string' && !value.trim()))
|
|
112
113
|
) {
|
|
113
114
|
return 'must not be empty or blank';
|
|
114
115
|
}
|
|
115
116
|
return null;
|
|
116
|
-
}
|
|
117
|
+
},
|
|
117
118
|
};
|
|
118
119
|
|
|
119
|
-
validate(
|
|
120
|
+
validate(
|
|
121
|
+
values: Record<string, any>,
|
|
122
|
+
constraints: Record<string, Constraint>,
|
|
123
|
+
): Record<string, string[]> | null {
|
|
120
124
|
const result: Record<string, string[]> = {};
|
|
121
125
|
for (const [key, constraint] of Object.entries(constraints)) {
|
|
122
126
|
const value = values[key];
|
|
@@ -125,7 +129,11 @@ export class Validator {
|
|
|
125
129
|
continue;
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
const validationError = this._validators[validatorName as keyof Constraint](
|
|
132
|
+
const validationError = this._validators[validatorName as keyof Constraint](
|
|
133
|
+
value,
|
|
134
|
+
options,
|
|
135
|
+
key,
|
|
136
|
+
);
|
|
129
137
|
if (validationError == null) {
|
|
130
138
|
continue;
|
|
131
139
|
}
|
|
@@ -139,7 +147,6 @@ export class Validator {
|
|
|
139
147
|
}
|
|
140
148
|
return util.isEmpty(result) ? null : result;
|
|
141
149
|
}
|
|
142
|
-
|
|
143
150
|
}
|
|
144
151
|
|
|
145
152
|
export const validator = new Validator();
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import '@colors/colors';
|
|
1
|
+
import {console, logger, util} from '@appium/support';
|
|
3
2
|
import morgan from 'morgan';
|
|
4
3
|
import type {Request, RequestHandler, Response} from 'express';
|
|
5
4
|
import {log} from './logger';
|
|
@@ -23,17 +22,18 @@ export const startLogFormatter: RequestHandler = morgan(startLogFormatterHandler
|
|
|
23
22
|
type MorganTokens = unknown;
|
|
24
23
|
type FormatFn = (tokens: MorganTokens, req: Request, res: Response) => string;
|
|
25
24
|
|
|
26
|
-
function endLogFormatterHandler(tokens: MorganTokens, req: Request, res: Response):
|
|
25
|
+
function endLogFormatterHandler(tokens: MorganTokens, req: Request, res: Response): undefined {
|
|
27
26
|
log.info(requestEndLoggingFormat(tokens, req, res));
|
|
27
|
+
return undefined;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function startLogFormatterHandler(tokens: unknown, req: Request, res: Response):
|
|
30
|
+
function startLogFormatterHandler(tokens: unknown, req: Request, res: Response): undefined {
|
|
31
31
|
let reqBody = '';
|
|
32
32
|
if (req.body) {
|
|
33
33
|
try {
|
|
34
34
|
reqBody = util.truncateString(
|
|
35
35
|
typeof req.body === 'string' ? req.body : JSON.stringify(req.body),
|
|
36
|
-
{length: MAX_LOG_BODY_LENGTH}
|
|
36
|
+
{length: MAX_LOG_BODY_LENGTH},
|
|
37
37
|
);
|
|
38
38
|
} catch {
|
|
39
39
|
// ignore
|
|
@@ -41,8 +41,9 @@ function startLogFormatterHandler(tokens: unknown, req: Request, res: Response):
|
|
|
41
41
|
}
|
|
42
42
|
log.info(
|
|
43
43
|
requestStartLoggingFormat(tokens, req, res),
|
|
44
|
-
logger.markSensitive(
|
|
44
|
+
logger.markSensitive(console.styleText('grey', reqBody)),
|
|
45
45
|
);
|
|
46
|
+
return undefined;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
// Copied the morgan compile function over so that cooler formats may be configured
|
|
@@ -55,29 +56,25 @@ function compile(fmt: string): FormatFn {
|
|
|
55
56
|
return new Function('tokens', 'req', 'res', js) as FormatFn;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
function requestEndLoggingFormat(
|
|
59
|
-
tokens: MorganTokens,
|
|
60
|
-
req: Request,
|
|
61
|
-
res: Response
|
|
62
|
-
): string {
|
|
59
|
+
function requestEndLoggingFormat(tokens: MorganTokens, req: Request, res: Response): string {
|
|
63
60
|
const status = res.statusCode;
|
|
64
61
|
let statusStr = ':status';
|
|
65
62
|
if (status >= 500) {
|
|
66
|
-
statusStr =
|
|
63
|
+
statusStr = console.styleText('red', statusStr);
|
|
67
64
|
} else if (status >= 400) {
|
|
68
|
-
statusStr =
|
|
65
|
+
statusStr = console.styleText('yellow', statusStr);
|
|
69
66
|
} else if (status >= 300) {
|
|
70
|
-
statusStr =
|
|
67
|
+
statusStr = console.styleText('cyan', statusStr);
|
|
71
68
|
} else {
|
|
72
|
-
statusStr =
|
|
69
|
+
statusStr = console.styleText('green', statusStr);
|
|
73
70
|
}
|
|
74
71
|
const fn = compile(
|
|
75
|
-
`${'<-- :method :url '
|
|
72
|
+
`${console.styleText('white', '<-- :method :url ')}${statusStr} ${console.styleText('grey', ':response-time ms - :res[content-length]')}`,
|
|
76
73
|
);
|
|
77
74
|
return fn(tokens, req, res);
|
|
78
75
|
}
|
|
79
76
|
|
|
80
77
|
const requestStartLoggingFormat = compile(
|
|
81
|
-
`${'-->'
|
|
78
|
+
`${console.styleText('white', '-->')} ${console.styleText('white', ':method')} ${console.styleText('white', ':url')}`,
|
|
82
79
|
);
|
|
83
80
|
// #endregion
|
|
@@ -31,7 +31,7 @@ const MAX_CACHED_PAYLOAD_SIZE_BYTES = 1 * 1024 * 1024; // 1 MiB
|
|
|
31
31
|
export async function handleIdempotency(
|
|
32
32
|
req: Request,
|
|
33
33
|
res: Response,
|
|
34
|
-
next: NextFunction
|
|
34
|
+
next: NextFunction,
|
|
35
35
|
): Promise<void> {
|
|
36
36
|
const keyOrArr = req.headers[IDEMPOTENCY_KEY_HEADER];
|
|
37
37
|
if (util.isEmpty(keyOrArr) || !keyOrArr) {
|
|
@@ -114,7 +114,7 @@ function cacheResponse(key: string, req: Request, res: Response): void {
|
|
|
114
114
|
const patchedWriter = (
|
|
115
115
|
chunk: unknown,
|
|
116
116
|
encoding: BufferEncoding | (() => void),
|
|
117
|
-
next?: (() => void) | ((err?: Error) => void)
|
|
117
|
+
next?: (() => void) | ((err?: Error) => void),
|
|
118
118
|
): boolean => {
|
|
119
119
|
if (errorMessage || !responseRef.deref()) {
|
|
120
120
|
responseChunks = [];
|
|
@@ -122,7 +122,7 @@ function cacheResponse(key: string, req: Request, res: Response): void {
|
|
|
122
122
|
return originalSocketWriter(
|
|
123
123
|
chunk as string | Buffer | Uint8Array,
|
|
124
124
|
encoding as BufferEncoding,
|
|
125
|
-
next as (err?: Error) => void
|
|
125
|
+
next as (err?: Error | null) => void,
|
|
126
126
|
);
|
|
127
127
|
}
|
|
128
128
|
|
|
@@ -139,7 +139,7 @@ function cacheResponse(key: string, req: Request, res: Response): void {
|
|
|
139
139
|
return originalSocketWriter(
|
|
140
140
|
chunk as string | Buffer | Uint8Array,
|
|
141
141
|
encoding as BufferEncoding,
|
|
142
|
-
next as (err?: Error) => void
|
|
142
|
+
next as (err?: Error | null) => void,
|
|
143
143
|
);
|
|
144
144
|
};
|
|
145
145
|
socket.write = patchedWriter as typeof socket.write;
|
|
@@ -153,7 +153,7 @@ function cacheResponse(key: string, req: Request, res: Response): void {
|
|
|
153
153
|
if (!IDEMPOTENT_RESPONSES.has(key)) {
|
|
154
154
|
log.info(
|
|
155
155
|
`Could not cache the response identified by '${key}'. ` +
|
|
156
|
-
`Cache consistency has been damaged
|
|
156
|
+
`Cache consistency has been damaged`,
|
|
157
157
|
);
|
|
158
158
|
} else {
|
|
159
159
|
log.info(`Could not cache the response identified by '${key}': ${errorMessage}`);
|
|
@@ -175,7 +175,7 @@ function cacheResponse(key: string, req: Request, res: Response): void {
|
|
|
175
175
|
if (!IDEMPOTENT_RESPONSES.has(key)) {
|
|
176
176
|
log.info(
|
|
177
177
|
`Could not cache the response identified by '${key}'. ` +
|
|
178
|
-
`Cache consistency has been damaged
|
|
178
|
+
`Cache consistency has been damaged`,
|
|
179
179
|
);
|
|
180
180
|
} else if (errorMessage) {
|
|
181
181
|
log.info(`Could not cache the response identified by '${key}': ${errorMessage}`);
|
|
@@ -21,7 +21,7 @@ export function allowCrossDomain(req: Request, res: Response, next: NextFunction
|
|
|
21
21
|
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS, DELETE');
|
|
22
22
|
res.header(
|
|
23
23
|
'Access-Control-Allow-Headers',
|
|
24
|
-
'Cache-Control, Pragma, Origin, X-Requested-With, Content-Type, Accept, User-Agent'
|
|
24
|
+
'Cache-Control, Pragma, Origin, X-Requested-With, Content-Type, Accept, User-Agent',
|
|
25
25
|
);
|
|
26
26
|
|
|
27
27
|
if (req.method === 'OPTIONS') {
|
|
@@ -42,10 +42,10 @@ export function allowCrossDomainAsyncExecute(basePath: string): RequestHandler {
|
|
|
42
42
|
function allowCrossDomainAsyncExecuteHandler(
|
|
43
43
|
req: Request,
|
|
44
44
|
res: Response,
|
|
45
|
-
next: NextFunction
|
|
45
|
+
next: NextFunction,
|
|
46
46
|
): void {
|
|
47
47
|
const receiveAsyncResponseRegExp = new RegExp(
|
|
48
|
-
`${util.escapeRegExp(basePath)}/session/[a-f0-9-]+/(appium/)?receive_async_response
|
|
48
|
+
`${util.escapeRegExp(basePath)}/session/[a-f0-9-]+/(appium/)?receive_async_response`,
|
|
49
49
|
);
|
|
50
50
|
if (!receiveAsyncResponseRegExp.test(req.url)) {
|
|
51
51
|
next();
|
|
@@ -72,9 +72,11 @@ export function handleLogContext(req: Request, _res: Response, next: NextFunctio
|
|
|
72
72
|
{
|
|
73
73
|
requestId,
|
|
74
74
|
...sessionInfo,
|
|
75
|
-
isSensitive: ['true', '1', 'yes'].includes(
|
|
75
|
+
isSensitive: ['true', '1', 'yes'].includes(
|
|
76
|
+
String(isSensitiveHeaderValue ?? '').toLowerCase(),
|
|
77
|
+
),
|
|
76
78
|
},
|
|
77
|
-
true
|
|
79
|
+
true,
|
|
78
80
|
);
|
|
79
81
|
|
|
80
82
|
next();
|
|
@@ -83,11 +85,7 @@ export function handleLogContext(req: Request, _res: Response, next: NextFunctio
|
|
|
83
85
|
/**
|
|
84
86
|
* Ensures requests default to JSON content-type when none is provided.
|
|
85
87
|
*/
|
|
86
|
-
export function defaultToJSONContentType(
|
|
87
|
-
req: Request,
|
|
88
|
-
_res: Response,
|
|
89
|
-
next: NextFunction
|
|
90
|
-
): void {
|
|
88
|
+
export function defaultToJSONContentType(req: Request, _res: Response, next: NextFunction): void {
|
|
91
89
|
if (!req.headers['content-type']) {
|
|
92
90
|
req.headers['content-type'] = 'application/json; charset=utf-8';
|
|
93
91
|
}
|
|
@@ -107,7 +105,7 @@ export function tryHandleWebSocketUpgrade(
|
|
|
107
105
|
req: IncomingMessage,
|
|
108
106
|
socket: Duplex,
|
|
109
107
|
head: Buffer,
|
|
110
|
-
webSocketsMapping: StringRecord<WSServer
|
|
108
|
+
webSocketsMapping: StringRecord<WSServer>,
|
|
111
109
|
): boolean {
|
|
112
110
|
if (String(req.headers?.upgrade ?? '').toLowerCase() !== 'websocket') {
|
|
113
111
|
return false;
|
|
@@ -157,7 +155,7 @@ export function catchAllHandler(
|
|
|
157
155
|
err: Error,
|
|
158
156
|
_req: Request,
|
|
159
157
|
res: Response,
|
|
160
|
-
next: NextFunction
|
|
158
|
+
next: NextFunction,
|
|
161
159
|
): void {
|
|
162
160
|
if (res.headersSent) {
|
|
163
161
|
next(err);
|
package/lib/express/server.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import express from 'express';
|
|
3
2
|
import type {Express, RequestHandler} from 'express';
|
|
4
3
|
import http from 'node:http';
|
|
5
4
|
import type {Server as HttpServer} from 'node:http';
|
|
6
|
-
import
|
|
5
|
+
import {createRequire} from 'node:module';
|
|
7
6
|
import bodyParser from 'body-parser';
|
|
8
7
|
import methodOverride from 'method-override';
|
|
9
8
|
import {log} from './logger';
|
|
@@ -19,14 +18,9 @@ import {
|
|
|
19
18
|
catch404Handler,
|
|
20
19
|
handleLogContext,
|
|
21
20
|
} from './middleware';
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
guineaPigAppBanner,
|
|
26
|
-
welcome,
|
|
27
|
-
STATIC_DIR,
|
|
28
|
-
} from './static';
|
|
29
|
-
import {produceError, produceCrash} from './crash';
|
|
21
|
+
// Import env helper directly — not from the test-pages barrel — so Express handlers and
|
|
22
|
+
// fixture code stay unloaded unless APPIUM_ENABLE_LEGACY_TEST_PAGES is set.
|
|
23
|
+
import {isLegacyTestPagesEnabled} from '../test-pages/env';
|
|
30
24
|
import {
|
|
31
25
|
addWebSocketHandler,
|
|
32
26
|
removeWebSocketHandler,
|
|
@@ -53,10 +47,7 @@ export interface RouteConfiguringFunctionOpts {
|
|
|
53
47
|
}
|
|
54
48
|
|
|
55
49
|
/** A function which configures routes */
|
|
56
|
-
export type RouteConfiguringFunction = (
|
|
57
|
-
app: Express,
|
|
58
|
-
opts?: RouteConfiguringFunctionOpts
|
|
59
|
-
) => void;
|
|
50
|
+
export type RouteConfiguringFunction = (app: Express, opts?: RouteConfiguringFunctionOpts) => void;
|
|
60
51
|
|
|
61
52
|
/** Options for {@linkcode server} */
|
|
62
53
|
export interface ServerOpts {
|
|
@@ -83,6 +74,12 @@ export interface ConfigureServerOpts {
|
|
|
83
74
|
useLegacyUpgradeHandler?: boolean;
|
|
84
75
|
}
|
|
85
76
|
|
|
77
|
+
/** @internal */
|
|
78
|
+
export interface ConfigureServerInternalOpts extends ConfigureServerOpts {
|
|
79
|
+
/** @deprecated Appium 4 */
|
|
80
|
+
registerTestPages?: (app: Express, opts: {basePath: string}) => void;
|
|
81
|
+
}
|
|
82
|
+
|
|
86
83
|
/** Options for {@linkcode configureHttp} */
|
|
87
84
|
export interface ConfigureHttpOpts {
|
|
88
85
|
httpServer: HttpServer;
|
|
@@ -134,6 +131,11 @@ export async function server(opts: ServerOpts): Promise<AppiumServer> {
|
|
|
134
131
|
gracefulShutdownTimeout: cliArgs.shutdownTimeout,
|
|
135
132
|
});
|
|
136
133
|
const useLegacyUpgradeHandler = !hasShouldUpgradeCallback(httpServer);
|
|
134
|
+
let registerTestPages: ConfigureServerInternalOpts['registerTestPages'];
|
|
135
|
+
if (isLegacyTestPagesEnabled()) {
|
|
136
|
+
const require = createRequire(__filename);
|
|
137
|
+
registerTestPages = require('../test-pages').registerTestPages;
|
|
138
|
+
}
|
|
137
139
|
configureServer({
|
|
138
140
|
app,
|
|
139
141
|
addRoutes: routeConfiguringFunction,
|
|
@@ -142,7 +144,8 @@ export async function server(opts: ServerOpts): Promise<AppiumServer> {
|
|
|
142
144
|
extraMethodMap,
|
|
143
145
|
webSocketsMapping: appiumServer.webSocketsMapping,
|
|
144
146
|
useLegacyUpgradeHandler,
|
|
145
|
-
|
|
147
|
+
registerTestPages,
|
|
148
|
+
} as ConfigureServerInternalOpts);
|
|
146
149
|
// allow extensions to update the app and http server objects
|
|
147
150
|
for (const updater of serverUpdaters) {
|
|
148
151
|
await updater(app, appiumServer, cliArgs);
|
|
@@ -174,27 +177,25 @@ export async function server(opts: ServerOpts): Promise<AppiumServer> {
|
|
|
174
177
|
*
|
|
175
178
|
* @param opts - Configuration options
|
|
176
179
|
*/
|
|
177
|
-
export function configureServer({
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
180
|
+
export function configureServer(opts: ConfigureServerOpts): void {
|
|
181
|
+
const {
|
|
182
|
+
app,
|
|
183
|
+
addRoutes,
|
|
184
|
+
allowCors = true,
|
|
185
|
+
basePath: rawBasePath = DEFAULT_BASE_PATH,
|
|
186
|
+
extraMethodMap = {},
|
|
187
|
+
webSocketsMapping = {},
|
|
188
|
+
useLegacyUpgradeHandler = true,
|
|
189
|
+
} = opts;
|
|
190
|
+
const {registerTestPages} = opts as ConfigureServerInternalOpts;
|
|
191
|
+
const basePath = normalizeBasePath(rawBasePath);
|
|
187
192
|
|
|
188
193
|
app.use(endLogFormatter);
|
|
189
194
|
app.use(handleLogContext);
|
|
190
195
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
// crash routes, for testing
|
|
196
|
-
app.use(`${basePath}/produce_error`, produceError);
|
|
197
|
-
app.use(`${basePath}/crash`, produceCrash);
|
|
196
|
+
if (registerTestPages) {
|
|
197
|
+
registerTestPages(app, {basePath});
|
|
198
|
+
}
|
|
198
199
|
|
|
199
200
|
// Only use legacy Express middleware for WebSocket upgrades if shouldUpgradeCallback is not available.
|
|
200
201
|
// When shouldUpgradeCallback is available, upgrades are handled directly on the HTTP server
|
|
@@ -220,12 +221,6 @@ export function configureServer({
|
|
|
220
221
|
app.use(startLogFormatter);
|
|
221
222
|
|
|
222
223
|
addRoutes(app, {basePath, extraMethodMap});
|
|
223
|
-
|
|
224
|
-
// dynamic routes for testing, etc.
|
|
225
|
-
app.all('/welcome', welcome);
|
|
226
|
-
app.all('/test/guinea-pig', guineaPig);
|
|
227
|
-
app.all('/test/guinea-pig-scrollable', guineaPigScrollable);
|
|
228
|
-
app.all('/test/guinea-pig-app-banner', guineaPigAppBanner);
|
|
229
224
|
}
|
|
230
225
|
|
|
231
226
|
/**
|
|
@@ -251,18 +246,13 @@ export function normalizeBasePath(basePath: string): string {
|
|
|
251
246
|
return basePath;
|
|
252
247
|
}
|
|
253
248
|
|
|
254
|
-
async function createServer(
|
|
255
|
-
app: Express,
|
|
256
|
-
cliArgs?: Partial<ServerArgs>
|
|
257
|
-
): Promise<HttpServer> {
|
|
249
|
+
async function createServer(app: Express, cliArgs?: Partial<ServerArgs>): Promise<HttpServer> {
|
|
258
250
|
const {sslCertificatePath, sslKeyPath} = cliArgs ?? {};
|
|
259
251
|
if (!sslCertificatePath && !sslKeyPath) {
|
|
260
252
|
return http.createServer(app);
|
|
261
253
|
}
|
|
262
254
|
if (!sslCertificatePath || !sslKeyPath) {
|
|
263
|
-
throw new Error(
|
|
264
|
-
`Both certificate path and key path must be provided to enable TLS`
|
|
265
|
-
);
|
|
255
|
+
throw new Error(`Both certificate path and key path must be provided to enable TLS`);
|
|
266
256
|
}
|
|
267
257
|
|
|
268
258
|
const certKey = [sslCertificatePath, sslKeyPath];
|
|
@@ -272,20 +262,19 @@ async function createServer(
|
|
|
272
262
|
[keyExists, 'key', sslKeyPath],
|
|
273
263
|
]) {
|
|
274
264
|
if (!exists) {
|
|
275
|
-
throw new Error(
|
|
276
|
-
`The provided SSL ${desc} at '${p}' does not exist or is not accessible`
|
|
277
|
-
);
|
|
265
|
+
throw new Error(`The provided SSL ${desc} at '${p}' does not exist or is not accessible`);
|
|
278
266
|
}
|
|
279
267
|
}
|
|
280
|
-
const [cert, key] = await Promise.all(
|
|
281
|
-
|
|
282
|
-
|
|
268
|
+
const [cert, key] = (await Promise.all(certKey.map((p) => fs.readFile(p, 'utf8')))) as [
|
|
269
|
+
string,
|
|
270
|
+
string,
|
|
271
|
+
];
|
|
283
272
|
log.debug('Enabling TLS/SPDY on the server using the provided certificate');
|
|
284
273
|
|
|
285
274
|
const spdy = require('spdy') as {
|
|
286
275
|
createServer: (
|
|
287
276
|
options: {cert: string; key: string; spdy: {plain: boolean; ssl: boolean}},
|
|
288
|
-
requestListener: RequestHandler
|
|
277
|
+
requestListener: RequestHandler,
|
|
289
278
|
) => HttpServer;
|
|
290
279
|
};
|
|
291
280
|
return spdy.createServer(
|
|
@@ -297,7 +286,7 @@ async function createServer(
|
|
|
297
286
|
ssl: true,
|
|
298
287
|
},
|
|
299
288
|
},
|
|
300
|
-
app
|
|
289
|
+
app,
|
|
301
290
|
);
|
|
302
291
|
}
|
|
303
292
|
|
|
@@ -329,7 +318,9 @@ function configureHttp({
|
|
|
329
318
|
// See: https://github.com/nodejs/node/pull/59824
|
|
330
319
|
if (hasShouldUpgradeCallback(httpServer)) {
|
|
331
320
|
// shouldUpgradeCallback only returns a boolean to indicate if the upgrade should proceed
|
|
332
|
-
(
|
|
321
|
+
(
|
|
322
|
+
appiumServer as unknown as {shouldUpgradeCallback?: (req: http.IncomingMessage) => boolean}
|
|
323
|
+
).shouldUpgradeCallback = (req) =>
|
|
333
324
|
String(req.headers?.upgrade ?? '').toLowerCase() === 'websocket';
|
|
334
325
|
appiumServer.on('upgrade', (req, socket, head) => {
|
|
335
326
|
if (!tryHandleWebSocketUpgrade(req, socket, head, appiumServer.webSocketsMapping)) {
|
|
@@ -340,7 +331,9 @@ function configureHttp({
|
|
|
340
331
|
|
|
341
332
|
// http.Server.close() only stops new connections, but we need to wait until
|
|
342
333
|
// all connections are closed and the `close` event is emitted
|
|
343
|
-
const originalClose = appiumServer.close.bind(appiumServer)
|
|
334
|
+
const originalClose = appiumServer.close.bind(appiumServer) as (
|
|
335
|
+
callback?: (err?: Error | null) => void,
|
|
336
|
+
) => void;
|
|
344
337
|
appiumServer.close = async () =>
|
|
345
338
|
await new Promise<void>((_resolve, _reject) => {
|
|
346
339
|
log.info('Closing Appium HTTP server');
|
|
@@ -350,7 +343,7 @@ function configureHttp({
|
|
|
350
343
|
log.info(
|
|
351
344
|
`Not all active connections have been closed within ${gracefulShutdownTimeout}ms. ` +
|
|
352
345
|
`This timeout might be customized by the --shutdown-timeout command line ` +
|
|
353
|
-
`argument. Closing the server anyway
|
|
346
|
+
`argument. Closing the server anyway.`,
|
|
354
347
|
);
|
|
355
348
|
}
|
|
356
349
|
process.exit(process.exitCode ?? 0);
|
|
@@ -358,13 +351,13 @@ function configureHttp({
|
|
|
358
351
|
const onClose = () => {
|
|
359
352
|
log.info(
|
|
360
353
|
`Appium HTTP server has been successfully closed after ` +
|
|
361
|
-
`${timer.getDuration().asMilliSeconds.toFixed(0)}ms
|
|
354
|
+
`${timer.getDuration().asMilliSeconds.toFixed(0)}ms`,
|
|
362
355
|
);
|
|
363
356
|
clearTimeout(onTimeout);
|
|
364
357
|
_resolve();
|
|
365
358
|
};
|
|
366
359
|
httpServer.once('close', onClose);
|
|
367
|
-
originalClose((err?: Error) => {
|
|
360
|
+
originalClose((err?: Error | null) => {
|
|
368
361
|
if (err) {
|
|
369
362
|
clearTimeout(onTimeout);
|
|
370
363
|
httpServer.removeListener('close', onClose);
|
|
@@ -376,14 +369,13 @@ function configureHttp({
|
|
|
376
369
|
appiumServer.once('error', (err: NodeJS.ErrnoException) => {
|
|
377
370
|
if (err.code === 'EADDRNOTAVAIL') {
|
|
378
371
|
log.error(
|
|
379
|
-
'Could not start REST http interface listener. ' +
|
|
380
|
-
'Requested address is not available.'
|
|
372
|
+
'Could not start REST http interface listener. ' + 'Requested address is not available.',
|
|
381
373
|
);
|
|
382
374
|
} else {
|
|
383
375
|
log.error(
|
|
384
376
|
'Could not start REST http interface listener. The requested ' +
|
|
385
377
|
'port may already be in use. Please make sure there is no ' +
|
|
386
|
-
'other instance of this server running already.'
|
|
378
|
+
'other instance of this server running already.',
|
|
387
379
|
);
|
|
388
380
|
}
|
|
389
381
|
reject(err);
|
package/lib/express/websocket.ts
CHANGED
|
@@ -10,7 +10,7 @@ export const DEFAULT_WS_PATHNAME_PREFIX = '/ws';
|
|
|
10
10
|
export async function addWebSocketHandler(
|
|
11
11
|
this: AppiumServer,
|
|
12
12
|
handlerPathname: string,
|
|
13
|
-
handlerServer: WSServer
|
|
13
|
+
handlerServer: WSServer,
|
|
14
14
|
): Promise<void> {
|
|
15
15
|
this.webSocketsMapping[handlerPathname] = handlerServer;
|
|
16
16
|
}
|
|
@@ -21,7 +21,7 @@ export async function addWebSocketHandler(
|
|
|
21
21
|
*/
|
|
22
22
|
export async function getWebSocketHandlers(
|
|
23
23
|
this: AppiumServer,
|
|
24
|
-
keysFilter: string | null = null
|
|
24
|
+
keysFilter: string | null = null,
|
|
25
25
|
): Promise<Record<string, WSServer>> {
|
|
26
26
|
return Object.entries(this.webSocketsMapping).reduce<Record<string, WSServer>>(
|
|
27
27
|
(acc, [pathname, wsServer]) => {
|
|
@@ -30,7 +30,7 @@ export async function getWebSocketHandlers(
|
|
|
30
30
|
}
|
|
31
31
|
return acc;
|
|
32
32
|
},
|
|
33
|
-
{}
|
|
33
|
+
{},
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -40,7 +40,7 @@ export async function getWebSocketHandlers(
|
|
|
40
40
|
*/
|
|
41
41
|
export async function removeWebSocketHandler(
|
|
42
42
|
this: AppiumServer,
|
|
43
|
-
handlerPathname: string
|
|
43
|
+
handlerPathname: string,
|
|
44
44
|
): Promise<boolean> {
|
|
45
45
|
const wsServer = this.webSocketsMapping?.[handlerPathname];
|
|
46
46
|
if (!wsServer) {
|
|
@@ -71,9 +71,7 @@ export async function removeAllWebSocketHandlers(this: AppiumServer): Promise<bo
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
const results = await Promise.all(
|
|
74
|
-
Object.keys(this.webSocketsMapping).map((pathname) =>
|
|
75
|
-
this.removeWebSocketHandler(pathname)
|
|
76
|
-
)
|
|
74
|
+
Object.keys(this.webSocketsMapping).map((pathname) => this.removeWebSocketHandler(pathname)),
|
|
77
75
|
);
|
|
78
76
|
return results.some(Boolean);
|
|
79
77
|
}
|