multi-automator
Version:
Multi terminal automation
454 lines (453 loc) • 15.1 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/**
* @desc: WebDriverAgent
* @author: john_chen
* @date: 2024.12.30
*/
const portfinder_1 = __importDefault(require("portfinder"));
const child_process_1 = require("child_process");
const config_1 = require("../config");
const time_1 = require("../utils/time");
const axios_1 = __importDefault(require("axios"));
class WDA {
constructor(uuid, wdaProjPath) {
this.uuid = uuid;
this.wdaProjPath = wdaProjPath;
this.webDriverAgent = null;
this.iProxy = null;
this.ip = '127.0.0.1';
this.port = 0;
this.timeout = 20000;
this.sessionId = '';
}
async init() {
config_1.logger.info('[WDA.init]');
try {
await this.stop();
await this.start();
config_1.logger.info('[WDA.init] start success');
}
catch (error) {
await this.stop();
const errorMessage = error instanceof Error ? error.message : String(error);
config_1.logger.error(`[WDA.init] start failed: ${errorMessage}`);
throw new Error(`[WDA.init] start failed: ${errorMessage}`);
}
}
/**
* 启动 WDA
*/
async start(timeout = 90000) {
const expiredTime = (0, time_1.currentTimestamp)() + timeout;
try {
await this.checkIproxyInstall();
await this.launchWda();
this.port = await portfinder_1.default.getPortPromise();
if (!this.port) {
throw new Error('[WDA.start] allocate port failed');
}
this.iProxy = await this.launchIproxy(this.port, 8100);
// 优化状态检查逻辑
const isReady = await this.waitForReady(expiredTime);
if (!isReady) {
throw new Error(`initialize iOS device timeout ${timeout}ms`);
}
await this.initializeSession();
await this.home();
config_1.logger.info(`[IOS.init] sessionId: ${this.sessionId}`);
}
catch (error) {
await this.stop();
throw error;
}
}
/**
* 停止 WDA
*/
async stop() {
await this.close();
await this.clear();
}
/**
* 关闭wda不正常结束的进程
*/
async close() {
if (this.webDriverAgent && this.webDriverAgent.kill) {
this.webDriverAgent.kill('SIGINT');
}
config_1.logger.info('[IOS.close] close wda process');
if (this.iProxy && this.iProxy.kill) {
this.iProxy.kill('SIGINT');
}
config_1.logger.info('[IOS.close] close iproxy process');
}
/**
* 关闭xcodebuild、iproxy代理进程
*/
async clear() {
(0, child_process_1.spawnSync)(`ps -A | grep -v 'grep' | grep 'xcodebuild' | grep ${this.uuid} | awk '{print $1}' | xargs kill`, {
shell: true,
timeout: 20000
});
config_1.logger.info('[IOS.clear] clear wda process');
(0, child_process_1.spawnSync)(`ps -A | grep -v 'grep' | grep 'iproxy' | grep ${this.uuid} | awk '{print $1}' | xargs kill`, {
shell: true,
timeout: 20000
});
config_1.logger.info('[IOS.clear] clear iproxy process');
}
/**
* home 键
*
* @param {number} timeout 超时时间
*/
async home(timeout = 20000) {
return await this.post('/wda/homescreen', {}, { timeout, withSession: false });
}
/**
* 启动 APP
*
* @param packageName 包名
* @returns
*/
async launchApp(packageName) {
return await this.post('/wda/apps/launch', {
bundleId: packageName,
arguments: [],
environment: {},
shouldWaitForQuiescence: false
}, {
withSession: true
});
}
/**
* 终止 APP
*
* @param packageName 包名
*/
async terminateApp(packageName) {
return await this.post('/wda/apps/terminate', { bundleId: packageName }, {
withSession: true
});
}
/**
* 激活 APP
*
* @param packageName 包名
*/
async activateApp(packageName) {
return await this.post('/wda/apps/activate', { bundleId: packageName }, {
withSession: true
});
}
/**
* 获取当前设备页面 dom 树
*
* @param {number} timeout 超时时间
* @returns {Promise<any>}
*/
async getSource(timeout = 20000) {
return await this.get('/source', { timeout, withSession: false });
}
/**
* 获取当前设备屏幕信息
*
* @returns {Promise<any>}
*/
async getScreenInfo() {
return await this.get('/wda/screen', { timeout: this.timeout, withSession: true });
}
/**
* 获取屏幕宽高
*/
async getScreenSize() {
return await this.get('/window/size', { timeout: this.timeout, withSession: true });
}
/**
* 获取当前设备页面截图
*
* @returns {Promise<Buffer>}
*/
async screenshot() {
const res = await this.get('/screenshot', { timeout: this.timeout, withSession: false });
return Buffer.from(res, 'base64');
}
/**
* 屏幕点击
*
* @param {number} x 横坐标
* @param {number} y 纵坐标
*/
async tap(x, y) {
return await this.post('/wda/tap', { x, y }, { timeout: this.timeout, withSession: true });
}
/**
* 长按屏幕
*
* @param {number} x 横坐标
* @param {number} y 纵坐标
* @param {number} duration 长按时间(s)
*/
async longpress(x, y, duration) {
return await this.post('/wda/touchAndHold', {
x,
y,
duration
}, { timeout: this.timeout, withSession: true });
}
/**
* 跳转页面(通过safari)
*
* @param {string} url 页面地址
*/
async openUrl(url) {
return await this.post('/url', { url }, { timeout: this.timeout, withSession: true });
}
/**
* 从某个点滚动到某个点
*
* @param {number} fromX 起点横坐标
* @param {number} fromY 起点纵坐标
* @param {number} toX 终点横坐标
* @param {number} toY 终点纵坐标
* @param {number} duration 滑动时间
*/
async drag(fromX, fromY, toX, toY, duration) {
return await this.post('/wda/dragfromtoforduration', {
fromX,
fromY,
toX,
toY,
duration
}, { withSession: true });
}
/**
* 重新激活当前活动的应用(先home桌面,再打开该应用)
*/
async deactivateApp() {
return await this.post('/wda/deactivateApp', {}, { withSession: true });
}
async findElements(using, value) {
return await this.post('/elements', { using, value }, { withSession: true });
}
/**
* 等待 WDA 启动
*
* @param {number} expiredTime 超时时间
* @returns {boolean} 是否启动成功
*/
async waitForReady(expiredTime) {
while ((0, time_1.currentTimestamp)() < expiredTime) {
try {
const response = await this.get('/status', {
withSession: false,
timeout: 30000
});
if (response.ready)
return true;
}
catch (error) {
// 忽略错误,继续尝试
config_1.logger.warn(`[WDA.waitForReady] ${error.message}`);
}
await (0, time_1.delay)(50);
}
return false;
}
/**
* 初始化 WDA 会话
*/
async initializeSession() {
const capabilities = {
bundleId: 'com.apple.mobilesafari',
arguments: [],
environment: {},
shouldWaitForQuiescence: false,
defaultAlertAction: 'accept'
};
const response = await this.post('/session', {
desiredCapabilities: capabilities,
capabilities: {
alwaysMatch: capabilities
}
}, {
withSession: false,
timeout: 60000
});
const typedResponse = response;
this.sessionId = typedResponse.sessionId;
if (!this.sessionId) {
throw new Error('初始化 WDA 会话失败');
}
}
/**
* 发送 WebDriverAgent GET 请求
*
* @param {string} path 请求路径
* @param {WDARequestOptions} options 请求选项
* @returns {Promise<T>} 响应数据
*/
async get(path, options) {
const { retry = 3, duration = 1000, timeout = this.timeout, withSession = true } = options;
const fullURL = this.buildURL(path, withSession);
await (0, time_1.delay)(1000);
return this.sendRequest('get', fullURL, undefined, retry, duration, timeout);
}
/**
* 发送 WebDriverAgent POST 请求
*
* @param {string} path 请求路径
* @param {unknown} data 请求数据
* @param {WDARequestOptions} options 请求选项
* @returns {Promise<T>} 响应数据
*/
async post(path, data = {}, options) {
const { retry = 3, duration = 1000, timeout = this.timeout, withSession = true } = options;
const fullURL = this.buildURL(path, withSession);
await (0, time_1.delay)(2000);
return this.sendRequest('post', fullURL, data, retry, duration, timeout);
}
/**
* 发送 WebDriverAgent 请求
*
* @param {string} method 请求方法
* @param {string} url 请求 URL
* @param {unknown} data 请求数据
* @param {number} retry 重试次数
* @param {number} duration 请求间隔时间
* @param {number} timeout 请求超时时间
* @returns {Promise<T>} 响应数据
*/
async sendRequest(method, url, data, retry = 3, duration = 1000, timeout = this.timeout) {
for (let attempt = 0; attempt <= retry; attempt++) {
try {
const config = {
timeout,
headers: { 'Content-Type': 'application/json' }
};
const response = method === 'get'
? await axios_1.default.get(url, config)
: await axios_1.default.post(url, data, config);
return response.data.value;
}
catch (err) {
const isConnectionError = err instanceof Error &&
['ECONNRESET', 'ECONNABORTED'].includes(err.code);
if (isConnectionError) {
await (0, time_1.delay)(method === 'get' ? 30000 : 10000);
}
else if (attempt === retry) {
throw new Error(`WebDriverAgent ${method.toUpperCase()} 请求 ${url} 失败: ${err instanceof Error ? err.message : String(err)}`);
}
await (0, time_1.delay)(duration);
}
}
throw new Error(`请求失败: 超过最大重试次数`);
}
/**
* 构建 WebDriverAgent 请求 URL
*
* @param {string} path 请求路径
* @param {boolean} withSession 是否包含会话 ID
* @returns {string} 请求 URL
*/
buildURL(path, withSession) {
path = path.startsWith('/') ? path : `/${path}`;
if (withSession) {
path = `/session/${this.sessionId}${path}`;
}
return `http://${this.ip}:${this.port}${path}`;
}
/**
* 检查 iProxy 是否安装
*/
async checkIproxyInstall() {
let { stderr, status } = (0, child_process_1.spawnSync)('iProxy -v', {
shell: true,
timeout: 20000
});
if (0 !== status) {
throw new Error(`未正确安装 iProxy: ${stderr.toString()},请执行命令 'brew install libimobiledevice' 进行安装`);
}
}
/**
* 启动 WDA
*/
async launchWda() {
let wda = (0, child_process_1.spawn)('xcodebuild', [
`-project ${this.wdaProjPath}`,
'-scheme WebDriverAgentRunner',
`-destination "id=${this.uuid}"`,
'test',
], { shell: true });
await new Promise((resolve, reject) => {
wda.stdout.on('data', res => {
if (res.toString().includes('ServerURLHere')) {
config_1.logger.info('[IOS.launchWda] WDA Start');
resolve(true);
config_1.logger.info(`[WDA ServerURL] ${res.toString()}`);
}
});
wda.stderr.on('data', res => {
if (res.toString().includes('Testing failed') || res.toString().includes('Failing tests')) {
config_1.logger.error('[IOS.launchWda] WDA Fail');
wda.kill();
this.launchWda();
}
});
wda.on('close', code => {
if (0 !== code) {
reject(new Error(`wda 执行异常,请检查 WebDriverAgent.xcodeproj 项目 ${this.wdaProjPath} 是否正确配置: ` + `错误代号-${code}`));
}
});
});
this.webDriverAgent = wda;
}
/**
* 获取 iProxy 类型
*/
async getIproxyType() {
let iproxy = 'libusbmuxd';
let { stdout } = (0, child_process_1.spawnSync)('iProxy -v', {
shell: true,
timeout: 20000
});
if (!stdout.toString().split('\n')[0].startsWith('iproxy')) {
iproxy = 'usbmuxd';
}
return iproxy;
}
/**
* 启动 iProxy
*
* @param {number} localIp 本地端口号
* @param {number} remoteIp 远程端口号
* @returns {any} iProxy 进程
*/
async launchIproxy(localIp, remoteIp) {
let iproxyType = await this.getIproxyType();
let iproxyCmd = ['-u', this.uuid, localIp, remoteIp];
if (iproxyType === 'usbmuxd') {
iproxyCmd = [localIp, remoteIp, this.uuid];
}
let iProxy = (0, child_process_1.spawn)('iproxy', iproxyCmd.map(String), { shell: true });
iProxy.stdout.on('data', (res) => {
if (res.toString().includes('Creating')) {
config_1.logger.info(`[IOS.launchIproxy] iProxyStdOut: ${res.toString().split('\n')[0]}`);
}
});
iProxy.stderr.on('data', res => {
config_1.logger.error(`[IOS.launchIproxy] iProxyStdErr: ${res.toString()}`);
});
iProxy.on('error', err => {
throw new Error(`iOS iProxy 异常退出:${err.message}`);
});
config_1.logger.info(`[IOS.launchIproxy] local:${localIp} remote:${remoteIp}`);
return iProxy;
}
}
exports.default = WDA;