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

未关闭
#4,265 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

@twotnguyen 已经在做这个了。

开始于 2026年11月6日。

  • #4273 来自 @twotnguyen —— 未关闭

评估

难度
4/5
预计耗时
3-5 天
新手友好度
35/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
停滞
技术栈
ios, node.js, typescript
领域
cli, mobile, testing

调研方向

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.

由索引模型根据 Issue 内容生成。

描述

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

主要语言
TypeScript
星标
5.3k
派生
377
平均合并
14 小时 6 分钟
30 天内合并 PR
650

环境准备

  • 没有 Dockerfile 或 Docker Compose 文件
  • 没有 Pull Request 模板
  • 阅读贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

callstack/agent-device 的其他 Issue

查看 callstack/agent-device 的全部 Issue

相似的 Issue

更多 TypeScript Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。