Merged
fix(harmony): uitest shell layout dump hang and TCP probe corruption #60
fix(harmony): uitest shell layout dump hang and TCP probe corruption #60
Merged
gyangeAdd on 26 days ago
Total: 5 files changed+135-64
@@ -66,6 +66,7 @@ export interface CPTFConfigFile {
66 appPath?: string;66 appPath?: string;
67 statusBarHeight?: number;67 statusBarHeight?: number;
68 devices?: 'auto' | Array<{ id?: string; screenWidth?: number; screenHeight?: number }>;68 devices?: 'auto' | Array<{ id?: string; screenWidth?: number; screenHeight?: number }>;
69+ platformOptions?: Record<string, unknown>;
69 };70 };
70 android?: {71 android?: {
71 toolPath?: string;72 toolPath?: string;
@@ -73,6 +74,7 @@ export interface CPTFConfigFile {
73 appPath?: string;74 appPath?: string;
74 statusBarHeight?: number;75 statusBarHeight?: number;
75 devices?: 'auto' | Array<{ id?: string; screenWidth?: number; screenHeight?: number }>;76 devices?: 'auto' | Array<{ id?: string; screenWidth?: number; screenHeight?: number }>;
77+ platformOptions?: Record<string, unknown>;
76 };78 };
77 ios?: {79 ios?: {
78 toolPath?: string;80 toolPath?: string;
@@ -146,7 +148,9 @@ function parseHostVersionsEnv(raw: string | undefined): Record<string, string> |
146 }148 }
147 return Object.keys(out).length > 0 ? out : undefined;149 return Object.keys(out).length > 0 ? out : undefined;
148 }150 }
149- } catch { /* malformed env is ignored, gate stays inert */ }151+ } catch {
152+ /* malformed env is ignored, gate stays inert */
153+ }
150 return undefined;154 return undefined;
151}155}
152 156 
@@ -163,7 +167,9 @@ export async function resolveConfigAsync(
163 }167 }
164 168 
165 if (!configData.deepLinkScheme) {169 if (!configData.deepLinkScheme) {
166- throw new Error('deepLinkScheme 未配置:请在 cptf.config.json 中设置 deepLinkScheme(如 "rnoh-test"/"flutter"/"myapp"),框架不再提供默认值');170+ throw new Error(
171+ 'deepLinkScheme 未配置:请在 cptf.config.json 中设置 deepLinkScheme(如 "rnoh-test"/"flutter"/"myapp"),框架不再提供默认值'
172+ );
167 }173 }
168 174 
169 const deviceList = platformConfig.devices;175 const deviceList = platformConfig.devices;
@@ -193,7 +199,7 @@ export async function resolveConfigAsync(
193 }199 }
194 200 
195 const toolPath = platformConfig.toolPath || getDefaultToolPath(platform);201 const toolPath = platformConfig.toolPath || getDefaultToolPath(platform);
196- 202+ 
197 let statusBarHeight: number | undefined;203 let statusBarHeight: number | undefined;
198 // 状态栏高度取值顺序:显式配置 → 设备缓存 → 实时探测(失败重试 1 次) → 密度估算。204 // 状态栏高度取值顺序:显式配置 → 设备缓存 → 实时探测(失败重试 1 次) → 密度估算。
199 // 缓存的原因:探测依赖 hidumper 读 SCBGestureTopBar,负载下偶发漏行 → 回退 69px205 // 缓存的原因:探测依赖 hidumper 读 SCBGestureTopBar,负载下偶发漏行 → 回退 69px
@@ -247,13 +253,16 @@ export async function resolveConfigAsync(
247 casesDir: configData.casesDir,253 casesDir: configData.casesDir,
248 scoring: configData.scoring,254 scoring: configData.scoring,
249 hostVersions: configData.hostVersions ?? parseHostVersionsEnv(process.env.CPTF_HOST_VERSIONS),255 hostVersions: configData.hostVersions ?? parseHostVersionsEnv(process.env.CPTF_HOST_VERSIONS),
250- platformOptions: platform === 'ios' ? {256+ platformOptions:
251- wdaProjectPath: resolveWdaProjectPath(platformConfig as IosPlatformConfig),257+ platform === 'ios'
252- wdaScheme: (platformConfig as IosPlatformConfig).wdaScheme || 'WebDriverAgentRunner',258+ ? {
253- devTeam: resolveDevTeam(platformConfig as IosPlatformConfig),259+ wdaProjectPath: resolveWdaProjectPath(platformConfig as IosPlatformConfig),
254- wdaPort: (platformConfig as IosPlatformConfig).wdaPort || 8100,260+ wdaScheme: (platformConfig as IosPlatformConfig).wdaScheme || 'WebDriverAgentRunner',
255- wdaBundleId: (platformConfig as IosPlatformConfig).wdaBundleId,261+ devTeam: resolveDevTeam(platformConfig as IosPlatformConfig),
256- } : undefined,262+ wdaPort: (platformConfig as IosPlatformConfig).wdaPort || 8100,
263+ wdaBundleId: (platformConfig as IosPlatformConfig).wdaBundleId,
264+ }
265+ : (platformConfig as { platformOptions?: Record<string, unknown> }).platformOptions,
257 };266 };
258}267}
259 268 
@@ -352,7 +361,7 @@ export async function detectStatusBarHeight(options: {
352 if (first !== undefined) return first;361 if (first !== undefined) return first;
353 // ios 没有探测命令,重试必然仍 undefined,不值得干等362 // ios 没有探测命令,重试必然仍 undefined,不值得干等
354 if (options.platform !== 'android' && options.platform !== 'harmony') return undefined;363 if (options.platform !== 'android' && options.platform !== 'harmony') return undefined;
355- await new Promise((resolve) => setTimeout(resolve, options.retryDelayMs ?? 800));364+ await new Promise(resolve => setTimeout(resolve, options.retryDelayMs ?? 800));
356 return probeStatusBarHeightOnce(options);365 return probeStatusBarHeightOnce(options);
357}366}
358 367 
@@ -386,7 +395,10 @@ async function probeStatusBarHeightOnce(options: {
386 let first: RegExpExecArray | null = null;395 let first: RegExpExecArray | null = null;
387 while ((match = pattern.exec(stdout)) !== null) {396 while ((match = pattern.exec(stdout)) !== null) {
388 if (!first) first = match;397 if (!first) first = match;
389- if ((match[5] || '').includes('visible=true')) { first = match; break; }398+ if ((match[5] || '').includes('visible=true')) {
399+ first = match;
400+ break;
401+ }
390 }402 }
391 if (!first) return undefined;403 if (!first) return undefined;
392 return clamp(parseInt(first[4]) - parseInt(first[2]));404 return clamp(parseInt(first[4]) - parseInt(first[2]));
@@ -444,7 +456,7 @@ async function resolveDeviceSize(options: {
444 maxBuffer: 1024 * 1024,456 maxBuffer: 1024 * 1024,
445 });457 });
446 const name = stdout.trim() || 'HarmonyOS Device';458 const name = stdout.trim() || 'HarmonyOS Device';
447- 459+ 
448 try {460 try {
449 const { stdout: displayStdout } = await execAsync(461 const { stdout: displayStdout } = await execAsync(
450 `${toolPath} -t ${deviceId} shell "hidumper -s DisplayManagerService -a '-a'"`,462 `${toolPath} -t ${deviceId} shell "hidumper -s DisplayManagerService -a '-a'"`,
@@ -464,9 +476,7 @@ async function resolveDeviceSize(options: {
464 };476 };
465 }477 }
466 // 备选:最新一条旋转事件的 width/height(反映当前方向,规避过期的历史记录)478 // 备选:最新一条旋转事件的 width/height(反映当前方向,规避过期的历史记录)
467- const rotationMatches = Array.from(479+ const rotationMatches = Array.from(displayStdout.matchAll(/rotation:\s*\d+\s+width:\s*(\d+)\s+height:\s*(\d+)/g));
468- displayStdout.matchAll(/rotation:\s*\d+\s+width:\s*(\d+)\s+height:\s*(\d+)/g)
469- );
470 if (rotationMatches.length > 0) {480 if (rotationMatches.length > 0) {
471 const last = rotationMatches[rotationMatches.length - 1];481 const last = rotationMatches[rotationMatches.length - 1];
472 return {482 return {
@@ -476,36 +486,36 @@ async function resolveDeviceSize(options: {
476 };486 };
477 }487 }
478 } catch {}488 } catch {}
479- 489+ 
480 await execAsync(`${toolPath} -t ${deviceId} shell uitest dumpLayout`, {490 await execAsync(`${toolPath} -t ${deviceId} shell uitest dumpLayout`, {
481 timeout: 10000,491 timeout: 10000,
482- maxBuffer: 1024 * 1024492+ maxBuffer: 1024 * 1024,
483 });493 });
484- 494+ 
485 await new Promise(resolve => setTimeout(resolve, 1000));495 await new Promise(resolve => setTimeout(resolve, 1000));
486- 496+ 
487 const { stdout: listStdout } = await execAsync(497 const { stdout: listStdout } = await execAsync(
488 `${toolPath} -t ${deviceId} shell "ls -t /data/local/tmp/layout_*.json | head -1"`,498 `${toolPath} -t ${deviceId} shell "ls -t /data/local/tmp/layout_*.json | head -1"`,
489 { timeout: 5000, maxBuffer: 1024 * 1024 }499 { timeout: 5000, maxBuffer: 1024 * 1024 }
490 );500 );
491- 501+ 
492 const latestFile = listStdout.trim();502 const latestFile = listStdout.trim();
493- 503+ 
494 if (!latestFile) {504 if (!latestFile) {
495 throw new Error(`无法获取HarmonyOS设备屏幕尺寸: ${deviceId} (布局文件未生成)`);505 throw new Error(`无法获取HarmonyOS设备屏幕尺寸: ${deviceId} (布局文件未生成)`);
496 }506 }
497- 507+ 
498- const { stdout: layoutStdout } = await execAsync(508+ const { stdout: layoutStdout } = await execAsync(`${toolPath} -t ${deviceId} shell "cat ${latestFile}"`, {
499- `${toolPath} -t ${deviceId} shell "cat ${latestFile}"`,509+ timeout: 5000,
500- { timeout: 5000, maxBuffer: 1024 * 1024 }510+ maxBuffer: 1024 * 1024,
501- );511+ });
502- 512+ 
503 const boundsMatch = layoutStdout.match(/bounds["\s:]*\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);513 const boundsMatch = layoutStdout.match(/bounds["\s:]*\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
504- 514+ 
505 if (!boundsMatch) {515 if (!boundsMatch) {
506 throw new Error(`无法获取HarmonyOS设备屏幕尺寸: ${deviceId} (bounds解析失败)`);516 throw new Error(`无法获取HarmonyOS设备屏幕尺寸: ${deviceId} (bounds解析失败)`);
507 }517 }
508- 518+ 
509 return {519 return {
510 name,520 name,
511 width: parseInt(boundsMatch[3]),521 width: parseInt(boundsMatch[3]),
@@ -606,18 +616,18 @@ async function resolveDeviceSize(options: {
606 maxBuffer: 1024 * 1024,616 maxBuffer: 1024 * 1024,
607 });617 });
608 const name = modelStdout.trim() || 'Android Device';618 const name = modelStdout.trim() || 'Android Device';
609- 619+ 
610- const { stdout: sizeStdout } = await execAsync(620+ const { stdout: sizeStdout } = await execAsync(`${toolPath} -s ${deviceId} shell wm size`, {
611- `${toolPath} -s ${deviceId} shell wm size`,621+ timeout: 5000,
612- { timeout: 5000, maxBuffer: 1024 * 1024 }622+ maxBuffer: 1024 * 1024,
613- );623+ });
614- 624+ 
615 const sizeMatch = sizeStdout.match(/Physical size:\s*(\d+)x(\d+)/i);625 const sizeMatch = sizeStdout.match(/Physical size:\s*(\d+)x(\d+)/i);
616- 626+ 
617 if (!sizeMatch) {627 if (!sizeMatch) {
618 throw new Error(`无法获取Android设备屏幕尺寸: ${deviceId}`);628 throw new Error(`无法获取Android设备屏幕尺寸: ${deviceId}`);
619 }629 }
620- 630+ 
621 return {631 return {
622 name,632 name,
623 width: parseInt(sizeMatch[1]),633 width: parseInt(sizeMatch[1]),
@@ -650,11 +660,17 @@ function statusBarCachePath(configData: { casesDir?: string }, platform: string,
650async function readStatusBarHeightCache(660async function readStatusBarHeightCache(
651 configData: { casesDir?: string },661 configData: { casesDir?: string },
652 platform: string,662 platform: string,
653- deviceId: string,663+ deviceId: string
654): Promise<number | undefined> {664): Promise<number | undefined> {
655 try {665 try {
656 const p = statusBarCachePath(configData, platform, deviceId);666 const p = statusBarCachePath(configData, platform, deviceId);
657- if (!(await fsp.stat(p).then(() => true).catch(() => false))) return undefined;667+ if (
668+ !(await fsp
669+ .stat(p)
670+ .then(() => true)
671+ .catch(() => false))
672+ )
673+ return undefined;
658 const raw = JSON.parse(await fsp.readFile(p, 'utf-8'));674 const raw = JSON.parse(await fsp.readFile(p, 'utf-8'));
659 const v = raw?.statusBarHeight;675 const v = raw?.statusBarHeight;
660 return typeof v === 'number' && v > 0 ? v : undefined;676 return typeof v === 'number' && v > 0 ? v : undefined;
@@ -667,14 +683,16 @@ async function writeStatusBarHeightCache(
667 configData: { casesDir?: string },683 configData: { casesDir?: string },
668 platform: string,684 platform: string,
669 deviceId: string,685 deviceId: string,
670- value: number,686+ value: number
671): Promise<void> {687): Promise<void> {
672 try {688 try {
673 const p = statusBarCachePath(configData, platform, deviceId);689 const p = statusBarCachePath(configData, platform, deviceId);
674 // 文件独占一设备,整写即可,不再 read-modify-write690 // 文件独占一设备,整写即可,不再 read-modify-write
675 await fsp.mkdir(path.dirname(p), { recursive: true });691 await fsp.mkdir(path.dirname(p), { recursive: true });
676 await fsp.writeFile(p, JSON.stringify({ statusBarHeight: value, detectedAt: new Date().toISOString() }, null, 2));692 await fsp.writeFile(p, JSON.stringify({ statusBarHeight: value, detectedAt: new Date().toISOString() }, null, 2));
677- } catch { /* 缓存写失败不影响主流程,下轮重探 */ }693+ } catch {
694+ /* 缓存写失败不影响主流程,下轮重探 */
695+ }
678}696}
679 697 
680export async function getDeviceInfoAsync(options: {698export async function getDeviceInfoAsync(options: {
@@ -698,4 +716,4 @@ export function getAvailablePlatforms(configData: CPTFConfigFile): Platform[] {
698 }716 }
699 717 
700 return platforms;718 return platforms;
701-}719+}
@@ -473,6 +473,7 @@ export class ParallelTestRunner {
473 deviceManager.registerDevice(id, platform, resolvedConfig.toolPath!, {473 deviceManager.registerDevice(id, platform, resolvedConfig.toolPath!, {
474 bundleId: resolvedConfig.bundleId,474 bundleId: resolvedConfig.bundleId,
475 driverMode: this.options.driver,475 driverMode: this.options.driver,
476+ uitestTcpUnsupported: resolvedConfig.platformOptions?.['uitestTcpUnsupported'] === true,
476 })477 })
477 )478 )
478 );479 );
@@ -575,6 +576,7 @@ export class ParallelTestRunner {
575 md = await dm.registerDevice(resolvedConfig.deviceId!, targetPlatform, resolvedConfig.toolPath!, {576 md = await dm.registerDevice(resolvedConfig.deviceId!, targetPlatform, resolvedConfig.toolPath!, {
576 bundleId: resolvedConfig.bundleId,577 bundleId: resolvedConfig.bundleId,
577 driverMode: this.options.driver,578 driverMode: this.options.driver,
579+ uitestTcpUnsupported: resolvedConfig.platformOptions?.['uitestTcpUnsupported'] === true,
578 });580 });
579 if (md.localPort) localPort = md.localPort;581 if (md.localPort) localPort = md.localPort;
580 if (md.uitestCapability === UitestCapability.TCP_UNSUPPORTED) uitestTcpUnsupported = true;582 if (md.uitestCapability === UitestCapability.TCP_UNSUPPORTED) uitestTcpUnsupported = true;
@@ -26,6 +26,7 @@ export interface ManagedDevice {
26 toolPath: string;26 toolPath: string;
27 bundleId?: string;27 bundleId?: string;
28 driverMode?: string;28 driverMode?: string;
29+ uitestTcpUnsupported?: boolean;
29}30}
30 31 
31/**32/**
@@ -69,10 +70,19 @@ export class DeviceManager {
69 id: string,70 id: string,
70 platform: Platform,71 platform: Platform,
71 toolPath: string,72 toolPath: string,
72- opts?: { name?: string; bundleId?: string; screenWidth?: number; screenHeight?: number; driverMode?: string }73+ opts?: {
74+ name?: string;
75+ bundleId?: string;
76+ screenWidth?: number;
77+ screenHeight?: number;
78+ driverMode?: string;
79+ uitestTcpUnsupported?: boolean;
80+ }
73 ): Promise<ManagedDevice> {81 ): Promise<ManagedDevice> {
74 const existing = this.devices.get(id);82 const existing = this.devices.get(id);
75 if (existing) {83 if (existing) {
84+ if (opts?.uitestTcpUnsupported !== undefined) existing.uitestTcpUnsupported = opts.uitestTcpUnsupported;
85+ if (opts?.driverMode !== undefined) existing.driverMode = opts.driverMode;
76 return existing;86 return existing;
77 }87 }
78 88 
@@ -86,6 +96,7 @@ export class DeviceManager {
86 screenHeight: opts?.screenHeight,96 screenHeight: opts?.screenHeight,
87 uitestCapability: UitestCapability.UNKNOWN,97 uitestCapability: UitestCapability.UNKNOWN,
88 driverMode: opts?.driverMode,98 driverMode: opts?.driverMode,
99+ uitestTcpUnsupported: opts?.uitestTcpUnsupported,
89 };100 };
90 101 
91 const prober = this.probers.get(platform);102 const prober = this.probers.get(platform);
@@ -140,10 +151,14 @@ export class DeviceManager {
140 private async queryDeviceName(deviceId: string, platform: Platform, toolPath: string): Promise<string | undefined> {151 private async queryDeviceName(deviceId: string, platform: Platform, toolPath: string): Promise<string | undefined> {
141 try {152 try {
142 if (platform === 'harmony') {153 if (platform === 'harmony') {
143- const { stdout } = await execAsync(`${toolPath} -t ${deviceId} shell param get const.product.model`, { timeout: 5000 });154+ const { stdout } = await execAsync(`${toolPath} -t ${deviceId} shell param get const.product.model`, {
155+ timeout: 5000,
156+ });
144 return stdout.trim() || undefined;157 return stdout.trim() || undefined;
145 } else if (platform === 'android') {158 } else if (platform === 'android') {
146- const { stdout } = await execAsync(`${toolPath} -s ${deviceId} shell getprop ro.product.model`, { timeout: 5000 });159+ const { stdout } = await execAsync(`${toolPath} -s ${deviceId} shell getprop ro.product.model`, {
160+ timeout: 5000,
161+ });
147 return stdout.trim() || undefined;162 return stdout.trim() || undefined;
148 }163 }
149 } catch {}164 } catch {}
@@ -28,6 +28,13 @@ export class HarmonyDeviceProber implements DeviceProber {
28 this.logger.info(`DeviceManager: ${device.id} native驱动模式,跳过uitest探测`);28 this.logger.info(`DeviceManager: ${device.id} native驱动模式,跳过uitest探测`);
29 return;29 return;
30 }30 }
31+ if (device.uitestTcpUnsupported) {
32+ device.uitestCapability = UitestCapability.TCP_UNSUPPORTED;
33+ this.logger.info(
34+ `DeviceManager: ${device.id} 配置声明 uitestTcpUnsupported,跳过uitest探测(直接使用hdc shell)`
35+ );
36+ return;
37+ }
31 device.uitestCapability = await this.probeUitestCapability(device.id, device.toolPath);38 device.uitestCapability = await this.probeUitestCapability(device.id, device.toolPath);
32 if (device.uitestCapability === UitestCapability.TCP_SUPPORTED) {39 if (device.uitestCapability === UitestCapability.TCP_SUPPORTED) {
33 device.localPort = await this.portManager.allocate(device.id, 8012, 'tcp', 'harmony');40 device.localPort = await this.portManager.allocate(device.id, 8012, 'tcp', 'harmony');
@@ -51,12 +58,14 @@ export class HarmonyDeviceProber implements DeviceProber {
51 }58 }
52 59 
53 const existingPs = await this.execHdcShell(60 const existingPs = await this.execHdcShell(
54- hdcPath, deviceId,61+ hdcPath,
62+ deviceId,
55 'ps -ef | grep "uitest start-daemon" | grep -v grep | head -1'63 'ps -ef | grep "uitest start-daemon" | grep -v grep | head -1'
56 );64 );
57 if (existingPs && existingPs.trim().length > 0) {65 if (existingPs && existingPs.trim().length > 0) {
58 const tcpPreCheck = await this.execHdcShell(66 const tcpPreCheck = await this.execHdcShell(
59- hdcPath, deviceId,67+ hdcPath,
68+ deviceId,
60 'cat /proc/net/tcp /proc/net/tcp6 2>/dev/null | grep "1F4C" | head -1'69 'cat /proc/net/tcp /proc/net/tcp6 2>/dev/null | grep "1F4C" | head -1'
61 );70 );
62 if (tcpPreCheck && tcpPreCheck.includes('1F4C')) {71 if (tcpPreCheck && tcpPreCheck.includes('1F4C')) {
@@ -66,7 +75,8 @@ export class HarmonyDeviceProber implements DeviceProber {
66 return result;75 return result;
67 }76 }
68 const unixPreCheck = await this.execHdcShell(77 const unixPreCheck = await this.execHdcShell(
69- hdcPath, deviceId,78+ hdcPath,
79+ deviceId,
70 'cat /proc/net/unix 2>/dev/null | grep uitest_socket | head -1'80 'cat /proc/net/unix 2>/dev/null | grep uitest_socket | head -1'
71 );81 );
72 if (unixPreCheck && unixPreCheck.includes('uitest_socket')) {82 if (unixPreCheck && unixPreCheck.includes('uitest_socket')) {
@@ -78,11 +88,16 @@ export class HarmonyDeviceProber implements DeviceProber {
78 }88 }
79 89 
80 try {90 try {
81- await this.execHdcShell(hdcPath, deviceId, 'ps -ef | grep "uitest start-daemon" | grep -v grep | awk \'{print $2}\' | xargs kill -9 2>/dev/null');91+ await this.execHdcShell(
92+ hdcPath,
93+ deviceId,
94+ 'ps -ef | grep "uitest start-daemon" | grep -v grep | awk \'{print $2}\' | xargs kill -9 2>/dev/null'
95+ );
82 } catch {}96 } catch {}
83 for (let i = 0; i < 5; i++) {97 for (let i = 0; i < 5; i++) {
84 const psKill = await this.execHdcShell(98 const psKill = await this.execHdcShell(
85- hdcPath, deviceId,99+ hdcPath,
100+ deviceId,
86 'ps -ef | grep "uitest start-daemon" | grep -v grep | head -1'101 'ps -ef | grep "uitest start-daemon" | grep -v grep | head -1'
87 );102 );
88 if (!psKill || psKill.trim().length === 0) break;103 if (!psKill || psKill.trim().length === 0) break;
@@ -97,9 +112,14 @@ export class HarmonyDeviceProber implements DeviceProber {
97 await this.execHdcShell(hdcPath, deviceId, cmd, 5000);112 await this.execHdcShell(hdcPath, deviceId, cmd, 5000);
98 } catch {}113 } catch {}
99 for (let i = 0; i < 15; i++) {114 for (let i = 0; i < 15; i++) {
100- if (await this.checkTcpListening(hdcPath, deviceId) || await this.checkUnixSocketExists(hdcPath, deviceId)) {115+ if (
116+ (await this.checkTcpListening(hdcPath, deviceId)) ||
117+ (await this.checkUnixSocketExists(hdcPath, deviceId))
118+ ) {
101 lastToken = token;119 lastToken = token;
102- this.logger.debug(`DeviceManager: ${deviceId} uitest端口监听确认 (token: ${token || '无'}, ${(i + 1) * 300}ms)`);120+ this.logger.debug(
121+ `DeviceManager: ${deviceId} uitest端口监听确认 (token: ${token || '无'}, ${(i + 1) * 300}ms)`
122+ );
103 break;123 break;
104 }124 }
105 await new Promise(resolve => setTimeout(resolve, 300));125 await new Promise(resolve => setTimeout(resolve, 300));
@@ -115,7 +135,8 @@ export class HarmonyDeviceProber implements DeviceProber {
115 }135 }
116 136 
117 const psCheck = await this.execHdcShell(137 const psCheck = await this.execHdcShell(
118- hdcPath, deviceId,138+ hdcPath,
139+ deviceId,
119 'ps -ef | grep "uitest start-daemon" | grep -v grep | head -1'140 'ps -ef | grep "uitest start-daemon" | grep -v grep | head -1'
120 );141 );
121 if (!psCheck || psCheck.trim().length === 0) {142 if (!psCheck || psCheck.trim().length === 0) {
@@ -127,11 +148,16 @@ export class HarmonyDeviceProber implements DeviceProber {
127 148 
128 this.logger.info(`DeviceManager: ${deviceId} uitest进程运行但未监听TCP:8012,尝试重启uitest`);149 this.logger.info(`DeviceManager: ${deviceId} uitest进程运行但未监听TCP:8012,尝试重启uitest`);
129 try {150 try {
130- await this.execHdcShell(hdcPath, deviceId, 'ps -ef | grep "uitest start-daemon" | grep -v grep | awk \'{print $2}\' | xargs kill -9 2>/dev/null');151+ await this.execHdcShell(
152+ hdcPath,
153+ deviceId,
154+ 'ps -ef | grep "uitest start-daemon" | grep -v grep | awk \'{print $2}\' | xargs kill -9 2>/dev/null'
155+ );
131 } catch {}156 } catch {}
132 for (let i = 0; i < 10; i++) {157 for (let i = 0; i < 10; i++) {
133 const psKill = await this.execHdcShell(158 const psKill = await this.execHdcShell(
134- hdcPath, deviceId,159+ hdcPath,
160+ deviceId,
135 'ps -ef | grep "uitest start-daemon" | grep -v grep | head -1'161 'ps -ef | grep "uitest start-daemon" | grep -v grep | head -1'
136 );162 );
137 if (!psKill || psKill.trim().length === 0) break;163 if (!psKill || psKill.trim().length === 0) break;
@@ -145,9 +171,14 @@ export class HarmonyDeviceProber implements DeviceProber {
145 await this.execHdcShell(hdcPath, deviceId, cmd, 5000);171 await this.execHdcShell(hdcPath, deviceId, cmd, 5000);
146 } catch {}172 } catch {}
147 for (let i = 0; i < 15; i++) {173 for (let i = 0; i < 15; i++) {
148- if (await this.checkTcpListening(hdcPath, deviceId) || await this.checkUnixSocketExists(hdcPath, deviceId)) {174+ if (
175+ (await this.checkTcpListening(hdcPath, deviceId)) ||
176+ (await this.checkUnixSocketExists(hdcPath, deviceId))
177+ ) {
149 restartToken = token;178 restartToken = token;
150- this.logger.debug(`DeviceManager: ${deviceId} uitest重启后端口监听确认 (token: ${token || '无'}, ${(i + 1) * 300}ms)`);179+ this.logger.debug(
180+ `DeviceManager: ${deviceId} uitest重启后端口监听确认 (token: ${token || '无'}, ${(i + 1) * 300}ms)`
181+ );
151 break;182 break;
152 }183 }
153 await new Promise(resolve => setTimeout(resolve, 300));184 await new Promise(resolve => setTimeout(resolve, 300));
@@ -171,7 +202,8 @@ export class HarmonyDeviceProber implements DeviceProber {
171 private async checkTcpListening(hdcPath: string, deviceId: string): Promise<boolean> {202 private async checkTcpListening(hdcPath: string, deviceId: string): Promise<boolean> {
172 try {203 try {
173 const result = await this.execHdcShell(204 const result = await this.execHdcShell(
174- hdcPath, deviceId,205+ hdcPath,
206+ deviceId,
175 'cat /proc/net/tcp /proc/net/tcp6 2>/dev/null | grep "1F4C" | head -1'207 'cat /proc/net/tcp /proc/net/tcp6 2>/dev/null | grep "1F4C" | head -1'
176 );208 );
177 return !!(result && result.includes('1F4C'));209 return !!(result && result.includes('1F4C'));
@@ -183,7 +215,8 @@ export class HarmonyDeviceProber implements DeviceProber {
183 private async checkUnixSocketExists(hdcPath: string, deviceId: string): Promise<boolean> {215 private async checkUnixSocketExists(hdcPath: string, deviceId: string): Promise<boolean> {
184 try {216 try {
185 const result = await this.execHdcShell(217 const result = await this.execHdcShell(
186- hdcPath, deviceId,218+ hdcPath,
219+ deviceId,
187 'cat /proc/net/unix 2>/dev/null | grep uitest_socket | head -1'220 'cat /proc/net/unix 2>/dev/null | grep uitest_socket | head -1'
188 );221 );
189 return !!(result && result.includes('uitest_socket'));222 return !!(result && result.includes('uitest_socket'));
@@ -170,10 +170,13 @@ export abstract class HarmonyLayoutProvider extends BaseSystemMixin<HarmonySyste
170 * /data/local/tmp 下任意时刻至多存在 1 个 layout_*.json。170 * /data/local/tmp 下任意时刻至多存在 1 个 layout_*.json。
171 */171 */
172 protected async dumpLayoutViaShell(): Promise<string> {172 protected async dumpLayoutViaShell(): Promise<string> {
173- const cmd =173+ const dumpCmd = 'rm -f /data/local/tmp/layout_*.json 2>/dev/null; uitest dumpLayout 2>/dev/null';
174- 'rm -f /data/local/tmp/layout_*.json 2>/dev/null; uitest dumpLayout >/dev/null 2>&1; for i in 1 2 3 4 5 6 7 8 9 10; do f=$(ls -t /data/local/tmp/layout_*.json 2>/dev/null | head -1); [ -n "$f" ] && cat "$f" && break; sleep 0.3; done';174+ await this.execCommand(dumpCmd, this.commandTimeouts.layout);
175- const result = await this.execCommand(cmd, this.commandTimeouts.layout + 5000);175+ const readCmd =
176- return result.trim();176+ 'for i in 1 2 3 4 5 6 7 8 9 10; do f=$(ls /data/local/tmp/layout_*.json 2>/dev/null | head -1); [ -n "$f" ] && cat "$f" && break; sleep 0.3; done';
177+ const result = await this.execCommand(readCmd, this.commandTimeouts.layout + 5000);
178+ const jsonStart = result.indexOf('{');
179+ return jsonStart >= 0 ? result.substring(jsonStart).trim() : result.trim();
177 }180 }
178}181}
179 182