appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
406 lines • 18.7 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.optionalTunnelAvailabilityCheck = exports.OptionalTunnelAvailabilityCheck = exports.optionalIosRemoteXpcDependencyCheck = exports.OptionalIosRemoteXpcDependencyCheck = exports.optionalFfmpegCheck = exports.OptionalFfmpegCheck = exports.optionalApplesimutilsCheck = exports.OptionalApplesimutilsCommandCheck = exports.optionalSimulatorCheck = exports.OptionalSimulatorCheck = void 0;
const utils_1 = require("./utils");
const support_1 = require("appium/support");
const axios_1 = __importDefault(require("axios"));
require("@colors/colors");
const teen_process_1 = require("teen_process");
const memoize_1 = __importDefault(require("lodash/memoize"));
class OptionalSimulatorCheck {
static SUPPORTED_SIMULATOR_PLATFORMS = [
{
displayName: 'iOS',
name: 'iphonesimulator',
},
{
displayName: 'tvOS',
name: 'appletvsimulator',
},
];
log;
async diagnose() {
try {
// https://github.com/appium/appium/issues/12093#issuecomment-459358120
await (0, teen_process_1.exec)('xcrun', ['simctl', 'help']);
}
catch (err) {
return support_1.doctor.nokOptional(`Testing on Simulator is not possible. Cannot run 'xcrun simctl': ${err?.stderr || err.message}`);
}
const sdks = await this._listInstalledSdks();
for (const { displayName, name } of OptionalSimulatorCheck.SUPPORTED_SIMULATOR_PLATFORMS) {
const errorPrefix = `Testing on ${displayName} Simulator is not possible`;
if (!sdks.some(({ platform }) => platform === name)) {
return support_1.doctor.nokOptional(`${errorPrefix}: SDK is not installed`);
}
}
return support_1.doctor.okOptional(`The following Simulator SDKs are installed:\n` +
sdks
.filter(({ platform }) => OptionalSimulatorCheck.SUPPORTED_SIMULATOR_PLATFORMS.some(({ name }) => name === platform))
.map(({ displayName }) => `\t→ ${displayName}`)
.join('\n'));
}
async fix() {
return `Install the desired Simulator SDK from Xcode's Settings -> Components`;
}
hasAutofix() {
return false;
}
isOptional() {
return true;
}
async _listInstalledSdks() {
const { stdout } = await (0, teen_process_1.exec)('xcodebuild', ['-json', '-showsdks']);
return JSON.parse(stdout);
}
}
exports.OptionalSimulatorCheck = OptionalSimulatorCheck;
exports.optionalSimulatorCheck = new OptionalSimulatorCheck();
class OptionalApplesimutilsCommandCheck {
static README_LINK = 'https://github.com/appium/appium-xcuitest-driver/blob/master/docs/reference/execute-methods.md#mobile-setpermission';
log;
async diagnose() {
const applesimutilsPath = await (0, utils_1.resolveExecutablePath)('applesimutils');
return applesimutilsPath
? support_1.doctor.okOptional(`applesimutils is installed at: ${applesimutilsPath}`)
: support_1.doctor.nokOptional('applesimutils are not installed');
}
async fix() {
return `Why ${'applesimutils'.bold} is needed and how to install it: ${OptionalApplesimutilsCommandCheck.README_LINK}`;
}
hasAutofix() {
return false;
}
isOptional() {
return true;
}
}
exports.OptionalApplesimutilsCommandCheck = OptionalApplesimutilsCommandCheck;
exports.optionalApplesimutilsCheck = new OptionalApplesimutilsCommandCheck();
class OptionalFfmpegCheck {
static FFMPEG_BINARY = 'ffmpeg';
static FFMPEG_INSTALL_LINK = 'https://www.ffmpeg.org/download.html';
log;
async diagnose() {
const ffmpegPath = await (0, utils_1.resolveExecutablePath)(OptionalFfmpegCheck.FFMPEG_BINARY);
return ffmpegPath
? support_1.doctor.okOptional(`${OptionalFfmpegCheck.FFMPEG_BINARY} exists at '${ffmpegPath}'`)
: support_1.doctor.nokOptional(`${OptionalFfmpegCheck.FFMPEG_BINARY} cannot be found`);
}
async fix() {
return (`${`${OptionalFfmpegCheck.FFMPEG_BINARY}`.bold} is used to capture screen recordings from the device under test. ` +
`Please read ${OptionalFfmpegCheck.FFMPEG_INSTALL_LINK}.`);
}
hasAutofix() {
return false;
}
isOptional() {
return true;
}
}
exports.OptionalFfmpegCheck = OptionalFfmpegCheck;
exports.optionalFfmpegCheck = new OptionalFfmpegCheck();
const REMOTE_XPC_PACKAGE_NAME = 'appium-ios-remotexpc';
const isRemoteXpcDependencyAvailable = (0, memoize_1.default)(async function ensureRemoteXpcDependencyAvailable() {
try {
// We only care that the module can be imported; we don't need to use it here.
await import(REMOTE_XPC_PACKAGE_NAME);
return true;
}
catch {
return false;
}
});
const getXcuitestDriverRoot = (0, memoize_1.default)(function getXcuitestDriverRoot() {
return support_1.node.getModuleRootSync('appium-xcuitest-driver', __filename);
});
class OptionalIosRemoteXpcDependencyCheck {
static README_LINK = 'https://github.com/appium/appium-ios-remotexpc';
log;
async diagnose() {
const available = await isRemoteXpcDependencyAvailable();
if (available) {
return support_1.doctor.okOptional(`${REMOTE_XPC_PACKAGE_NAME} is installed and can be imported. ` +
`Remote XPC-based features are available for real devices (iOS/tvOS 18+).`);
}
return support_1.doctor.nokOptional(`${REMOTE_XPC_PACKAGE_NAME} is not installed or cannot be imported. ` +
`Install it as an optional dependency if you plan to use Remote XPC-based features ` +
`on real devices (iOS/tvOS 18+). Tests may still run without it, but some ` +
`advanced functionality might not work or be unavailable.`);
}
async fix() {
const driverRoot = getXcuitestDriverRoot();
const locationHint = driverRoot ? `cd "${driverRoot}"; ` : '';
return (`${`${REMOTE_XPC_PACKAGE_NAME}`.bold} provides Remote XPC communication ` +
`and tunneling support for real devices (iOS/tvOS 18+). ` +
`Run '${locationHint}npm install ${REMOTE_XPC_PACKAGE_NAME}'. ` +
`For more information, see ${OptionalIosRemoteXpcDependencyCheck.README_LINK}.`);
}
hasAutofix() {
return false;
}
isOptional() {
return true;
}
}
exports.OptionalIosRemoteXpcDependencyCheck = OptionalIosRemoteXpcDependencyCheck;
exports.optionalIosRemoteXpcDependencyCheck = new OptionalIosRemoteXpcDependencyCheck();
const TUNNEL_SCRIPT_TIMEOUT_MS = 5000;
const API_READY_PATTERN = /:\d+\/remotexpc\/tunnels/;
class OptionalTunnelAvailabilityCheck {
static README_LINK = 'https://github.com/appium/appium-ios-tuntap';
static TUNNEL_CREATION_COMMAND = 'sudo appium driver run xcuitest tunnel-creation';
log;
async diagnose() {
const remoteXpcAvailable = await isRemoteXpcDependencyAvailable();
if (!remoteXpcAvailable) {
return support_1.doctor.nokOptional(`Remote XPC tunnel availability cannot be checked because ` +
`${REMOTE_XPC_PACKAGE_NAME} is not installed or cannot be imported. ` +
`Install it first using the '${REMOTE_XPC_PACKAGE_NAME}' optional check.`);
}
const platform = process.platform;
if (platform !== 'darwin' && platform !== 'linux') {
return support_1.doctor.okOptional(`Tunnel availability status cannot be automatically verified on platform '${platform}'.`);
}
const candidatePorts = await this._getListeningTcpPorts();
if (candidatePorts.length > 0) {
const registryResult = await this._probeTunnelRegistry(candidatePorts);
if (registryResult) {
return registryResult;
}
}
return await this._runTunnelCreationScript();
}
async fix() {
return (`The Remote XPC tunnel infrastructure is used for IPv6 tunneling when testing against real ` +
`devices (iOS/tvOS 18+). ` +
`To explicitly start or verify tunnels when needed, run ` +
`'${OptionalTunnelAvailabilityCheck.TUNNEL_CREATION_COMMAND}' with sudo/root privileges. ` +
`See ${OptionalTunnelAvailabilityCheck.README_LINK} for more details about tunnel usage.`);
}
hasAutofix() {
return false;
}
isOptional() {
return true;
}
/**
* Returns listening TCP ports. Uses pure Node on Linux (/proc/net/tcp, tcp6); uses netstat on macOS.
*/
async _getListeningTcpPorts() {
if (process.platform === 'linux') {
return await this._getListeningTcpPortsLinux();
}
if (process.platform === 'darwin') {
return await this._getListeningTcpPortsDarwin();
}
return [];
}
/**
* Linux: parse /proc/net/tcp and /proc/net/tcp6 (pure Node, no exec). State 0A = LISTEN.
*/
async _getListeningTcpPortsLinux() {
const ports = new Set();
const files = ['/proc/net/tcp', '/proc/net/tcp6'];
for (const file of files) {
try {
const raw = await support_1.fs.readFile(file, 'utf8');
const lines = raw.split('\n');
for (let i = 1; i < lines.length; i++) {
const parts = lines[i].trim().split(/\s+/);
if (parts.length < 4) {
continue;
}
const state = parts[3];
if (state !== '0A') {
continue; // 0A = LISTEN
}
const localAddr = parts[1];
const colon = localAddr.lastIndexOf(':');
if (colon === -1) {
continue;
}
const portHex = localAddr.slice(colon + 1);
const port = Number.parseInt(portHex, 16);
if (Number.isInteger(port) && port > 0 && port <= 65535) {
ports.add(port);
}
}
}
catch {
// File missing or unreadable (e.g. not Linux or permissions)
}
}
return Array.from(ports);
}
/**
* macOS: netstat -anv -p tcp (Node has no API for system-wide listening ports).
*/
async _getListeningTcpPortsDarwin() {
try {
const { stdout } = await (0, teen_process_1.exec)('netstat', ['-anv', '-p', 'tcp']);
const ports = new Set();
for (const line of stdout.split('\n')) {
const trimmed = line.trim();
if (!trimmed || !trimmed.toLowerCase().startsWith('tcp')) {
continue;
}
const parts = trimmed.split(/\s+/);
if (parts.length < 4) {
continue;
}
const portMatch = /\.(\d+)$/.exec(parts[3]);
if (!portMatch) {
continue;
}
const port = Number.parseInt(portMatch[1], 10);
if (Number.isInteger(port) && port > 0) {
ports.add(port);
}
}
return Array.from(ports);
}
catch {
return [];
}
}
/**
* Probes candidate ports for the tunnel registry API in parallel; resolves as soon as any succeed, else null.
*/
async _probeTunnelRegistry(ports) {
if (ports.length === 0) {
return null;
}
return await new Promise((resolve) => {
let settled = false;
let remaining = ports.length;
const maybeResolveNull = () => {
remaining -= 1;
if (!settled && remaining === 0) {
settled = true;
resolve(null);
}
};
for (const port of ports) {
void (async () => {
try {
const res = await axios_1.default.get(`http://127.0.0.1:${port}/remotexpc/tunnels`, {
timeout: 1000,
validateStatus: (status) => status === 200,
});
const data = res.data;
if (!settled && data != null && typeof data === 'object' && data.status === 'OK') {
settled = true;
resolve(support_1.doctor.okOptional(`Detected an active Remote XPC tunnel registry process on port ${port}. ` +
`The Remote XPC tunnel infrastructure appears to be available, so Remote XPC-based ` +
`features for real devices (iOS/tvOS 18+) should be available.`));
return;
}
}
catch {
// Ignore individual probe failures; we'll resolve to null only if all fail.
}
if (!settled) {
maybeResolveNull();
}
})();
}
});
}
/**
* Runs the tunnel-creation driver script as a subprocess to avoid blocking doctor if the script hangs.
* Waits for exit, TUNNEL_SCRIPT_TIMEOUT_MS (5s), or output string indicating registry is up;
* then evaluates or stops the process.
*/
async _runTunnelCreationScript() {
const homeCwd = process.env.HOME || process.cwd();
const driverRoot = getXcuitestDriverRoot();
let combinedOutput = '';
let resolveApiReady;
const apiReadyPromise = new Promise((resolve) => {
resolveApiReady = () => resolve({ reason: 'api' });
});
const sub = driverRoot != null
? new teen_process_1.SubProcess(process.execPath, ['./scripts/tunnel-creation.mjs'], { cwd: driverRoot })
: new teen_process_1.SubProcess('appium', ['driver', 'run', 'xcuitest', 'tunnel-creation'], {
cwd: homeCwd,
});
const appendLine = (line) => {
combinedOutput += line + '\n';
if (API_READY_PATTERN.test(line)) {
resolveApiReady();
}
};
sub.on('line-stdout', appendLine);
sub.on('line-stderr', appendLine);
const exitPromise = new Promise((resolve) => {
sub.once('exit', (code, signal) => resolve({ reason: 'exit', code, signal }));
});
const timeoutPromise = new Promise((resolve) => {
setTimeout(() => resolve({ reason: 'timeout' }), TUNNEL_SCRIPT_TIMEOUT_MS);
});
try {
await sub.start(0);
}
catch (err) {
const message = (err.stderr || err.message || '').toString();
return support_1.doctor.nokOptional(`Could not start '${OptionalTunnelAvailabilityCheck.TUNNEL_CREATION_COMMAND}'. ` +
`Without a working tunnel, Remote XPC-based functionality on real devices (iOS/tvOS 18+) might not work or be unavailable. ` +
`Details: ${message}`);
}
const winner = await Promise.race([exitPromise, timeoutPromise, apiReadyPromise]);
if (winner.reason === 'exit') {
const code = winner.code;
return this._evaluateTunnelScriptOutput(combinedOutput.trim(), code);
}
if (sub.isRunning) {
try {
await sub.stop('SIGTERM', 500);
}
catch {
// Subprocess did not exit within 500ms; force-kill so doctor process can exit
if (sub.pid != null) {
try {
process.kill(sub.pid, 'SIGKILL');
}
catch {
// ignore if already gone
}
}
}
}
return support_1.doctor.okOptional(`The tunnel script was started; the registry was detected or the check timed out. ` +
`Tunnel infrastructure for real devices (iOS/tvOS 18+) should be available when run with sufficient privileges.`);
}
/**
* Interprets tunnel-creation script stdout+stderr and optional exit code; returns the appropriate doctor result.
* Output pattern matches take priority over a non-zero exit code.
*/
_evaluateTunnelScriptOutput(combinedOutput, exitCode) {
if (/No devices found/i.test(combinedOutput)) {
return support_1.doctor.okOptional(`The Remote XPC tunnel-creation script can be invoked via '${OptionalTunnelAvailabilityCheck.TUNNEL_CREATION_COMMAND}', ` +
`but no real devices are currently connected.`);
}
if (/must be run as root|operation not permitted|permission denied/i.test(combinedOutput)) {
return support_1.doctor.okOptional(`The tunnel-creation script '${OptionalTunnelAvailabilityCheck.TUNNEL_CREATION_COMMAND}' is available, ` +
`but did not run with elevated privileges (e.g. root check or TUN/TAP creation). ` +
`This is expected when not running with sudo/root. ` +
`When you actually need Remote XPC-based functionality for real devices (iOS/tvOS 18+), ` +
`run the same command with sufficient privileges to establish the tunnel.`);
}
if (exitCode != null && exitCode !== 0) {
return support_1.doctor.nokOptional(`The tunnel script exited with code ${exitCode}. ` +
`Without a working tunnel, Remote XPC-based functionality on real devices (iOS/tvOS 18+) might not work. ` +
(combinedOutput ? `Output:\n${combinedOutput}` : ''));
}
return support_1.doctor.okOptional(`Successfully ran '${OptionalTunnelAvailabilityCheck.TUNNEL_CREATION_COMMAND}'. ` +
`The Remote XPC tunnel infrastructure should be available for creating tunnels.` +
(combinedOutput ? `\nLast output:\n${combinedOutput}` : ''));
}
}
exports.OptionalTunnelAvailabilityCheck = OptionalTunnelAvailabilityCheck;
exports.optionalTunnelAvailabilityCheck = new OptionalTunnelAvailabilityCheck();
//# sourceMappingURL=optional-checks.js.map