@appium/support
Version:
Support libs used across appium packages
543 lines • 21.6 kB
JavaScript
;
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.shellParse = exports.uuidV5 = exports.uuidV4 = exports.uuidV3 = exports.uuidV1 = exports.KiB = exports.MiB = exports.GiB = exports.W3C_WEB_ELEMENT_IDENTIFIER = 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.wrapElement = wrapElement;
exports.unwrapElement = unwrapElement;
exports.filterObject = filterObject;
exports.toReadableSizeString = toReadableSizeString;
exports.isSubPath = isSubPath;
exports.isSameDestination = isSameDestination;
exports.compareVersions = compareVersions;
exports.coerceVersion = coerceVersion;
exports.quote = quote;
exports.jsonStringify = jsonStringify;
exports.pluralize = pluralize;
exports.toInMemoryBase64 = toInMemoryBase64;
exports.getLockFileGuard = getLockFileGuard;
const bluebird_1 = __importDefault(require("bluebird"));
const lodash_1 = __importDefault(require("lodash"));
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
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 stream_1 = __importDefault(require("stream"));
const base64_stream_1 = require("base64-stream");
const 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"));
const W3C_WEB_ELEMENT_IDENTIFIER = 'element-6066-11e4-a52e-4f735466cecf';
exports.W3C_WEB_ELEMENT_IDENTIFIER = W3C_WEB_ELEMENT_IDENTIFIER;
const KiB = 1024;
exports.KiB = KiB;
const MiB = KiB * 1024;
exports.MiB = MiB;
const GiB = MiB * 1024;
exports.GiB = GiB;
/**
* @template {string} T
* @param {T} val
* @returns {val is NonEmptyString<T>}
*/
function hasContent(val) {
return lodash_1.default.isString(val) && val !== '';
}
/**
* return true if the the value is not `undefined`, `null`, or `NaN`.
*
* XXX: `NaN` is not expressible in TypeScript.
* @template T
* @param {T} val
* @returns {val is NonNullable<T>}
*/
function hasValue(val) {
// avoid incorrectly evaluating `0` as false
if (lodash_1.default.isNumber(val)) {
return !lodash_1.default.isNaN(val);
}
return !lodash_1.default.isUndefined(val) && !lodash_1.default.isNull(val);
}
// escape spaces in string, for commandline calls
function escapeSpace(str) {
return str.split(/ /).join('\\ ');
}
function escapeSpecialChars(str, quoteEscape) {
if (typeof str !== 'string') {
return str;
}
if (typeof quoteEscape === 'undefined') {
quoteEscape = false;
}
str = str
.replace(/[\\]/g, '\\\\')
.replace(/[\/]/g, '\\/') // eslint-disable-line no-useless-escape
.replace(/[\b]/g, '\\b')
.replace(/[\f]/g, '\\f')
.replace(/[\n]/g, '\\n')
.replace(/[\r]/g, '\\r')
.replace(/[\t]/g, '\\t')
.replace(/[\"]/g, '\\"') // eslint-disable-line no-useless-escape
.replace(/\\'/g, "\\'");
if (quoteEscape) {
let re = new RegExp(quoteEscape, 'g');
str = str.replace(re, `\\${quoteEscape}`);
}
return str;
}
function localIp() {
let ip = lodash_1.default.chain(os_1.default.networkInterfaces())
.values()
.flatten()
// @ts-ignore this filter works fine
.filter(({ family, internal }) => family === 'IPv4' && internal === false)
.map('address')
.first()
.value();
return ip;
}
/*
* Creates a promise that is cancellable, and will timeout
* after `ms` delay
*/
function cancellableDelay(ms) {
let timer;
let resolve;
let reject;
const delay = new bluebird_1.default.Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
timer = setTimeout(function () {
resolve();
}, ms);
});
// override Bluebird's `cancel`, which does not work when using `await` on
// a promise, since `resolve`/`reject` are never called
delay.cancel = function () {
clearTimeout(timer);
// eslint-disable-next-line import/no-named-as-default-member
reject(new bluebird_1.default.CancellationError());
};
return delay;
}
function multiResolve(roots, ...args) {
return roots.map((root) => path_1.default.resolve(root, ...args));
}
/**
* Parses an object if possible. Otherwise returns the object without parsing.
*
* @param {any} obj
* @returns {any}
*/
function safeJsonParse(obj) {
try {
return JSON.parse(obj);
}
catch {
// ignore: this is not json parsable
return obj;
}
}
/**
* Stringifies the object passed in, converting Buffers into Strings for better
* display. This mimics JSON.stringify (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)
* except the `replacer` argument can only be a function.
*
* @param {any} obj - the object to be serialized
* @param {((key:any, value:any) => any)?} replacer - function to transform the properties added to the
* serialized object
* @param {number|string|undefined} space - used to insert white space into the output JSON
* string for readability purposes. Defaults to 2
* @returns {string} - the JSON object serialized as a string
*/
function jsonStringify(obj, replacer = null, space = 2) {
// if no replacer is passed, or it is not a function, just use a pass-through
const replacerFunc = lodash_1.default.isFunction(replacer) ? replacer : (k, v) => v;
// Buffers cannot be serialized in a readable way
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 {
// restore the function, so as to not break further serialization
Buffer.prototype.toJSON = bufferToJSON;
}
}
/**
* Removes the wrapper from element, if it exists.
* { ELEMENT: 4 } becomes 4
* { element-6066-11e4-a52e-4f735466cecf: 5 } becomes 5
* @param {import('@appium/types').Element|string} el
* @returns {string}
*/
function unwrapElement(el) {
for (const propName of [W3C_WEB_ELEMENT_IDENTIFIER, 'ELEMENT']) {
if (lodash_1.default.has(el, propName)) {
return /** @type {string} */ (el[propName]);
}
}
return /** @type {string} */ (el);
}
/**
*
* @param {string} elementId
* @returns {import('@appium/types').Element}
*/
function wrapElement(elementId) {
return {
ELEMENT: elementId,
[W3C_WEB_ELEMENT_IDENTIFIER]: elementId,
};
}
/*
* Returns object consisting of all properties in the original element
* which were truthy given the predicate.
* If the predicate is
* * missing - it will remove all properties whose values are `undefined`
* * a scalar - it will test all properties' values against that value
* * a function - it will pass each value and the original object into the function
*/
function filterObject(obj, predicate) {
let newObj = lodash_1.default.clone(obj);
if (lodash_1.default.isUndefined(predicate)) {
// remove any element from the object whose value is undefined
predicate = (v) => !lodash_1.default.isUndefined(v);
}
else if (!lodash_1.default.isFunction(predicate)) {
// make predicate into a function
const valuePredicate = predicate;
predicate = (v) => v === valuePredicate;
}
for (const key of Object.keys(obj)) {
if (!predicate(obj[key], obj)) {
delete newObj[key];
}
}
return newObj;
}
/**
* Converts number of bytes to a readable size string.
*
* @param {number|string} bytes - The actual number of bytes.
* @returns {string} The actual string representation, for example
* '1.00 KB' for '1024 B'
* @throws {Error} If bytes count cannot be converted to an integer or
* if it is less than zero.
*/
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 >= GiB) {
return `${(intBytes / (GiB * 1.0)).toFixed(2)} GB`;
}
else if (intBytes >= MiB) {
return `${(intBytes / (MiB * 1.0)).toFixed(2)} MB`;
}
else if (intBytes >= KiB) {
return `${(intBytes / (KiB * 1.0)).toFixed(2)} KB`;
}
return `${intBytes} B`;
}
/**
* Checks whether the given path is a subpath of the
* particular root folder. Both paths can include .. and . specifiers
*
* @param {string} originalPath The absolute file/folder path
* @param {string} root The absolute root folder path
* @param {?boolean} forcePosix Set it to true if paths must be interpreted in POSIX format
* @returns {boolean} true if the given original path is the subpath of the root folder
* @throws {Error} if any of the given paths is not absolute
*/
function isSubPath(originalPath, root, forcePosix = null) {
const pathObj = forcePosix ? path_1.default.posix : 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 are pointing to the same file system
* destination.
*
* @param {string} path1 - Absolute or relative path to a file/folder
* @param {string} path2 - Absolute or relative path to a file/folder
* @param {...string} pathN - Zero or more absolute or relative paths to files/folders
* @returns {Promise<boolean>} true if all paths are pointing to the same file system item
*/
async function isSameDestination(path1, path2, ...pathN) {
const allPaths = [path1, path2, ...pathN];
if (!(await bluebird_1.default.reduce(allPaths, async (a, b) => a && (await fs_1.fs.exists(b)), true))) {
return false;
}
const areAllItemsEqual = (arr) => !!arr.reduce((a, b) => (a === b ? a : NaN));
if (areAllItemsEqual(allPaths)) {
return true;
}
let mapCb = async (x) => (await fs_1.fs.stat(x, {
bigint: true,
})).ino;
return areAllItemsEqual(await bluebird_1.default.map(allPaths, mapCb));
}
/**
* Coerces the given number/string to a valid version string
*
* @template {boolean} [Strict=true]
* @param {string} ver - Version string to coerce
* @param {Strict} [strict] - If `true` then an exception will be thrown
* if `ver` cannot be coerced
* @returns {Strict extends true ? string : string|null} Coerced version number or null if the string cannot be
* coerced and strict mode is disabled
* @throws {Error} if strict mode is enabled and `ver` cannot be coerced
*/
function coerceVersion(ver, strict = /** @type {Strict} */ (true)) {
const result = semver.valid(semver.coerce(`${ver}`));
if (strict && !result) {
throw new Error(`'${ver}' cannot be coerced to a valid version number`);
}
return /** @type {Strict extends true ? string : string?} */ (result);
}
const SUPPORTED_OPERATORS = ['==', '!=', '>', '<', '>=', '<=', '='];
/**
* Compares two version strings
*
* @param {string} ver1 - The first version number to compare. Should be a valid
* version number supported by semver parser.
* @param {string} ver2 - The second version number to compare. Should be a valid
* version number supported by semver parser.
* @param {string} operator - One of supported version number operators:
* ==, !=, >, <, <=, >=, =
* @returns {boolean} true or false depending on the actual comparison result
* @throws {Error} if an unsupported operator is supplied or any of the supplied
* version strings 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 result = semver.satisfies(coerceVersion(ver1), `${semverOperator}${coerceVersion(ver2)}`);
return operator === '!=' ? !result : result;
}
/**
* Add appropriate quotes to command arguments. See https://github.com/substack/node-shell-quote
* for more details
*
* @param {string|string[]} args - The arguments that will be parsed
* @returns {string} - The arguments, quoted
*/
function quote(args) {
return (0, shell_quote_1.quote)(lodash_1.default.castArray(args));
}
/**
* @typedef PluralizeOptions
* @property {boolean} [inclusive=false] - Whether to prefix with the number (e.g., 3 ducks)
*/
/**
* Get the form of a word appropriate to the count
*
* @param {string} word - The word to pluralize
* @param {number} count - How many of the word exist
* @param {PluralizeOptions|boolean} options - options for word pluralization,
* or a boolean indicating the options.inclusive property
* @returns {string} The word pluralized according to the number
*/
function pluralize(word, count, options = {}) {
let inclusive = false;
if (lodash_1.default.isBoolean(options)) {
// if passed in as a boolean
inclusive = options;
}
else if (lodash_1.default.isBoolean(options?.inclusive)) {
// if passed in as an options hash
inclusive = options.inclusive;
}
return (0, pluralize_1.default)(word, count, inclusive);
}
/**
* @typedef EncodingOptions
* @property {number} [maxSize=1073741824] The maximum size of
* the resulting buffer in bytes. This is set to 1GB by default, because
* Appium limits the maximum HTTP body size to 1GB. Also, the NodeJS heap
* size must be enough to keep the resulting object (usually this size is
* limited to 1.4 GB)
*/
/**
* Converts contents of a local file to an in-memory base-64 encoded buffer.
* The operation is memory-usage friendly and should be used while encoding
* large files to base64
*
* @param {string} srcPath The full path to the file being encoded
* @param {EncodingOptions} opts
* @returns {Promise<Buffer>} base64-encoded content of the source file as memory buffer
* @throws {Error} if there was an error while reading the source file
* or the source file is too
*/
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 * GiB } = opts;
const resultBuffers = [];
let resultBuffersSize = 0;
const resultWriteStream = new 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 resultWriteStreamPromise = new bluebird_1.default((resolve, reject) => {
resultWriteStream.once('error', (e) => {
readerStream.unpipe(base64EncoderStream);
base64EncoderStream.unpipe(resultWriteStream);
readerStream.destroy();
reject(e);
});
resultWriteStream.once('finish', resolve);
});
const readStreamPromise = new bluebird_1.default((resolve, reject) => {
readerStream.once('close', resolve);
readerStream.once('error', (e) => reject(new Error(`Failed to read '${srcPath}': ${e.message}`)));
});
readerStream.pipe(base64EncoderStream);
base64EncoderStream.pipe(resultWriteStream);
await bluebird_1.default.all([readStreamPromise, resultWriteStreamPromise]);
return Buffer.concat(resultBuffers);
}
/**
* @typedef LockFileOptions
* @property {number} [timeout=120] The max time in seconds to wait for the lock
* @property {boolean} [tryRecovery=false] Whether to try lock recovery if
* the first attempt to acquire it timed out.
*/
/**
* Create an async function which, when called, will not proceed until a certain file is no
* longer present on the system. This allows for preventing concurrent behavior across processes
* using a known lockfile path.
*
* @template T
* @param {string} lockFile The full path to the file used for the lock
* @param {LockFileOptions} opts
* @returns async function that takes another async function defining the locked
* behavior
*/
function getLockFileGuard(lockFile, opts = {}) {
const { timeout = 120, tryRecovery = false } = opts;
const lock = /** @type {(lockfile: string, opts: import('lockfile').Options)=>B<void>} */ (bluebird_1.default.promisify(_lockfile.lock));
const check = bluebird_1.default.promisify(_lockfile.check);
const unlock = bluebird_1.default.promisify(_lockfile.unlock);
/**
* @param {(...args: any[]) => T} behavior
* @returns {Promise<T>}
*/
const guard = async (behavior) => {
let triedRecovery = false;
do {
try {
// if the lockfile doesn't exist, lock it synchronously to make sure no other call
// on the same spin of the event loop can also initiate a lock. If the lockfile does exist
// then just use the regular async 'lock' method which will wait on the lock.
if (_lockfile.checkSync(lockFile)) {
await lock(lockFile, { wait: timeout * 1000 });
}
else {
_lockfile.lockSync(lockFile);
}
break;
}
catch (e) {
if (lodash_1.default.includes(e.message, 'EEXIST') && tryRecovery && !triedRecovery) {
// There could be cases where a process has been forcefully terminated
// without a chance to clean up pending locks: https://github.com/npm/lockfile/issues/26
_lockfile.unlockSync(lockFile);
triedRecovery = true;
continue;
}
throw new Error(`Could not acquire lock on '${lockFile}' after ${timeout}s. ` +
`Original error: ${e.message}`);
}
// eslint-disable-next-line no-constant-condition
} while (true);
try {
return await behavior();
}
finally {
// whether the behavior succeeded or not, get rid of the lock
await unlock(lockFile);
}
};
guard.check = async () => await check(lockFile);
return guard;
}
/**
* A `string` which is never `''`.
*
* @template {string} T
* @typedef {T extends '' ? never : T} NonEmptyString
*/
//# sourceMappingURL=util.js.map