WithTerminal(): per-replica interactive terminal sessions (Aspire side, draft)#16760
Draft
mitchdenny wants to merge 22 commits intomicrosoft:mainfrom
Draft
WithTerminal(): per-replica interactive terminal sessions (Aspire side, draft)#16760mitchdenny wants to merge 22 commits intomicrosoft:mainfrom
mitchdenny wants to merge 22 commits intomicrosoft:mainfrom
Conversation
…xtension method Implements Phase 1 of the live terminal support feature (microsoft#16317). - TerminalAnnotation: IResourceAnnotation with TerminalOptions (Columns, Rows, Shell) and a SocketPath property for the UDS path set by the orchestrator. - TerminalHostResource: Internal hidden resource (IResourceWithParent) that will manage the Hex1b-based terminal host process for a parent resource. - WithTerminal<T>(): Extension method that adds TerminalAnnotation to a resource, creates a hidden TerminalHostResource, and adds a WaitAnnotation so the parent waits for the terminal host to be started. - Tests: 8 unit tests covering annotation creation, custom options, hidden resource creation, wait annotation wiring, chaining, container support, manifest exclusion, and null argument handling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- docs/specs/terminal-protocol.md: Full protocol specification defining
the binary framing format for terminal I/O over Unix domain sockets.
Covers HELLO, DATA, RESIZE, EXIT, and CLOSE message types with wire
examples and implementation notes for DCP (Go) and Aspire (C#).
- src/Shared/Terminal/: Shared protocol types (TerminalProtocol constants,
TerminalFrameReader, TerminalFrameWriter, TerminalFrame) designed to be
linked into Aspire.Hosting, Dashboard, and CLI projects.
- playground/Terminals/: Two-project playground demonstrating WithTerminal
without DCP:
- Terminals.TerminalHost: .NET console app using Hex1b with a custom
IHex1bTerminalPresentationAdapter that implements the Aspire Terminal
Protocol over UDS. Receives socket path via TERMINAL_SOCKET_PATH env var.
- Terminals.AppHost: Aspire AppHost that launches the terminal host as
a child process with a custom resource lifecycle (OnInitializeResource),
demonstrating the full WithTerminal flow before DCP PTY support lands.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Terminals.Client: Standalone console app that connects to an Aspire Terminal Protocol UDS server, performs the HELLO handshake, puts the local console in raw mode, and bridges stdin/stdout bidirectionally. Supports Ctrl+] to detach. Verified end-to-end: TerminalHost starts pwsh with Hex1b PTY, listens on UDS. Client connects, receives HELLO(v1, 80x24, Pty), and gets a fully interactive PowerShell session with prompt rendering and command execution working correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
DCP Model: - TerminalSpec: New model type with enabled, socketPath, columns, rows - Added Terminal property to ExecutableSpec and ContainerSpec - ExecutableCreator populates TerminalSpec from TerminalAnnotation Backchannel: - GetTerminalInfoRequest/Response in BackchannelDataTypes - GetTerminalInfoAsync on AuxiliaryBackchannelRpcTarget (server) - GetTerminalInfoAsync on AppHostAuxiliaryBackchannel (client) - Added to IAppHostAuxiliaryBackchannel interface CLI: - New 'aspire terminal <resource>' command (TerminalCommand.cs) - Connects to AppHost backchannel, gets terminal UDS path - Connects to UDS, performs HELLO handshake - Puts console in raw mode, bridges stdin/stdout bidirectionally - Ctrl+] to detach, handles EXIT/CLOSE frames - Registered in RootCommand and DI container Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a second WithTerminal overload that accepts a Func<CancellationToken, Task<string>> socketPathProvider for resources that manage their own terminal server (e.g., remote SSH, cloud resources). Unlike the standard overload, this does NOT create a hidden TerminalHostResource — the caller is responsible for running a server that speaks the Aspire Terminal Protocol on the provided socket path. Also adds SocketPathProvider property to TerminalAnnotation and two new tests (10 total, all passing). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…s integration Dashboard terminal view that replaces Console Logs for terminal-enabled resources: - TerminalView.razor: Blazor component wrapping xterm.js via JS interop - TerminalView.razor.js: xterm.js initialization, WebSocket connection, resize handling - TerminalWebSocketProxy.cs: ASP.NET Core middleware at /api/terminal that bridges browser WebSocket to UDS using the Aspire Terminal Protocol (HELLO/DATA/RESIZE/EXIT/CLOSE) - Vendored xterm.js 5.5.0 + fit addon in wwwroot/js/xterm/ ConsoleLogs integration: - ConsoleLogs.razor: Conditionally renders TerminalView instead of LogViewer when the selected resource has terminal.enabled property - ConsoleLogs.razor.cs: Detects terminal resources in SubscribeAsync, skips console log subscription for terminal resources Infrastructure: - KnownProperties.Terminal.Enabled/SocketPath constants in shared model - DashboardServiceData: Injects terminal.enabled and terminal.socketPath properties into resource snapshots when TerminalAnnotation is present - ResourceViewModelExtensions: HasTerminal() and TryGetTerminalSocketPath() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…JS interop Two fixes for the Blazor unhandled error: 1. xterm.min.js is UMD format, not ES module — cannot use dynamic import(). Changed to load via script tags into window.Terminal / window.FitAddon. 2. initTerminal returned a plain JS object which can't be marshaled as IJSObjectReference. Changed to return an int ID and use a Map-based registry on the JS side. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
New project: src/Aspire.TerminalHost/
- Console app using Hex1b with UnixDomainSocketPresentationAdapter
- Speaks Aspire Terminal Protocol over UDS
- Receives config via TERMINAL_SOCKET_PATH, TERMINAL_COLUMNS/ROWS/SHELL env vars
- Bridges PTY shell ↔ UDS clients
AppHost discovery (following Dashboard pattern):
- DcpOptions.TerminalHostPath for path resolution
- Three-tier discovery: env var (ASPIRE_TERMINAL_HOST_PATH) → config → assembly metadata
- Assembly metadata key: 'aspireterminalhostpath'
- MSBuild target SetTerminalHostDiscoveryAttributes in AppHost.in.targets
- Development path: artifacts/bin/Aspire.TerminalHost/{Config}/net8.0/
WithTerminal lifecycle:
- AddTerminalHostResource now generates UDS path and sets it on TerminalAnnotation
- OnInitializeResource resolves terminal host binary via DcpOptions
- Launches terminal host as child process with env var configuration
- Forwards stderr to resource logs
- Manages process lifecycle with clean shutdown
DCP flow:
- TerminalAnnotation.SocketPath → TerminalSpec on ExecutableSpec/ContainerSpec
- DCP receives terminal.enabled + terminal.socketPath in the CRD spec
- DCP can use socketPath to forward PTY I/O (Go implementation separate)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…l host The playground's TerminalDemoResource manages its own terminal host process lifecycle, so it should use WithTerminal(socketPathProvider) instead of the bare WithTerminal() which now also launches a terminal host. Using the custom overload avoids creating a conflicting hidden TerminalHostResource. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaces the presentation adapter approach with a presentation filter architecture (modeled after Hex1b's DiagnosticsSocketListener): - Terminal runs headless (Hex1b manages internal screen state) - TerminalSocketServer is an IHex1bTerminalPresentationFilter that intercepts all output via OnOutputAsync and broadcasts to clients - On client connect: CreateSnapshot().ToAnsi() captures current screen state and sends it as the first DATA frame after HELLO (with REPLAY flag) - On client disconnect: terminal keeps running, accepts new connections - On reconnect: fresh snapshot replayed, then live streaming resumes This enables navigating away from the terminal in the Dashboard and returning to find the same terminal state preserved. Key changes: - New TerminalSocketServer.cs (filter-based, session management) - Program.cs: WithHeadless() + AddPresentationFilter(server) instead of WithPresentation(adapter) - Old UnixDomainSocketPresentationAdapter kept for playground compatibility Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Refactor the WithTerminal API to match the new architecture decided for the
13.4 end-to-end work:
- TerminalAnnotation: drop SocketPath / SocketPathProvider; carry a strong
reference to the hidden TerminalHostResource and the user-supplied
TerminalOptions instead. The host owns its own UDS layout.
- TerminalHostResource: now public and derives from ExecutableResource so
DCP launches it as a regular hidden executable. Carries a Parent
reference plus the per-resource TerminalHostLayout. Constructed with a
placeholder command (UnresolvedCommand) so we can rewrite it later.
- TerminalHostLayout (new): per-resource, per-run UDS layout — N producer
paths under {tmp}/aspire-term-{guid}/dcp/, N consumer paths under
host/, and one control.sock. Built via Directory.CreateTempSubdirectory
per the repo temp-directory convention.
- TerminalHostEventingSubscriber (new): subscribes to BeforeStartEvent and
resolves the real terminal host binary from DcpOptions.TerminalHostPath
before DCP launches the resource. Emits a warning if the path is unset
or if the parent's replica count drifted between WithTerminal() and
start. Registered via TryAddEventingSubscriber in the builder.
- TerminalResourceBuilderExtensions: single overload, eager UDS layout,
hidden host as ExecutableResource, args wired via a callback
(--replica-count, --producer-uds xN, --consumer-uds xN, --control-uds,
--columns, --rows, --shell). Adds a WaitAnnotation on the host
(WaitUntilStarted for now; Phase 2 will upgrade to WaitUntilHealthy
once the host exposes a health probe). Throws on double WithTerminal
call.
- Stub leftovers in ExecutableCreator, AuxiliaryBackchannelRpcTarget, and
DashboardServiceData for proper wire-up in Phases 4/5/7. Each stub is
marked with a comment.
- Update WithTerminalTests to cover the new design (15 tests, all green).
- Drop playground/Terminals/Terminals.AppHost/TerminalDemoResource.cs and
stub Terminals.AppHost itself; full playground rebuild lands in Phase 8.
- Add a GetTerminalInfoAsync stub on TestAppHostAuxiliaryBackchannel so
the CLI test fake satisfies the interface.
Build is green with /p:SkipNativeBuild=true. WithTerminalTests pass 15/15.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaces the custom binary frame protocol with Hex1b 0.137 HMP v1 and adopts the per-replica UDS pair design from Phase 1. The host now creates one independent Hex1bTerminal per replica using WithHmp1UdsClient(producerUds[i]).WithHmp1UdsServer(consumerUds[i]). DCP runs the HMP v1 producer side; viewers (CLI / Dashboard) connect to the consumer side. State replay on reconnect is handled by Hex1b. A small StreamJsonRpc control listener on a separate UDS exposes GetReplicas() and Shutdown() so the AppHost backchannel can populate GetTerminalInfoAsync() without sharing the data plane. The host no longer auto-exits when all replicas exit -- DCP owns the host lifetime via cancellation or the control protocol. Removed: src/Shared/Terminal/* (custom protocol), TerminalSocketServer, UnixDomainSocketPresentationAdapter, playground/Terminals.TerminalHost. Added: tests/Aspire.TerminalHost.Tests with 17 tests (12 args, 5 app) covering arg parsing edge cases plus end-to-end control-listener + replica startup over real UDS sockets. All 17 pass on Windows. Phase 1 WithTerminalTests (15/15) still pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…leCreator ExecutableCreator now populates spec.Terminal per replica when the resource has a TerminalAnnotation, indexing into TerminalHostLayout.ProducerUdsPaths via the ResourceReplicaIndex annotation. Gated on Windows for the 13.4 ship; logs a warning + skips on other platforms (Linux/macOS PTY support tracked as a follow-up). TerminalSpec.cs aligned with the Go-side DCP API in microsoft/dcp PR microsoft#133: enabled / udsPath / cols / rows JSON tags with no client-side defaults (DCP applies 80x24 if zero). Adds two DcpExecutorTests cases: Project_WithTerminal_PopulatesPerReplicaTerminalSpecOnWindows (assertion-rich, replica-ordered, Windows-gated) and Project_WithoutTerminal_HasNullTerminalSpec (negative case). Tracking: microsoft#16317 + microsoft/dcp#6 + DCP PR microsoft/dcp#133. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
GetTerminalInfoAsync now opens the hidden terminal host's control UDS, calls getReplicas, and returns a TerminalReplicaInfo[] populated with the AppHost-canonical consumer UDS path for each replica. Connection retries are bounded by a 3 s budget so a request issued while DCP is still launching the host doesn't fail-fast. Wire shape evolution is additive: SocketPath/Columns/Rows are preserved, Replicas is added as a new optional array. Older CLI builds that only check IsAvailable continue to work. New clients gate UI on the new terminals.v1 capability advertised by GetCapabilitiesAsync. Out-of-range replica indices reported by the host are skipped with a warning so a buggy host can never crash a backchannel call. Tests cover the resource-not-found, no-annotation, unreachable-host, happy-path, out-of-range-index, and capability-advertisement scenarios. Tracking: microsoft#16317. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaces the placeholder TerminalCommand with the full discovery, selection,
and attach flow defined in the Phase 6 plan.
- Add IAppHostAuxiliaryBackchannel.SupportsTerminalsV1 capability gate
alongside the existing SupportsV2 surface so the CLI can fail fast against
pre-13.4 AppHosts with a clear "Update Aspire.Hosting" message instead of
a misleading "resource not found" or socket-connect error.
- Add Hex1b PackageReference to Aspire.Cli (already pinned via
Directory.Packages.props; Hex1b 0.135+ is fully Native AOT-compatible so
no new IL/AOT warnings are introduced versus the baseline).
- TerminalCommand flow:
1. Resolve AppHost via AppHostConnectionResolver.
2. Verify SupportsTerminalsV1; otherwise return AppHostIncompatible.
3. Look up the resource via ResourceSnapshotMapper.WhereMatchesResourceName
(matches by Name OR DisplayName so users can target replicated
resources by the parent name).
4. Canonicalise to the parent name (matches[0].DisplayName ?? Name) so
GetTerminalInfoAsync receives the same identifier the AppHost knows.
5. Call backchannel.GetTerminalInfoAsync.
6. Pick a replica:
- --replica/-r N → exact match (out-of-range → InvalidCommand)
- 1 replica → auto-pick
- non-interactive → require --replica explicitly
- interactive multi → PromptForSelectionAsync
7. If the chosen replica has exited, warn and continue (the historical
buffer is still served via HMP v1 StateSync).
8. Hand off to Hex1bTerminal.CreateBuilder().WithHmp1UdsClient(...).Build()
and await RunAsync. Ctrl+C cancels via the SCL cancellation token.
- Catches OperationCanceledException, SocketException, and
IOException(SocketException) explicitly so connect failures and mid-session
disconnects produce friendly messages instead of stack traces.
Tests (10 new in TerminalCommandTests):
- Help works.
- Missing resource argument fails parsing.
- No running AppHost returns Success (matches LogsCommand convention).
- Lacking SupportsTerminalsV1 returns AppHostIncompatible.
- Resource not found returns InvalidCommand.
- IsAvailable=false / empty replicas array return InvalidCommand.
- --replica out-of-range returns InvalidCommand.
- DisplayName lookup canonicalises to the parent resource name when calling
GetTerminalInfoAsync (verified via a CapturingTerminalAppHostBackchannel
decorator).
- Non-interactive multi-replica without --replica returns InvalidCommand.
The CLI test helper now registers TerminalCommand alongside the other
commands so the new tests can resolve the RootCommand.
…alView
Wires the Aspire Dashboard end-to-end with the per-replica HMP v1 producer
endpoints introduced in Phase 1-5: the dashboard now exposes an authenticated
WebSocket endpoint at /api/terminal that proxies xterm.js byte streams to the
correct per-replica UDS without trusting any browser-supplied filesystem path.
Server side:
* New abstraction: Aspire.Dashboard.Terminal.ITerminalConnectionResolver
exposes (resourceName, replicaIndex) -> Stream resolution. The default
implementation (DefaultTerminalConnectionResolver) walks the live
IDashboardClient.GetResources() snapshot, matches by display name +
TryGetTerminalReplicaInfo, and connects via Hex1b
Hmp1Transports.ConnectUnixSocket. NullTerminalConnectionResolver is kept
as a hook for tests / unsupported hosts.
* TerminalWebSocketProxy is rewritten:
- Endpoint /api/terminal is mapped with RequireAuthorization(Frontend)
so only authenticated dashboard users can open a session.
- Query string ?resource=&replica= is the only client-controlled state;
the consumer UDS path is resolved server-side via the resolver, not
accepted from the browser.
- Two pumps:
* inbound (browser -> producer): binary frames carry keystroke
bytes (forwarded as HMP v1 Input); text frames carry JSON resize
control messages parsed via Utf8JsonReader.
* outbound (producer -> browser): VT byte stream from the
Hmp1WorkloadAdapter is sent as binary WS frames; resize hints
from the producer become JSON text frames.
- ReassembledFrame uses ArrayPool<byte> to handle multi-fragment WS
reads without per-message allocation.
- Graceful close via WebSocket.TryCloseAsync; resolver/protocol errors
return an HTTP 5xx instead of leaking diagnostic detail.
* DefaultTerminalConnectionResolver registered as singleton in
DashboardWebApplication.cs alongside the other resource-snapshot-aware
services.
* Hex1b PackageReference added to Aspire.Dashboard.csproj (Hex1b 0.137,
pinned via Directory.Packages.props).
Property contract:
* KnownProperties.Terminal: replaced the legacy single SocketPath constant
with per-replica ReplicaIndex, ReplicaCount, and ConsumerUdsPath. The
per-replica index is resolved from DcpInstancesAnnotation
(DCP-allocated, stable) rather than parsing the random-suffixed
resource name.
* ResourceViewModelExtensions: TryGetTerminalSocketPath is replaced by
TryGetTerminalReplicaInfo(out int replicaIndex, out int replicaCount)
and TryGetTerminalConsumerUdsPath(out string?).
* Aspire.Hosting/Dashboard/DashboardServiceData stamps these per-replica
properties on each snapshot. ConsumerUdsPath is marked
IsSensitive=true so the dashboard UI masks it; the value still rides
the gRPC stream because it is required server-side, but it is never
echoed back to the browser through the WS endpoint.
Browser side:
* TerminalView.razor.cs takes ResourceName + ReplicaIndex parameters
instead of SocketPath; builds the WS URL as
/api/terminal?resource=...&replica=... using the request's authority.
ReconnectAsync(string?, int) is the new reconnect signature.
* TerminalView.razor.js sends keystrokes as binary frames via
TextEncoder; resize messages remain text JSON. Framing is
WS-frame-type-driven, not content sniffed.
* ConsoleLogs.razor / ConsoleLogs.razor.cs forward DisplayName +
ReplicaIndex into TerminalView (no socket path leaves the server).
Tests:
* Aspire.Dashboard.Tests.Terminal.DefaultTerminalConnectionResolverTests
covers: client-disabled, resource-not-found, replica mismatch,
missing terminal-enabled marker, missing UDS path, and the
bad-path-throws negative case.
* Aspire.Dashboard.Tests.Model.ResourceViewModelExtensionsTerminalTests
covers HasTerminal, TryGetTerminalReplicaInfo, and
TryGetTerminalConsumerUdsPath positive / negative paths.
Verified:
* dotnet build src/Aspire.Dashboard -> 0 warnings, 0 errors
* dotnet build src/Aspire.Hosting -> 0 warnings, 0 errors
* full ./build.cmd -> 0 warnings, 0 errors
* Aspire.Hosting.Tests *WithTerminal* -> 16/16 passing
* Aspire.Cli.Tests *Terminal* -> 11/11 passing
* Aspire.Dashboard.Tests *Terminal* -> 13/13 passing
* Aspire.Dashboard.Tests (excl Playwright) -> 1255/1255 passing
* Aspire.Hosting.Tests Dashboard ns -> 98/98 passing
* AOT publish: no new IL warnings from Terminal/* or TerminalView*
(pre-existing Dashboard AOT warnings are unchanged; Dashboard is not
AOT-compiled in the ship pipeline)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wires up the Terminals playground end-to-end so WithTerminal() can be exercised against both a project-style resource (REPL, 2 replicas) and a plain executable (cmd.exe on Windows). Validated full pipeline: DCP ConPTY -> HMP1 producer UDS -> Aspire.TerminalHost (per-replica Hex1bTerminal) -> HMP1 consumer UDS -> external viewer. Changes: * New playground project Terminals.Repl: interactive ANSI REPL with help/whoami/time/size/echo/rainbow/clear/exit. Produces ANSI banner + prompt suitable for exercising terminal emulation through the full WithTerminal pipeline. * Terminals.AppHost: add the REPL with WithReplicas(2) and WithTerminal(120x32); add a Windows-gated 'shell' resource (AddExecutable cmd.exe + WithTerminal()) to cover the plain-executable path. * Bug fix in ExecutableCreator.PreparePlainExecutables: plain executables added via AddExecutable() were missing both ResourceReplicaIndex and ResourceReplicaCount annotations, which caused BuildExecutableConfiguration's per-replica producer UDS lookup to fail silently and skip the spec.Terminal wire-up entirely. Added regression test PlainExecutable_WithTerminal_PopulatesTerminalSpecOnWindows. * docs/specs/with-terminal.md: replaces the deleted terminal-protocol.md (which described an obsolete custom protocol) with the current Aspire-side architecture spec for WithTerminal(). Validation: * Aspire.Hosting + Terminals.AppHost + Terminals.Repl all build clean (0 warnings 0 errors). * All 15 WithTerminalTests pass. * New PlainExecutable_WithTerminal_PopulatesTerminalSpecOnWindows test passes alongside existing Project_WithTerminal_/WithoutTerminal_ tests. * Manually validated end-to-end with a small HMP1 probe: connecting to each consumer UDS yields a Hello frame and a multi-KB StateSync frame containing the live PTY output (cmd.exe banner / REPL banner). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Dashboard ConsoleLogs page reuses the same TerminalView instance when the user switches between terminal-enabled resources (or between replicas of the same resource), so firstRender is never true again after the first switch. The original implementation only initialized the xterm.js / WebSocket bridge on firstRender, which left the view stuck on whichever resource was initially selected. Track the (resource, replica) pair we last connected to, and call the existing ReconnectAsync path from OnAfterRenderAsync whenever the parameters change. The JS side already has reconnectTerminal which closes the old WebSocket, clears the screen, and opens a new connection — the StateSync replay from the new producer fills the buffer with the right replica's content. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 16760Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 16760" |
ContainerCreator now mirrors the per-resource TerminalAnnotation -> DCP TerminalSpec wiring that ExecutableCreator already does. Containers are single-replica in DCP, so we always reference index 0 of the host's UDS layout (the same layout the executable path uses for replica index 0). The companion DCP-side change is in microsoft/dcp#138 (stacked on microsoft/dcp#133): when ContainerSpec.Terminal is set, DCP creates the container with `-t -i` and runs `docker start --attach --interactive` under a host ConPTY exposing the resulting byte stream as an HMP v1 producer at TerminalSpec.UDSPath. The Aspire-side terminal host then connects as an HMP v1 client, identical to the executable case. Like the executable path, this is currently gated behind a Windows OS check; on other platforms ContainerCreator logs a warning and leaves TerminalSpec unset so the container runs without an attachable terminal. Playground: adds a `nodebox` container resource (node:lts) with `WithEntrypoint(""/bin/bash"")` so users can attach the dashboard terminal and use `npx` / `node` interactively. Also re-enables `WithTerminal()` on the existing `shell` (cmd.exe) executable that was commented out for IDE-debug investigation in Phase 8. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Member
Author
|
Container support has been split out and stacked on top of this PR: #16762 (Aspire side) + microsoft/dcp#138 (DCP side). |
WithEntrypoint(""/bin/bash"") only overrides the image's ENTRYPOINT;
the image's CMD (""node"") is still inherited, so docker actually
executes `/bin/bash node` which makes bash treat `node` as a
missing script file and exit immediately - the container is gone
before the dashboard's Terminal tab even gets a chance to attach.
Switching to `WithArgs(""bash"", ""-l"")` keeps the image's
docker-entrypoint.sh in place and overrides the CMD, so the
entrypoint exec's an interactive login bash, which sticks around
for the terminal session and exits cleanly when the user types
`exit` (which then propagates through the host PTY's
docker-start-attach process and signals container exit via
Session.Done()).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pairs with the matching DCP change. Previously the terminal host dialed DCP (WithHmp1UdsClient) on the producer UDS; now the terminal host LISTENS on that UDS and DCP dials in. This guarantees the host is receiving from the very first byte the PTY emits, so a long-running shell's initial prompt makes it into the host's scrollback even for dashboard viewers that attach later. The HMP v1 protocol roles are unchanged: DCP holds the PTY and so must remain the HMP1 server; the terminal host remains the HMP1 client. Hex1b's WithHmp1UdsClient/WithHmp1UdsServer convenience helpers couple the HMP1 protocol role with the TCP role, which we don't want here. We compose the lower-level WithHmp1Client(Func<CT, Task<Stream>>) with Hmp1Transports.ListenUnixSocket(...) instead, taking the first stream the listener accepts and using that as the HMP1 client transport. The consumer side (WithHmp1UdsServer) is unchanged - the terminal host keeps listening on the consumer UDS for dashboard/CLI viewers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
xterm.js was constructed at the default 80x24 and only ever reported the viewer's true dimensions to the host through term.onResize, which fires when xterm INTERNAL dimensions change. The first fit() ran before the WebSocket was open, so the resulting onResize was silently dropped (state.ws null). Subsequent fits saw the same dimensions and didn't fire onResize again. Net effect: the host received no Resize from the viewer and replayed its initial StateSync at producer dimensions, so when the viewer's xterm grid was larger than 80x24 the replayed content appeared squeezed into a corner. Fix: in ws.onopen, re-fit and explicitly send a resize JSON control frame using the post-fit term.cols/term.rows. This is the first thing the viewer sends to the host, guaranteeing that any post-handshake StateSync re-emission (and all subsequent output) is rendered at the viewer's actual viewport. Refactored the onResize-driven send into a shared sendResize(state) helper and reused it from both the onopen path and the term.onResize hook. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds the Aspire-side implementation of
WithTerminal(...)— letting anAppHost author opt a resource into a real interactive terminal session
that the dashboard, the
aspireCLI, or any other HMP v1 viewer canattach to and detach from at will.
End-to-end pipeline (Windows executables today; container/Linux/macOS
follow-ups tracked separately):
This is a draft because:
Tracks #16317.
What's in this PR
Public API (
src/Aspire.Hosting/ApplicationModel)WithTerminal()extension method +TerminalAnnotation/TerminalOptions/TerminalHostResource. Per-replica producer + consumer UDS layout owned bythe
TerminalHostResource; lifecycle is wired throughTerminalHostEventingSubscriber.DCP wire-up (
src/Aspire.Hosting/Dcp/Model/TerminalSpec.cs,src/Aspire.Hosting/Dcp/ExecutableCreator.cs)TerminalSpecmirrors the DCP API (Enabled / UdsPath / Cols / Rows).ExecutableCreatorpopulatesspec.Terminalper replica from the layouton Windows when a
TerminalAnnotationis present.PreparePlainExecutables: plain executables now getResourceReplicaCount=1/ResourceReplicaIndex=0annotations so theper-replica UDS lookup succeeds (without this,
WithTerminal()onAddExecutable(...)silently no-op'd).Out-of-process Terminal Host (
src/Aspire.TerminalHost)Hex1bTerminalper replica acting as an HMP v1 client to DCP and anHMP v1 server to viewers. Native AOT-compatible.
Backchannel (
src/Aspire.Hosting.Cli/...+src/Aspire.Cli/...)consumer UDS path) over the existing CLI backchannel.
CLI (
src/Aspire.Cli/Commands/TerminalCommand.cs)aspire terminal <resource> [--replica N]connects to the consumer UDSvia Hex1b's HMP v1 client and bridges the local terminal.
Dashboard (
src/Aspire.Dashboard/...)TerminalViewBlazor component (xterm.js + a thin JS module)./api/terminalWebSocket proxy (Terminal/TerminalWebSocketProxy.cs)bridging the browser WebSocket to the per-replica consumer UDS.
ConsoleLogspage swaps the log viewer for the terminal view when theselected resource has a terminal session, and now correctly rebinds the
view when the user switches between terminal-enabled resources/replicas.
Playground (
playground/Terminals/...)Terminals.Repl— interactive ANSI REPL (help,whoami,time,size,echo,rainbow,clear,exit).Terminals.AppHost— hosts the REPL withWithReplicas(2)+WithTerminal(120x32)and a Windows-gatedshellresource(
AddExecutable("cmd.exe") + WithTerminal()).Spec
docs/specs/with-terminal.mddocuments the architecture, the per-replicaUDS layout, and the lifecycle.
Validation
End-to-end requires the matching DCP build from
microsoft/dcp#133 and the
freshly-built
Aspire.TerminalHost, both pointed at via env vars:Expected:
shell,repl-r0,repl-r1. Each entry's "Console Logs" tab renders a livexterm.js terminal instead of the log viewer.
attached terminal in place (state replay courtesy of
Hex1bTerminal).aspire terminal repl --replica 1from a real conhost session attachesto replica 1 of the REPL with full interactivity.
%LocalAppData%\Temp\aspire-dcp*\resource-executable-{guid}.logshow
Starting process under PTY...andTerminal session listening.Unit / integration tests:
dotnet test tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj -- --filter-class "*.WithTerminalTests" --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true"(15 pass)--filter-method "*.Project_WithTerminal_PopulatesPerReplicaTerminalSpecOnWindows" --filter-method "*.Project_WithoutTerminal_HasNullTerminalSpec" --filter-method "*.PlainExecutable_WithTerminal_PopulatesTerminalSpecOnWindows"(3 pass)Known limitations / non-goals (deferred)
ExecutionType.IDEis in effect (projectresources running under VS / VS Code), DCP's IDE runner ignores
spec.Terminaland forwards the launch to the IDE, which wiresstdin/stdout to its own debug console. The terminal view in the
dashboard will be empty in that case. The
Processrunner fallback(no-debug, CLI scenarios) honors
spec.Terminaland works as designed.Proper fix is cross-component (Aspire + DCP + VS / VSCode extension).
(
creack/pty).docker/podman --tty).width:80,height:24even when
WithTerminal(Cols=120,Rows=32)is set — needsinvestigation of whether DCP's PTY allocation or Hex1b's headless
presentation is overriding the configured dims. Doesn't break
rendering (xterm.js auto-fits), but should be fixed before shipping.
here, follow-up.
Checklist
Linux/macOS, containers, debugger-attach support).
<remarks />and<code />elements on your triple slash comments?aspire.devissue: TBDCompanion PR
DCP-side: microsoft/dcp#133 — Add Windows PTY support for executables (HMP v1 over UDS)