Merged
fix(harmony): uitest shell layout dump hang and TCP probe corruption #60
gyangeAdd on 26 days ago
fix(harmony): uitest shell layout dump hang and TCP probe corruption #60
Merged
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,负载下偶发漏行 → 回退 69px | 205 | // 缓存的原因:探测依赖 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 * 1024 | 492 | + 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, | |||
| 650 | async function readStatusBarHeightCache( | 660 | async 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-write | 690 | // 文件独占一设备,整写即可,不再 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 | ||
| 680 | export async function getDeviceInfoAsync(options: { | 698 | export 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 | ||