appium
Version:
Automation for Apps.
445 lines • 20.4 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveAppiumHome = exports.validate = exports.getSchema = exports.finalizeSchema = exports.readConfigFile = void 0;
exports.main = main;
exports.init = init;
const ws_1 = require("ws");
const logsink_1 = require("./logsink"); // this import needs to come first since it sets up global npmlog
const logger_1 = __importDefault(require("./logger")); // logger needs to remain second
const base_driver_1 = require("@appium/base-driver");
const support_1 = require("@appium/support");
const lodash_1 = __importDefault(require("lodash"));
const appium_1 = require("./appium");
const extension_1 = require("./cli/extension");
const setup_command_1 = require("./cli/setup-command");
const parser_1 = require("./cli/parser");
const config_1 = require("./config");
const config_file_1 = require("./config-file");
const extension_2 = require("./extension");
const constants_1 = require("./constants");
const grid_register_1 = __importDefault(require("./grid-register"));
const schema_1 = require("./schema/schema");
const utils_1 = require("./utils");
const node_net_1 = __importDefault(require("node:net"));
const extension_command_1 = require("./cli/extension-command");
const { resolveAppiumHome } = support_1.env;
exports.resolveAppiumHome = resolveAppiumHome;
/*
* By default Node.js shows a warning
* if the actual amount of listeners exceeds the maximum amount,
* which equals to 10 by default. It is known that multiple drivers/plugins
* may assign custom listeners to the server process to handle, for example,
* the graceful shutdown scenario.
*/
const MAX_SERVER_PROCESS_LISTENERS = 100;
/**
*
* @param {ParsedArgs} args
* @param {boolean} [throwInsteadOfExit]
*/
async function preflightChecks(args, throwInsteadOfExit = false) {
try {
(0, config_1.checkNodeOk)();
if (args.longStacktrace) {
Error.stackTraceLimit = constants_1.LONG_STACKTRACE_LIMIT;
}
if (args.showBuildInfo) {
await (0, config_1.showBuildInfo)();
process.exit(0);
}
(0, schema_1.validate)(args);
if (args.tmpDir) {
await (0, config_1.requireDir)(args.tmpDir, !args.noPermsCheck, 'tmpDir argument value');
}
}
catch (err) {
logger_1.default.error(err.message.red);
if (throwInsteadOfExit) {
throw err;
}
process.exit(1);
}
}
/**
* @param {Args} args
*/
function logNonDefaultArgsWarning(args) {
logger_1.default.info('Non-default server args:');
(0, utils_1.inspect)(args);
}
/**
* @param {Args['defaultCapabilities']} caps
*/
function logDefaultCapabilitiesWarning(caps) {
logger_1.default.info('Default capabilities, which will be added to each request ' +
'unless overridden by desired capabilities:');
(0, utils_1.inspect)(caps);
}
/**
* @param {ParsedArgs} args
*/
async function logStartupInfo(args) {
let welcome = `Welcome to Appium v${config_1.APPIUM_VER}`;
let appiumRev = await (0, config_1.getGitRev)();
if (appiumRev) {
welcome += ` (REV ${appiumRev})`;
}
logger_1.default.info(welcome);
let showArgs = (0, config_1.getNonDefaultServerArgs)(args);
if (lodash_1.default.size(showArgs)) {
logNonDefaultArgsWarning(showArgs);
}
if (!lodash_1.default.isEmpty(args.defaultCapabilities)) {
logDefaultCapabilitiesWarning(args.defaultCapabilities);
}
// TODO: bring back loglevel reporting below once logger is flushed out
// logger.info('Console LogLevel: ' + logger.transports.console.level);
// if (logger.transports.file) {
// logger.info('File LogLevel: ' + logger.transports.file.level);
// }
}
/**
* Gets a list of `updateServer` functions from all extensions
* @param {DriverNameMap} driverClasses
* @param {PluginNameMap} pluginClasses
* @returns {import('@appium/types').UpdateServerCallback[]}
*/
function getServerUpdaters(driverClasses, pluginClasses) {
return lodash_1.default.compact(lodash_1.default.map([...driverClasses.keys(), ...pluginClasses.keys()], 'updateServer'));
}
/**
* Makes a big `MethodMap` from all the little `MethodMap`s in the extensions
* @param {DriverNameMap} driverClasses
* @param {PluginNameMap} pluginClasses
* @returns {import('@appium/types').MethodMap<import('@appium/types').Driver>}
*/
function getExtraMethodMap(driverClasses, pluginClasses) {
return [...driverClasses.keys(), ...pluginClasses.keys()].reduce((map, klass) => ({
...map,
...(klass.newMethodMap ?? {}),
}), {});
}
/**
* @param {string?} [appiumHomeFromArgs] - Appium home value retrieved from progrmmatic server args
* @returns {string}
*/
function determineAppiumHomeSource(appiumHomeFromArgs) {
if (!lodash_1.default.isNil(appiumHomeFromArgs)) {
return 'appiumHome config value';
}
else if (process.env.APPIUM_HOME) {
return 'APPIUM_HOME environment variable';
}
return 'autodetected Appium home path';
}
/**
* Initializes Appium, but does not start the server.
*
* Use this to get at the configuration schema.
*
* If `args` contains a non-empty `subcommand` which is not `server`, this function will return an empty object.
*
* @template {CliCommand} [Cmd=ServerCommand]
* @template {CliExtensionSubcommand|void} [SubCmd=void]
* @param {Args<Cmd, SubCmd>} [args] - Partial args (programmatic usage only)
* @returns {Promise<InitResult<Cmd>>}
* @example
* import {init, getSchema} from 'appium';
* const options = {}; // config object
* await init(options);
* const schema = getSchema(); // entire config schema including plugins and drivers
*/
async function init(args) {
const appiumHome = args?.appiumHome ?? (await resolveAppiumHome());
const appiumHomeSourceName = determineAppiumHomeSource(args?.appiumHome);
// We verify the writeability later based on requested server arguments
// Here we just need to make sure the path exists and is a folder
await (0, config_1.requireDir)(appiumHome, false, appiumHomeSourceName);
(0, utils_1.adjustNodePath)();
const { driverConfig, pluginConfig } = await (0, extension_2.loadExtensions)(appiumHome);
const parser = (0, parser_1.getParser)();
let throwInsteadOfExit = false;
/** @type {Args<Cmd, SubCmd>} */
let preConfigArgs;
if (args) {
// if we have a containing package instead of running as a CLI process,
// that package might not appreciate us calling 'process.exit' willy-
// nilly, so give it the option to have us throw instead of exit
if (args.throwInsteadOfExit) {
throwInsteadOfExit = true;
// but remove it since it's not a real server arg per se
delete args.throwInsteadOfExit;
}
preConfigArgs = { ...args, subcommand: args.subcommand ?? constants_1.SERVER_SUBCOMMAND };
}
else {
// otherwise parse from CLI
preConfigArgs = /** @type {Args<Cmd, SubCmd>} */ (parser.parseArgs());
}
const configResult = await (0, config_file_1.readConfigFile)(preConfigArgs.configFile);
if (!lodash_1.default.isEmpty(configResult.errors)) {
throw new Error(`Errors in config file ${configResult.filepath}:\n ${configResult.reason ?? configResult.errors}`);
}
// merge config and apply defaults.
// the order of precedence is:
// 1. command line args
// 2. config file
// 3. defaults from config file.
if ((0, utils_1.isServerCommandArgs)(preConfigArgs)) {
const defaults = (0, schema_1.getDefaultsForSchema)(false);
/** @type {ParsedArgs} */
const serverArgs = lodash_1.default.defaultsDeep({}, preConfigArgs, configResult.config?.server, defaults);
if (preConfigArgs.showConfig) {
(0, config_1.showConfig)((0, config_1.getNonDefaultServerArgs)(preConfigArgs), configResult, defaults, serverArgs);
return /** @type {InitResult<Cmd>} */ ({});
}
if (preConfigArgs.showDebugInfo) {
await (0, config_1.showDebugInfo)({
driverConfig,
pluginConfig,
appiumHome,
});
return /** @type {InitResult<Cmd>} */ ({});
}
await (0, logsink_1.init)(serverArgs);
if (serverArgs.logFilters) {
const { issues, rules } = await logger_1.default.unwrap().loadSecureValuesPreprocessingRules(serverArgs.logFilters);
const argToLog = lodash_1.default.truncate(JSON.stringify(serverArgs.logFilters), {
length: 150
});
if (!lodash_1.default.isEmpty(issues)) {
throw new Error(`The log filtering rules config ${argToLog} has issues: ` +
JSON.stringify(issues, null, 2));
}
if (lodash_1.default.isEmpty(rules)) {
logger_1.default.warn(`Found no log filtering rules in the ${argToLog} config. ` +
`Is that expected?`);
}
else {
// Filtering aims to "hide" these values from the log,
// so it would be nice to not show them in the log as well.
logger_1.default.info(`Loaded ${support_1.util.pluralize('filtering rule', rules.length, true)}`);
}
}
if (!serverArgs.noPermsCheck) {
await (0, config_1.requireDir)(appiumHome, true, appiumHomeSourceName);
}
const appiumDriver = new appium_1.AppiumDriver(
/** @type {import('@appium/types').DriverOpts<import('./appium').AppiumDriverConstraints>} */ (serverArgs));
// set the config on the umbrella driver so it can match drivers to caps
appiumDriver.driverConfig = driverConfig;
await preflightChecks(serverArgs, throwInsteadOfExit);
return /** @type {InitResult<Cmd>} */ ({
appiumDriver,
parsedArgs: serverArgs,
driverConfig,
pluginConfig,
appiumHome,
});
}
else if ((0, utils_1.isSetupCommandArgs)(preConfigArgs)) {
await (0, setup_command_1.runSetupCommand)(preConfigArgs, driverConfig, pluginConfig);
return /** @type {InitResult<Cmd>} */ ({});
}
else {
await (0, config_1.requireDir)(appiumHome, true, appiumHomeSourceName);
if ((0, utils_1.isExtensionCommandArgs)(preConfigArgs)) {
// if the user has requested the 'driver' CLI, don't run the normal server,
// but instead pass control to the driver CLI
if ((0, utils_1.isDriverCommandArgs)(preConfigArgs)) {
await (0, extension_1.runExtensionCommand)(preConfigArgs, driverConfig);
}
if ((0, utils_1.isPluginCommandArgs)(preConfigArgs)) {
await (0, extension_1.runExtensionCommand)(preConfigArgs, pluginConfig);
}
if ((0, utils_1.isDriverCommandArgs)(preConfigArgs) || (0, utils_1.isPluginCommandArgs)(preConfigArgs)) {
// @ts-ignore The linter suggest using dot
const cmd = preConfigArgs.driverCommand || preConfigArgs.pluginCommand;
if (cmd === 'install') {
await (0, extension_command_1.injectAppiumSymlinks)(driverConfig, pluginConfig, logger_1.default);
}
}
}
return /** @type {InitResult<Cmd>} */ ({});
}
}
/**
* Prints the actual server address and the list of URLs that
* could be used to connect to the current server.
* Properly replaces broadcast addresses in client URLs.
*
* @param {string} url The URL the server is listening on
*/
function logServerAddress(url) {
const urlObj = new URL(url);
logger_1.default.info(`Appium REST http interface listener started on ${url}`);
if (!(0, utils_1.isBroadcastIp)(urlObj.hostname)) {
return;
}
const interfaces = (0, utils_1.fetchInterfaces)(urlObj.hostname === utils_1.V4_BROADCAST_IP ? 4 : 6);
const toLabel = (/** @type {import('node:os').NetworkInterfaceInfo} */ iface) => {
const href = urlObj.href.replace(urlObj.hostname, iface.address);
return iface.internal ? `${href} (only accessible from the same host)` : href;
};
logger_1.default.info(`You can provide the following ${interfaces.length === 1 ? 'URL' : 'URLs'} ` +
`in your client code to connect to this server:\n` +
interfaces.map((iface) => `\t${toLabel(iface)}`).join('\n'));
}
/**
* Initializes Appium's config. Starts server if appropriate and resolves the
* server instance if so; otherwise resolves w/ `undefined`.
* @template {CliCommand} [Cmd=ServerCommand]
* @template {CliExtensionSubcommand|void} [SubCmd=void]
* @param {Args<Cmd, SubCmd>} [args] - Arguments from CLI or otherwise
* @returns {Promise<Cmd extends ServerCommand ? import('@appium/types').AppiumServer : void>}
*/
async function main(args) {
const initResult = await init(args);
if (lodash_1.default.isEmpty(initResult)) {
// if this branch is taken, we've run a different subcommand, so there's nothing
// left to do here.
return /** @type {Cmd extends ServerCommand ? import('@appium/types').AppiumServer : void} */ (undefined);
}
const { appiumDriver, pluginConfig, driverConfig, parsedArgs, appiumHome } =
/** @type {InitResult<ServerCommand>} */ (initResult);
const pluginClasses = await (0, extension_2.getActivePlugins)(pluginConfig, parsedArgs.pluginsImportChunkSize, parsedArgs.usePlugins);
// set the active plugins on the umbrella driver so it can use them for commands
appiumDriver.pluginClasses = pluginClasses;
await logStartupInfo(parsedArgs);
// handle the insecure feature configuration since some features may apply globally
appiumDriver.configureGlobalFeatures();
const appiumHomeSourceName = determineAppiumHomeSource(args?.appiumHome);
logger_1.default.debug(`The ${appiumHomeSourceName}: ${appiumHome}`);
let routeConfiguringFunction = (0, base_driver_1.routeConfiguringFunction)(appiumDriver);
const driverClasses = await (0, extension_2.getActiveDrivers)(driverConfig, parsedArgs.driversImportChunkSize, parsedArgs.useDrivers);
const serverUpdaters = getServerUpdaters(driverClasses, pluginClasses);
const extraMethodMap = getExtraMethodMap(driverClasses, pluginClasses);
/** @type {import('@appium/base-driver').ServerOpts} */
const serverOpts = {
routeConfiguringFunction,
port: parsedArgs.port,
hostname: parsedArgs.address,
allowCors: parsedArgs.allowCors,
basePath: parsedArgs.basePath,
serverUpdaters,
extraMethodMap,
cliArgs: parsedArgs,
};
const normalizedBasePath = (0, base_driver_1.normalizeBasePath)(parsedArgs.basePath);
for (const timeoutArgName of ['keepAliveTimeout', 'requestTimeout']) {
if (lodash_1.default.isInteger(parsedArgs[timeoutArgName])) {
serverOpts[timeoutArgName] = parsedArgs[timeoutArgName] * 1000;
}
}
let server;
const bidiServer = new ws_1.WebSocketServer({ noServer: true });
bidiServer.on('connection', appiumDriver.onBidiConnection.bind(appiumDriver));
bidiServer.on('error', appiumDriver.onBidiServerError.bind(appiumDriver));
try {
server = await (0, base_driver_1.server)(serverOpts);
const bidiBasePath = `${normalizedBasePath}${constants_1.BIDI_BASE_PATH}`;
server.addWebSocketHandler(bidiBasePath, bidiServer);
server.addWebSocketHandler(`${bidiBasePath}/:sessionId`, bidiServer);
}
catch (err) {
logger_1.default.error(`Could not configure Appium server. It's possible that a driver or plugin tried ` +
`to update the server and failed. Original error: ${err.message}`);
logger_1.default.debug(err.stack);
return process.exit(1);
}
if (parsedArgs.allowCors) {
logger_1.default.warn('You have enabled CORS requests from any host. Be careful not ' +
'to visit sites which could maliciously try to start Appium ' +
'sessions on your machine');
}
appiumDriver.server = server;
try {
// configure as node on grid, if necessary
// falsy values should not cause this to run
if (parsedArgs.nodeconfig) {
await (0, grid_register_1.default)(parsedArgs.nodeconfig, parsedArgs.address, parsedArgs.port, normalizedBasePath);
}
}
catch (err) {
await server.close();
throw err;
}
process.setMaxListeners(MAX_SERVER_PROCESS_LISTENERS);
for (const signal of ['SIGINT', 'SIGTERM']) {
process.once(signal, async function onSignal() {
logger_1.default.info(`Received ${signal} - shutting down`);
try {
await appiumDriver.shutdown(`The process has received ${signal} signal`);
await server.close();
process.exit(0);
}
catch (e) {
logger_1.default.warn(e);
process.exit(1);
}
});
}
const protocol = server.isSecure() ? 'https' : 'http';
const address = node_net_1.default.isIPv6(parsedArgs.address) ? `[${parsedArgs.address}]` : parsedArgs.address;
logServerAddress(`${protocol}://${address}:${parsedArgs.port}${normalizedBasePath}`);
driverConfig.print();
pluginConfig.print([...pluginClasses.values()]);
return /** @type {Cmd extends ServerCommand ? import('@appium/types').AppiumServer : void} */ (server);
}
// NOTE: this is here for backwards compat for any scripts referencing `main.js` directly
// (more specifically, `build/lib/main.js`)
// the executable is now `../index.js`, so that module will typically be `require.main`.
if (require.main === module) {
main();
}
// everything below here is intended to be a public API.
var config_file_2 = require("./config-file");
Object.defineProperty(exports, "readConfigFile", { enumerable: true, get: function () { return config_file_2.readConfigFile; } });
var schema_2 = require("./schema/schema");
Object.defineProperty(exports, "finalizeSchema", { enumerable: true, get: function () { return schema_2.finalizeSchema; } });
Object.defineProperty(exports, "getSchema", { enumerable: true, get: function () { return schema_2.getSchema; } });
Object.defineProperty(exports, "validate", { enumerable: true, get: function () { return schema_2.validate; } });
/**
* @typedef {import('@appium/types').DriverType} DriverType
* @typedef {import('@appium/types').PluginType} PluginType
* @typedef {import('@appium/types').DriverClass} DriverClass
* @typedef {import('@appium/types').PluginClass} PluginClass
* @typedef {import('appium/types').CliCommand} CliCommand
* @typedef {import('appium/types').CliExtensionSubcommand} CliExtensionSubcommand
* @typedef {import('appium/types').CliExtensionCommand} CliExtensionCommand
* @typedef {import('appium/types').CliCommandServer} ServerCommand
* @typedef {import('appium/types').CliCommandDriver} DriverCommand
* @typedef {import('appium/types').CliCommandPlugin} PluginCommand
* @typedef {import('appium/types').CliCommandSetup} SetupCommand
* @typedef {import('./extension').DriverNameMap} DriverNameMap
* @typedef {import('./extension').PluginNameMap} PluginNameMap
*/
/**
* Literally an empty object
* @typedef { {} } ExtCommandInitResult
*/
/**
* @typedef ServerInitData
* @property {import('./appium').AppiumDriver} appiumDriver - The Appium driver
* @property {import('appium/types').ParsedArgs} parsedArgs - The parsed arguments
* @property {string} appiumHome - The full path to the Appium home folder
*/
/**
* @template {CliCommand} Cmd
* @typedef {Cmd extends ServerCommand ? ServerInitData & import('./extension').ExtensionConfigs : ExtCommandInitResult} InitResult
*/
/**
* @template {CliCommand} [Cmd=ServerCommand]
* @template {CliExtensionSubcommand|void} [SubCmd=void]
* @typedef {import('appium/types').Args<Cmd, SubCmd>} Args
*/
/**
* @template {CliCommand} [Cmd=ServerCommand]
* @template {CliExtensionSubcommand|void} [SubCmd=void]
* @typedef {import('appium/types').ParsedArgs<Cmd, SubCmd>} ParsedArgs
*/
//# sourceMappingURL=main.js.map