iOS simulator: in-flight `prepare ios-runner` keeps retrying the old runner port after the runner is relaunched on a new port

Aperta
#4,265 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@twotnguyen ci sta già lavorando.

Dal 6/11/2026.

  • #4273 di @twotnguyen — aperta

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
35/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Ferma
Stack tecnologico
ios, node.js, typescript
Ambito
cli, mobile, testing

Direzione di ricerca

Start with the ios_runner_connect retry loop in runner-startup-transport.js and trace how it handles a stopped or replaced runner session. Check the child process exit and signal state, and how the daemon updates session ports during relaunch. Done means an in-flight prepare request follows the new session or fails promptly with a clear replacement error; open pull request #4273 indicates work is already underway.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Environment

  • agent-device 0.21.27 (CLI through npx, daemon mode)
  • Xcode 26.3
  • iOS 26.2 simulator, x86_64 host (macOS 15, Darwin 24.6)
  • Command: agent-device prepare ios-runner --timeout 240000
  • Host load average during the failing runs: 5–43 (1-minute)

Summary

The first xcodebuild test-without-building ends after exactly 45 s with ** BUILD INTERRUPTED **.
The daemon then runs build-for-testing and starts a new runner on a new port.
The new runner logs AGENT_DEVICE_RUNNER_LISTENER_READY.
The in-flight prepare request does not move to the new session.
It retries the old port until the full 240 s budget is used, then fails with Runner did not accept connection.

A longer --timeout does not help: the new runner is ready more than 2 minutes before the request gives up.

Three runs, same pattern

Run (UTC) Port the request retries Port of the relaunched runner New runner ready Request exhausted Attempts
2026-11-06 00:23:43 61846 62919 00:26:10 00:27:44 957 / 957
2026-11-06 01:07:40 56766 58194 01:10:13 01:11:40 960 / 960
2026-11-06 02:15:17 57548 58795 02:17:05 02:19:18 960 / 960

In each run, the first test-without-building process ends at 00:45 elapsed.

Timeline (run 3, UTC)

02:15:11  xcodebuild test-without-building starts (pid A, port 57548)
02:15:17  prepare request starts
02:15:18  ios_runner_session_reuse  sessionId=<SIM_UDID>:57548:…  ready=false
02:15:18  ios_runner_readiness_preflight_skipped  command=uptime reason=read_only_startup_command
02:15:18  retry ios_runner_connect attempt 1/960, remainingMs=239865
02:15:56  pid A ends after 45 s, runner.log: ** BUILD INTERRUPTED **
02:15:57  xcodebuild build-for-testing starts (58 s)
02:16:55  xcodebuild test-without-building starts (pid B, port 58795)
02:17:05  runner.log: AGENT_DEVICE_RUNNER_LISTENER_READY, AGENT_DEVICE_RUNNER_PORT=58795
02:19:18  retry ios_runner_connect exhausted 960/960
02:19:18  ios_runner_command_send error: Runner did not accept connection
02:19:18  runner_xctestrun_cache action=clean reason="Runner did not accept connection"
02:19:18  request_failed: prepare ios-runner timed out
02:19:20  daemon.log ios_runner_session_detach_skipped sessionId=<SIM_UDID>:58795:…

The request log shows only one ios_runner_session_reuse event (port 57548).
The relaunch on port 58795 is not in the request log.

Request log excerpt (run 3)

{"phase":"ios_runner_session_reuse","command":"prepare","data":{"sessionId":"<SIM_UDID>:57548:1793931310734","ready":false}}
{"phase":"retry","command":"prepare","data":{"phase":"ios_runner_connect","event":"attempt_failed","attempt":1,"maxAttempts":960,"elapsedMs":5,"remainingMs":239865}}
{"phase":"retry","command":"prepare","data":{"phase":"ios_runner_connect","event":"exhausted","attempt":960,"maxAttempts":960,"elapsedMs":239871,"remainingMs":0}}
{"phase":"ios_runner_command_send","level":"error","command":"prepare","durationMs":239871,"data":{"command":"uptime","readOnly":true,"sessionReady":false,"timeoutMs":239870,"error":"Runner did not accept connection"}}
{"phase":"request_failed","command":"prepare","data":{"error":"prepare ios-runner timed out"}}

daemon.log excerpt (run 3)

{"ts":"2026-11-06T02:19:20.493Z","phase":"ios_runner_session_detach_skipped","session":"daemon","data":{"sessionId":"<SIM_UDID>:58795:1793931415348"}}

Where we think it happens (reading the published dist)

  1. In runner-startup-transport.js, the connect loop (phase: ios_runner_connect) reads endpoint.port once and resolves the route once before the retry loop.
    For simulators the route is http://127.0.0.1:<port>/command and is not resolved again inside the loop.
  2. The per-attempt guard stops the loop only when session.child.exitCode is not null.
    In Node.js, a child that a signal ends has exitCode === null and a non-null signalCode.
    The session stop path sends SIGTERM / SIGKILL.
    So if the 45 s launch budget (runner_launch_budget_exhausted) stops the first xcodebuild, the in-flight loop does not see it.
    This is our hypothesis. We did not confirm the exit signal of pid A.
  3. From 0.21.30, the guard also returns early when session.state !== 'starting', but it does not stop the loop.
    The session object stays the old one, so the loop keeps the old port.

We checked 0.21.28 to 0.21.36 and 0.21.37-nightly.20261103.128.
The connect loop has the same shape in all of them.

Expected behavior

When the daemon invalidates or replaces the runner session that a request waits on, the request does one of these:

  • moves to the new session and its port, or
  • fails fast with a clear "runner session replaced" error so that the caller can retry.

Workaround we use

None that works reliably. A longer --timeout does not help. Pre-warming with prepare ios-runner did not lower the failure rate (11 runs).

Lingua principale
TypeScript
Stelle
5.3k
Fork
377
Merge medio
14h 6m
PR unite (30g)
650

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di callstack/agent-device

Tutte le issue di callstack/agent-device

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.