Skip to content

native-devtools-status reports connected: true for a backgrounded app, then every native-* tool burns a full timeout on an opaque "ViewInspector RPC timed out" #762

Description

@hubgan

Found during an end-to-end pass over the v0.19.0 + 7 commits release candidate, on a clean single-tool-server host (iPhone 17 Pro, iOS 26.5).

Summary

When an injected app is running but not in the foreground, native-devtools-status reports a fully healthy connection:

{ "envSetup": true, "appRunning": true, "connected": true,
  "requiresRestart": false, "nextLaunchWillBeInjected": true, "injectable": true }

but every app-scoped native tool then hangs for its full timeout and fails with an opaque:

[Tool:native-describe-screen] ViewInspector RPC timed out: ViewHierarchy.describeScreen

Nothing in either response mentions that the app is backgrounded, which is the actual cause.

Reproduction

  1. Launch an injectable app with restart-app and confirm native-devtools-status → connected: true.
  2. Foreground a different app (in my case a flow run whose launch targeted com.apple.Preferences; any launch-app does it).
  3. Call native-describe-screen with the original bundleId.

Result: the RPC times out. Reproduced twice in a row, so it is not a transient.

  1. restart-app the original bundle to bring it forward, then call the same tool again.

Result: succeeds immediately, returning the expected element list.

native-devtools-status returns the same healthy payload at every step above, including while the RPCs are failing.

Why this is worse than a plain timeout

The tool's own contract routes recovery off requiresRestart:

If requiresRestart is true: call restart-app, then proceed with the native feature.

Here requiresRestart is false and connected is true, so the documented decision procedure says "everything is fine, proceed" — while the thing you are about to call cannot work. An agent following the contract has no signal to act on and simply burns a timeout per call.

Compare with the non-injectable path, which is handled well: injectable: false is explicitly documented as terminal, tells the caller not to retry, and names the alternative (describe / screenshot). That is the standard this path does not meet.

There is also a real cost per occurrence: the failure is a full RPC timeout rather than a fast rejection, so a multi-step investigation stalls for a long time before producing an error that does not say what to do.

Suggested fix

Two parts, either of which helps on its own:

  • Report app state in native-devtools-status. connected currently means "the dylib registered a connection", which stays true across backgrounding. Either add a foreground/app-state field, or make connected reflect whether the runtime can actually service a request.
  • Make the timeout diagnosable. When the target bundle is not the foreground app, fail fast with that reason and the remedy (restart-app / launch-app to bring it forward) instead of a bare ViewInspector RPC timed out: <method>. As a fallback, the timeout message should at least name the bundle and its observed app state.

Notes

This is not the stuck-injection failure in #561 — that one pins connected: false and never recovers without a tool-server restart. This one reports connected: true, and foregrounding the app fixes it immediately. Both were observed in the same session and are distinct.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    b:minorBug that either affects a small cohort of users and affects a minor feature.bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions