react-native-community / react-native-community/cli

run-android fails with "No connected devices!" on a cold-booting emulator (waits for adb, not for sys.boot_completed)

Open
#2,824 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
2.9k
Forks
949
PR merge metrics
No merged PRs in 30d

Description

Environment

npx react-native info is not representative here (pnpm monorepo), so the relevant
versions collected by hand:

System:
    OS: macOS 26.5.2 (25F84)
    Node: 24.15.0
    Java: openjdk 17.0.20 2026-07-21 LTS
  SDKs:
    Android SDK Platform-Tools: 37.0.0 (adb 1.0.41)
    Android Emulator: 36.6.11.0 (build_id 15507667)
  npmPackages:
    react-native: 0.86.0
    @react-native-community/cli: 20.1.0
    @react-native-community/cli-platform-android: 20.1.0

Also reproduces against maintryLaunchEmulator.ts is unchanged since
2514405b (Apr 2024), and the two relevant lines are identical in v20.2.0.

Description

When run-android has to launch the emulator itself, and that emulator does a
genuine cold boot (no quick-boot snapshot — a fresh AVD, an AVD whose
snapshot was invalidated, -no-snapshot-load, or CI), the build fails:

error Failed to install the app. Command failed with exit code 1: ./gradlew app:installDevDebug …
com.android.builder.testing.api.DeviceException: No connected devices!

with Gradle having logged Device is still booting just before.

Root cause

launchEmulator in
packages/cli-platform-android/src/commands/runAndroid/tryLaunchEmulator.ts
treats the emulator as ready the moment adb devices lists it:

const bootCheckInterval = setInterval(async () => {
  const devices = adb.getDevices(adbPath);
  const connected = port
    ? devices.find((d) => d.includes(`${port}`))
    : devices.length > 0;
  if (connected) {
    cleanup();
    resolve(true);          // ← too early
  }
}, 1000);

adb.getDevices only keeps devices whose state is device
(adb.ts#L25),
which is an honest signal — but it is a signal about adbd, not about the
Android framework. adbd starts early in boot; ActivityManager finishes much
later. run-android therefore resolves, hands off to Gradle, and Gradle's
install task refuses a device that is still booting.

Measurements

Timing one cold boot on the machine above (AVD launched at t=0):

Event t
adb devices first reports state device 9.3 s
getprop sys.boot_completed first returns 1 20.2 s

That is a ~10.9 s window in which the current check is satisfied and the
device cannot actually be installed to. Anything that fits inside that window —
Gradle configuration being warm, a cached build — loses the race. Anything that
takes longer than 10.9 s (a cold Gradle daemon, a first build) accidentally
survives it, which is why this is so often written off as flaky.

Suggested fix

Gate on sys.boot_completed, which is set once ActivityManager has finished
starting and is therefore a strictly later — and safe — signal:

adb -s <serial> shell getprop sys.boot_completed

One consequence worth calling out: with that gate in place the existing
timeout = 30 no longer budgets for "adb answers", it budgets for "the whole
framework is up". On this machine that is 20.2 s of the 30 s, leaving ~10 s of
headroom; slower hardware and CI runners exceed it routinely. The timeout needs
to grow alongside the gate, otherwise the fix converts a wrong-success into a
spurious timeout.

I have this running as a local patch and can open a PR — say the word and I'll
send it. (Happy to make the timeout a flag instead of a constant if you'd
prefer that shape.)

Reproducible Demo

npx @react-native-community/cli init BootRace --version 0.86.0
cd BootRace

# make sure no emulator is running, and force a true cold boot
adb devices          # expect: empty
emulator -list-avds  # pick one, then wipe its snapshot:
emulator @<avd> -no-snapshot-save -wipe-data &  # let it boot once, then kill it

npx react-native run-android

The failure is timing-sensitive by nature. To see the window directly, without
building anything:

emulator @<avd> -no-snapshot-load &
START=$(date +%s)
while ! adb devices | grep -q "device$"; do sleep 0.1; done
echo "adb says 'device' after $(( $(date +%s) - START ))s"
while [ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" != "1" ]; do sleep 0.1; done
echo "sys.boot_completed after $(( $(date +%s) - START ))s"

Every second between those two lines is a second in which run-android believes
the emulator is ready and Gradle disagrees.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in packages/cli-platform-android/src/commands/runAndroid/tryLaunchEmulator.ts and inspect launchEmulator's adb readiness check and its 30-second timeout. Reproduce the cold-boot timing with the emulator and adb commands from the issue. Done means run-android waits for sys.boot_completed and allows enough time for the framework to start without causing premature Gradle installation or spurious timeouts.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, react-native, typescript
Domain
cli, mobile-dev
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.