multi-automator
Version:
Multi terminal automation
325 lines (324 loc) • 12.7 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 (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__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 });
/**
* @desc: atx operation
* @author: john_chen
* @date: 2025.02.22
*/
const axios_1 = __importDefault(require("axios"));
const portfinder_1 = __importDefault(require("portfinder"));
const config_1 = require("../config");
const time_1 = require("../utils/time");
const env_1 = require("./env");
const adb = __importStar(require("./adb"));
const ATX_NAME = 'atx-agent';
const ATX_PATH = `${env_1.DEVICE_TMP_DIR}/${ATX_NAME}`;
const ATX_DEVICE_PORT = 7900;
class Atx {
constructor({ deviceId, connectType, timeout = 20000 }) {
this.id = deviceId;
this.connectType = connectType;
this.timeout = timeout;
this.ip = '127.0.0.1';
this.port = null;
this.atxIsRunning = false;
this.uiautomatorIsRunning = false;
}
// 公共接口
async init() {
config_1.logger.info('[atx.init]');
await this.setupConnection();
await this.restartAtxServer();
await this.startUiautomator();
}
async close() {
config_1.logger.info('[atx.close]');
if (this.uiautomatorIsRunning) {
await this.stopUiautomatorServer();
this.uiautomatorIsRunning = false;
}
if (this.atxIsRunning) {
await this.stopAtxServer();
this.atxIsRunning = false;
}
}
async info() {
return this.get('/info');
}
// 应用包管理
async packages() {
const res = await this.get('/packages');
return res.map(item => ({
appId: item.packageName,
version: item.versionName,
name: item.label
}));
}
async packageInfo(packageName) {
const { data } = await this.get(`/packages/${packageName}/info`);
return {
appId: data.packageName,
version: data.versionName,
name: data.label,
activity: data.mainActivity
};
}
async source(timeout = this.timeout) {
const res = await this.get('/dump/hierarchy', { timeout });
return res.result;
}
async deviceInfo(timeout = 20000) {
let res = await this.jsonrpc('deviceInfo', [], { timeout });
config_1.logger.info(`[atx.deviceInfo] ${JSON.stringify(res)}`);
return res;
}
// TODO: 有点问题、待解决
async screenshot(timeout = 20000) {
let res = await this.get('/screenshot/0', { responseType: 'arraybuffer', timeout });
return Buffer.from(res);
}
// HTTP 请求封装
async get(path = '', options) {
const fullPath = path.startsWith('/') ? path : `/${path}`;
const { timeout = this.timeout, responseType, } = options || {};
const baseUrl = `http://${this.ip}:${this.port}`;
// 构建请求配置
const config = {
baseURL: baseUrl,
timeout
};
if (responseType) {
config.responseType = responseType;
}
try {
const { data } = await axios_1.default.get(fullPath, config);
return data;
}
catch (err) {
config_1.logger.error(`[atx.get] ${baseUrl}${fullPath} ${err.message}`);
throw new Error(`[Atx.get] ${fullPath}: ${err.message}`);
}
}
async post(path = '', data = {}, options) {
const fullPath = path.startsWith('/') ? path : `/${path}`;
const { timeout = this.timeout } = options || {};
try {
const { data: responseData } = await axios_1.default.post(fullPath, data, {
baseURL: `http://${this.ip}:${this.port}`,
timeout
});
return responseData;
}
catch (err) {
if (err.message.includes('502') && fullPath === '/jsonrpc/0') {
throw new Error('Jsonrpc 502');
}
throw new Error(`[Atx.post] ${fullPath}: ${err.message}`);
}
}
async jsonrpc(method, params = [], options = {}) {
const { timeout = this.timeout } = options;
await this.checkUiautomator();
const requestPayload = {
jsonrpc: '2.0',
method,
id: String(Math.floor((0, time_1.currentTimestamp)() / 1000)),
params
};
const res = await this.post('/jsonrpc/0', requestPayload, { timeout });
// logger.info(`[atx.jsonrpc] res: ${JSON.stringify(res)}`);
if (res === null || res === void 0 ? void 0 : res.result) {
return res.result;
}
this.handleJsonRpcError(res.error);
}
handleJsonRpcError(error) {
if (error) {
const errorMessage = error.message || error.data || 'Unknown error occurred';
throw new Error(errorMessage);
}
throw new Error('jsonrpc request failed: no result or error provided');
}
// 连接管理
async setupConnection() {
if (this.connectType === 'usb') {
this.port = await portfinder_1.default.getPortPromise();
await adb.forward(this.id, `tcp:${this.port}`, `tcp:${ATX_DEVICE_PORT}`);
config_1.logger.info(`[atx.init] forward ${this.id} ${this.port} to ${ATX_DEVICE_PORT}`);
}
else if (this.connectType === 'wifi') {
this.ip = await adb.ip(this.id);
}
else {
throw new Error(`不支持连接类型: ${this.connectType}`);
}
config_1.logger.info(`[atx.init] http://${this.ip}:${this.port}`);
}
// ATX Server 管理
async restartAtxServer() {
await this.stopAtxServer();
await this.startAtxServer();
}
async startAtxServer() {
config_1.logger.info('[atx.startAtxServer]');
let grepAtxCmd = `${ATX_NAME} server --addr=:${ATX_DEVICE_PORT} --nouia -d`;
for (let ps of ['ps -ef', 'ps']) {
let grepCmd = `${ps} | grep -v 'grep' | grep '${grepAtxCmd}'`;
let atxProcesses = await adb.shell(this.id, grepCmd);
if (atxProcesses) {
config_1.logger.info(`[atx.startAtxServer] ${atxProcesses}`);
return;
}
}
let startCmd = `${ATX_PATH} server --addr=:${ATX_DEVICE_PORT} --nouia -d`;
let res = await adb.shell(this.id, startCmd);
if (res.includes('run atx-agent')) {
this.atxIsRunning = true;
config_1.logger.info('[atx.startAtxServer] success');
}
else {
throw new Error(`[atx.startAtxServer] ${res}`);
}
}
async stopAtxServer() {
config_1.logger.info('[atx.stopAtxServer]');
for (let ps of ['ps -ef', 'ps']) {
let atxProcesses = await adb.shell(this.id, `${ps} | grep -v 'grep' | grep 'atx-agent'`);
if (atxProcesses) {
let atxProcessList = atxProcesses.split('\n');
for (let atxProcess of atxProcessList) {
let atxProcessColumns = atxProcess.split(' ');
let atxProcessValidColumns = [];
for (let atxProcessColumn of atxProcessColumns) {
if (0 !== atxProcessValidColumns.length ||
atxProcessColumn.startsWith('atx-agent')) {
if ('-d' !== atxProcessColumn) {
atxProcessValidColumns.push(atxProcessColumn);
}
}
}
let cmd = `${env_1.DEVICE_TMP_DIR}/${atxProcessValidColumns.join(' ')} --stop`;
let res = await adb.shell(this.id, cmd);
if (res.includes('stop server self')) {
config_1.logger.info('[atx.stopAtxServer] success');
}
else {
throw new Error(`[atx.stopAtxServer] ${res}`);
}
}
}
}
}
// UIAutomator 管理
async startUiautomator(timeout = 20000) {
config_1.logger.info('[atx.startUiautomator]');
const expiredTime = (0, time_1.currentTimestamp)() + timeout;
let retryCount = 0;
while ((0, time_1.currentTimestamp)() <= expiredTime) {
try {
retryCount++;
await this.startUiautomatorServer();
await this.setTimeoutUiautomator();
if (await this.getUiautomatorState()) {
this.uiautomatorIsRunning = true;
config_1.logger.info('[atx.startUiautomator] success');
return;
}
}
catch (err) {
config_1.logger.error(`[atx.startUiautomator] 启动异常:${err.message}`);
// await this.killUiautomatorProcess();
}
if (retryCount > 3) {
config_1.logger.info('[atx.startUiautomator] 重启 ATX Server');
await this.restartAtxServer();
}
await (0, time_1.delay)(1000);
}
throw new Error(`[atx.startUiautomator] 耗时 ${timeout} ms 仍未启动`);
}
async checkUiautomator(timeout = 15000) {
config_1.logger.info('[atx.checkUiautomator]');
const expiredTime = (0, time_1.currentTimestamp)() + timeout;
do {
try {
let isRunning = await this.getUiautomatorState();
if (isRunning)
return;
await this.startUiautomatorServer();
await this.setTimeoutUiautomator();
}
catch (err) {
config_1.logger.warn('[atx.checkUiautomator] continue');
}
await adb.checkConnect(this.id);
await (0, time_1.delay)(1000);
} while ((0, time_1.currentTimestamp)() <= expiredTime);
throw new Error(`[atx.checkUiautomator] 超时 ${timeout}ms`);
}
async startUiautomatorServer() {
const res = await this.post('/services/uiautomator');
config_1.logger.info(`[atx.startUiautomatorServer] ${res.description}`);
if (!['success', 'already started', 'successfully started'].includes(res.description)) {
throw new Error(`startUiautomator fail: ${res.description}`);
}
config_1.logger.info(`[atx.startUiautomatorServer] ${res.description}`);
}
async stopUiautomatorServer() {
config_1.logger.info('[atx.stopUiautomatorServer]');
try {
const res = await axios_1.default.delete('/uiautomator', {
baseURL: `http://${this.ip}:${this.port}`,
timeout: 6000
});
config_1.logger.info(`[atx.stopUiautomatorServer] ${res.data}`);
}
catch (err) {
config_1.logger.error(`[atx.stopUiautomatorServer] ${err.stack}`);
}
}
async setTimeoutUiautomator() {
const res = await this.post('/newCommandTimeout', '10800');
if (!res.success) {
throw new Error(`setTimeoutUiautomator fail: ${res.description}`);
}
config_1.logger.info(`[atx.setTimeoutUiautomator] ${res.description}`);
}
async getUiautomatorState() {
try {
const { running } = await this.get('/services/uiautomator');
config_1.logger.info(`[atx.getUiautomatorState] status: ${running}`);
return running;
}
catch {
return false;
}
}
}
exports.default = Atx;