Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions Scripts/build-webdriveragent.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ const WDA_BUNDLE_TV_PATH = path.join(DERIVED_DATA_PATH, 'Build', 'Products', 'De
const TARGETS = ['runner', 'tv_runner'];
const SDKS = ['sim', 'tv_sim'];

/**
* Build WebDriverAgent and pack the app bundle into a zip archive.
*
* @param {string} [xcodeVersion] Xcode version to include in archive name.
*/
async function buildWebDriverAgent (xcodeVersion) {
const target = process.env.TARGET;
const sdk = process.env.SDK;
Expand Down Expand Up @@ -77,10 +82,12 @@ async function buildWebDriverAgent (xcodeVersion) {
}

if (isMainModule) {
buildWebDriverAgent().catch((e) => {
try {
await buildWebDriverAgent();
} catch (e) {
LOG.error(e);
process.exit(1);
});
}
}

export default buildWebDriverAgent;
Expand Down
18 changes: 12 additions & 6 deletions Scripts/fetch-prebuilt-wda.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ import { fileURLToPath } from 'node:url';
import { readFileSync } from 'node:fs';
import axios from 'axios';
import { logger, fs, mkdirp, net } from '@appium/support';
import _ from 'lodash';
import B from 'bluebird';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === __filename;

const log = logger.getLogger('WDA');

/**
* Download all prebuilt WebDriverAgent archives for the current package version.
*/
async function fetchPrebuiltWebDriverAgentAssets () {
const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8'));
const tag = packageJson.version;
Expand Down Expand Up @@ -51,20 +52,25 @@ async function fetchPrebuiltWebDriverAgentAssets () {
const url = asset.browser_download_url;
log.info(`Downloading: ${url}`);
try {
const nameOfAgent = _.last(url.split('/'));
const nameOfAgent = url.split('/').at(-1);
if (!nameOfAgent) {
continue;
}
agentsDownloading.push(downloadAgent(url, path.join(webdriveragentsDir, nameOfAgent)));
} catch { }
}

// Wait for them all to finish
return await B.all(agentsDownloading);
return await Promise.all(agentsDownloading);
}

if (isMainModule) {
fetchPrebuiltWebDriverAgentAssets().catch((e) => {
try {
await fetchPrebuiltWebDriverAgentAssets();
} catch (e) {
log.error(e);
process.exit(1);
});
}
}

export default fetchPrebuiltWebDriverAgentAssets;
Expand Down
33 changes: 18 additions & 15 deletions lib/check-dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,9 @@ import {WDA_SCHEME, SDK_SIMULATOR, WDA_RUNNER_APP} from './constants';
import {BOOTSTRAP_PATH} from './utils';
import type {XcodeBuild} from './xcodebuild';

async function buildWDASim(): Promise<void> {
const args = [
'-project',
path.join(BOOTSTRAP_PATH, 'WebDriverAgent.xcodeproj'),
'-scheme',
WDA_SCHEME,
'-sdk',
SDK_SIMULATOR,
'CODE_SIGN_IDENTITY=""',
'CODE_SIGNING_REQUIRED="NO"',
'GCC_TREAT_WARNINGS_AS_ERRORS=0',
];
await exec('xcodebuild', args);
}

/**
* Ensure simulator WDA is built and return the resulting app bundle path.
*/
export async function bundleWDASim(xcodebuild: XcodeBuild): Promise<string> {
const derivedDataPath = await xcodebuild.retrieveDerivedDataPath();
if (!derivedDataPath) {
Expand All @@ -38,3 +26,18 @@ export async function bundleWDASim(xcodebuild: XcodeBuild): Promise<string> {
await buildWDASim();
return wdaBundlePath;
}

async function buildWDASim(): Promise<void> {
const args = [
'-project',
path.join(BOOTSTRAP_PATH, 'WebDriverAgent.xcodeproj'),
'-scheme',
WDA_SCHEME,
'-sdk',
SDK_SIMULATOR,
'CODE_SIGN_IDENTITY=""',
'CODE_SIGNING_REQUIRED="NO"',
'GCC_TREAT_WARNINGS_AS_ERRORS=0',
];
await exec('xcodebuild', args);
}
137 changes: 97 additions & 40 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import {exec, SubProcess} from 'teen_process';
import path, {dirname} from 'node:path';
import {fileURLToPath} from 'node:url';
import {log} from './logger';
import _ from 'lodash';
import {PLATFORM_NAME_TVOS} from './constants';
import B from 'bluebird';
import _fs from 'node:fs';
import {waitForCondition} from 'asyncbox';
import {arch} from 'node:os';
Expand All @@ -19,13 +17,18 @@ const currentFilename =

const currentDirname = dirname(currentFilename);

let moduleRootCache: string | undefined;

/**
* Calculates the path to the current module's root folder
*
* @returns {string} The full path to module root
* @throws {Error} If the current module root folder cannot be determined
*/
const getModuleRoot = _.memoize(function getModuleRoot(): string {
const getModuleRoot = function getModuleRoot(): string {
if (moduleRootCache) {
return moduleRootCache;
}
let currentDir = currentDirname;
let isAtFsRoot = false;
while (!isAtFsRoot) {
Expand All @@ -35,22 +38,37 @@ const getModuleRoot = _.memoize(function getModuleRoot(): string {
_fs.existsSync(manifestPath) &&
JSON.parse(_fs.readFileSync(manifestPath, 'utf8')).name === 'appium-webdriveragent'
) {
moduleRootCache = currentDir;
return currentDir;
}
} catch {}
currentDir = path.dirname(currentDir);
isAtFsRoot = currentDir.length <= path.dirname(currentDir).length;
}
throw new Error('Cannot find the root folder of the appium-webdriveragent Node.js module');
});
};

export const BOOTSTRAP_PATH = getModuleRoot();

/**
* Arguments for setting xctestrun file
*/
export interface XctestrunFileArgs {
deviceInfo: DeviceInfo;
sdkVersion: string;
bootstrapPath: string;
wdaRemotePort: number | string;
wdaBindingIP?: string;
}

/**
* Find and terminate all processes matching the given pgrep pattern.
*/
export async function killAppUsingPattern(pgrepPattern: string): Promise<void> {
const signals = [2, 15, 9];
for (const signal of signals) {
const matchedPids = await getPIDsUsingPattern(pgrepPattern);
if (_.isEmpty(matchedPids)) {
if (matchedPids.length === 0) {
return;
}
const args = [`-${signal}`, ...matchedPids];
Expand All @@ -59,21 +77,24 @@ export async function killAppUsingPattern(pgrepPattern: string): Promise<void> {
} catch (err: any) {
log.debug(`kill ${args.join(' ')} -> ${err.message}`);
}
if (signal === _.last(signals)) {
if (signal === signals[signals.length - 1]) {
// there is no need to wait after SIGKILL
return;
}
try {
await waitForCondition(
async () => {
const pidCheckPromises = matchedPids.map((pid) =>
exec('kill', ['-0', pid])
const pidCheckPromises = matchedPids.map(async (pid) => {
try {
await exec('kill', ['-0', pid]);
// the process is still alive
.then(() => false)
return false;
} catch {
// the process is dead
.catch(() => true),
);
return (await B.all(pidCheckPromises)).every((x) => x === true);
return true;
}
});
return (await Promise.all(pidCheckPromises)).every((x) => x === true);
},
{
waitMs: 1000,
Expand All @@ -93,9 +114,12 @@ export async function killAppUsingPattern(pgrepPattern: string): Promise<void> {
* @returns Return true if the platformName is tvOS
*/
export function isTvOS(platformName: string): boolean {
return _.toLower(platformName) === _.toLower(PLATFORM_NAME_TVOS);
return platformName?.toLowerCase() === PLATFORM_NAME_TVOS.toLowerCase();
}

/**
* Configure keychain access required for real-device code signing.
*/
export async function setRealDeviceSecurity(
keychainPath: string,
keychainPassword: string,
Expand All @@ -106,17 +130,6 @@ export async function setRealDeviceSecurity(
await exec('security', ['set-keychain-settings', '-t', '3600', '-l', keychainPath]);
}

/**
* Arguments for setting xctestrun file
*/
export interface XctestrunFileArgs {
deviceInfo: DeviceInfo;
sdkVersion: string;
bootstrapPath: string;
wdaRemotePort: number | string;
wdaBindingIP?: string;
}

/**
* Creates xctestrun file per device & platform version.
* We expects to have WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device
Expand All @@ -140,7 +153,7 @@ export async function setXctestrunFile(args: XctestrunFileArgs): Promise<string>
wdaRemotePort,
wdaBindingIP,
);
const newXctestRunContent = _.merge(xctestRunContent, updateWDAPort);
const newXctestRunContent = mergeObjects(xctestRunContent, updateWDAPort);
await plist.updatePlistFile(xctestrunFilePath, newXctestRunContent, true);

return xctestrunFilePath;
Expand Down Expand Up @@ -285,6 +298,23 @@ export async function getWDAUpgradeTimestamp(): Promise<number | null> {
return mtime.getTime();
}

/**
* Escape regular expression metacharacters in a string.
*/
export function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

/**
* Truncate a string to the given length and append ellipsis if needed.
*/
export function truncateString(value: string, length: number): string {
if (value.length <= length) {
return value;
}
return `${value.slice(0, Math.max(0, length - 1))}…`;
}

/**
* Kills running XCTest processes for the particular device.
*/
Expand All @@ -296,7 +326,7 @@ export async function resetTestProcesses(udid: string, isSimulator: boolean): Pr
processPatterns.push(`xctest.*${udid}`);
}
log.debug(`Killing running processes '${processPatterns.join(', ')}' for the device ${udid}...`);
await B.all(processPatterns.map(killAppUsingPattern));
await Promise.all(processPatterns.map(killAppUsingPattern));
}

/**
Expand Down Expand Up @@ -329,22 +359,25 @@ export async function getPIDsListeningOnPort(
return result;
}

if (!_.isFunction(filteringFunc)) {
if (typeof filteringFunc !== 'function') {
return result;
}
return await B.filter(result, async (pid) => {
let stdout: string;
try {
({stdout} = await exec('ps', ['-p', pid, '-o', 'command']));
} catch (e: any) {
if (e.code === 1) {
// The process does not exist anymore, there's nothing to filter
return false;
const filtered = await Promise.all(
result.map(async (pid) => {
let stdout: string;
try {
({stdout} = await exec('ps', ['-p', pid, '-o', 'command']));
} catch (e: any) {
if (e.code === 1) {
// The process does not exist anymore, there's nothing to filter
return null;
}
throw e;
}
throw e;
}
return await filteringFunc(stdout);
});
return (await filteringFunc(stdout)) ? pid : null;
}),
);
return filtered.filter((pid): pid is string => Boolean(pid));
}

// Private functions
Expand All @@ -359,7 +392,7 @@ async function getPIDsUsingPattern(pattern: string): Promise<string[]> {
return stdout
.split(/\s+/)
.map((x) => parseInt(x, 10))
.filter(_.isInteger)
.filter(Number.isInteger)
.map((x) => `${x}`);
} catch (err: any) {
log.debug(
Expand All @@ -368,3 +401,27 @@ async function getPIDsUsingPattern(pattern: string): Promise<string[]> {
return [];
}
}

function mergeObjects<T extends Record<string, any>, U extends Record<string, any>>(
target: T,
source: U,
): T & U {
const output: Record<string, any> = {...target};
for (const [key, sourceValue] of Object.entries(source)) {
const targetValue = output[key];
if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
output[key] = mergeObjects(targetValue, sourceValue);
continue;
}
output[key] = sourceValue;
}
return output as T & U;
}

function isPlainObject(value: unknown): value is Record<string, any> {
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
return false;
}
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
}
Loading
Loading