修改bug
This commit is contained in:
@@ -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);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user