Skip to content

[build-tools] Add worker-side SSH session helper - #4030

Open
gwdp wants to merge 15 commits into
mainfrom
gwdp/eng-24547-build-tools-ssh-session
Open

[build-tools] Add worker-side SSH session helper#4030
gwdp wants to merge 15 commits into
mainfrom
gwdp/eng-24547-build-tools-ssh-session

Conversation

@gwdp

@gwdp gwdp commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Why

The worker needs a piece that starts an SSH session on the build VM and keeps the worker alive while someone is connected.

How

New helper resolves the upterm client vendored in the worker package, dials the relay, reports the connection details back so the CLI can attach, and holds on an idle timer. Redialing is capped at a few attempts per drop so a flapping relay can't retry forever. Connection secrets are scrubbed from logs.

Test Plan

Unit tests cover the connection parsing, secret redaction, process-liveness detection, the capped redial, and the idle hold. CI passes.

@linear-code

linear-code Bot commented Jul 16, 2026

Copy link
Copy Markdown

ENG-24547

gwdp commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 63.95%. Comparing base (3f8ffac) to head (37ebacc).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4030      +/-   ##
==========================================
+ Coverage   63.74%   63.95%   +0.21%     
==========================================
  Files        1030     1033       +3     
  Lines       47477    47747     +270     
  Branches     9990    10043      +53     
==========================================
+ Hits        30260    30530     +270     
  Misses      17116    17116              
  Partials      101      101              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gwdp
gwdp force-pushed the gwdp/eng-24547-eas-build-job-ssh branch from 72fb902 to 25fb902 Compare July 16, 2026 19:05
@gwdp
gwdp force-pushed the gwdp/eng-24547-build-tools-ssh-session branch 3 times, most recently from a7b6154 to c9e54b9 Compare July 16, 2026 22:20
@gwdp
gwdp force-pushed the gwdp/eng-24547-eas-build-job-ssh branch from 25fb902 to 19047e6 Compare July 16, 2026 22:20
@gwdp
gwdp force-pushed the gwdp/eng-24547-build-tools-ssh-session branch from c9e54b9 to 12f0a14 Compare July 17, 2026 02:37
@gwdp
gwdp force-pushed the gwdp/eng-24547-eas-build-job-ssh branch from 19047e6 to 1277e59 Compare July 21, 2026 02:08
@gwdp
gwdp force-pushed the gwdp/eng-24547-build-tools-ssh-session branch from 12f0a14 to 12feb7a Compare July 21, 2026 02:08
@gwdp gwdp changed the title [build-tools] add worker-side ssh session helper [build-tools] Add worker-side SSH session helper Jul 21, 2026
@gwdp
gwdp force-pushed the gwdp/eng-24547-eas-build-job-ssh branch from 1277e59 to 1c822ac Compare July 24, 2026 00:52
@gwdp
gwdp force-pushed the gwdp/eng-24547-build-tools-ssh-session branch 2 times, most recently from cb90fa2 to 9409c52 Compare July 24, 2026 00:58
@gwdp
gwdp force-pushed the gwdp/eng-24547-eas-build-job-ssh branch from 1c822ac to 45758d8 Compare July 24, 2026 03:10
@gwdp
gwdp force-pushed the gwdp/eng-24547-build-tools-ssh-session branch from 9409c52 to daee76d Compare July 24, 2026 03:10
@gwdp
gwdp force-pushed the gwdp/eng-24547-eas-build-job-ssh branch from 45758d8 to 3906608 Compare July 24, 2026 03:13
@gwdp
gwdp force-pushed the gwdp/eng-24547-build-tools-ssh-session branch from daee76d to 7154d05 Compare July 24, 2026 03:13
@gwdp
gwdp requested a review from sjchmiela July 24, 2026 03:25
@gwdp
gwdp marked this pull request as ready for review July 24, 2026 03:25

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@gwdp
gwdp force-pushed the gwdp/eng-24547-build-tools-ssh-session branch 2 times, most recently from 420bbef to 3dc29fa Compare July 24, 2026 03:37
@brentvatne brentvatne added the ai-review Commits pushed to PRs with this label be automatically reviewed. label Jul 24, 2026
@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown

🤖 AI code review

Decision: Request changes

Overall PR risk: Low. The change is additive: it introduces a new SSH relay session helper (upterm host spawn, GraphQL session registration, redial/idle supervision) without editing any existing function body, and the cross-cutting review found no caller in this diff wiring the new helper into an existing build or job-run path, so the new code stays inert until a later change calls it. The most serious verified issue is a critical secret-leak path: redactSpawnErrorForLog can let the unredacted SSH session secret reach the build log through the output array on a spawn error. Two further warnings cover a missing SIGKILL escalation that can leak upterm host processes, and a stale-connection-state bug that can make a dead redial look alive and block further reconnects. Given the critical secrets finding, changes are requested before merge.

🔴 Critical (1)

  • redactSpawnErrorForLog copies the unredacted output array of the spawn errorpackages/build-tools/src/utils/upterm.ts:102 (secrets) · id:6d0ee6ce557e
    Confidence: Medium — the spread copies output verbatim; the leak reaches the log when the build logger writes debug records.
    Impact if shipped: High — the upterm session secret can reach the build log. That secret gives an SSH shell on the build worker.
    Suggested remediation: Redact every string in output too, for example ...(Array.isArray(spawnErr.output) ? { output: spawnErr.output.map(item => (typeof item === 'string' ? redactConnectionSecrets(item) : item)) } : {}). A safer option is to build the logged object from an explicit list of fields, instead of spreading the error.

    Evidence and reasoning

    redactSpawnErrorForLog spreads the spawn error first, then overwrites only message, stdout, and stderr.

    A rejected @expo/turtle-spawn promise carries the SpawnResult fields on the error. SpawnResult includes output: string[], which holds the same stdout and stderr strings. This repo already reads that field on a spawn error: packages/eas-cli/src/run/ios/xcrun.ts:26 checks Array.isArray(e.output), and packages/eas-build-cache-provider/src/helpers.ts:4 types a spawn error as Error & SpawnResult. Those properties are own and enumerable, so ...spawnErr copies output into the returned object without redaction.

    The host process runs with stdio: ['ignore', 'pipe', 'pipe'], so upterm stdout is captured. The file redacts stdout on its own, because that stream carries the session id (see the comment at line 80). The same text stays readable in output[0].

    The sink is line 278: ctx.logger.debug({ err: redactSpawnErrorForLog(err) }, ...). packages/logger/src/index.ts:10 sets serializers: bunyan.stdSerializers. The err serializer returns the value unchanged when the value has no stack. The spread drops stack, because stack is not enumerable on an Error. So bunyan writes the whole plain object, including the unredacted output array.

    The session id is the value stored as SshConnectionConfig.secret. A reader of the log can use it to open the SSH session and get a login shell with the job environment.

🟡 Warning (2)

  • killProcessGroup has no SIGKILL escalation after SIGTERMpackages/build-tools/src/utils/processes.ts:13 (correctness) · id:218f27b04044
    Confidence: High — direct comparison with the sibling teardown helper in the same package shows the wait-and-escalate step is missing.
    Impact if shipped: Medium — an upterm host process, or its long-running keep-alive child, can survive stop and redial if it ignores SIGTERM, leaking processes on the build machine for up to 6 hours.
    Suggested remediation: After sending SIGTERM, poll isChildProcessAlive for a bounded time, then send SIGKILL to the process group (or the single pid) if the process is still alive, mirroring stopDetachedProcessAsync.

    Evidence and reasoning

    packages/build-tools/src/steps/utils/remoteDeviceRunSession.ts (stopDetachedProcessAsync) sets the pattern for killing a detached process group in this package: send SIGTERM to the negated pid, poll isProcessRunning for up to 5 seconds, and escalate to SIGKILL on the group (falling back to the single pid) if the process is still alive.

    killProcessGroup in processes.ts solves the same problem for the upterm host process used by stopAsync and redialAsync in upterm.ts, but it sends SIGTERM only once and falls back to child.kill() (still a default-signal kill) only when process.kill throws, not when the process is merely still alive. The caller in upterm.ts races the exit against a 5-second timeout and then only logs a debug message; it never escalates the signal. Because dialAsync() calls killProcessGroup on every redial, up to MAX_SSH_REDIALS times per session, a single unresponsive process group can leave an orphaned upterm host and its child process running for the rest of the build.

  • Failed redial leaves a stale connectionConfig while isAlive() still reports truepackages/build-tools/src/utils/upterm.ts:312 (correctness) · id:2c84785b8069
    Confidence: Medium — the code path is directly traceable, but the trigger needs the upterm process to keep running after it fails to register a session in time.
    Impact if shipped: Medium — the SSH relay session can get stuck reporting as connected while it is actually dead, blocking further reconnect attempts.
    Suggested remediation: In dialAsync, clear currentProcess (or stop the new process) when waitForConnectionConfigAsync throws, so isAlive() reflects the real connection state and a later ensureConnectedAsync retry re-enters the redial branch.

    Evidence and reasoning

    dialAsync (line 281) sets currentProcess = uptermProcess before it awaits waitForConnectionConfigAsync. If that wait times out and throws, dialAsync rejects, but currentProcess still points at the new process. redialAsync (line 312-315) only assigns the outer connectionConfig variable after dialAsync resolves, so on a throw the getter keeps returning the old connectionConfig, which points at a session that stopCurrentProcessAsync already killed at the top of the same dialAsync call.

    In packages/build-tools/src/utils/turtleSshSession.ts, ensureConnectedAsync calls host.redialAsync() and, on a throw, retries after a backoff. On the retry it first checks if (!host.isAlive()). If the new, unconfirmed process from the failed redial is still running, isAlive() returns true, so the retry skips redialAsync() and calls createOrUpdateSessionAsync with the stale host.connectionConfig, marking reconnecting: false even though the session behind that config is dead. This can report the SSH relay as restored when it is not, and it blocks further redial attempts because isAlive() keeps returning true for the leftover process.


This review is advisory — it never blocks a merge and never auto-approves.

@gwdp
gwdp requested a review from sjchmiela July 29, 2026 00:11
@gwdp
gwdp force-pushed the gwdp/eng-24547-eas-build-job-ssh-settings branch from 6973f85 to 0a7a451 Compare July 29, 2026 22:28
Base automatically changed from gwdp/eng-24547-eas-build-job-ssh-settings to main July 29, 2026 22:44
@gwdp
gwdp force-pushed the gwdp/eng-24547-build-tools-ssh-session branch 2 times, most recently from 2e5bca4 to 61e8f23 Compare August 3, 2026 20:12
Comment thread packages/build-tools/src/utils/processes.ts Outdated
Comment thread packages/build-tools/src/utils/processes.ts Outdated
Comment thread packages/build-tools/src/utils/upterm.ts Outdated
Comment thread packages/build-tools/src/utils/turtleSshSession.ts Outdated
Comment thread packages/build-tools/src/utils/turtleSshSession.ts Outdated
Comment thread packages/build-tools/src/utils/upterm.ts
Comment thread packages/build-tools/src/utils/turtleSshSession.ts
Comment on lines +268 to +283
if (connectedClientCount !== null) {
if (connectedClientCount > previousClientCount) {
logger.info(
connectedClientCount === 1
? 'An SSH client connected.'
: `An SSH client connected (${connectedClientCount} connected).`
);
} else if (connectedClientCount < previousClientCount) {
logger.info(
connectedClientCount === 0
? 'The SSH client disconnected.'
: `An SSH client disconnected (${connectedClientCount} still connected).`
);
}
previousClientCount = connectedClientCount;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this feels slightly misleading cause ti may happen that a user disconnects and another connects and we won't print anything because the count is still 1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point,, but the limitation is that upterm session current --output json only hands back a count, it does len(connected_clients) server-side and drops the list, so from the worker a 1-for-1 swap looks identical to a steady connection.

To log real join/leave events I think I can read upterm's connected_clients (they carry a public-key fingerprint) off its gRPC admin api instead of the CLI. I might defer now as a known limitation and go for a follow-up if that works for you?

Comment thread packages/build-tools/src/utils/turtleSshSession.ts
Comment thread packages/build-tools/src/utils/turtleSshSession.ts Outdated
@gwdp
gwdp force-pushed the gwdp/eng-24547-build-tools-ssh-session branch 4 times, most recently from b862057 to db1ab53 Compare August 15, 2026 08:00
@gwdp

gwdp commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

Applied changes but want to do a little more testing before requesting review.

@gwdp
gwdp requested a review from sjchmiela August 17, 2026 16:18
Comment thread packages/build-tools/src/utils/turtleSshSession.ts Outdated
Comment thread packages/build-tools/src/utils/turtleSshSession.ts Outdated
Comment thread packages/build-tools/src/utils/turtleSshSession.ts Outdated
Comment on lines +102 to +103
turtleJobRunId: target.turtleJobRunId ?? null,
turtleBuildId: target.turtleBuildId ?? null,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gwdp gwdp Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm... that's a good one, I think I do prefer your way 🤔 (baking a change)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread packages/build-tools/src/utils/turtleSshSession.ts
Comment thread packages/build-tools/src/utils/upterm.ts
Comment thread packages/build-tools/src/utils/upterm.ts
Comment thread packages/build-tools/src/utils/upterm.ts
gwdp added 14 commits August 20, 2026 16:46
Update connectionConfig on every redial, linearize ensureConnectedAsync, and
parse upterm session objects without a JSON stringify round-trip.
Invalid upterm host URLs, missing process pid, stdout chunks, unknown
client counts during the job, and turtleBuild targets.
…w fixes

Zod-parse upterm session JSON and move isChildProcessAlive into processes.ts
(claimed in review but never landed). Expand unit coverage and stop gitignoring
baked upterm binaries now that the worker fetches from GCS.
Restore the orphaned review-fix tip: prefer an image/PATH upterm, else
download darwin-arm64 / linux-amd64 from gs://turtle-v2/upterm. Keeps the
stricter zod session parse, process-group/admin-socket comments, and GCS
cache/path unit coverage.
@gwdp
gwdp force-pushed the gwdp/eng-24547-build-tools-ssh-session branch from e675651 to 01bf840 Compare August 20, 2026 23:51
@github-actions

Copy link
Copy Markdown

✅ Thank you for adding the changelog entry!

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

Labels

ai-review Commits pushed to PRs with this label be automatically reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants