UNPKG

@appium/base-driver

Version:

Base driver class for Appium drivers

272 lines 12.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.server = server; exports.configureServer = configureServer; exports.normalizeBasePath = normalizeBasePath; const lodash_1 = __importDefault(require("lodash")); const node_path_1 = __importDefault(require("node:path")); const express_1 = __importDefault(require("express")); const node_http_1 = __importDefault(require("node:http")); const serve_favicon_1 = __importDefault(require("serve-favicon")); const body_parser_1 = __importDefault(require("body-parser")); const method_override_1 = __importDefault(require("method-override")); const logger_1 = require("./logger"); const express_logging_1 = require("./express-logging"); const middleware_1 = require("./middleware"); const static_1 = require("./static"); const crash_1 = require("./crash"); const websocket_1 = require("./websocket"); const bluebird_1 = __importDefault(require("bluebird")); const constants_1 = require("../constants"); const support_1 = require("@appium/support"); const KEEP_ALIVE_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes /** * @param opts - Server options * @returns Promise resolving to the Appium server instance */ async function server(opts) { const { routeConfiguringFunction, port, hostname, cliArgs = {}, allowCors = true, basePath = constants_1.DEFAULT_BASE_PATH, extraMethodMap = {}, serverUpdaters = [], keepAliveTimeout = KEEP_ALIVE_TIMEOUT_MS, requestTimeout, } = opts; const app = (0, express_1.default)(); const httpServer = await createServer(app, cliArgs); return await new bluebird_1.default(async (resolve, reject) => { // we put an async function as the promise constructor because we want some things to happen in // serial (application of plugin updates, for example). But we still need to use a promise here // because some elements of server start failure only happen in httpServer listeners. So the // way we resolve it is to use an async function here but to wrap all the inner logic in // try/catch so any errors can be passed to reject. try { const appiumServer = configureHttp({ httpServer, reject, keepAliveTimeout, gracefulShutdownTimeout: cliArgs.shutdownTimeout, }); const useLegacyUpgradeHandler = !hasShouldUpgradeCallback(httpServer); configureServer({ app, addRoutes: routeConfiguringFunction, allowCors, basePath, extraMethodMap, webSocketsMapping: appiumServer.webSocketsMapping, useLegacyUpgradeHandler, }); // allow extensions to update the app and http server objects for (const updater of serverUpdaters) { await updater(app, appiumServer, cliArgs); } // once all configurations and updaters have been applied, make sure to set up a catchall // handler so that anything unknown 404s. But do this after everything else since we don't // want to block extensions' ability to add routes if they want. app.all('/*all', middleware_1.catch404Handler); await startServer({ httpServer, hostname, port, keepAliveTimeout, requestTimeout, }); resolve(appiumServer); } catch (err) { reject(err); } }); } /** * Sets up Express middleware and routes. * * @param opts - Configuration options */ function configureServer({ app, addRoutes, allowCors = true, basePath = constants_1.DEFAULT_BASE_PATH, extraMethodMap = {}, webSocketsMapping = {}, useLegacyUpgradeHandler = true, }) { basePath = normalizeBasePath(basePath); app.use(express_logging_1.endLogFormatter); app.use(middleware_1.handleLogContext); // set up static assets app.use((0, serve_favicon_1.default)(node_path_1.default.resolve(static_1.STATIC_DIR, 'favicon.ico'))); app.use(express_1.default.static(static_1.STATIC_DIR)); // crash routes, for testing app.use(`${basePath}/produce_error`, crash_1.produceError); app.use(`${basePath}/crash`, crash_1.produceCrash); // Only use legacy Express middleware for WebSocket upgrades if shouldUpgradeCallback is not available. // When shouldUpgradeCallback is available, upgrades are handled directly on the HTTP server // to avoid Express middleware timeout issues with long-lived connections if (useLegacyUpgradeHandler) { app.use((0, middleware_1.handleUpgrade)(webSocketsMapping)); } if (allowCors) { app.use(middleware_1.allowCrossDomain); } else { app.use((0, middleware_1.allowCrossDomainAsyncExecute)(basePath)); } app.use(middleware_1.handleIdempotency); app.use(middleware_1.defaultToJSONContentType); app.use(body_parser_1.default.urlencoded({ extended: true })); app.use((0, method_override_1.default)()); app.use(middleware_1.catchAllHandler); // make sure appium never fails because of a file size upload limit app.use(body_parser_1.default.json({ limit: '1gb' })); // set up start logging (which depends on bodyParser doing its thing) app.use(express_logging_1.startLogFormatter); addRoutes(app, { basePath, extraMethodMap }); // dynamic routes for testing, etc. app.all('/welcome', static_1.welcome); app.all('/test/guinea-pig', static_1.guineaPig); app.all('/test/guinea-pig-scrollable', static_1.guineaPigScrollable); app.all('/test/guinea-pig-app-banner', static_1.guineaPigAppBanner); } /** * Normalize base path string (leading slash, no trailing slash). * * @param basePath - Raw base path * @returns Normalized base path */ function normalizeBasePath(basePath) { if (!lodash_1.default.isString(basePath)) { throw new Error(`Invalid path prefix ${basePath}`); } // ensure the path prefix does not end in '/', since our method map starts all paths with '/' basePath = basePath.replace(/\/$/, ''); // likewise, ensure the path prefix does always START with /, unless the path // is empty meaning no base path at all if (basePath !== '' && !basePath.startsWith('/')) { basePath = `/${basePath}`; } return basePath; } async function createServer(app, cliArgs) { const { sslCertificatePath, sslKeyPath } = cliArgs ?? {}; if (!sslCertificatePath && !sslKeyPath) { return node_http_1.default.createServer(app); } if (!sslCertificatePath || !sslKeyPath) { throw new Error(`Both certificate path and key path must be provided to enable TLS`); } const certKey = [sslCertificatePath, sslKeyPath]; const zipped = lodash_1.default.zip(await bluebird_1.default.all(certKey.map((p) => support_1.fs.exists(p))), ['certificate', 'key'], certKey); for (const [exists, desc, p] of zipped) { if (!exists) { throw new Error(`The provided SSL ${desc} at '${p}' does not exist or is not accessible`); } } const [cert, key] = await bluebird_1.default.all(certKey.map((p) => support_1.fs.readFile(p, 'utf8'))); logger_1.log.debug('Enabling TLS/SPDY on the server using the provided certificate'); const spdy = require('spdy'); return spdy.createServer({ cert, key, spdy: { plain: false, ssl: true, }, }, app); } /** * Attaches Appium-specific behavior to the HTTP server and returns it as {@linkcode AppiumServer}. * Mutates the `httpServer` parameter. * * @param opts - Configuration options * @returns The same server instance typed as AppiumServer */ function configureHttp({ httpServer, reject, keepAliveTimeout, gracefulShutdownTimeout, }) { const appiumServer = httpServer; appiumServer.webSocketsMapping = {}; appiumServer.addWebSocketHandler = websocket_1.addWebSocketHandler; appiumServer.removeWebSocketHandler = websocket_1.removeWebSocketHandler; appiumServer.removeAllWebSocketHandlers = websocket_1.removeAllWebSocketHandlers; appiumServer.getWebSocketHandlers = websocket_1.getWebSocketHandlers; appiumServer.isSecure = function isSecure() { return Boolean(this._spdyState?.secure); }; // This avoids Express middleware timeout issues with long-lived WebSocket connections // See: https://github.com/appium/appium/issues/20760 // See: https://github.com/nodejs/node/pull/59824 if (hasShouldUpgradeCallback(httpServer)) { // shouldUpgradeCallback only returns a boolean to indicate if the upgrade should proceed appiumServer.shouldUpgradeCallback = (req) => lodash_1.default.toLower(req.headers?.upgrade) === 'websocket'; appiumServer.on('upgrade', (req, socket, head) => { if (!(0, middleware_1.tryHandleWebSocketUpgrade)(req, socket, head, appiumServer.webSocketsMapping)) { socket.destroy(); } }); } // http.Server.close() only stops new connections, but we need to wait until // all connections are closed and the `close` event is emitted const originalClose = appiumServer.close.bind(appiumServer); appiumServer.close = async () => await new bluebird_1.default((_resolve, _reject) => { logger_1.log.info('Closing Appium HTTP server'); const timer = new support_1.timing.Timer().start(); const onTimeout = setTimeout(() => { if ((gracefulShutdownTimeout ?? 0) > 0) { logger_1.log.info(`Not all active connections have been closed within ${gracefulShutdownTimeout}ms. ` + `This timeout might be customized by the --shutdown-timeout command line ` + `argument. Closing the server anyway.`); } process.exit(process.exitCode ?? 0); }, gracefulShutdownTimeout ?? 0); const onClose = () => { logger_1.log.info(`Appium HTTP server has been successfully closed after ` + `${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); clearTimeout(onTimeout); _resolve(); }; httpServer.once('close', onClose); originalClose((err) => { if (err) { clearTimeout(onTimeout); httpServer.removeListener('close', onClose); _reject(err); } }); }); appiumServer.once('error', (err) => { if (err.code === 'EADDRNOTAVAIL') { logger_1.log.error('Could not start REST http interface listener. ' + 'Requested address is not available.'); } else { logger_1.log.error('Could not start REST http interface listener. The requested ' + 'port may already be in use. Please make sure there is no ' + 'other instance of this server running already.'); } reject(err); }); appiumServer.on('connection', (socket) => socket.setTimeout(keepAliveTimeout)); return appiumServer; } async function startServer({ httpServer, port, hostname, keepAliveTimeout, requestTimeout, }) { // If the hostname is omitted, the server will accept connections on any IP address const start = bluebird_1.default.promisify(httpServer.listen, { context: httpServer, }); const startPromise = start(port, hostname); httpServer.keepAliveTimeout = keepAliveTimeout; if (lodash_1.default.isInteger(requestTimeout)) { httpServer.requestTimeout = Number(requestTimeout); } // headers timeout must be greater than keepAliveTimeout httpServer.headersTimeout = keepAliveTimeout + 5 * 1000; await startPromise; } /** * Checks if the server supports `shouldUpgradeCallback` (Node.js v22.21.0+ / v24.9.0+). * * @param server - The HTTP server instance * @returns true if shouldUpgradeCallback is available */ function hasShouldUpgradeCallback(server) { // Check if shouldUpgradeCallback is available on http.Server // This is a runtime check that works regardless of TypeScript types try { return (typeof server.shouldUpgradeCallback !== 'undefined'); } catch { return false; } } //# sourceMappingURL=server.js.map