修改bug

This commit is contained in:
2025-08-13 14:21:19 +08:00
parent 063ce6eb70
commit 5d3f8b12cf
2 changed files with 139 additions and 179 deletions

View File

@@ -415,7 +415,7 @@ export class WebsocketProxy extends Mw {
})
.catch(err => {
console.error('Error:', err);
console.error('watchNode Error:', err);
});
@@ -722,10 +722,14 @@ export class WebsocketProxy extends Mw {
}
}
})
}).catch(err => {
console.error('dumpNode Error:', err);
});
} else if (parsedMessage.action === 'test') {
dump(parsedMessage.udid).then((data) => {
console.log('dump ui树', data.xml)
}).catch((err) => {
console.error('dump ui树失败: ', err);
})
}

View File

@@ -1,187 +1,143 @@
import { execFile } from 'child_process';
import { promisify } from 'util';
import path from 'path';
const xml2js = require('xml2js');
const execFileAsync = promisify(execFile);
/**
* 调用编译后的 .exe 文件获取设备 UI 层级
* @param udid 设备udid
* @returns Promise<{udid: string, xml: string}>
*/
// export function dumpHierarchy(udid: string): Promise<any> {
// const exePath = path.resolve(__dirname, 'uiauto_dump.exe'); // 替换为 .exe 路径
// return new Promise((resolve, reject) => {
// execFile(exePath, [udid], (error, stdout, stderr) => {
// if (error) {
// reject(new Error(`执行 .exe 文件失败:${error.message}`));
// return;
// }
// if (stderr) {
// reject(new Error(`.exe 文件输出错误:${stderr}`));
// return;
// }
// try {
// // console.log(stdout)
// const data = JSON.parse(stdout);
// resolve(data);
// } catch (e) {
// reject(new Error(`解析输出数据失败:${e}`));
// }
// });
// });
// }
/**
* 调用打包后的 .exe监听某个 resource-id 节点是否出现
* @param udid 设备 ID
* @param resourceId 节点 resource-id如 com.xx:id/ok_btn
* @param timeout 超时时间,单位秒,默认 30
* @returns Promise<{ udid: string, xml: string }>
*/
export function watchNode(
udid: string,
resourceId: string,
timeout: number
): Promise<{ udid: string; resource_id: string; found: boolean; count: number; nodes: any[] }> {
const exePath = path.resolve(__dirname, 'watch_node.exe'); // 替换为你实际打包的 exe 文件名
return new Promise((resolve, reject) => {
execFile(exePath, ['watch', udid, resourceId, timeout.toString()], (error, stdout, stderr) => {
if (error) {
return reject(new Error(`执行 .exe 文件失败:${error.message}`));
}
if (stderr) {
return reject(new Error(`.exe 文件输出错误:${stderr}`));
}
try {
const result = JSON.parse(stdout);
resolve(result);
} catch (e) {
reject(new Error(`解析输出数据失败:${(e as Error).message}\n原始输出: ${stdout}`));
}
});
});
}
export function dumpNode(
udid: string,
resourceId: string
): Promise<{
udid: string
resource_id: string
found: boolean
count: number
nodes: any[]
}> {
const exePath = path.resolve(__dirname, 'watch_node.exe')
return new Promise((resolve, reject) => {
execFile(exePath, ['dump', udid], async (error, stdout, stderr) => {
if (error) return reject(new Error(`执行 .exe 文件失败:${error.message}`))
if (stderr) return reject(new Error(`.exe 文件输出错误:${stderr}`))
let data: any
try {
data = JSON.parse(stdout)
} catch (e) {
return reject(new Error(`解析输出数据失败:${(e as Error).message}\n原始输出: ${stdout}`))
}
if (!data.xml) {
return reject(new Error(`未返回 xml 内容:${JSON.stringify(data)}`))
}
try {
const result = await xml2js.parseStringPromise(data.xml, {
explicitArray: false,
attrkey: '$',
})
const nodes: any[] = []
// 递归提取一个节点的基本信息(及其子节点)
const extractNodeInfo = (node: any): any => {
const info: any = {
text: node.$?.text || '',
'resource-id': node.$?.['resource-id'] || '',
class: node.$?.class || '',
bounds: node.$?.bounds || '',
children: []
}
// 递归提取 children
for (const value of Object.values(node)) {
if (typeof value === 'object') {
if (Array.isArray(value)) {
for (const child of value) {
if (child?.$) {
info.children.push(extractNodeInfo(child))
}
}
} else if ((value as any)?.$) {
info.children.push(extractNodeInfo(value))
}
}
}
return info
}
// 遍历整棵树查找符合 resource-id 的节点,并附加子树结构
const walk = function (node: any) {
if (!node) return
if (node.$?.['resource-id'] === resourceId) {
nodes.push(extractNodeInfo(node))
}
Object.values(node).forEach((child) => {
if (typeof child === 'object') {
if (Array.isArray(child)) {
child.forEach(walk)
} else {
walk(child)
}
}
})
}
walk(result)
resolve({
udid: data.udid,
resource_id: resourceId,
found: nodes.length > 0,
count: nodes.length,
nodes
})
} catch (e) {
reject(new Error(`XML 解析失败:${(e as Error).message}`))
}
})
})
// —— 新增:设备级互斥(单飞) ——
const deviceLocks = new Map<string, Promise<any>>();
async function withDeviceLock<T>(udid: string, fn: () => Promise<T>): Promise<T> {
const prev = deviceLocks.get(udid) || Promise.resolve();
let release!: (v?: unknown) => void;
const cur = new Promise(res => (release = res));
deviceLocks.set(udid, prev.then(() => cur));
try { return await fn(); }
finally { release(); if (deviceLocks.get(udid) === cur) deviceLocks.delete(udid); }
}
export function dump(
udid: string,
): Promise<{ udid: string; xml: string }> {
const exePath = path.resolve(__dirname, 'watch_node.exe'); // 替换为你实际打包的 exe 文件名
// —— 新增:错误识别 & 自救清理 ——
function isUiaConflict(msg: string) {
return /already registered|UiAutomation not connected/i.test(msg);
}
function isNoXml(msg: string) {
return /未返回 xml 内容|no xml/i.test(msg);
}
async function cleanupUia2(udid: string) {
// 按你的环境调整关键字wetest.uia2 / uiautomator / io.appium.uiautomator2.server
// Windows 下 adb 在 PATH 中;否则改成绝对路径
await execFileAsync('adb', ['-s', udid, 'shell', 'pkill', '-f', 'uiautomator']).catch(() => { });
await execFileAsync('adb', ['-s', udid, 'shell', 'pkill', '-f', 'wetest.uia2']).catch(() => { });
await execFileAsync('adb', ['-s', udid, 'shell', 'input', 'keyevent', '3']).catch(() => { }); // HOME 稳定焦点
}
return new Promise((resolve, reject) => {
execFile(exePath, ['dump', udid], (error, stdout, stderr) => {
if (error) {
return reject(new Error(`执行 .exe 文件失败:${error.message}`));
}
if (stderr) {
return reject(new Error(`.exe 文件输出错误:${stderr}`));
}
// —— 原有底层调用,稍加 timeout/错误透传 ——
function runWatchExe(args: string[], timeoutMs = 35_000) {
const exePath = path.resolve(__dirname, 'watch_node.exe');
return execFileAsync(exePath, args, { timeout: timeoutMs, windowsHide: true });
}
try {
const data = JSON.parse(stdout);
resolve(data);
} catch (e) {
reject(new Error(`解析输出数据失败:${(e as Error).message}\n原始输出: ${stdout}`));
}
});
// 你原有的三个“裸”函数保持不变(可改名为 *Raw供编排器调用
export function watchNodeRaw(udid: string, resourceId: string, timeout: number) {
return new Promise<{ udid: string; resource_id: string; found: boolean; count: number; nodes: any[] }>(async (resolve, reject) => {
try {
const { stdout, stderr } = await runWatchExe(['watch', udid, resourceId, timeout.toString()], (timeout + 5) * 1000);
if (stderr) return reject(new Error(`.exe 文件输出错误:${stderr}`));
resolve(JSON.parse(stdout));
} catch (e: any) {
reject(new Error(`执行 .exe 失败:${e?.message || e}`));
}
});
}
}
export function dumpRaw(udid: string) {
return new Promise<{ udid: string; xml: string }>(async (resolve, reject) => {
try {
const { stdout, stderr } = await runWatchExe(['dump', udid], 25_000);
if (stderr) return reject(new Error(`.exe 文件输出错误:${stderr}`));
resolve(JSON.parse(stdout));
} catch (e: any) {
reject(new Error(`执行 .exe 失败:${e?.message || e}`));
}
});
}
export function dumpNodeRaw(udid: string, resourceId: string) {
return new Promise<{
udid: string; resource_id: string; found: boolean; count: number; nodes: any[];
}>(async (resolve, reject) => {
try {
const { stdout, stderr } = await runWatchExe(['dump', udid], 25_000);
if (stderr) return reject(new Error(`.exe 文件输出错误:${stderr}`));
const data = JSON.parse(stdout);
if (!data.xml) return reject(new Error(`未返回 xml 内容:${JSON.stringify(data)}`));
const result = await xml2js.parseStringPromise(data.xml, { explicitArray: false, attrkey: '$' });
const nodes: any[] = [];
const extractNodeInfo = (node: any): any => {
const info: any = {
text: node.$?.text || '',
'resource-id': node.$?.['resource-id'] || '',
class: node.$?.class || '',
bounds: node.$?.bounds || '',
children: []
};
for (const value of Object.values(node)) {
if (typeof value === 'object') {
if (Array.isArray(value)) value.forEach((child) => child?.$ && info.children.push(extractNodeInfo(child)));
else if ((value as any)?.$) info.children.push(extractNodeInfo(value));
}
}
return info;
};
const walk = (node: any) => {
if (!node) return;
if (node.$?.['resource-id'] === resourceId) nodes.push(extractNodeInfo(node));
Object.values(node).forEach((child) => {
if (typeof child === 'object') {
if (Array.isArray(child)) child.forEach(walk);
else walk(child);
}
});
};
walk(result);
resolve({ udid: data.udid, resource_id: resourceId, found: nodes.length > 0, count: nodes.length, nodes });
} catch (e: any) {
reject(new Error(`XML 解析或执行失败:${e?.message || e}`));
}
});
}
// —— 新增Safe 包装(互斥 + 自救 + 重试),供业务层使用 ——
// 重试一次即可初次失败→cleanup→重试
async function withRecovery<T>(udid: string, fn: () => Promise<T>): Promise<T> {
try {
return await fn();
} catch (e: any) {
const msg = String(e?.message || e);
if (isUiaConflict(msg) || isNoXml(msg)) {
await cleanupUia2(udid);
await new Promise(r => setTimeout(r, 800));
return await fn();
}
throw e;
}
}
export async function watchNodeSafe(udid: string, resourceId: string, timeout: number) {
return withDeviceLock(udid, () => withRecovery(udid, () => watchNodeRaw(udid, resourceId, timeout)));
}
export async function dumpSafe(udid: string) {
return withDeviceLock(udid, () => withRecovery(udid, () => dumpRaw(udid)));
}
export async function dumpNodeSafe(udid: string, resourceId: string) {
return withDeviceLock(udid, () => withRecovery(udid, () => dumpNodeRaw(udid, resourceId)));
}
export { watchNodeSafe as watchNode, dumpNodeSafe as dumpNode, dumpSafe as dump };