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

Ouverte
#4,265 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Les mainteneurs répondent en général sous 1 jour

@twotnguyen y travaille déjà.

Depuis le 6/11/2026.

  • #4273 par @twotnguyen — ouverte

Évaluation

Difficulté
4/5
Temps estimé
3-5 jours
Accessibilité débutants
35/100
Type d'issue
Bug
Clarté
Clairement spécifiée
Activité
À l'abandon
Stack technique
ios, node.js, typescript
Domaine
cli, mobile, testing

Piste de recherche

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.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

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

Langage dominant
TypeScript
Étoiles
5.3k
Forks
377
Merge moyen
14 h 6 min
PR mergées (30 j)
650

Préparer son environnement

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de callstack/agent-device

Toutes les issues de callstack/agent-device

Issues similaires

Plus d'issues TypeScript

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.