UNPKG

@appium/support

Version:

Support libs used across Appium packages

507 lines 19.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GiB = exports.MiB = exports.KiB = exports.W3C_WEB_ELEMENT_IDENTIFIER = exports.uuidV5 = exports.uuidV4 = exports.uuidV3 = exports.uuidV1 = exports.shellParse = void 0; exports.hasContent = hasContent; exports.hasValue = hasValue; exports.escapeSpace = escapeSpace; exports.escapeSpecialChars = escapeSpecialChars; exports.localIp = localIp; exports.cancellableDelay = cancellableDelay; exports.multiResolve = multiResolve; exports.safeJsonParse = safeJsonParse; exports.jsonStringify = jsonStringify; exports.unwrapElement = unwrapElement; exports.wrapElement = wrapElement; exports.filterObject = filterObject; exports.toReadableSizeString = toReadableSizeString; exports.isSubPath = isSubPath; exports.isSameDestination = isSameDestination; exports.coerceVersion = coerceVersion; exports.compareVersions = compareVersions; exports.quote = quote; exports.pluralize = pluralize; exports.toInMemoryBase64 = toInMemoryBase64; exports.getLockFileGuard = getLockFileGuard; const bluebird_1 = __importDefault(require("bluebird")); const lodash_1 = __importDefault(require("lodash")); const node_os_1 = __importDefault(require("node:os")); const node_path_1 = __importDefault(require("node:path")); const node_stream_1 = __importDefault(require("node:stream")); const node_util_1 = require("node:util"); const asyncbox_1 = require("asyncbox"); const fs_1 = require("./fs"); const semver = __importStar(require("semver")); const shell_quote_1 = require("shell-quote"); Object.defineProperty(exports, "shellParse", { enumerable: true, get: function () { return shell_quote_1.parse; } }); const pluralize_1 = __importDefault(require("pluralize")); const base64_stream_1 = require("base64-stream"); var uuid_1 = require("uuid"); Object.defineProperty(exports, "uuidV1", { enumerable: true, get: function () { return uuid_1.v1; } }); Object.defineProperty(exports, "uuidV3", { enumerable: true, get: function () { return uuid_1.v3; } }); Object.defineProperty(exports, "uuidV4", { enumerable: true, get: function () { return uuid_1.v4; } }); Object.defineProperty(exports, "uuidV5", { enumerable: true, get: function () { return uuid_1.v5; } }); const _lockfile = __importStar(require("lockfile")); /** W3C WebDriver element identifier key used in element objects. */ exports.W3C_WEB_ELEMENT_IDENTIFIER = 'element-6066-11e4-a52e-4f735466cecf'; /** Size of one kibibyte in bytes (1024). */ exports.KiB = 1024; /** Size of one mebibyte in bytes (1024 * 1024). */ exports.MiB = exports.KiB * 1024; /** Size of one gibibyte in bytes (1024 * 1024 * 1024). */ exports.GiB = exports.MiB * 1024; /** * Type guard: returns true if the value is a non-empty string. * * @param val - Value to check * @returns `true` if `val` is a string with at least one character */ function hasContent(val) { return lodash_1.default.isString(val) && val !== ''; } /** * Type guard: returns true if the value is not `undefined`, `null`, or `NaN`. * * @param val - Value to check * @returns `true` if `val` is non-null and non-undefined (and not NaN for numbers) */ function hasValue(val) { if (lodash_1.default.isNumber(val)) { return !lodash_1.default.isNaN(val); } return !lodash_1.default.isUndefined(val) && !lodash_1.default.isNull(val); } /** * Escapes spaces in a string for use in command-line arguments (e.g. ` ` → `\ `). * * @param str - String that may contain spaces * @returns String with spaces escaped by a backslash */ function escapeSpace(str) { return str.split(/ /).join('\\ '); } /** * Escapes special characters in a string (backslash, slash, quotes, control chars). * If `quoteEscape` is provided, that character is also escaped. * * @param str - String to escape, or non-string value (returned unchanged) * @param quoteEscape - Optional character to escape, or `false` to skip * @returns Escaped string, or original value if `str` is not a string */ function escapeSpecialChars(str, quoteEscape) { if (typeof str !== 'string') { return str; } const result = str .replace(/[\\]/g, '\\\\') .replace(/[/]/g, '\\/') .replace(/[\b]/g, '\\b') .replace(/[\f]/g, '\\f') .replace(/[\n]/g, '\\n') .replace(/[\r]/g, '\\r') .replace(/[\t]/g, '\\t') .replace(/["]/g, '\\"') .replace(/\\'/g, "\\'"); if (!quoteEscape) { return result; } const re = new RegExp(quoteEscape, 'g'); return result.replace(re, `\\${quoteEscape}`); } /** * Returns the first non-internal IPv4 address of the machine, if any. * * @returns The local IPv4 address, or `undefined` if none found */ function localIp() { const ifaces = node_os_1.default.networkInterfaces(); for (const addrs of Object.values(ifaces)) { if (!addrs) { continue; } for (const iface of addrs) { if (iface.family === 'IPv4' && !iface.internal) { return iface.address; } } } return undefined; } /** * Creates a promise that resolves after a delay and can be cancelled via `.cancel()`. * * @param ms - Delay in milliseconds before the promise resolves * @returns A Bluebird promise with a `cancel()` method; cancel rejects with CancellationError */ // TODO: replace with a native implementation in Appium 4 function cancellableDelay(ms) { let timer; let resolve; let reject; const delay = new bluebird_1.default((_resolve, _reject) => { resolve = _resolve; reject = _reject; timer = setTimeout(() => resolve(), ms); }); delay.cancel = function () { clearTimeout(timer); reject(new bluebird_1.default.CancellationError()); }; return delay; } /** * Resolves each root path with the given path segments, returning an array of absolute paths. * * @param roots - Base directory paths to resolve against * @param args - Path segments to join with each root (e.g. 'foo', 'bar' → root/foo/bar) * @returns Array of absolute paths, one per root */ function multiResolve(roots, ...args) { return roots.map((root) => node_path_1.default.resolve(root, ...args)); } /** * Parses a value as JSON if it is a string; otherwise returns the value as-is. * * @param obj - String (to parse) or other value (returned unchanged) * @returns Parsed object or original value */ function safeJsonParse(obj) { try { return JSON.parse(obj); } catch { return obj; } } /** * Stringifies an object to JSON, converting Buffers to strings for readable output. * * @param obj - Object to serialize * @param replacer - Optional replacer function (same as JSON.stringify) * @param space - Indentation for pretty-printing. Defaults to 2 * @returns JSON string */ function jsonStringify(obj, replacer = null, space = 2) { const replacerFunc = lodash_1.default.isFunction(replacer) ? replacer : (_k, v) => v; const bufferToJSON = Buffer.prototype.toJSON; delete Buffer.prototype.toJSON; try { return JSON.stringify(obj, (key, value) => { const updatedValue = Buffer.isBuffer(value) ? value.toString('utf8') : value; return replacerFunc(key, updatedValue); }, space); } finally { Buffer.prototype.toJSON = bufferToJSON; } } /** * Extracts the element ID from a W3C or JSONWP element object, or returns the string if already an ID. * * @param el - Element object (with ELEMENT or W3C identifier) or raw element ID string * @returns The element ID string */ function unwrapElement(el) { const elObj = el; for (const propName of [exports.W3C_WEB_ELEMENT_IDENTIFIER, 'ELEMENT']) { if (lodash_1.default.has(elObj, propName)) { return elObj[propName]; } } return el; } /** * Wraps an element ID string in an element object compatible with both W3C and JSONWP. * * @param elementId - The element ID to wrap * @returns Element object with both ELEMENT and W3C identifier keys */ function wrapElement(elementId) { return { ELEMENT: elementId, [exports.W3C_WEB_ELEMENT_IDENTIFIER]: elementId, }; } /** * Returns a copy of the object containing only properties that pass the predicate. * If the predicate is missing, removes properties whose values are undefined. * If the predicate is a scalar, keeps only properties whose value equals that scalar. * If the predicate is a function, calls it for each (value, obj) and keeps properties where it returns true. * * @param obj - Source object to filter * @param predicate - Optional filter: undefined (drop undefined values), scalar (value match), or function * @returns New object with only the properties that pass the predicate */ function filterObject(obj, predicate) { const newObj = lodash_1.default.clone(obj); let pred; if (lodash_1.default.isUndefined(predicate)) { pred = (v) => !lodash_1.default.isUndefined(v); } else if (!lodash_1.default.isFunction(predicate)) { const valuePredicate = predicate; pred = (v) => v === valuePredicate; } else { pred = predicate; } for (const key of Object.keys(obj)) { if (!pred(obj[key], obj)) { delete newObj[key]; } } return newObj; } /** * Converts a byte count to a human-readable size string (e.g. "1.50 MB"). * * @param bytes - Number of bytes (or string coercible to a number) * @returns Formatted string like "123 B", "1.50 KB", "2.00 MB", "3.00 GB" * @throws {Error} If bytes cannot be converted to a non-negative integer */ function toReadableSizeString(bytes) { const intBytes = parseInt(String(bytes), 10); if (isNaN(intBytes) || intBytes < 0) { throw new Error(`Cannot convert '${bytes}' to a readable size format`); } if (intBytes >= exports.GiB) { return `${(intBytes / (exports.GiB * 1.0)).toFixed(2)} GB`; } else if (intBytes >= exports.MiB) { return `${(intBytes / (exports.MiB * 1.0)).toFixed(2)} MB`; } else if (intBytes >= exports.KiB) { return `${(intBytes / (exports.KiB * 1.0)).toFixed(2)} KB`; } return `${intBytes} B`; } /** * Checks whether the given path is a subpath of the given root folder. * * @param originalPath - The absolute file or folder path to test * @param root - The absolute root folder path * @param forcePosix - If true, interpret paths in POSIX format (e.g. on Windows) * @returns `true` if `originalPath` is under `root` * @throws {Error} If either path is not absolute */ function isSubPath(originalPath, root, forcePosix = null) { const pathObj = forcePosix ? node_path_1.default.posix : node_path_1.default; for (const p of [originalPath, root]) { if (!pathObj.isAbsolute(p)) { throw new Error(`'${p}' is expected to be an absolute path`); } } const normalizedRoot = pathObj.normalize(root); const normalizedPath = pathObj.normalize(originalPath); return normalizedPath.startsWith(normalizedRoot); } /** * Checks whether the given paths refer to the same file system entity (same inode). * All paths must exist. * * @param path1 - First path * @param path2 - Second path * @param pathN - Additional paths to compare * @returns `true` if all paths resolve to the same file/directory */ async function isSameDestination(path1, path2, ...pathN) { const allPaths = [path1, path2, ...pathN]; if (!(await (0, asyncbox_1.asyncmap)(allPaths, async (p) => fs_1.fs.exists(p))).every(Boolean)) { return false; } const areAllItemsEqual = (arr) => !!arr.reduce((a, b) => (a === b ? a : NaN)); if (areAllItemsEqual(allPaths)) { return true; } const mapCb = async (x) => (await fs_1.fs.stat(x, { bigint: true })).ino; return areAllItemsEqual(await (0, asyncbox_1.asyncmap)(allPaths, mapCb)); } function coerceVersion(ver, strict = true) { let result = semver.valid(`${ver}`); if (!result) { result = semver.valid(semver.coerce(`${ver}`)); } if (strict && !result) { throw new Error(`'${ver}' cannot be coerced to a valid version number`); } return result; } const SUPPORTED_OPERATORS = ['==', '!=', '>', '<', '>=', '<=', '=']; /** * Compares two version strings using the given operator. * * @param ver1 - First version string * @param operator - One of: ==, !=, >, <, >=, <=, = * @param ver2 - Second version string * @returns `true` if ver1 operator ver2 holds (e.g. "2.0.0" >= "1.0.0") * @throws {Error} If operator is unsupported or either version cannot be coerced */ function compareVersions(ver1, operator, ver2) { if (!SUPPORTED_OPERATORS.includes(operator)) { throw new Error(`The '${operator}' comparison operator is not supported. ` + `Only '${JSON.stringify(SUPPORTED_OPERATORS)}' operators are supported`); } const semverOperator = ['==', '!='].includes(operator) ? '=' : operator; const v1 = coerceVersion(ver1, true); const v2 = coerceVersion(ver2, true); const result = semver.satisfies(v1, `${semverOperator}${v2}`); return operator === '!=' ? !result : result; } /** * Quotes and escapes command-line arguments so they can be safely passed to a shell. * * @param args - Single argument or array of arguments to quote * @returns Quoted string suitable for shell parsing */ function quote(args) { return (0, shell_quote_1.quote)(lodash_1.default.castArray(args)); } /** * Returns the plural or singular form of a word appropriate to the count (e.g. "duck" + 1 → "duck", + 2 → "ducks"). * * @param word - The word to pluralize (or singularize when count is 1) * @param count - The count used to choose singular vs plural * @param options - Options object or boolean: use `inclusive: true` (or `true`) to prefix with the number (e.g. "3 ducks") * @returns The correctly inflected word, optionally prefixed with the count */ function pluralize(word, count, options = {}) { let inclusive = false; if (lodash_1.default.isBoolean(options)) { inclusive = options; } else if (lodash_1.default.isBoolean(options?.inclusive)) { inclusive = options.inclusive; } return (0, pluralize_1.default)(word, count, inclusive); } /** * Reads a file and returns its contents as a base64-encoded buffer. * * @param srcPath - Full path to the file to encode * @param opts - Encoding options (e.g. maxSize to cap buffer size) * @returns Buffer containing the base64-encoded file content * @throws {Error} If the file does not exist, is a directory, cannot be read, or exceeds maxSize */ async function toInMemoryBase64(srcPath, opts = {}) { if (!(await fs_1.fs.exists(srcPath)) || (await fs_1.fs.stat(srcPath)).isDirectory()) { throw new Error(`No such file: ${srcPath}`); } const { maxSize = 1 * exports.GiB } = opts; const resultBuffers = []; let resultBuffersSize = 0; const resultWriteStream = new node_stream_1.default.Writable({ write(buffer, _encoding, next) { resultBuffers.push(buffer); resultBuffersSize += buffer.length; if (maxSize > 0 && resultBuffersSize > maxSize) { resultWriteStream.emit('error', new Error(`The size of the resulting buffer must not be greater than ${toReadableSizeString(maxSize)}`)); } next(); }, }); const readerStream = fs_1.fs.createReadStream(srcPath); const base64EncoderStream = new base64_stream_1.Base64Encode(); const encoderWritable = base64EncoderStream; const encoderReadable = base64EncoderStream; const resultWriteStreamPromise = new Promise((resolve, reject) => { resultWriteStream.once('error', (e) => { readerStream.unpipe(encoderWritable); encoderReadable.unpipe(resultWriteStream); readerStream.destroy(); reject(e); }); resultWriteStream.once('finish', () => resolve()); }); const readStreamPromise = new Promise((resolve, reject) => { readerStream.once('close', () => resolve()); readerStream.once('error', (e) => reject(new Error(`Failed to read '${srcPath}': ${e.message}`))); }); readerStream.pipe(encoderWritable); encoderReadable.pipe(resultWriteStream); await Promise.all([readStreamPromise, resultWriteStreamPromise]); return Buffer.concat(resultBuffers); } /** * Creates a guard that serializes access to a critical section using a lock file. * The returned function acquires the lock, runs the given behavior, then releases the lock. * Also exposes `.check()` to test whether the lock is currently held. * * @param lockFile - Full path to the lock file * @param opts - Options (see {@link LockFileOptions}) * @returns Async function that accepts a callback to run under the lock, plus a `.check()` method */ function getLockFileGuard(lockFile, opts = {}) { const { timeout = 120, tryRecovery = false } = opts; const lock = (0, node_util_1.promisify)(_lockfile.lock); const checkLock = (0, node_util_1.promisify)(_lockfile.check); const unlock = (0, node_util_1.promisify)(_lockfile.unlock); const guard = Object.assign(async (behavior) => { let triedRecovery = false; let acquired = false; while (!acquired) { try { if (_lockfile.checkSync(lockFile)) { await lock(lockFile, { wait: timeout * 1000 }); } else { _lockfile.lockSync(lockFile); } acquired = true; } catch (e) { const err = e; if (lodash_1.default.includes(err.message, 'EEXIST') && tryRecovery && !triedRecovery) { _lockfile.unlockSync(lockFile); triedRecovery = true; } else { throw new Error(`Could not acquire lock on '${lockFile}' after ${timeout}s. ` + `Original error: ${err.message}`); } } } try { return await behavior(); } finally { await unlock(lockFile); } }, { check: () => checkLock(lockFile) }); return guard; } //# sourceMappingURL=util.js.map