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