Skip to content

Handle SIGTERM in terminal host and GC orphaned sockets (#19302) - #19344

Merged
David Pine (IEvangelist) merged 9 commits into
mainfrom
dapine/fix-terminal-socket-leak
Aug 15, 2026
Merged

Handle SIGTERM in terminal host and GC orphaned sockets (#19302)#19344
David Pine (IEvangelist) merged 9 commits into
mainfrom
dapine/fix-terminal-socket-leak

Conversation

@IEvangelist

@IEvangelist David Pine (IEvangelist) commented Aug 13, 2026

Copy link
Copy Markdown
Member

Description

WithTerminal() could leave per-replica Unix-domain sockets under ~/.aspire/trmnl/ when a terminal host was stopped with SIGTERM or when its owning AppHost exited unexpectedly.

This change makes terminal lifecycle cleanup resilient across graceful stops, forced termination, process crashes, PID reuse, reboots, containers, and concurrent AppHosts:

  1. Shared terminal-host shutdown handling. Both the standalone Aspire.TerminalHost executable and the bundled aspire-managed terminalhost path now use the same runner. It handles SIGINT/SIGTERM (plus Windows Ctrl+Break), cancels TerminalHostApp, and lets its existing teardown unlink the sockets.
  2. Parent-process watchdog. Each terminal host receives the AppHost PID plus stable process identity and exits when that exact owner disappears, covering AppHost crashes that do not signal children normally.
  3. Run-scoped replica identities. Every materialization uses a random per-run replica ID. An old AppHost or child therefore cannot unlink or rebind a newer run's sockets, eliminating the deterministic-path replacement race and removing the need for active lock files.
  4. Background orphan collection. A once-per-AppHost background sweep reclaims only replicas whose owner is provably gone. Current metadata records PID, stable process identity, machine/PID-namespace scope, and Linux boot ID. Released schema v1 and preview schema v2 remain conservatively readable; inaccessible owners are treated as alive. Sockets are deleted before metadata so partial failures remain discoverable, and legacy locks are honored and reclaimed safely.
  5. Run-mode isolation. Terminal hosts, sidecars, and cleanup are materialized only in run mode; publish mode does not mutate per-user terminal state.

On Windows, TerminateProcess cannot be intercepted. Graceful console signals use the shared runner, while exact-path AppHost cleanup, the parent watchdog, and the next startup sweep provide the cleanup backstops for forced termination.

Tests

Coverage includes process-level SIGTERM for the bundled host, parent-death cleanup for bundled and standalone hosts, random identity isolation, schema compatibility, PID reuse, process-scope and Linux boot-ID handling, metadata-last deletion, legacy-lock interoperability, singleton background sweeping, and publish-mode behavior.

Fixes #19302

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No

WithTerminal() leaked two per-replica Unix-domain-socket files ({id}.dcp.sock producer and {id}.host.sock consumer) under ~/.aspire/trmnl/ on a graceful `aspire stop`.

Root cause: the aspire.terminalhost child only handled SIGINT (Console.CancelKeyPress). DCP stops the resource with SIGTERM and waits a grace period before SIGKILL, so the child's cooperative teardown (which unbinds and deletes the sockets) never ran; the recycle loop re-bound the producer/consumer UDS after the AppHost's ApplicationStopped cleanup, then SIGKILL left both files bound on disk.

Fix (two parts):
1. Program.cs now routes SIGINT + SIGTERM (and SIGQUIT on Windows) through PosixSignalRegistration into the existing graceful teardown, with a Console.CancelKeyPress fallback for platforms that lack PosixSignalRegistration. A clean stop now unlinks both sockets before exit.
2. MaterializeTerminalHosts sweeps the shared trmnl directory on startup and reclaims files whose owning AppHost PID is provably dead (keyed off the sidecar's appHostPid), cleaning up orphans left by ungraceful exits. PID-liveness makes it safe: live AppHosts are never touched.

Also corrects the now-inaccurate ApplicationStopped backstop comment. Adds a child-side graceful-cleanup test and two AppHost-side startup-GC tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c3b3d047-2db6-43af-a787-315dd291a16f
Copilot AI balanced review requested due to automatic review settings August 13, 2026 14:38
@github-actions github-actions Bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Aug 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 19344

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 19344"

@github-actions

This comment has been minimized.

Copilot AI left a comment

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.

Pull request overview

Adds graceful terminal-host shutdown and startup cleanup for orphaned Unix socket artifacts.

Changes:

  • Handles SIGINT/SIGTERM through cooperative cancellation.
  • Sweeps terminal files owned by dead AppHost processes.
  • Adds shutdown and orphan-cleanup regression tests.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
src/Aspire.TerminalHost/Program.cs Registers process signal handlers.
src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs Adds startup orphan cleanup.
tests/Aspire.TerminalHost.Tests/TerminalHostAppTests.cs Tests socket cleanup after cancellation.
tests/Aspire.Hosting.Tests/WithTerminalTests.cs Tests dead/live owner sweeping.

Comment thread src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs Outdated
Comment thread src/Aspire.TerminalHost/Program.cs Outdated
Comment thread src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs Outdated
@IEvangelist

Copy link
Copy Markdown
Member Author

PR Testing Report

PR Information

Artifact Version Verification

  • Expected Commit: 35a2558bebc585799606dde32a279baf8d298f53
  • Installed Version: 13.6.0-pr.19344.g35a2558b
  • Artifact: Dogfood CLI and bundled aspire-managed from PR workflow run 31711156146
  • Status: PASS - the installed artifact matches the current PR head

Changes Analyzed

Files Changed

  • src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs - startup GC for dead AppHost sidecars
  • src/Aspire.TerminalHost/Program.cs - POSIX signal registration for the standalone terminal host
  • tests/Aspire.Hosting.Tests/WithTerminalTests.cs - dead/live owner GC tests
  • tests/Aspire.TerminalHost.Tests/TerminalHostAppTests.cs - cancellation-driven socket cleanup test

Change Categories

  • CLI command changes
  • Hosting changes
  • Terminal host process changes
  • Dashboard changes
  • Template changes
  • Client/component changes
  • VS Code extension changes
  • CI infrastructure changes
  • Test changes

Test Scenarios Executed

Scenario 1: PR artifact identity

Objective: Ensure runtime testing uses the latest PR artifact.

Coverage Type: Artifact integrity

Status: PASS

The dogfood CLI reported 13.6.0-pr.19344.g35a2558b, matching the current PR head 35a2558bebc585799606dde32a279baf8d298f53.

Evidence:

  • artifact-install.log
  • artifact-version.txt

Scenario 2: Graceful aspire stop socket cleanup

Objective: Verify the user-facing fix: DCP stops the bundled terminal host gracefully and no replica socket files remain.

Coverage Type: Happy path / regression

Status: FAIL

Steps:

  1. Created a fresh aspire-empty app from the PR hive.
  2. Added a long-running executable with .WithTerminal().
  3. Started it with the PR CLI and waited for the terminal host process plus its control and consumer Unix sockets.
  4. Confirmed the resource was healthy.
  5. Ran aspire stop --apphost <apphost.cs>.
  6. Waited 20 seconds for all files belonging to replica BEf1ISlGaKM to disappear.

Observed:

  • Before stop: BEf1ISlGaKM.ctrl.sock, BEf1ISlGaKM.host.sock, and BEf1ISlGaKM.metadata.json existed.
  • aspire stop reported success and the bundled terminal host process exited.
  • After the 20-second wait, BEf1ISlGaKM.dcp.sock and BEf1ISlGaKM.host.sock still existed.
  • The metadata and control socket were removed, so the new startup GC has no sidecar from which to discover these two leaked sockets.

Expected: No BEf1ISlGaKM.* files remain after graceful stop.

Evidence:

  • scenario-graceful/evidence/start.log
  • scenario-graceful/evidence/describe-before-stop.json
  • scenario-graceful/evidence/processes-before-stop.txt
  • scenario-graceful/evidence/files-before-stop.txt
  • scenario-graceful/evidence/stop.log
  • scenario-graceful/evidence/files-after-stop.txt
  • scenario-graceful/evidence/processes-after-stop.txt
  • scenario-graceful/evidence/apphost-detached.log

Diagnostic: Direct SIGTERM to the bundled terminal host

Objective: Separate DCP/AppHost shutdown timing from signal handling in the shipped terminal-host executable.

Coverage Type: Targeted failure diagnosis

Status: FAIL

The test launched:

aspire-managed terminalhost --producer-uds ... --consumer-uds ... --control-uds ...

After all three sockets were bound, it sent SIGTERM directly to that process.

Observed:

  • Process exit code: 143 (128 + SIGTERM)
  • Remaining files: .ctrl.sock, .dcp.sock, and .host.sock
  • The process did not perform cooperative teardown.

Root cause:

Normal dogfood execution does not use src/Aspire.TerminalHost/Program.cs. The AppHost launches the multi-mode aspire-managed terminalhost bundle, whose dispatcher is implemented by RunTerminalHost in src/Aspire.Managed/Program.cs. At the tested PR head, that method still registers only Console.CancelKeyPress, so SIGTERM retains its default termination behavior. The PR changed only the standalone terminal-host entrypoint.

The new TerminalHostAppTests test cancels the token directly. It verifies teardown after cancellation, but it does not launch either executable or prove that SIGTERM reaches that token.

Evidence:

  • scenario-graceful/direct-sigterm-evidence/pid.txt
  • scenario-graceful/direct-sigterm-evidence/files-before-sigterm.txt
  • scenario-graceful/direct-sigterm-evidence/exit-code.txt
  • scenario-graceful/direct-sigterm-evidence/files-after-sigterm.txt
  • scenario-graceful/direct-sigterm-evidence/terminalhost.log

Scenario 3: Crash-orphan startup recovery

Objective: Verify startup GC removes files whose metadata identifies a dead AppHost.

Coverage Type: Unhappy path / crash recovery

Status: PASS

Steps:

  1. Created and started a fresh OrphanProducerApp with .WithTerminal().
  2. Waited for its real terminal-host sockets and metadata sidecar.
  3. Force-killed the terminal host and AppHost.
  4. Confirmed bH9JiEwQOXg.ctrl.sock, bH9JiEwQOXg.host.sock, and bH9JiEwQOXg.metadata.json remained.
  5. Started a different fresh OrphanSweeperApp with .WithTerminal().
  6. Confirmed all bH9JiEwQOXg.* files were removed during startup.

Evidence:

  • scenario-orphan/evidence/process-ids.txt
  • scenario-orphan/evidence/files-before-kill.txt
  • scenario-orphan/evidence/files-after-kill.txt
  • scenario-orphan/evidence/orphan-files-after-sweeper-start.txt
  • scenario-orphan/evidence/producer-apphost.log
  • scenario-orphan/evidence/sweeper-apphost.log
  • scenario-orphan/evidence/result.txt

Scenario 4: Live-owner safety boundary

Objective: Verify machine-wide startup GC never deletes terminal files owned by another running AppHost.

Coverage Type: Boundary / concurrency safety

Status: PASS

Steps:

  1. Started a fresh LiveOwnerApp and waited for its terminal host.
  2. Captured the owner PID, metadata, and replica files.
  3. Started an independent fresh LivePeerApp.
  4. Verified the owner process remained alive.
  5. Verified the owner's metadata, control socket, and consumer socket remained unchanged.

The owner file listing before and after peer startup was identical:

3tiJQAreFQM.ctrl.sock s
3tiJQAreFQM.host.sock s
3tiJQAreFQM.metadata.json f

Evidence:

  • scenario-live-owner/evidence/owner-pid.txt
  • scenario-live-owner/evidence/owner-files-before-peer.txt
  • scenario-live-owner/evidence/owner-files-after-peer-start.txt
  • scenario-live-owner/evidence/owner-metadata-before-peer.json
  • scenario-live-owner/evidence/owner-metadata-after-peer.json
  • scenario-live-owner/evidence/owner-apphost.log
  • scenario-live-owner/evidence/peer-apphost.log
  • scenario-live-owner/evidence/result.txt

Summary

Scenario Status Notes
PR artifact identity PASS Dogfood version matches current PR head
Graceful aspire stop cleanup FAIL .dcp.sock and .host.sock remain
Direct bundled SIGTERM diagnostic FAIL Exit 143; all three sockets remain
Crash-orphan startup recovery PASS Dead-owner sidecar and sockets swept
Live-owner safety boundary PASS Live peer files preserved

Overall Result

FAIL - ISSUES FOUND

The startup-GC half of the PR works for orphans that retain a metadata sidecar, and it correctly protects live AppHosts. The primary graceful-shutdown fix does not affect the bundled executable used by dogfood and normal CLI bundle execution, so the original aspire stop leak remains.

Recommendations

  1. Apply the SIGINT/SIGTERM registration to Aspire.Managed.RunTerminalHost, preferably by sharing one signal-to-cancellation helper with the standalone entrypoint.
  2. Add a process-level test that launches the bundled aspire-managed terminalhost, sends SIGTERM, and verifies exit code 0 plus removal of producer, consumer, and control sockets.
  3. Keep the existing cancellation test as teardown coverage, but do not treat it as signal-delivery coverage.

@IEvangelist David Pine (IEvangelist) left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

3 issues found: 2 correctness issues (the bundled SIGTERM path is unchanged, and metadata schema versions are ignored) and 1 regression-coverage gap for live peer ownership.

Comment thread src/Aspire.TerminalHost/Program.cs Outdated
Comment thread tests/Aspire.Hosting.Tests/WithTerminalTests.cs Outdated
Comment thread src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs Outdated
Comment thread src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs Outdated
Comment thread src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs Outdated
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ec34791b-0507-43c6-b846-916aa51f7f80
Copilot AI review requested due to automatic review settings August 13, 2026 16:31
@github-actions

This comment has been minimized.

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs:565

  • The startup GC cannot reclaim orphans created by existing releases. Those sidecars use schema v1 and omit the newly required appHostProcessStartTimeUnixMilliseconds, so deserialization throws here (and the later current-schema check would reject them anyway). This contradicts the PR's stated goal of reclaiming the reporter's already-stale sidecar. Add an explicit v1 compatibility path that validates the filename/metadata ID and uses PID-only liveness (a reused live PID should conservatively skip cleanup), plus a regression test using the actual v1 JSON shape.
                    var json = await File.ReadAllTextAsync(candidatePath, cancellationToken).ConfigureAwait(false);
                    metadata = JsonSerializer.Deserialize<TerminalHostMetadata>(json);

src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs:446

  • This makes every per-replica lock file permanent: DeleteReplicaFiles omits it, and both shutdown cleanup and startup GC call that method. Each distinct terminal replica therefore still leaves an artifact forever, and Directory.GetFiles makes every later startup scan the growing set of abandoned locks. Add a safe lock-reclamation strategy (for example, serialize lock-file deletion under a directory-wide lock) or use synchronization that does not require permanent per-replica files.
        // Keep the lock file persistent. Deleting a locked file on Unix would let another
        // process create a new inode at the same path and acquire a second, independent lock.

Comment thread src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs Outdated
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ec34791b-0507-43c6-b846-916aa51f7f80
Copilot AI review requested due to automatic review settings August 13, 2026 16:50

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs:351

  • Lock acquisition is inside the best-effort sidecar-write try, whose catch also handles UnauthorizedAccessException. If opening {id}.lock fails due to permissions, materialization therefore continues without synchronization or a new ownership sidecar, despite the collision-safety requirement documented below. Acquire the lock before entering the write-only try so permission failures propagate just like the lock timeout.
            using var replicaLock = await AcquireReplicaLockAsync(
                trmnlDirectory,
                metadata.ReplicaId,
                cancellationToken).ConfigureAwait(false);

src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs:595

  • The startup GC cannot reclaim the leaks created by the already-shipped schema-v1 sidecars. Those files omit AppHostProcessStartTimeUnixMilliseconds, so deserialization can fail on the required member, and this condition rejects schema 1 regardless. This means the stale files from #19302—including the reporter's existing sidecar—survive every startup. Treat schema 1 as a known legacy shape: validate the filename/content ID, delete only when its PID no longer exists, and preserve it when the PID is live/reused; add a v1 regression test.
                if (metadata.SchemaVersion != TerminalHostMetadata.CurrentSchemaVersion
                    || !string.Equals(metadata.ReplicaId, replicaId, StringComparison.Ordinal)
                    || metadata.AppHostPid <= 0
                    || metadata.AppHostProcessStartTimeUnixMilliseconds <= 0)

@github-actions

This comment has been minimized.

Comment thread src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs Outdated
Comment thread tests/Aspire.Managed.Tests/TerminalHostSignalTests.cs
Comment thread src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs Outdated
@github-actions

This comment has been minimized.

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs:302

  • ApplicationStopped does not guarantee that DCP's terminal-host children have exited; that false assumption is the root condition documented in #19302. The new child teardown and random IDs make this callback safe despite the overlap, so describe that actual guarantee rather than preserving the disproven ordering assumption.
        // Why ApplicationStopped (not ApplicationStopping): deleting after the children have fully
        // exited avoids racing a child that is still mid-drain.

src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs:364

  • Creating/truncating the sidecar before the cancellable write can leave an empty or partially written JSON file when WriteAsync is canceled or fails mid-write. The orphan sweep treats malformed JSON as permanently uncollectable, so if the host later creates sockets (for example after a partial I/O failure that is swallowed below), those artifacts can never be reclaimed. Write to a temporary file and atomically move it into place, or delete the incomplete sidecar on every unsuccessful/canceled write.
            using (var fs = new FileStream(
                metadataPath,
                FileMode.Create,
                FileAccess.Write,

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

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.

Reviewed the current head of this branch locally (built clean, and the new WithTerminalTests / TerminalHostSignalTests all pass on macOS). The redesign around per-run replica IDs and the background collector reads well and the earlier threads look genuinely resolved.

Four findings, all new against the current state:

  • 1 correctness/robustness bug: SIGTERM is now intercepted with no bounded force-exit backstop.
  • 1 durability gap: the metadata sidecar write is not atomic, and any unreadable sidecar becomes permanently unreclaimable.
  • 1 log-noise issue: the sweep is scheduled before all resources have written their sidecars.
  • 1 test-maintainability nit: fragile assembly lookup in the new process tests.

Comment thread src/Aspire.TerminalHost/TerminalHostProcessRunner.cs Outdated
Comment thread src/Aspire.Hosting/Lifecycle/TerminalHostOrphanCleanupService.cs
Comment thread src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs Outdated
Comment thread tests/Aspire.Managed.Tests/TerminalHostSignalTests.cs Outdated
@karolz-ms

Copy link
Copy Markdown
Contributor

PR Testing Report

PR Information

Artifact Version Verification

  • Expected Commit: d7676d6ae4e3164ae39e2edb76e6ec42483e22e4
  • Installed Version: 13.6.0-pr.19344.gd7676d6a
  • Status: ✅ Verified

The PR head was rechecked after testing and had not changed.

Test Environment

  • Runtime target: Ubuntu 24.04 Linux/arm64 through the repository container runner
  • Container engine: Podman 6.0.2 through a temporary Docker-compatible shim
  • SDK: .NET SDK 10.0.201 installed into the isolated runner state volume
  • Source-test host: macOS arm64
  • Generated app feed: PR package hive plus the repository-approved dotnet-public mirror because NuGet.org was unreachable from the Podman VM

Changes Analyzed

Files Changed

  • src/Aspire.Hosting/ApplicationModel/TerminalHostLayout.cs
  • src/Aspire.Hosting/Lifecycle/TerminalHostOrphanCleanupService.cs
  • src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs
  • src/Aspire.Managed/Aspire.Managed.csproj
  • src/Aspire.Managed/Program.cs
  • src/Aspire.TerminalHost/Aspire.TerminalHost.csproj
  • src/Aspire.TerminalHost/Program.cs
  • src/Aspire.TerminalHost/TerminalHostProcessRunner.cs
  • src/Shared/KnownConfigNames.cs
  • src/Shared/ParentProcessWatchdog.cs
  • src/Shared/ProcessStartTimeHelper.cs
  • src/Shared/TerminalHost/TerminalHostMetadata.cs
  • src/Shared/TerminalHost/TerminalHostPaths.cs
  • Six targeted test files under tests/

Change Categories

  • Hosting lifecycle changes
  • Bundled managed-helper changes
  • Terminal-host process changes
  • Shared process-identity/watchdog changes
  • Test changes
  • Dashboard changes
  • Template changes
  • Client/component changes
  • VS Code extension changes
  • CI infrastructure changes

Test Scenarios Executed

Scenario 1: Targeted source regression coverage

Objective: Exercise the changed hosting cleanup, signal handling, watchdog, metadata, compatibility, and path behavior at the exact PR head.

Coverage Type: Unit/process integration

Status: ✅ Passed for PR-related coverage

Results:

  • Aspire.Hosting.Tests: 49 passed, 1 platform skip, 0 failed.
  • Aspire.Managed.Tests: 6 passed, including bundled SIGTERM and bundled/standalone owner-death tests.
  • Newly added GracefulCancellationDeletesProducerAndConsumerSockets: 1 passed.
  • Full TerminalHostAppTests with short TMPDIR: 12 passed, 1 failed.

The sole failure was DownstreamPrimaryResizeIsForwardedUpstreamAsRawResizeFrame, which is unchanged by this PR. It failed identically at merge-base commit f721513cf34b4abdc5217ccb001b52ccf2008f4a, so it is not attributable to PR #19344.

Evidence:

  • source-hosting-tests.log
  • source-managed-tests.log
  • source-terminalhost-tests-short-tmp.log
  • source-terminalhost-changed-test.log
  • base-terminalhost-resize-test.log
  • base-comparison.txt

Scenario 2: Dogfood happy path and graceful cleanup

Objective: Verify a generated PR app can run an executable with .WithTerminal(), materialize schema-v3 metadata and sockets, and remove all replica artifacts during graceful stop.

Coverage Type: Happy path

Status: ✅ Passed

Observations:

  • The worker reached up.
  • One random 11-character replica ID was created.
  • Schema-v3 metadata contained AppHost PID, stable process identity, process scope, and Linux boot ID.
  • The control and consumer sockets were present while DCP held the accepted producer connection.
  • aspire stop removed the terminal host and left the terminal artifact directory empty.
  • The scenario passed twice with distinct replica IDs.

Evidence:

  • scenario-happy/evidence-pass/
  • scenario-happy/evidence-repeat/
  • scenario-happy-pass.log
  • scenario-happy-repeat.log

Scenario 3: Bundled SIGTERM and parent-death cleanup

Objective: Test the installed PR's bundled aspire-managed terminalhost binary directly, without relying only on source tests.

Coverage Type: Signal and unhappy path

Status: ✅ Passed

Results:

  1. Direct SIGTERM: all three listen sockets were bound, SIGTERM was sent, the process exited with code 0, and all sockets were unlinked.
  2. Parent death: the terminal host received a real parent PID plus boot-relative stable identity, the parent was killed, the watchdog stopped the host with exit code 0, and all sockets were unlinked.

Evidence:

  • scenario-signal/evidence/sigterm-process.txt
  • scenario-signal/evidence/sigterm-artifacts-before.txt
  • scenario-signal/evidence/sigterm-artifacts-after.txt
  • scenario-signal/evidence/parent-death-processes.txt
  • scenario-signal/evidence/parent-death-artifacts-before.txt
  • scenario-signal/evidence/parent-death-artifacts-after.txt

Scenario 4: Forced AppHost death and orphan recovery

Objective: Verify an exact owner crash does not leak terminal-host sockets and that the next same-scope AppHost reclaims the discoverable metadata.

Coverage Type: Unhappy path and recovery

Status: ✅ Passed

Steps and observations:

  1. Started a fresh .WithTerminal() AppHost and captured its AppHost, DCP, controller, worker, dashboard, and terminal-host PIDs.
  2. Sent SIGKILL to the exact AppHost PID.
  3. Verified the terminal host exited, both remaining sockets disappeared, and the metadata sidecar remained.
  4. Waited for the original DCP/resource tree to drain.
  5. Started a replacement AppHost in the same process scope.
  6. Verified the old replica metadata was reclaimed, a different random replica ID was materialized, and the replacement stayed running.
  7. Gracefully stopped the replacement and verified the artifact directory was empty.

Expected Unhappy-Path Outcome: Socket cleanup occurs immediately after owner death; metadata remains discoverable until the next safe sweep.

Evidence:

  • scenario-owner-retry/evidence-full-teardown/artifacts-before-kill.txt
  • scenario-owner-retry/evidence-full-teardown/artifacts-after-kill.txt
  • scenario-owner-retry/evidence-full-teardown/processes-after-teardown.txt
  • scenario-owner-retry/evidence-full-teardown/reclaim.log
  • scenario-owner-retry/evidence-full-teardown/artifacts-after-sweep.txt
  • scenario-owner-retry/evidence-full-teardown/artifacts-final.txt

Scenario 5: Concurrent owner safety and unsupported metadata

Objective: Ensure one AppHost never deletes another live AppHost's terminal artifacts and unknown metadata is preserved conservatively.

Coverage Type: Boundary and negative

Status: ✅ Passed

Observations:

  • Two isolated AppHosts shared one terminal directory with distinct random replica IDs.
  • Starting the second AppHost preserved the first AppHost's metadata and sockets.
  • Stopping the second AppHost removed only its own artifacts; the first worker remained up.
  • Stopping the first AppHost removed its artifacts.
  • Schema version 999 metadata and its placeholder artifacts remained untouched.
  • Both AppHosts logged the expected unsupported-schema warning.

Expected Unhappy-Path Outcome: Unknown metadata must be retained with a warning rather than deleted.

Evidence:

  • scenario-concurrent/evidence/artifacts-both-running.txt
  • scenario-concurrent/evidence/artifacts-after-b-stop.txt
  • scenario-concurrent/evidence/artifacts-final.txt
  • scenario-concurrent/evidence/unsupported-schema-warnings.txt
  • scenario-concurrent/evidence/processes-both-running.txt

Diagnostic Controls and Environment Notes

  • A plain AppHost without .WithTerminal() started, remained alive for five seconds, and stopped successfully under the same Podman state.
  • The default macOS temporary path exceeded Unix-domain socket limits for the TerminalHost tests; rerunning with TMPDIR=/tmp removed those path-length failures.
  • Starting a replacement before the killed AppHost's old DCP controller tree drained produced runner-level process overlap. Waiting for the full old process tree before restart made the owner-death scenario deterministic and successful.
  • No repository files were modified by testing.

Summary

Scenario Status Notes
Artifact/head verification ✅ Passed Installed version matches d7676d6
Targeted source regression ✅ Passed One unchanged test fails identically on merge base
Graceful .WithTerminal() lifecycle ✅ Passed All replica artifacts removed
Bundled SIGTERM cleanup ✅ Passed Exit 0; sockets unlinked
Stable parent-death watchdog ✅ Passed Exit 0; sockets unlinked
Forced AppHost death and sweep ✅ Passed Old metadata reclaimed after full DCP teardown
Concurrent-owner safety ✅ Passed Live owner preserved
Unsupported schema safety ✅ Passed Artifacts preserved with warning

Overall Result

✅ PR VERIFIED

No PR-caused failures or blocking issues were found.

Comment thread src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs Outdated
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 15, 2026 00:26
@github-actions

This comment has been minimized.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Comment thread src/Aspire.Hosting/Lifecycle/TerminalHostOrphanCleanupService.cs Outdated
Comment thread src/Aspire.Hosting/Lifecycle/TerminalHostOrphanCleanupService.cs Outdated
Copilot AI review requested due to automatic review settings August 15, 2026 00:34
@github-actions

This comment has been minimized.

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

src/Aspire.Hosting/Lifecycle/TerminalHostOrphanCleanupService.cs:239

  • The sweep only enumerates metadata sidecars, and there is no code that checks or reclaims the legacy per-replica lock files promised by the PR description. In particular, an active schema-v2 owner in another PID namespace can appear dead to this process and have its sockets deleted even while it holds the legacy lock; stale lock files are never reclaimed either. Honor the legacy lock before deleting v1/v2 artifacts and remove it only after ownership is safely acquired.
            foreach (var candidatePath in Directory.GetFiles(trmnlDirectory, $"*.{TerminalHostPaths.MetadataSuffix}"))

src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs:328

  • A hard process crash can leave exactly this .tmp file, but the startup sweep only enumerates *.metadata.json, and exact-path shutdown cleanup also omits the temporary path. These files therefore accumulate permanently. Include stale metadata temp files in bounded startup cleanup, or use a crash-safe temporary-file mechanism that cannot leave persistent artifacts.
            // Write and chmod a sibling temporary file before atomically replacing the sidecar.
            // A crash during serialization can then leave only an undiscoverable .tmp file, never
            // a truncated metadata document that permanently blocks orphan recovery.

Comment thread src/Aspire.Hosting/Lifecycle/TerminalHostOrphanCleanupService.cs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 15, 2026 13:22
@github-actions

Copy link
Copy Markdown
Contributor

Tests selector (audit mode)

The full test matrix and all jobs still run in audit mode. The tests and jobs below are what selective CI would run under enforcement.

56 / 101 test projects · 5 jobs, from 20 changed files.

Selected test projects (56 / 101)

Aspire.Cli.EndToEnd.Tests, Aspire.Cli.Tests, Aspire.Dashboard.Components.Tests, Aspire.Dashboard.Tests, Aspire.EndToEnd.Tests, Aspire.Hosting.Analyzers.Tests, Aspire.Hosting.Azure.Kubernetes.Tests, Aspire.Hosting.Azure.Kusto.Tests, Aspire.Hosting.Azure.Tests, Aspire.Hosting.Blazor.Tests, Aspire.Hosting.Browsers.Tests, Aspire.Hosting.CodeGeneration.Go.Tests, Aspire.Hosting.CodeGeneration.Java.Tests, Aspire.Hosting.CodeGeneration.Python.Tests, Aspire.Hosting.CodeGeneration.Rust.Tests, Aspire.Hosting.CodeGeneration.TypeScript.Tests, Aspire.Hosting.Containers.Tests, Aspire.Hosting.DevTunnels.Tests, Aspire.Hosting.Docker.Tests, Aspire.Hosting.Dotnet.Tests, Aspire.Hosting.DotnetTool.Tests, Aspire.Hosting.EntityFrameworkCore.Tests, Aspire.Hosting.Foundry.Tests, Aspire.Hosting.Garnet.Tests, Aspire.Hosting.GitHub.Models.Tests, Aspire.Hosting.Go.Tests, Aspire.Hosting.JavaScript.Tests, Aspire.Hosting.Kafka.Tests, Aspire.Hosting.Keycloak.Tests, Aspire.Hosting.Kubernetes.Tests, Aspire.Hosting.Maui.Tests, Aspire.Hosting.Milvus.Tests, Aspire.Hosting.MongoDB.Tests, Aspire.Hosting.MySql.Tests, Aspire.Hosting.Nats.Tests, Aspire.Hosting.OpenAI.Tests, Aspire.Hosting.Oracle.Tests, Aspire.Hosting.Orleans.Tests, Aspire.Hosting.PostgreSQL.Tests, Aspire.Hosting.Python.Tests, Aspire.Hosting.Qdrant.Tests, Aspire.Hosting.RabbitMQ.Tests, Aspire.Hosting.Radius.Tests, Aspire.Hosting.Redis.Tests, Aspire.Hosting.RemoteHost.Tests, Aspire.Hosting.Rust.Tests, Aspire.Hosting.Seq.Tests, Aspire.Hosting.SqlServer.Tests, Aspire.Hosting.Testing.Tests, Aspire.Hosting.Tests, Aspire.Hosting.Valkey.Tests, Aspire.Hosting.Yarp.Tests, Aspire.Managed.Tests, Aspire.Playground.Tests, Aspire.Templates.Tests, Aspire.TerminalHost.Tests

Selected jobs (5)

cli-starter, deployment-e2e, extension-e2e, polyglot, typescript-api-compat


How these were chosen — grouped by what changed

⚠️ 39 of the 56 selected test projects come from a single change — src/Aspire.Hosting/ApplicationModel/TerminalHostLayout.cs.

🔧 src/Aspire.Hosting/ApplicationModel/TerminalHostLayout.cs (changed source)
39 via the project graph

show 39

Aspire.Hosting.Analyzers.Tests (2 hops), Aspire.Hosting.Azure.Kubernetes.Tests (2 hops), Aspire.Hosting.Azure.Kusto.Tests (2 hops), Aspire.Hosting.Azure.Tests, Aspire.Hosting.CodeGeneration.Go.Tests, Aspire.Hosting.CodeGeneration.Java.Tests, Aspire.Hosting.CodeGeneration.Python.Tests, Aspire.Hosting.CodeGeneration.Rust.Tests, Aspire.Hosting.CodeGeneration.TypeScript.Tests, Aspire.Hosting.DevTunnels.Tests (2 hops), Aspire.Hosting.Docker.Tests (2 hops), Aspire.Hosting.DotnetTool.Tests (2 hops), Aspire.Hosting.EntityFrameworkCore.Tests (2 hops), Aspire.Hosting.Foundry.Tests (2 hops), Aspire.Hosting.Garnet.Tests (2 hops), Aspire.Hosting.Go.Tests (2 hops), Aspire.Hosting.JavaScript.Tests (2 hops), Aspire.Hosting.Kafka.Tests (2 hops), Aspire.Hosting.Keycloak.Tests (2 hops), Aspire.Hosting.Kubernetes.Tests (2 hops), Aspire.Hosting.Maui.Tests, Aspire.Hosting.Milvus.Tests (2 hops), Aspire.Hosting.MongoDB.Tests (2 hops), Aspire.Hosting.MySql.Tests (2 hops), Aspire.Hosting.Nats.Tests (2 hops), Aspire.Hosting.Oracle.Tests (2 hops), Aspire.Hosting.Orleans.Tests (2 hops), Aspire.Hosting.PostgreSQL.Tests (2 hops), Aspire.Hosting.Python.Tests (2 hops), Aspire.Hosting.Qdrant.Tests (2 hops), Aspire.Hosting.RabbitMQ.Tests (2 hops), Aspire.Hosting.Redis.Tests (2 hops), Aspire.Hosting.RemoteHost.Tests, Aspire.Hosting.Rust.Tests (2 hops), Aspire.Hosting.Seq.Tests (2 hops), Aspire.Hosting.SqlServer.Tests (2 hops), Aspire.Hosting.Valkey.Tests (2 hops), Aspire.Hosting.Yarp.Tests (2 hops), Aspire.Playground.Tests

🔧 src/Shared/KnownConfigNames.cs (changed source)
9 via the project graph: Aspire.Cli.Tests, Aspire.Dashboard.Components.Tests, Aspire.Dashboard.Tests, Aspire.Hosting.Browsers.Tests, Aspire.Hosting.Containers.Tests, Aspire.Hosting.GitHub.Models.Tests, Aspire.Hosting.OpenAI.Tests, Aspire.Hosting.Testing.Tests, Aspire.Templates.Tests

🧪 tests/Aspire.Hosting.Tests/TerminalHostPathsTests.cs (changed test)
1 directly: Aspire.Hosting.Tests
3 via the project graph: Aspire.Hosting.Blazor.Tests, Aspire.Hosting.Dotnet.Tests, Aspire.Hosting.Radius.Tests

📦 affected project Aspire.Hosting
1 test: Aspire.EndToEnd.Tests

📦 affected project Aspire.Managed
1 test: Aspire.Cli.EndToEnd.Tests

🧪 tests/Aspire.Hosting.Tests/WithTerminalTests.cs (changed test)
1 directly: Aspire.Hosting.Tests

🧪 tests/Aspire.Managed.Tests/Aspire.Managed.Tests.csproj (changed test)
1 directly: Aspire.Managed.Tests

🧪 tests/Aspire.Managed.Tests/ParentProcessWatchdogTests.cs (changed test)
1 directly: Aspire.Managed.Tests

🧪 tests/Aspire.Managed.Tests/TerminalHostSignalTests.cs (changed test)
1 directly: Aspire.Managed.Tests

🧪 tests/Aspire.TerminalHost.Tests/TerminalHostAppTests.cs (changed test)
1 directly: Aspire.TerminalHost.Tests

Job reasons

Job Triggered by
cli-starter • affected project Aspire.Managed
• selected test Aspire.Cli.Tests
deployment-e2e affected project Aspire.Managed
extension-e2e src/Aspire.Hosting/ApplicationModel/TerminalHostLayout.cs, src/Aspire.Hosting/Lifecycle/TerminalHostOrphanCleanupService.cs, src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs
• affected project Aspire.Hosting
polyglot affected project Aspire.Managed
typescript-api-compat affected project Aspire.Hosting

Selection computed for commit ab6cba7.

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs:267

  • If startup is cancelled while writing replica N, this exception exits before RegisterReplicaArtifacts runs. Sidecars already written for replicas 0..N-1 are then absent from the ApplicationStopped cleanup list and remain orphaned. Register each generated replica ID before the cancellable write (or clean the partially materialized IDs in a cancellation/failure path) so interrupted startup cannot leak artifacts.
            await WriteMetadataSidecarAsync(

src/Aspire.Hosting/Lifecycle/TerminalHostOrphanCleanupService.cs:120

  • The background sweep is awaited without a timeout during DI disposal. If enumeration or file I/O under a network-mounted home directory stalls, AppHost shutdown can still hang indefinitely despite the bounded ApplicationStopped cleanup above. Bound this wait (and log a warning on timeout) just as the shutdown deletion path is bounded.
            await cleanupTask.ConfigureAwait(false);

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@IEvangelist
David Pine (IEvangelist) merged commit b777bb1 into main Aug 15, 2026
728 of 731 checks passed
@IEvangelist
David Pine (IEvangelist) deleted the dapine/fix-terminal-socket-leak branch August 15, 2026 16:44
@github-actions github-actions Bot added this to the 13.6 milestone Aug 15, 2026
@aspire-repo-bot

Copy link
Copy Markdown
Contributor

Pull request created: #1503

Generated by PR Documentation Check · auto · 38.8 AIC · ⌖ 7.94 AIC · ⊞ 19.6K

@aspire-repo-bot

Copy link
Copy Markdown
Contributor

📝 Documentation has been drafted in microsoft/aspire.dev#1503 targeting release/13.5.

Added a new "Terminal cleanup after a crash or forced stop" section to app-host/with-terminal.mdx, covering socket cleanup on normal stop, SIGTERM/Ctrl+C, AppHost crash (parent-process watchdog), and the startup orphan-sweep for reboots/unclean shutdowns, noting it's run-mode only.

Note

This draft PR needs human review before merging.

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

Labels

needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WithTerminal(): graceful aspire stop leaks {id}.dcp.sock and {id}.host.sock in ~/.aspire/trmnl (child re-binds UDS after cleanup runs)

4 participants