From ab495a9b42f2af30f5222bd978136f9b0a85b68a Mon Sep 17 00:00:00 2001 From: soloturn Date: Fri, 11 Sep 2026 13:30:31 +0200 Subject: [PATCH] fix: wait for the input service before using it Fixes #432, duplicate of #489. sys.boot_completed only means system_server reached its final boot phase - it doesn't guarantee every service has registered with ServiceManager by the time that property is externally observable over adb. `input keyevent 82` right after boot can race InputManagerService's own registration and throw ServiceNotFoundException: No service published for: input. Adds waitForService(), polling `service check ` before the input keyevent call. Independently reproduced with the same stack trace in kiwix/kiwix-android#5047. --- lib/emulator-manager.js | 29 +++++++++++++++++++++++++++++ src/emulator-manager.ts | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/lib/emulator-manager.js b/lib/emulator-manager.js index e878461a8..18a75fb0d 100644 --- a/lib/emulator-manager.js +++ b/lib/emulator-manager.js @@ -103,6 +103,8 @@ async function launchEmulator(avdName, disableAnimations, disableLinuxHardwareAc }); // wait for emulator to complete booting await waitForDevice(port, emulatorBootTimeout); + // boot_completed doesn't guarantee every service is registered yet (#432) + await waitForService(port, 'input'); await adb(port, `shell input keyevent 82`); if (disableAnimations) { console.log('Disabling animations.'); @@ -175,6 +177,33 @@ async function waitForDevice(port, emulatorBootTimeout) { attempts++; } } +/** + * Wait for a ServiceManager-registered service (e.g. 'input') to be available. + */ +async function waitForService(port, serviceName, maxAttempts = 15, retryIntervalSeconds = 1) { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + let result = ''; + try { + await exec.exec(`adb -s emulator-${port} shell service check ${serviceName}`, [], { + listeners: { + stdout: (data) => { + result += data.toString(); + }, + }, + }); + } + catch (error) { + console.warn(error instanceof Error ? error.message : error); + } + if (result.includes('found') && !result.includes('not found')) { + return; + } + if (attempt === maxAttempts) { + throw new Error(`Timeout waiting for the '${serviceName}' service to become available.`); + } + await delay(retryIntervalSeconds * 1000); + } +} function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/src/emulator-manager.ts b/src/emulator-manager.ts index d10943d43..831d1a140 100644 --- a/src/emulator-manager.ts +++ b/src/emulator-manager.ts @@ -97,6 +97,9 @@ export async function launchEmulator( // wait for emulator to complete booting await waitForDevice(port, emulatorBootTimeout); + + // boot_completed doesn't guarantee every service is registered yet (#432) + await waitForService(port, 'input'); await adb(port, `shell input keyevent 82`); if (disableAnimations) { @@ -170,6 +173,35 @@ async function waitForDevice(port: number, emulatorBootTimeout: number): Promise } } +/** + * Wait for a ServiceManager-registered service (e.g. 'input') to be available. + */ +async function waitForService(port: number, serviceName: string, maxAttempts = 15, retryIntervalSeconds = 1): Promise { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + let result = ''; + try { + await exec.exec(`adb -s emulator-${port} shell service check ${serviceName}`, [], { + listeners: { + stdout: (data: Buffer) => { + result += data.toString(); + }, + }, + }); + } catch (error) { + console.warn(error instanceof Error ? error.message : error); + } + + if (result.includes('found') && !result.includes('not found')) { + return; + } + + if (attempt === maxAttempts) { + throw new Error(`Timeout waiting for the '${serviceName}' service to become available.`); + } + await delay(retryIntervalSeconds * 1000); + } +} + function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); }