@appium/base-driver 10.1.2 → 10.2.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.map +1 -1
- 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 +160 -248
- 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 +71 -75
- 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 +55 -133
- 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 +33 -43
- 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/proxy.d.ts +4 -2
- package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.js +9 -4
- 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/errors.js.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 +2 -2
- 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} +215 -270
- 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} +106 -90
- package/lib/express/logger.ts +3 -0
- package/lib/express/middleware.ts +187 -0
- package/lib/express/{server.js → server.ts} +177 -170
- 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/proxy.js +7 -2
- 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 +4 -4
- 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 +15 -15
- 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/protocol/{index.js → index.ts} +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
|
-
import path from 'path';
|
|
2
|
+
import path from 'node:path';
|
|
3
3
|
import express from 'express';
|
|
4
|
-
import
|
|
4
|
+
import type {Express, RequestHandler} from 'express';
|
|
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);
|
|
@@ -122,14 +192,13 @@ export function configureServer({
|
|
|
122
192
|
|
|
123
193
|
// set up static assets
|
|
124
194
|
app.use(favicon(path.resolve(STATIC_DIR, 'favicon.ico')));
|
|
125
|
-
// eslint-disable-next-line import/no-named-as-default-member
|
|
126
195
|
app.use(express.static(STATIC_DIR));
|
|
127
196
|
|
|
128
197
|
// crash routes, for testing
|
|
129
198
|
app.use(`${basePath}/produce_error`, produceError);
|
|
130
199
|
app.use(`${basePath}/crash`, produceCrash);
|
|
131
200
|
|
|
132
|
-
// 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.
|
|
133
202
|
// When shouldUpgradeCallback is available, upgrades are handled directly on the HTTP server
|
|
134
203
|
// to avoid Express middleware timeout issues with long-lived connections
|
|
135
204
|
if (useLegacyUpgradeHandler) {
|
|
@@ -162,17 +231,17 @@ export function configureServer({
|
|
|
162
231
|
}
|
|
163
232
|
|
|
164
233
|
/**
|
|
165
|
-
* Normalize base path string
|
|
166
|
-
*
|
|
167
|
-
* @
|
|
234
|
+
* Normalize base path string (leading slash, no trailing slash).
|
|
235
|
+
*
|
|
236
|
+
* @param basePath - Raw base path
|
|
237
|
+
* @returns Normalized base path
|
|
168
238
|
*/
|
|
169
|
-
export function normalizeBasePath(basePath) {
|
|
239
|
+
export function normalizeBasePath(basePath: string): string {
|
|
170
240
|
if (!_.isString(basePath)) {
|
|
171
241
|
throw new Error(`Invalid path prefix ${basePath}`);
|
|
172
242
|
}
|
|
173
243
|
|
|
174
|
-
// ensure the path prefix does not end in '/', since our method map
|
|
175
|
-
// starts all paths with '/'
|
|
244
|
+
// ensure the path prefix does not end in '/', since our method map starts all paths with '/'
|
|
176
245
|
basePath = basePath.replace(/\/$/, '');
|
|
177
246
|
|
|
178
247
|
// likewise, ensure the path prefix does always START with /, unless the path
|
|
@@ -184,64 +253,78 @@ export function normalizeBasePath(basePath) {
|
|
|
184
253
|
return basePath;
|
|
185
254
|
}
|
|
186
255
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
* @returns {Promise<http.Server>}
|
|
192
|
-
*/
|
|
193
|
-
async function createServer (app, cliArgs) {
|
|
256
|
+
async function createServer(
|
|
257
|
+
app: Express,
|
|
258
|
+
cliArgs?: Partial<ServerArgs>
|
|
259
|
+
): Promise<HttpServer> {
|
|
194
260
|
const {sslCertificatePath, sslKeyPath} = cliArgs ?? {};
|
|
195
261
|
if (!sslCertificatePath && !sslKeyPath) {
|
|
196
262
|
return http.createServer(app);
|
|
197
263
|
}
|
|
198
264
|
if (!sslCertificatePath || !sslKeyPath) {
|
|
199
|
-
throw new Error(
|
|
265
|
+
throw new Error(
|
|
266
|
+
`Both certificate path and key path must be provided to enable TLS`
|
|
267
|
+
);
|
|
200
268
|
}
|
|
201
269
|
|
|
202
270
|
const certKey = [sslCertificatePath, sslKeyPath];
|
|
203
271
|
const zipped = _.zip(
|
|
204
272
|
await B.all(certKey.map((p) => fs.exists(p))),
|
|
205
273
|
['certificate', 'key'],
|
|
206
|
-
certKey
|
|
207
|
-
);
|
|
274
|
+
certKey
|
|
275
|
+
) as [boolean, string, string][];
|
|
208
276
|
for (const [exists, desc, p] of zipped) {
|
|
209
277
|
if (!exists) {
|
|
210
|
-
throw new Error(
|
|
278
|
+
throw new Error(
|
|
279
|
+
`The provided SSL ${desc} at '${p}' does not exist or is not accessible`
|
|
280
|
+
);
|
|
211
281
|
}
|
|
212
282
|
}
|
|
213
|
-
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];
|
|
214
286
|
log.debug('Enabling TLS/SPDY on the server using the provided certificate');
|
|
215
287
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
+
);
|
|
224
305
|
}
|
|
225
306
|
|
|
226
307
|
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* @
|
|
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
|
|
231
313
|
*/
|
|
232
|
-
function configureHttp({
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
314
|
+
function configureHttp({
|
|
315
|
+
httpServer,
|
|
316
|
+
reject,
|
|
317
|
+
keepAliveTimeout,
|
|
318
|
+
gracefulShutdownTimeout,
|
|
319
|
+
}: ConfigureHttpOpts): AppiumServer {
|
|
320
|
+
const appiumServer = httpServer as unknown as AppiumServer;
|
|
237
321
|
appiumServer.webSocketsMapping = {};
|
|
238
322
|
appiumServer.addWebSocketHandler = addWebSocketHandler;
|
|
239
323
|
appiumServer.removeWebSocketHandler = removeWebSocketHandler;
|
|
240
324
|
appiumServer.removeAllWebSocketHandlers = removeAllWebSocketHandlers;
|
|
241
325
|
appiumServer.getWebSocketHandlers = getWebSocketHandlers;
|
|
242
326
|
appiumServer.isSecure = function isSecure() {
|
|
243
|
-
|
|
244
|
-
return Boolean(this['_spdyState']?.secure);
|
|
327
|
+
return Boolean((this as unknown as {_spdyState?: {secure?: boolean}})._spdyState?.secure);
|
|
245
328
|
};
|
|
246
329
|
|
|
247
330
|
// This avoids Express middleware timeout issues with long-lived WebSocket connections
|
|
@@ -249,8 +332,8 @@ function configureHttp({httpServer, reject, keepAliveTimeout, gracefulShutdownTi
|
|
|
249
332
|
// See: https://github.com/nodejs/node/pull/59824
|
|
250
333
|
if (hasShouldUpgradeCallback(httpServer)) {
|
|
251
334
|
// shouldUpgradeCallback only returns a boolean to indicate if the upgrade should proceed
|
|
252
|
-
|
|
253
|
-
|
|
335
|
+
(appiumServer as unknown as {shouldUpgradeCallback?: (req: http.IncomingMessage) => boolean}).shouldUpgradeCallback = (req) =>
|
|
336
|
+
_.toLower(req.headers?.upgrade) === 'websocket';
|
|
254
337
|
appiumServer.on('upgrade', (req, socket, head) => {
|
|
255
338
|
if (!tryHandleWebSocketUpgrade(req, socket, head, appiumServer.webSocketsMapping)) {
|
|
256
339
|
socket.destroy();
|
|
@@ -262,73 +345,69 @@ function configureHttp({httpServer, reject, keepAliveTimeout, gracefulShutdownTi
|
|
|
262
345
|
// all connections are closed and the `close` event is emitted
|
|
263
346
|
const originalClose = appiumServer.close.bind(appiumServer);
|
|
264
347
|
appiumServer.close = async () =>
|
|
265
|
-
await new B((_resolve, _reject) => {
|
|
348
|
+
await new B<void>((_resolve, _reject) => {
|
|
266
349
|
log.info('Closing Appium HTTP server');
|
|
267
350
|
const timer = new timing.Timer().start();
|
|
268
351
|
const onTimeout = setTimeout(() => {
|
|
269
|
-
if (gracefulShutdownTimeout > 0) {
|
|
352
|
+
if ((gracefulShutdownTimeout ?? 0) > 0) {
|
|
270
353
|
log.info(
|
|
271
354
|
`Not all active connections have been closed within ${gracefulShutdownTimeout}ms. ` +
|
|
272
|
-
|
|
273
|
-
|
|
355
|
+
`This timeout might be customized by the --shutdown-timeout command line ` +
|
|
356
|
+
`argument. Closing the server anyway.`
|
|
274
357
|
);
|
|
275
358
|
}
|
|
276
359
|
process.exit(process.exitCode ?? 0);
|
|
277
|
-
}, gracefulShutdownTimeout);
|
|
278
|
-
|
|
360
|
+
}, gracefulShutdownTimeout ?? 0);
|
|
361
|
+
const onClose = () => {
|
|
279
362
|
log.info(
|
|
280
363
|
`Appium HTTP server has been successfully closed after ` +
|
|
281
|
-
|
|
364
|
+
`${timer.getDuration().asMilliSeconds.toFixed(0)}ms`
|
|
282
365
|
);
|
|
283
366
|
clearTimeout(onTimeout);
|
|
284
367
|
_resolve();
|
|
285
|
-
}
|
|
286
|
-
|
|
368
|
+
};
|
|
369
|
+
httpServer.once('close', onClose);
|
|
370
|
+
originalClose((err?: Error) => {
|
|
287
371
|
if (err) {
|
|
372
|
+
clearTimeout(onTimeout);
|
|
373
|
+
httpServer.removeListener('close', onClose);
|
|
288
374
|
_reject(err);
|
|
289
375
|
}
|
|
290
376
|
});
|
|
291
377
|
});
|
|
292
378
|
|
|
293
|
-
appiumServer.once(
|
|
294
|
-
'
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
'
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
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
|
+
);
|
|
308
391
|
}
|
|
309
|
-
|
|
392
|
+
reject(err);
|
|
393
|
+
});
|
|
310
394
|
|
|
311
395
|
appiumServer.on('connection', (socket) => socket.setTimeout(keepAliveTimeout));
|
|
312
396
|
|
|
313
397
|
return appiumServer;
|
|
314
398
|
}
|
|
315
399
|
|
|
316
|
-
/**
|
|
317
|
-
* Starts an {@linkcode AppiumServer}
|
|
318
|
-
* @param {StartServerOpts} opts
|
|
319
|
-
* @returns {Promise<void>}
|
|
320
|
-
*/
|
|
321
400
|
async function startServer({
|
|
322
401
|
httpServer,
|
|
323
402
|
port,
|
|
324
403
|
hostname,
|
|
325
404
|
keepAliveTimeout,
|
|
326
405
|
requestTimeout,
|
|
327
|
-
}) {
|
|
328
|
-
// If the hostname is omitted, the server will accept
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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>;
|
|
332
411
|
const startPromise = start(port, hostname);
|
|
333
412
|
httpServer.keepAliveTimeout = keepAliveTimeout;
|
|
334
413
|
if (_.isInteger(requestTimeout)) {
|
|
@@ -340,92 +419,20 @@ async function startServer({
|
|
|
340
419
|
}
|
|
341
420
|
|
|
342
421
|
/**
|
|
343
|
-
* Checks if the
|
|
344
|
-
*
|
|
345
|
-
* @param
|
|
346
|
-
* @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
|
|
347
426
|
*/
|
|
348
|
-
function hasShouldUpgradeCallback(server) {
|
|
427
|
+
function hasShouldUpgradeCallback(server: HttpServer): boolean {
|
|
349
428
|
// Check if shouldUpgradeCallback is available on http.Server
|
|
350
429
|
// This is a runtime check that works regardless of TypeScript types
|
|
351
430
|
try {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
431
|
+
return (
|
|
432
|
+
typeof (server as unknown as {shouldUpgradeCallback?: unknown}).shouldUpgradeCallback !==
|
|
433
|
+
'undefined'
|
|
434
|
+
);
|
|
355
435
|
} catch {
|
|
356
436
|
return false;
|
|
357
437
|
}
|
|
358
438
|
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Options for {@linkcode startServer}.
|
|
362
|
-
* @typedef StartServerOpts
|
|
363
|
-
* @property {import('http').Server} httpServer - HTTP server instance
|
|
364
|
-
* @property {number} port - Port to run on
|
|
365
|
-
* @property {number} keepAliveTimeout - Keep-alive timeout in milliseconds
|
|
366
|
-
* @property {string} [hostname] - Optional hostname
|
|
367
|
-
* @property {number} [requestTimeout] - The timeout value in milliseconds for
|
|
368
|
-
* receiving the entire request from the client
|
|
369
|
-
*/
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* @typedef {import('@appium/types').AppiumServer} AppiumServer
|
|
373
|
-
*/
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* @typedef {import('@appium/types').MethodMap<import('@appium/types').ExternalDriver>} MethodMap
|
|
377
|
-
*/
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Options for {@linkcode configureHttp}
|
|
381
|
-
* @typedef ConfigureHttpOpts
|
|
382
|
-
* @property {import('http').Server} httpServer - HTTP server instance
|
|
383
|
-
* @property {(error?: any) => void} reject - Rejection function from `Promise` constructor
|
|
384
|
-
* @property {number} keepAliveTimeout - Keep-alive timeout in milliseconds
|
|
385
|
-
* @property {number} gracefulShutdownTimeout - For how long the server should delay its
|
|
386
|
-
* shutdown before force-closing all open connections to it. Providing zero will force-close
|
|
387
|
-
* the server without waiting for any connections.
|
|
388
|
-
*/
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Options for {@linkcode server}
|
|
392
|
-
* @typedef ServerOpts
|
|
393
|
-
* @property {RouteConfiguringFunction} routeConfiguringFunction
|
|
394
|
-
* @property {number} port
|
|
395
|
-
* @property {import('@appium/types').ServerArgs} [cliArgs]
|
|
396
|
-
* @property {string} [hostname]
|
|
397
|
-
* @property {boolean} [allowCors]
|
|
398
|
-
* @property {string} [basePath]
|
|
399
|
-
* @property {MethodMap} [extraMethodMap]
|
|
400
|
-
* @property {import('@appium/types').UpdateServerCallback[]} [serverUpdaters]
|
|
401
|
-
* @property {number} [keepAliveTimeout]
|
|
402
|
-
* @property {number} [requestTimeout]
|
|
403
|
-
*/
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* A function which configures routes
|
|
407
|
-
* @callback RouteConfiguringFunction
|
|
408
|
-
* @param {import('express').Express} app
|
|
409
|
-
* @param {RouteConfiguringFunctionOpts} [opts]
|
|
410
|
-
* @returns {void}
|
|
411
|
-
*/
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Options for a {@linkcode RouteConfiguringFunction}
|
|
415
|
-
* @typedef RouteConfiguringFunctionOpts
|
|
416
|
-
* @property {string} [basePath]
|
|
417
|
-
* @property {MethodMap} [extraMethodMap]
|
|
418
|
-
*/
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* Options for {@linkcode configureServer}
|
|
422
|
-
* @typedef ConfigureServerOpts
|
|
423
|
-
* @property {import('express').Express} app
|
|
424
|
-
* @property {RouteConfiguringFunction} addRoutes
|
|
425
|
-
* @property {boolean} [allowCors]
|
|
426
|
-
* @property {string} [basePath]
|
|
427
|
-
* @property {MethodMap} [extraMethodMap]
|
|
428
|
-
* @property {import('@appium/types').StringRecord} [webSocketsMapping={}]
|
|
429
|
-
* @property {boolean} [useLegacyUpgradeHandler=true] - Whether to use legacy Express middleware for WebSocket upgrades.
|
|
430
|
-
* Set to false when using shouldUpgradeCallback on the HTTP server (Node.js >= 22.21.0 or >= 24.9.0).
|
|
431
|
-
*/
|
|
@@ -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
|
+
}
|