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

Abierto
#4,265 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

@twotnguyen ya está trabajando en esto.

Desde el 6/11/2026.

  • #4273 de @twotnguyen — abierto

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
35/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Estancado
Stack tecnológico
ios, node.js, typescript
Área
cli, mobile, testing

Línea de trabajo

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.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

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).

Lenguaje dominante
TypeScript
Estrellas
5.3k
Forks
377
Merge medio
14 h 6 min
PR fusionados (30 d)
650

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de callstack/agent-device

Todos los issues de callstack/agent-device

Issues similares

Más issues de TypeScript

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.