Skip to content

fix(android): handle missing adb reverse mapping in reverseRemove#4927

Open
duckdum wants to merge 1 commit intowix:masterfrom
duckdum:fix/android-reverse-remove-not-found
Open

fix(android): handle missing adb reverse mapping in reverseRemove#4927
duckdum wants to merge 1 commit intowix:masterfrom
duckdum:fix/android-reverse-remove-not-found

Conversation

@duckdum
Copy link
Copy Markdown

@duckdum duckdum commented Apr 1, 2026

Description

This pull request addresses an issue with parallel Android emulator test execution where ADB.reverseRemove() throws an unhandled error that causes random test failures.

Problem

When running Detox tests with multiple Android emulators (--maxWorkers 2+), tests randomly fail with:

Command failed: "adb" -s emulator-5554 reverse --remove tcp:61045
adb: error: listener 'tcp:61045' not found

followed by:

Detox can't seem to connect to the test app(s)\!

Root Cause

With multiple emulators sharing a single ADB server, the server occasionally drops the adb reverse port mapping on one emulator. When this happens:

  1. The instrumentation process loses its connection to the Detox test server and terminates
  2. The close event on the child process fires, triggering Instrumentation._onTerminated()MonitoredInstrumentation._onInstrumentationTerminated() → the AndroidDriver termination function
  3. The termination function calls ADB.reverseRemove(), which tries adb reverse --remove tcp:<port> — but the mapping is already gone, so it throws "listener not found"
  4. Since the close event callback is async but never awaited, this becomes an unhandled promise rejection
  5. Jest-circus captures unhandled rejections via process.on('unhandledRejection') and attributes them to whichever test is currently running — causing that test to fail even though the error is unrelated to test logic

The emulator itself recovers fine on the next device.launchApp() call (which re-creates the reverse mapping). The only damage is the collateral test failure from the unhandled rejection.

Fix

Catch the "not found" error in ADB.reverseRemove() since removing an already-absent mapping is a benign no-op. All other errors are still thrown.

This is consistent with how other ADB methods in this file handle expected failure cases (e.g., getState() catching "device not found", grantAllPermissions() catching "no permission specified", getFileSize() handling "No such file or directory").

Relation to #4900

PR #4900 addresses the broader ADB instability by giving each emulator its own ADB server. This PR is complementary — it provides resilience at the reverseRemove level regardless of the ADB server architecture. Even with per-emulator ADB servers, a mapping could still be absent during cleanup if the emulator crashed, so this defensive check is valuable in either case.


For features/enhancements:

  • N/A (bug fix)

For API changes:

  • N/A (internal behavior change only)

When the instrumentation process terminates unexpectedly (e.g., due to
ADB instability with multiple emulators sharing one ADB server), the
close event callback calls reverseRemove() to clean up the port mapping.

If the mapping was already removed (by the same ADB hiccup that killed
the instrumentation), reverseRemove() throws "listener not found". Since
the close event callback is async but never awaited, this becomes an
unhandled promise rejection.

Jest-circus captures unhandled rejections and attributes them to
whichever test is currently running, causing that test to fail even
though the error is unrelated to the test logic. The emulator itself
recovers fine on the next device.launchApp() call.

This change catches the "not found" error in reverseRemove() since
removing an already-absent mapping is a benign no-op. Other errors are
still thrown.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant