fix(ios): keep stale SSH attaches from reclaiming surfaces

Merged
TE
19d ago
+173-4

Problem

A canceled SSH attach can finish after a replacement attach starts for the same iOS surface. The old completion unconditionally clears attachTasks[surfaceID], installs its attachment, and its event callback keeps publishing output. This lets stale output resurrect after detach and can erase the replacement task.

Fix

  • Give each surface attach a generation token.
  • Guard late provider completions, event callbacks, input writes, resize, and error publication with that token and cancellation state.
  • Keep the token for the live attachment so its callback remains valid; clear it only when the attachment is gone.
  • Cancel pending attach tasks when a host connection closes, including surfaces without an attachment yet.
  • Treat an attach that ends before it returns as stale and detach the returned handle.

Tests

Added MobileSSHAttachOwnershipTests: canceled attach A is released after replacement B starts, A output is ignored, A is detached, and B output remains live.

Portable validation: swiftc -parse on the changed source and test, swift package dump-package, and git diff --check pass. Targeted Swift package tests need to run on the fleet because native package builds are not run on the Air.

Fixes #16147


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Fixes a race where canceled or superseded iOS SSH attaches could reclaim a surface, erase the replacement task, and let stale output resurface after detach.

  • Guards provider completions, event callbacks, input writes, resize, and error publication with a per-attach generation token.
  • Detaches an attach that returns after being canceled or superseded.
  • Cancels pending attach tasks when a host connection closes, including surfaces without an attachment yet and surfaces awaiting a grid.
  • Clears pending input when a surface detaches so stale keystrokes don't replay into the replacement attach.
  • Adds MobileSSHAttachOwnershipTests covering a canceled attach and a replacement attach on the same surface.

Fixes #16147.


[!NOTE]
Medium Risk
Changes async SSH attach lifecycle and teardown on iOS; wrong guards could drop live terminal I/O, but scope is localized and covered by a targeted ownership test.

Overview
Fixes an iOS SSH race where a canceled or superseded attach could still complete, clear the in-flight task, install its handle, and keep publishing terminal output on a surface that already started a replacement attach.

MobileSSHComputers now assigns a per-surface attach generation and uses it (with cancellation checks) to gate provider completion, event delivery, pending input replay, resize, error UI, and task cleanup so only the current attach owns the surface. Stale handles are detached when they return after cancel/supersede; detach also drops queued input and the generation token.

Host disconnect teardown now walks surfaces from attachTasks and attachAwaitingGrid as well as live attachments, so in-progress or grid-waiting attaches are canceled too.

Adds MobileSSHAttachOwnershipTests to assert a late attach A cannot emit output or write into replacement B.

Reviewed by Cursor Bugbot for commit 92d360f03d021c6d3fda59cc1cdd02f1459f3ac5. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Bug Fixes
    • SSH terminal attachments remain isolated when an attachment is canceled or replaced. Late results from an older request no longer send output, interfere with the active attachment, or report errors for a newer request.
    • Closing a workspace now detaches surfaces tied to active attachments, attachment attempts still in progress, and surfaces awaiting a grid, preventing stale terminal connections from lingering.
    • Canceled or superseded attachment attempts no longer apply queued input or resizing changes to a replacement attachment.