@appium/base-driver 10.2.0 → 10.2.2
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/LICENSE +201 -0
- package/build/lib/basedriver/capabilities.js +7 -7
- package/build/lib/basedriver/capabilities.js.map +1 -1
- package/build/lib/basedriver/commands/event.d.ts +1 -1
- package/build/lib/basedriver/commands/event.d.ts.map +1 -1
- package/build/lib/basedriver/commands/execute.d.ts +1 -1
- package/build/lib/basedriver/commands/execute.d.ts.map +1 -1
- package/build/lib/basedriver/commands/find.d.ts +1 -1
- package/build/lib/basedriver/commands/find.d.ts.map +1 -1
- package/build/lib/basedriver/commands/mixin.d.ts +1 -1
- package/build/lib/basedriver/commands/mixin.d.ts.map +1 -1
- package/build/lib/basedriver/commands/timeout.d.ts +1 -1
- package/build/lib/basedriver/commands/timeout.d.ts.map +1 -1
- package/build/lib/basedriver/device-settings.d.ts +14 -23
- package/build/lib/basedriver/device-settings.d.ts.map +1 -1
- package/build/lib/basedriver/device-settings.js +11 -26
- package/build/lib/basedriver/device-settings.js.map +1 -1
- package/build/lib/basedriver/helpers.d.ts +36 -57
- package/build/lib/basedriver/helpers.d.ts.map +1 -1
- package/build/lib/basedriver/helpers.js +148 -239
- package/build/lib/basedriver/helpers.js.map +1 -1
- package/build/lib/basedriver/logger.d.ts +1 -2
- package/build/lib/basedriver/logger.d.ts.map +1 -1
- package/build/lib/basedriver/logger.js +2 -2
- package/build/lib/basedriver/logger.js.map +1 -1
- package/build/lib/basedriver/validation.d.ts.map +1 -1
- package/build/lib/basedriver/validation.js +3 -3
- package/build/lib/basedriver/validation.js.map +1 -1
- package/build/lib/constants.d.ts +1 -1
- package/build/lib/constants.d.ts.map +1 -1
- package/build/lib/express/crash.d.ts +8 -2
- package/build/lib/express/crash.d.ts.map +1 -1
- package/build/lib/express/crash.js +6 -0
- package/build/lib/express/crash.js.map +1 -1
- package/build/lib/express/express-logging.d.ts +12 -2
- package/build/lib/express/express-logging.d.ts.map +1 -1
- package/build/lib/express/express-logging.js +34 -26
- package/build/lib/express/express-logging.js.map +1 -1
- package/build/lib/express/idempotency.d.ts +4 -10
- package/build/lib/express/idempotency.d.ts.map +1 -1
- package/build/lib/express/idempotency.js +69 -73
- package/build/lib/express/idempotency.js.map +1 -1
- package/build/lib/express/logger.d.ts +1 -2
- package/build/lib/express/logger.d.ts.map +1 -1
- package/build/lib/express/logger.js +2 -2
- package/build/lib/express/logger.js.map +1 -1
- package/build/lib/express/middleware.d.ts +37 -41
- package/build/lib/express/middleware.d.ts.map +1 -1
- package/build/lib/express/middleware.js +48 -60
- package/build/lib/express/middleware.js.map +1 -1
- package/build/lib/express/server.d.ts +57 -101
- package/build/lib/express/server.d.ts.map +1 -1
- package/build/lib/express/server.js +51 -128
- package/build/lib/express/server.js.map +1 -1
- package/build/lib/express/static.d.ts +10 -5
- package/build/lib/express/static.d.ts.map +1 -1
- package/build/lib/express/static.js +32 -42
- package/build/lib/express/static.js.map +1 -1
- package/build/lib/express/websocket.d.ts +22 -6
- package/build/lib/express/websocket.d.ts.map +1 -1
- package/build/lib/express/websocket.js +10 -15
- package/build/lib/express/websocket.js.map +1 -1
- package/build/lib/helpers/capabilities.d.ts +4 -16
- package/build/lib/helpers/capabilities.d.ts.map +1 -1
- package/build/lib/helpers/capabilities.js +36 -48
- package/build/lib/helpers/capabilities.js.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.d.ts +42 -78
- package/build/lib/jsonwp-proxy/protocol-converter.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.js +87 -139
- package/build/lib/jsonwp-proxy/protocol-converter.js.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.d.ts +1 -1
- package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.js +2 -2
- package/build/lib/jsonwp-proxy/proxy.js.map +1 -1
- package/build/lib/jsonwp-status/status.d.ts +113 -158
- package/build/lib/jsonwp-status/status.d.ts.map +1 -1
- package/build/lib/jsonwp-status/status.js +10 -14
- package/build/lib/jsonwp-status/status.js.map +1 -1
- package/build/lib/protocol/bidi-commands.d.ts +31 -36
- package/build/lib/protocol/bidi-commands.d.ts.map +1 -1
- package/build/lib/protocol/bidi-commands.js +5 -5
- package/build/lib/protocol/bidi-commands.js.map +1 -1
- package/build/lib/protocol/errors.d.ts.map +1 -1
- package/build/lib/protocol/helpers.d.ts +7 -11
- package/build/lib/protocol/helpers.d.ts.map +1 -1
- package/build/lib/protocol/helpers.js +5 -9
- package/build/lib/protocol/helpers.js.map +1 -1
- package/build/lib/protocol/index.d.ts +4 -21
- package/build/lib/protocol/index.d.ts.map +1 -1
- package/build/lib/protocol/index.js.map +1 -1
- package/build/lib/protocol/protocol.d.ts +15 -1
- package/build/lib/protocol/protocol.d.ts.map +1 -1
- package/build/lib/protocol/protocol.js +50 -20
- package/build/lib/protocol/protocol.js.map +1 -1
- package/build/lib/protocol/routes.d.ts +8 -15
- package/build/lib/protocol/routes.d.ts.map +1 -1
- package/build/lib/protocol/routes.js +18 -33
- package/build/lib/protocol/routes.js.map +1 -1
- package/lib/basedriver/capabilities.ts +1 -1
- package/lib/basedriver/commands/event.ts +2 -2
- package/lib/basedriver/commands/execute.ts +2 -2
- package/lib/basedriver/commands/find.ts +2 -2
- package/lib/basedriver/commands/mixin.ts +1 -1
- package/lib/basedriver/commands/timeout.ts +2 -2
- package/lib/basedriver/{device-settings.js → device-settings.ts} +24 -35
- package/lib/basedriver/{helpers.js → helpers.ts} +208 -266
- package/lib/basedriver/logger.ts +3 -0
- package/lib/basedriver/validation.ts +2 -2
- package/lib/constants.ts +1 -1
- package/lib/express/crash.ts +15 -0
- package/lib/express/express-logging.ts +84 -0
- package/lib/express/{idempotency.js → idempotency.ts} +105 -89
- package/lib/express/logger.ts +3 -0
- package/lib/express/middleware.ts +187 -0
- package/lib/express/{server.js → server.ts} +175 -167
- package/lib/express/static.ts +77 -0
- package/lib/express/websocket.ts +81 -0
- package/lib/helpers/capabilities.ts +83 -0
- package/lib/jsonwp-proxy/protocol-converter.ts +284 -0
- package/lib/jsonwp-proxy/proxy.js +1 -1
- package/lib/jsonwp-status/{status.js → status.ts} +12 -15
- package/lib/protocol/{bidi-commands.js → bidi-commands.ts} +7 -5
- package/lib/protocol/errors.ts +1 -1
- package/lib/protocol/{helpers.js → helpers.ts} +8 -11
- package/lib/protocol/protocol.ts +57 -26
- package/lib/protocol/{routes.js → routes.ts} +29 -40
- package/package.json +11 -11
- package/tsconfig.json +3 -1
- package/lib/basedriver/logger.js +0 -4
- package/lib/express/crash.js +0 -11
- package/lib/express/express-logging.js +0 -60
- package/lib/express/logger.js +0 -4
- package/lib/express/middleware.js +0 -171
- package/lib/express/static.js +0 -76
- package/lib/express/websocket.js +0 -79
- package/lib/helpers/capabilities.js +0 -93
- package/lib/jsonwp-proxy/protocol-converter.js +0 -317
- /package/lib/protocol/{index.js → index.ts} +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import express from 'express';
|
|
4
|
+
import type {Express, RequestHandler} from 'express';
|
|
4
5
|
import http from 'node:http';
|
|
6
|
+
import type {Server as HttpServer} from 'node:http';
|
|
5
7
|
import favicon from 'serve-favicon';
|
|
6
8
|
import bodyParser from 'body-parser';
|
|
7
9
|
import methodOverride from 'method-override';
|
|
8
|
-
import log from './logger';
|
|
10
|
+
import {log} from './logger';
|
|
9
11
|
import {startLogFormatter, endLogFormatter} from './express-logging';
|
|
10
12
|
import {
|
|
11
13
|
allowCrossDomain,
|
|
@@ -18,7 +20,13 @@ import {
|
|
|
18
20
|
catch404Handler,
|
|
19
21
|
handleLogContext,
|
|
20
22
|
} from './middleware';
|
|
21
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
guineaPig,
|
|
25
|
+
guineaPigScrollable,
|
|
26
|
+
guineaPigAppBanner,
|
|
27
|
+
welcome,
|
|
28
|
+
STATIC_DIR,
|
|
29
|
+
} from './static';
|
|
22
30
|
import {produceError, produceCrash} from './crash';
|
|
23
31
|
import {
|
|
24
32
|
addWebSocketHandler,
|
|
@@ -29,20 +37,81 @@ import {
|
|
|
29
37
|
import B from 'bluebird';
|
|
30
38
|
import {DEFAULT_BASE_PATH} from '../constants';
|
|
31
39
|
import {fs, timing} from '@appium/support';
|
|
40
|
+
import type {
|
|
41
|
+
AppiumServer,
|
|
42
|
+
ServerArgs,
|
|
43
|
+
UpdateServerCallback,
|
|
44
|
+
MethodMap,
|
|
45
|
+
ExternalDriver,
|
|
46
|
+
StringRecord,
|
|
47
|
+
} from '@appium/types';
|
|
32
48
|
|
|
33
49
|
const KEEP_ALIVE_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
34
50
|
|
|
51
|
+
/** Options for {@linkcode RouteConfiguringFunction} */
|
|
52
|
+
export interface RouteConfiguringFunctionOpts {
|
|
53
|
+
basePath?: string;
|
|
54
|
+
extraMethodMap?: MethodMap<ExternalDriver>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A function which configures routes */
|
|
58
|
+
export type RouteConfiguringFunction = (
|
|
59
|
+
app: Express,
|
|
60
|
+
opts?: RouteConfiguringFunctionOpts
|
|
61
|
+
) => void;
|
|
62
|
+
|
|
63
|
+
/** Options for {@linkcode server} */
|
|
64
|
+
export interface ServerOpts {
|
|
65
|
+
routeConfiguringFunction: RouteConfiguringFunction;
|
|
66
|
+
port: number;
|
|
67
|
+
cliArgs?: Partial<ServerArgs>;
|
|
68
|
+
hostname?: string;
|
|
69
|
+
allowCors?: boolean;
|
|
70
|
+
basePath?: string;
|
|
71
|
+
extraMethodMap?: MethodMap<ExternalDriver>;
|
|
72
|
+
serverUpdaters?: UpdateServerCallback[];
|
|
73
|
+
keepAliveTimeout?: number;
|
|
74
|
+
requestTimeout?: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Options for {@linkcode configureServer} */
|
|
78
|
+
export interface ConfigureServerOpts {
|
|
79
|
+
app: Express;
|
|
80
|
+
addRoutes: RouteConfiguringFunction;
|
|
81
|
+
allowCors?: boolean;
|
|
82
|
+
basePath?: string;
|
|
83
|
+
extraMethodMap?: MethodMap<ExternalDriver>;
|
|
84
|
+
webSocketsMapping?: StringRecord;
|
|
85
|
+
useLegacyUpgradeHandler?: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Options for {@linkcode configureHttp} */
|
|
89
|
+
export interface ConfigureHttpOpts {
|
|
90
|
+
httpServer: HttpServer;
|
|
91
|
+
reject: (error?: unknown) => void;
|
|
92
|
+
keepAliveTimeout: number;
|
|
93
|
+
gracefulShutdownTimeout?: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Options for {@linkcode startServer} */
|
|
97
|
+
export interface StartServerOpts {
|
|
98
|
+
httpServer: HttpServer;
|
|
99
|
+
port: number;
|
|
100
|
+
hostname?: string;
|
|
101
|
+
keepAliveTimeout: number;
|
|
102
|
+
requestTimeout?: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
35
105
|
/**
|
|
36
|
-
*
|
|
37
|
-
* @
|
|
38
|
-
* @returns {Promise<AppiumServer>}
|
|
106
|
+
* @param opts - Server options
|
|
107
|
+
* @returns Promise resolving to the Appium server instance
|
|
39
108
|
*/
|
|
40
|
-
export async function server(opts) {
|
|
109
|
+
export async function server(opts: ServerOpts): Promise<AppiumServer> {
|
|
41
110
|
const {
|
|
42
111
|
routeConfiguringFunction,
|
|
43
112
|
port,
|
|
44
113
|
hostname,
|
|
45
|
-
cliArgs =
|
|
114
|
+
cliArgs = {},
|
|
46
115
|
allowCors = true,
|
|
47
116
|
basePath = DEFAULT_BASE_PATH,
|
|
48
117
|
extraMethodMap = {},
|
|
@@ -54,7 +123,7 @@ export async function server(opts) {
|
|
|
54
123
|
const app = express();
|
|
55
124
|
const httpServer = await createServer(app, cliArgs);
|
|
56
125
|
|
|
57
|
-
return await new B(async (resolve, reject) => {
|
|
126
|
+
return await new B<AppiumServer>(async (resolve, reject) => {
|
|
58
127
|
// we put an async function as the promise constructor because we want some things to happen in
|
|
59
128
|
// serial (application of plugin updates, for example). But we still need to use a promise here
|
|
60
129
|
// because some elements of server start failure only happen in httpServer listeners. So the
|
|
@@ -103,8 +172,9 @@ export async function server(opts) {
|
|
|
103
172
|
}
|
|
104
173
|
|
|
105
174
|
/**
|
|
106
|
-
* Sets up
|
|
107
|
-
*
|
|
175
|
+
* Sets up Express middleware and routes.
|
|
176
|
+
*
|
|
177
|
+
* @param opts - Configuration options
|
|
108
178
|
*/
|
|
109
179
|
export function configureServer({
|
|
110
180
|
app,
|
|
@@ -114,7 +184,7 @@ export function configureServer({
|
|
|
114
184
|
extraMethodMap = {},
|
|
115
185
|
webSocketsMapping = {},
|
|
116
186
|
useLegacyUpgradeHandler = true,
|
|
117
|
-
}) {
|
|
187
|
+
}: ConfigureServerOpts): void {
|
|
118
188
|
basePath = normalizeBasePath(basePath);
|
|
119
189
|
|
|
120
190
|
app.use(endLogFormatter);
|
|
@@ -128,7 +198,7 @@ export function configureServer({
|
|
|
128
198
|
app.use(`${basePath}/produce_error`, produceError);
|
|
129
199
|
app.use(`${basePath}/crash`, produceCrash);
|
|
130
200
|
|
|
131
|
-
// Only use legacy Express middleware for WebSocket upgrades if shouldUpgradeCallback is not available
|
|
201
|
+
// Only use legacy Express middleware for WebSocket upgrades if shouldUpgradeCallback is not available.
|
|
132
202
|
// When shouldUpgradeCallback is available, upgrades are handled directly on the HTTP server
|
|
133
203
|
// to avoid Express middleware timeout issues with long-lived connections
|
|
134
204
|
if (useLegacyUpgradeHandler) {
|
|
@@ -161,17 +231,17 @@ export function configureServer({
|
|
|
161
231
|
}
|
|
162
232
|
|
|
163
233
|
/**
|
|
164
|
-
* Normalize base path string
|
|
165
|
-
*
|
|
166
|
-
* @
|
|
234
|
+
* Normalize base path string (leading slash, no trailing slash).
|
|
235
|
+
*
|
|
236
|
+
* @param basePath - Raw base path
|
|
237
|
+
* @returns Normalized base path
|
|
167
238
|
*/
|
|
168
|
-
export function normalizeBasePath(basePath) {
|
|
239
|
+
export function normalizeBasePath(basePath: string): string {
|
|
169
240
|
if (!_.isString(basePath)) {
|
|
170
241
|
throw new Error(`Invalid path prefix ${basePath}`);
|
|
171
242
|
}
|
|
172
243
|
|
|
173
|
-
// ensure the path prefix does not end in '/', since our method map
|
|
174
|
-
// starts all paths with '/'
|
|
244
|
+
// ensure the path prefix does not end in '/', since our method map starts all paths with '/'
|
|
175
245
|
basePath = basePath.replace(/\/$/, '');
|
|
176
246
|
|
|
177
247
|
// likewise, ensure the path prefix does always START with /, unless the path
|
|
@@ -183,64 +253,78 @@ export function normalizeBasePath(basePath) {
|
|
|
183
253
|
return basePath;
|
|
184
254
|
}
|
|
185
255
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
* @returns {Promise<http.Server>}
|
|
191
|
-
*/
|
|
192
|
-
async function createServer (app, cliArgs) {
|
|
256
|
+
async function createServer(
|
|
257
|
+
app: Express,
|
|
258
|
+
cliArgs?: Partial<ServerArgs>
|
|
259
|
+
): Promise<HttpServer> {
|
|
193
260
|
const {sslCertificatePath, sslKeyPath} = cliArgs ?? {};
|
|
194
261
|
if (!sslCertificatePath && !sslKeyPath) {
|
|
195
262
|
return http.createServer(app);
|
|
196
263
|
}
|
|
197
264
|
if (!sslCertificatePath || !sslKeyPath) {
|
|
198
|
-
throw new Error(
|
|
265
|
+
throw new Error(
|
|
266
|
+
`Both certificate path and key path must be provided to enable TLS`
|
|
267
|
+
);
|
|
199
268
|
}
|
|
200
269
|
|
|
201
270
|
const certKey = [sslCertificatePath, sslKeyPath];
|
|
202
271
|
const zipped = _.zip(
|
|
203
272
|
await B.all(certKey.map((p) => fs.exists(p))),
|
|
204
273
|
['certificate', 'key'],
|
|
205
|
-
certKey
|
|
206
|
-
);
|
|
274
|
+
certKey
|
|
275
|
+
) as [boolean, string, string][];
|
|
207
276
|
for (const [exists, desc, p] of zipped) {
|
|
208
277
|
if (!exists) {
|
|
209
|
-
throw new Error(
|
|
278
|
+
throw new Error(
|
|
279
|
+
`The provided SSL ${desc} at '${p}' does not exist or is not accessible`
|
|
280
|
+
);
|
|
210
281
|
}
|
|
211
282
|
}
|
|
212
|
-
const [cert, key] = await B.all(
|
|
283
|
+
const [cert, key] = await B.all(
|
|
284
|
+
certKey.map((p) => fs.readFile(p, 'utf8'))
|
|
285
|
+
) as [string, string];
|
|
213
286
|
log.debug('Enabling TLS/SPDY on the server using the provided certificate');
|
|
214
287
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
288
|
+
const spdy = require('spdy') as {
|
|
289
|
+
createServer: (
|
|
290
|
+
options: {cert: string; key: string; spdy: {plain: boolean; ssl: boolean}},
|
|
291
|
+
requestListener: RequestHandler
|
|
292
|
+
) => HttpServer;
|
|
293
|
+
};
|
|
294
|
+
return spdy.createServer(
|
|
295
|
+
{
|
|
296
|
+
cert,
|
|
297
|
+
key,
|
|
298
|
+
spdy: {
|
|
299
|
+
plain: false,
|
|
300
|
+
ssl: true,
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
app
|
|
304
|
+
);
|
|
223
305
|
}
|
|
224
306
|
|
|
225
307
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* @
|
|
308
|
+
* Attaches Appium-specific behavior to the HTTP server and returns it as {@linkcode AppiumServer}.
|
|
309
|
+
* Mutates the `httpServer` parameter.
|
|
310
|
+
*
|
|
311
|
+
* @param opts - Configuration options
|
|
312
|
+
* @returns The same server instance typed as AppiumServer
|
|
230
313
|
*/
|
|
231
|
-
function configureHttp({
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
314
|
+
function configureHttp({
|
|
315
|
+
httpServer,
|
|
316
|
+
reject,
|
|
317
|
+
keepAliveTimeout,
|
|
318
|
+
gracefulShutdownTimeout,
|
|
319
|
+
}: ConfigureHttpOpts): AppiumServer {
|
|
320
|
+
const appiumServer = httpServer as unknown as AppiumServer;
|
|
236
321
|
appiumServer.webSocketsMapping = {};
|
|
237
322
|
appiumServer.addWebSocketHandler = addWebSocketHandler;
|
|
238
323
|
appiumServer.removeWebSocketHandler = removeWebSocketHandler;
|
|
239
324
|
appiumServer.removeAllWebSocketHandlers = removeAllWebSocketHandlers;
|
|
240
325
|
appiumServer.getWebSocketHandlers = getWebSocketHandlers;
|
|
241
326
|
appiumServer.isSecure = function isSecure() {
|
|
242
|
-
|
|
243
|
-
return Boolean(this['_spdyState']?.secure);
|
|
327
|
+
return Boolean((this as unknown as {_spdyState?: {secure?: boolean}})._spdyState?.secure);
|
|
244
328
|
};
|
|
245
329
|
|
|
246
330
|
// This avoids Express middleware timeout issues with long-lived WebSocket connections
|
|
@@ -248,8 +332,8 @@ function configureHttp({httpServer, reject, keepAliveTimeout, gracefulShutdownTi
|
|
|
248
332
|
// See: https://github.com/nodejs/node/pull/59824
|
|
249
333
|
if (hasShouldUpgradeCallback(httpServer)) {
|
|
250
334
|
// shouldUpgradeCallback only returns a boolean to indicate if the upgrade should proceed
|
|
251
|
-
|
|
252
|
-
|
|
335
|
+
(appiumServer as unknown as {shouldUpgradeCallback?: (req: http.IncomingMessage) => boolean}).shouldUpgradeCallback = (req) =>
|
|
336
|
+
_.toLower(req.headers?.upgrade) === 'websocket';
|
|
253
337
|
appiumServer.on('upgrade', (req, socket, head) => {
|
|
254
338
|
if (!tryHandleWebSocketUpgrade(req, socket, head, appiumServer.webSocketsMapping)) {
|
|
255
339
|
socket.destroy();
|
|
@@ -261,73 +345,69 @@ function configureHttp({httpServer, reject, keepAliveTimeout, gracefulShutdownTi
|
|
|
261
345
|
// all connections are closed and the `close` event is emitted
|
|
262
346
|
const originalClose = appiumServer.close.bind(appiumServer);
|
|
263
347
|
appiumServer.close = async () =>
|
|
264
|
-
await new B((_resolve, _reject) => {
|
|
348
|
+
await new B<void>((_resolve, _reject) => {
|
|
265
349
|
log.info('Closing Appium HTTP server');
|
|
266
350
|
const timer = new timing.Timer().start();
|
|
267
351
|
const onTimeout = setTimeout(() => {
|
|
268
|
-
if (gracefulShutdownTimeout > 0) {
|
|
352
|
+
if ((gracefulShutdownTimeout ?? 0) > 0) {
|
|
269
353
|
log.info(
|
|
270
354
|
`Not all active connections have been closed within ${gracefulShutdownTimeout}ms. ` +
|
|
271
|
-
|
|
272
|
-
|
|
355
|
+
`This timeout might be customized by the --shutdown-timeout command line ` +
|
|
356
|
+
`argument. Closing the server anyway.`
|
|
273
357
|
);
|
|
274
358
|
}
|
|
275
359
|
process.exit(process.exitCode ?? 0);
|
|
276
|
-
}, gracefulShutdownTimeout);
|
|
277
|
-
|
|
360
|
+
}, gracefulShutdownTimeout ?? 0);
|
|
361
|
+
const onClose = () => {
|
|
278
362
|
log.info(
|
|
279
363
|
`Appium HTTP server has been successfully closed after ` +
|
|
280
|
-
|
|
364
|
+
`${timer.getDuration().asMilliSeconds.toFixed(0)}ms`
|
|
281
365
|
);
|
|
282
366
|
clearTimeout(onTimeout);
|
|
283
367
|
_resolve();
|
|
284
|
-
}
|
|
285
|
-
|
|
368
|
+
};
|
|
369
|
+
httpServer.once('close', onClose);
|
|
370
|
+
originalClose((err?: Error) => {
|
|
286
371
|
if (err) {
|
|
372
|
+
clearTimeout(onTimeout);
|
|
373
|
+
httpServer.removeListener('close', onClose);
|
|
287
374
|
_reject(err);
|
|
288
375
|
}
|
|
289
376
|
});
|
|
290
377
|
});
|
|
291
378
|
|
|
292
|
-
appiumServer.once(
|
|
293
|
-
'
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
'
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
reject(err);
|
|
379
|
+
appiumServer.once('error', (err: NodeJS.ErrnoException) => {
|
|
380
|
+
if (err.code === 'EADDRNOTAVAIL') {
|
|
381
|
+
log.error(
|
|
382
|
+
'Could not start REST http interface listener. ' +
|
|
383
|
+
'Requested address is not available.'
|
|
384
|
+
);
|
|
385
|
+
} else {
|
|
386
|
+
log.error(
|
|
387
|
+
'Could not start REST http interface listener. The requested ' +
|
|
388
|
+
'port may already be in use. Please make sure there is no ' +
|
|
389
|
+
'other instance of this server running already.'
|
|
390
|
+
);
|
|
307
391
|
}
|
|
308
|
-
|
|
392
|
+
reject(err);
|
|
393
|
+
});
|
|
309
394
|
|
|
310
395
|
appiumServer.on('connection', (socket) => socket.setTimeout(keepAliveTimeout));
|
|
311
396
|
|
|
312
397
|
return appiumServer;
|
|
313
398
|
}
|
|
314
399
|
|
|
315
|
-
/**
|
|
316
|
-
* Starts an {@linkcode AppiumServer}
|
|
317
|
-
* @param {StartServerOpts} opts
|
|
318
|
-
* @returns {Promise<void>}
|
|
319
|
-
*/
|
|
320
400
|
async function startServer({
|
|
321
401
|
httpServer,
|
|
322
402
|
port,
|
|
323
403
|
hostname,
|
|
324
404
|
keepAliveTimeout,
|
|
325
405
|
requestTimeout,
|
|
326
|
-
}) {
|
|
327
|
-
// If the hostname is omitted, the server will accept
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
406
|
+
}: StartServerOpts): Promise<void> {
|
|
407
|
+
// If the hostname is omitted, the server will accept connections on any IP address
|
|
408
|
+
const start = B.promisify(httpServer.listen, {
|
|
409
|
+
context: httpServer,
|
|
410
|
+
}) as (port: number, hostname?: string) => B<HttpServer>;
|
|
331
411
|
const startPromise = start(port, hostname);
|
|
332
412
|
httpServer.keepAliveTimeout = keepAliveTimeout;
|
|
333
413
|
if (_.isInteger(requestTimeout)) {
|
|
@@ -339,92 +419,20 @@ async function startServer({
|
|
|
339
419
|
}
|
|
340
420
|
|
|
341
421
|
/**
|
|
342
|
-
* Checks if the
|
|
343
|
-
*
|
|
344
|
-
* @param
|
|
345
|
-
* @returns
|
|
422
|
+
* Checks if the server supports `shouldUpgradeCallback` (Node.js v22.21.0+ / v24.9.0+).
|
|
423
|
+
*
|
|
424
|
+
* @param server - The HTTP server instance
|
|
425
|
+
* @returns true if shouldUpgradeCallback is available
|
|
346
426
|
*/
|
|
347
|
-
function hasShouldUpgradeCallback(server) {
|
|
427
|
+
function hasShouldUpgradeCallback(server: HttpServer): boolean {
|
|
348
428
|
// Check if shouldUpgradeCallback is available on http.Server
|
|
349
429
|
// This is a runtime check that works regardless of TypeScript types
|
|
350
430
|
try {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
431
|
+
return (
|
|
432
|
+
typeof (server as unknown as {shouldUpgradeCallback?: unknown}).shouldUpgradeCallback !==
|
|
433
|
+
'undefined'
|
|
434
|
+
);
|
|
354
435
|
} catch {
|
|
355
436
|
return false;
|
|
356
437
|
}
|
|
357
438
|
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Options for {@linkcode startServer}.
|
|
361
|
-
* @typedef StartServerOpts
|
|
362
|
-
* @property {import('http').Server} httpServer - HTTP server instance
|
|
363
|
-
* @property {number} port - Port to run on
|
|
364
|
-
* @property {number} keepAliveTimeout - Keep-alive timeout in milliseconds
|
|
365
|
-
* @property {string} [hostname] - Optional hostname
|
|
366
|
-
* @property {number} [requestTimeout] - The timeout value in milliseconds for
|
|
367
|
-
* receiving the entire request from the client
|
|
368
|
-
*/
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* @typedef {import('@appium/types').AppiumServer} AppiumServer
|
|
372
|
-
*/
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* @typedef {import('@appium/types').MethodMap<import('@appium/types').ExternalDriver>} MethodMap
|
|
376
|
-
*/
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Options for {@linkcode configureHttp}
|
|
380
|
-
* @typedef ConfigureHttpOpts
|
|
381
|
-
* @property {import('http').Server} httpServer - HTTP server instance
|
|
382
|
-
* @property {(error?: any) => void} reject - Rejection function from `Promise` constructor
|
|
383
|
-
* @property {number} keepAliveTimeout - Keep-alive timeout in milliseconds
|
|
384
|
-
* @property {number} gracefulShutdownTimeout - For how long the server should delay its
|
|
385
|
-
* shutdown before force-closing all open connections to it. Providing zero will force-close
|
|
386
|
-
* the server without waiting for any connections.
|
|
387
|
-
*/
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Options for {@linkcode server}
|
|
391
|
-
* @typedef ServerOpts
|
|
392
|
-
* @property {RouteConfiguringFunction} routeConfiguringFunction
|
|
393
|
-
* @property {number} port
|
|
394
|
-
* @property {import('@appium/types').ServerArgs} [cliArgs]
|
|
395
|
-
* @property {string} [hostname]
|
|
396
|
-
* @property {boolean} [allowCors]
|
|
397
|
-
* @property {string} [basePath]
|
|
398
|
-
* @property {MethodMap} [extraMethodMap]
|
|
399
|
-
* @property {import('@appium/types').UpdateServerCallback[]} [serverUpdaters]
|
|
400
|
-
* @property {number} [keepAliveTimeout]
|
|
401
|
-
* @property {number} [requestTimeout]
|
|
402
|
-
*/
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* A function which configures routes
|
|
406
|
-
* @callback RouteConfiguringFunction
|
|
407
|
-
* @param {import('express').Express} app
|
|
408
|
-
* @param {RouteConfiguringFunctionOpts} [opts]
|
|
409
|
-
* @returns {void}
|
|
410
|
-
*/
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Options for a {@linkcode RouteConfiguringFunction}
|
|
414
|
-
* @typedef RouteConfiguringFunctionOpts
|
|
415
|
-
* @property {string} [basePath]
|
|
416
|
-
* @property {MethodMap} [extraMethodMap]
|
|
417
|
-
*/
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Options for {@linkcode configureServer}
|
|
421
|
-
* @typedef ConfigureServerOpts
|
|
422
|
-
* @property {import('express').Express} app
|
|
423
|
-
* @property {RouteConfiguringFunction} addRoutes
|
|
424
|
-
* @property {boolean} [allowCors]
|
|
425
|
-
* @property {string} [basePath]
|
|
426
|
-
* @property {MethodMap} [extraMethodMap]
|
|
427
|
-
* @property {import('@appium/types').StringRecord} [webSocketsMapping={}]
|
|
428
|
-
* @property {boolean} [useLegacyUpgradeHandler=true] - Whether to use legacy Express middleware for WebSocket upgrades.
|
|
429
|
-
* Set to false when using shouldUpgradeCallback on the HTTP server (Node.js >= 22.21.0 or >= 24.9.0).
|
|
430
|
-
*/
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {log} from './logger';
|
|
3
|
+
import _ from 'lodash';
|
|
4
|
+
import {fs} from '@appium/support';
|
|
5
|
+
import B from 'bluebird';
|
|
6
|
+
import type {Request, Response} from 'express';
|
|
7
|
+
|
|
8
|
+
export const STATIC_DIR = _.isNull(path.resolve(__dirname).match(/build[/\\]lib[/\\]express$/))
|
|
9
|
+
? path.resolve(__dirname, '..', '..', 'static')
|
|
10
|
+
: path.resolve(__dirname, '..', '..', '..', 'static');
|
|
11
|
+
|
|
12
|
+
type TemplateParams = Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
/** Dynamic page mapped to /test/guinea-pig */
|
|
15
|
+
export async function guineaPig(req: Request, res: Response): Promise<void> {
|
|
16
|
+
await guineaPigTemplate(req, res, 'guinea-pig.html');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Dynamic page mapped to /test/guinea-pig-scrollable */
|
|
20
|
+
export async function guineaPigScrollable(req: Request, res: Response): Promise<void> {
|
|
21
|
+
await guineaPigTemplate(req, res, 'guinea-pig-scrollable.html');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Dynamic page mapped to /test/guinea-pig-app-banner */
|
|
25
|
+
export async function guineaPigAppBanner(req: Request, res: Response): Promise<void> {
|
|
26
|
+
await guineaPigTemplate(req, res, 'guinea-pig-app-banner.html');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Dynamic page mapped to /welcome */
|
|
30
|
+
export async function welcome(req: Request, res: Response): Promise<void> {
|
|
31
|
+
const params: TemplateParams = {message: "Let's browse!"};
|
|
32
|
+
log.debug(`Sending welcome response with params: ${JSON.stringify(params)}`);
|
|
33
|
+
const template = await getTemplate('welcome.html');
|
|
34
|
+
res.send(template(params));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function guineaPigTemplate(
|
|
38
|
+
req: Request,
|
|
39
|
+
res: Response,
|
|
40
|
+
page: string
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
const delay = parseInt(
|
|
43
|
+
String(req.params.delay ?? (req.query?.delay ?? 0)),
|
|
44
|
+
10
|
|
45
|
+
);
|
|
46
|
+
const throwError = String(req.params.throwError ?? req.query?.throwError ?? '');
|
|
47
|
+
const params: TemplateParams = {
|
|
48
|
+
throwError,
|
|
49
|
+
serverTime: new Date(),
|
|
50
|
+
userAgent: req.headers['user-agent'],
|
|
51
|
+
comment: 'None',
|
|
52
|
+
};
|
|
53
|
+
if (req.method === 'POST' && req.body && typeof req.body === 'object' && 'comments' in req.body) {
|
|
54
|
+
params.comment = String((req.body as {comments?: string}).comments ?? params.comment);
|
|
55
|
+
}
|
|
56
|
+
log.debug(`Sending guinea pig response with params: ${JSON.stringify(params)}`);
|
|
57
|
+
if (delay) {
|
|
58
|
+
log.debug(`Waiting ${delay}ms before responding`);
|
|
59
|
+
await B.delay(delay);
|
|
60
|
+
}
|
|
61
|
+
res.set('content-type', 'text/html');
|
|
62
|
+
res.cookie('guineacookie1', 'i am a cookie value', {path: '/'});
|
|
63
|
+
res.cookie('guineacookie2', 'cookié2', {path: '/'});
|
|
64
|
+
res.cookie('guineacookie3', 'cant access this', {
|
|
65
|
+
domain: '.blargimarg.com',
|
|
66
|
+
path: '/',
|
|
67
|
+
});
|
|
68
|
+
const template = await getTemplate(page);
|
|
69
|
+
res.send(template(params));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function getTemplate(
|
|
73
|
+
templateName: string
|
|
74
|
+
): Promise<(params: TemplateParams) => string> {
|
|
75
|
+
const content = await fs.readFile(path.resolve(STATIC_DIR, 'test', templateName));
|
|
76
|
+
return _.template(content.toString()) as (params: TemplateParams) => string;
|
|
77
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import B from 'bluebird';
|
|
3
|
+
import type {AppiumServer, WSServer} from '@appium/types';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_WS_PATHNAME_PREFIX = '/ws';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Adds a WebSocket handler to this server's mapping.
|
|
9
|
+
* @see AppiumServerExtension.addWebSocketHandler
|
|
10
|
+
*/
|
|
11
|
+
export async function addWebSocketHandler(
|
|
12
|
+
this: AppiumServer,
|
|
13
|
+
handlerPathname: string,
|
|
14
|
+
handlerServer: WSServer
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
this.webSocketsMapping[handlerPathname] = handlerServer;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns WebSocket handlers for this server, optionally filtered by pathname.
|
|
21
|
+
* @see AppiumServerExtension.getWebSocketHandlers
|
|
22
|
+
*/
|
|
23
|
+
export async function getWebSocketHandlers(
|
|
24
|
+
this: AppiumServer,
|
|
25
|
+
keysFilter: string | null = null
|
|
26
|
+
): Promise<Record<string, WSServer>> {
|
|
27
|
+
return _.toPairs(this.webSocketsMapping).reduce<Record<string, WSServer>>(
|
|
28
|
+
(acc, [pathname, wsServer]) => {
|
|
29
|
+
if (!_.isString(keysFilter) || pathname.includes(keysFilter)) {
|
|
30
|
+
acc[pathname] = wsServer;
|
|
31
|
+
}
|
|
32
|
+
return acc;
|
|
33
|
+
},
|
|
34
|
+
{}
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Removes a WebSocket handler by pathname.
|
|
40
|
+
* @see AppiumServerExtension.removeWebSocketHandler
|
|
41
|
+
*/
|
|
42
|
+
export async function removeWebSocketHandler(
|
|
43
|
+
this: AppiumServer,
|
|
44
|
+
handlerPathname: string
|
|
45
|
+
): Promise<boolean> {
|
|
46
|
+
const wsServer = this.webSocketsMapping?.[handlerPathname];
|
|
47
|
+
if (!wsServer) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
wsServer.close();
|
|
53
|
+
for (const client of wsServer.clients || []) {
|
|
54
|
+
client.terminate();
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
// ignore
|
|
59
|
+
} finally {
|
|
60
|
+
delete this.webSocketsMapping[handlerPathname];
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Removes all WebSocket handlers from this server.
|
|
67
|
+
* @see AppiumServerExtension.removeAllWebSocketHandlers
|
|
68
|
+
*/
|
|
69
|
+
export async function removeAllWebSocketHandlers(this: AppiumServer): Promise<boolean> {
|
|
70
|
+
if (_.isEmpty(this.webSocketsMapping)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return _.some(
|
|
75
|
+
await B.all(
|
|
76
|
+
_.keys(this.webSocketsMapping).map((pathname) =>
|
|
77
|
+
this.removeWebSocketHandler(pathname)
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
);
|
|
81
|
+
}
|