-
Notifications
You must be signed in to change notification settings - Fork 978
WithTerminal(): per-replica interactive terminal sessions (Aspire side, draft) #17866
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
96 commits
Select commit
Hold shift + click to select a range
3b649ab
Add WithTerminal API: TerminalAnnotation, TerminalHostResource, and e…
mitchdenny 3abd3b1
Add Aspire Terminal Protocol spec, shared codec, and playground
mitchdenny 7ce3362
Add terminal protocol test client and verify end-to-end
mitchdenny 38d6785
Add DCP model TerminalSpec, backchannel RPC, and CLI terminal command
mitchdenny 9277b2d
Add WithTerminal custom socket path provider overload
mitchdenny 2b04243
Add Dashboard terminal support: xterm.js, WebSocket proxy, ConsoleLog…
mitchdenny f777479
Fix Dashboard terminal: use script tags for xterm.js and int IDs for …
mitchdenny 50479bc
Add production Aspire.TerminalHost and wire into AppHost discovery
mitchdenny ec0c756
Fix playground: use custom socket path overload to avoid dual termina…
mitchdenny c95cd84
Redesign terminal host for reconnection with state replay
mitchdenny a821875
WithTerminal Phase 1: refactor to per-replica UDS pair design
mitchdenny 72e784e
WithTerminal Phase 2: Aspire.TerminalHost on Hex1b HMP v1
mitchdenny 875a550
WithTerminal Phase 4: wire per-replica TerminalSpec into DCP Executab…
mitchdenny 8c14060
WithTerminal Phase 5: backchannel exposes per-replica terminal endpoints
mitchdenny 74c39b2
WithTerminal Phase 6: aspire terminal CLI command on Hex1b HMP v1
mitchdenny 169456f
Phase 7: Dashboard /api/terminal WebSocket proxy + per-replica Termin…
mitchdenny f39ee87
Phase 8: WithTerminal end-to-end playground + plain-executable fix
mitchdenny 174eeee
Phase 8 fix: TerminalView reacts to resource/replica parameter changes
mitchdenny 6c37e6e
WithTerminal: container support + Node.js playground
mitchdenny 08c3615
Terminals playground: nodebox fix - override CMD instead of entrypoint
mitchdenny d6f429d
Flip producer-side connection direction in Aspire.TerminalHost
mitchdenny b1214f4
Dashboard terminal: send resize on WebSocket open before render
mitchdenny 17fdd7e
WithTerminal: process-restart support + Stop-regression hardening (Ph…
mitchdenny 813b0b0
Phase 11: CLI wires up HMP1 multi-head primary/secondary protocol
mitchdenny 33036bf
TerminalHost: bridge consumer-side resize to raw DCP HMP1 frame
mitchdenny efb6837
Update Hex1b to 0.147.0 + transient nuget.org Hex1b-only mapping
mitchdenny 2dc81db
Phase 17 Dashboard: transplant WebMuxerDemo terminal chrome + dumb by…
mitchdenny a1f4b44
Phase 17 CLI: split TerminalCommand and lift WebMuxerDemo viewer expe…
mitchdenny dc10147
Terminals playground: shell2 control resource for Stop bisection + sl…
mitchdenny 33d2b48
Update Hex1b 0.147.0 -> 0.150.0; drop transient nuget.org mapping
mitchdenny 5f813e8
Adapt to Hex1b 0.150.0 and rebased main
mitchdenny e8201bc
WithTerminal Phase 18: package Aspire.TerminalHost as per-RID NuGet p…
mitchdenny 354ebd7
WithTerminal Phase 19: integrate Aspire.TerminalHost into the polyglo…
mitchdenny 7ac56bf
docs/specs/with-terminal: fix MD040 fenced-code-language
mitchdenny 362acdb
WithTerminal Phase 20: fix CI failures from Phase 18/19
mitchdenny b97b221
WithTerminal Phase 22: review feedback cleanup pass 1
mitchdenny ceaef89
WithTerminal Phase 22: review feedback documentation pass
mitchdenny 053e54e
WithTerminal Phase 23: aspire terminal ps command + per-replica metadata
mitchdenny f69afc1
Per-replica TerminalHost: one host process per parent replica
mitchdenny 6ad0df0
chore: whitespace nudge to retrigger CI
mitchdenny 6116143
Defer per-replica TerminalHost creation to BeforeStartEvent
mitchdenny 670af13
Add ATS export for WithTerminal via parameterless dispatcher overload
mitchdenny d50d829
Fall back to ASPIRE_DASHBOARD_PATH when terminal host path is unset i…
mitchdenny 07cd081
Drop TerminalSpec.Enabled and Windows-only gate
8445464
playground: add Unix bash shell + auto-wire TerminalHost project ref
mitchdenny 5cf3129
Post-rebase fixups + bump Hex1b 0.154 → 0.161
mitchdenny 48572e1
Dashboard: lift terminal chrome into ConsoleLogs toolbar
mitchdenny d4e74a7
Dashboard: hide log-only toolbar items in terminal mode
mitchdenny 2649201
Dashboard: consolidate terminal primary badge + take-control button
mitchdenny 51e07d7
Dashboard terminal: diagnostics + pooled larger outbound buffer
mitchdenny 3d3adf2
Dashboard terminal: faster UDS drain to avoid Hex1b slow-peer eviction
mitchdenny bb70bbb
Dashboard terminal: auto-take primary when adjusting font/size
mitchdenny 2122e28
Dashboard terminal: apply local size before auto-promoting
mitchdenny 0904bdd
Dashboard terminal: theme-aware stage background + Aspire-purple glow
mitchdenny 9cd66d1
Dashboard terminal: bundle Cascadia Mono NF for Powerline/Nerd glyphs
mitchdenny 7ee41d1
Dashboard terminal: preload Cascadia Mono NF before xterm construction
mitchdenny 7192073
Use term.reset() instead of term.clear() on WS reconnect
Copilot 8adac5c
Bump Hex1b 0.161 → 0.163 + post-rebase fixups
mitchdenny 4cb1cb7
Validate Origin on terminal WebSocket upgrade (CSWSH defense)
mitchdenny 688e670
Restrict terminal-host control socket to 0600
mitchdenny 3f1dc02
Clean up terminal-host temp directory on ApplicationStopped
mitchdenny f692878
Pre-delete and post-delete producer/consumer UDS files in TerminalRep…
mitchdenny 0a09a06
WithTerminal: move per-replica UDS sockets and metadata sidecar under…
mitchdenny e9147ed
WithTerminal: rename control sock purpose 'control' -> 'ctrl'
mitchdenny 9fdd3a6
WithTerminal: assert manifest exclusion uses Ignore singleton
mitchdenny 7b4926c
WithTerminal: add ShowTerminalHosts option (default false)
mitchdenny 4c71b99
WithTerminal: drop StderrLoggerProvider, wire OTel exporters
mitchdenny c668f14
Drop bogus ReplicasStarted counter, add ConsumerConnections
mitchdenny 4b31190
Add ILogger<T> + OTel metrics for terminal host lifecycle events
mitchdenny 0e4b029
Remove dashboard diagnostics instrumentation from Program.cs
mitchdenny 1f7016f
Add startup OTLP env diagnostic to terminal host stderr
mitchdenny c5a2caa
Distribute Aspire.TerminalHost via CLI bundle only
mitchdenny 64968f9
Adopt DCP socketMode=connect, IHost-based terminal host, debug logging
mitchdenny 1450045
Review batch A: capability gates, doc fixes, dead code removal
mitchdenny a8b3528
TerminalHostArgs: adopt System.CommandLine for parsing (#6)
mitchdenny a35dd37
Review batch B: correctness/DoS/UX fixes
mitchdenny ff87bfb
Review batch C: test cleanup (#21-#24)
mitchdenny 0e304b5
WithTerminal: rename ShowTerminalHosts -> ShowTerminalHost
mitchdenny d08f9fe
Sdk.in.targets: revert gratuitous comment reflow
mitchdenny ef7b636
Backchannel: consolidate Terminals_PsV1 into Terminals_V1
mitchdenny d6c26ba
Comments: strip session-narration 'Phase N' references
mitchdenny 275fcbd
Terminal commands: adopt CommonCommandServices ctor
mitchdenny 35cae1d
Accept codegen snapshots for WithTerminal()
mitchdenny aa1f311
Address @adamint review feedback on PR #17866
mitchdenny 2099622
Dashboard: terminal UI polish
mitchdenny dbc5b03
Review #17: TerminalHost failure diagnostics + comment cleanup
27ade1c
CLI: always inject ASPIRE_TERMINAL_HOST_PATH from bundle
mitchdenny 6ea2944
TerminalHost: tolerate transient EAGAIN/EINTR in control accept loop
mitchdenny 41d9fe5
CLI: prefer repo-local aspire-managed for terminal host in DEBUG repo…
mitchdenny ece2a6a
CLI: inject terminal host env vars for guest AppHosts + TerminalsJs s…
mitchdenny f534635
Merge remote-tracking branch 'origin/main' into feature/with-terminal
mitchdenny 4be4daa
Mark WithTerminal experimental + gate terminal CLI commands behind fe…
mitchdenny a9ca800
Potential fix for pull request finding
mitchdenny c9984d9
Potential fix for pull request finding
mitchdenny 9a5e42a
Potential fix for pull request finding
mitchdenny 7505af8
Add THIRD-PARTY-NOTICES entries for Cascadia Code and xterm.js
mitchdenny File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| # `WithTerminal()` — Aspire interactive terminal architecture | ||
|
|
||
| **Status:** Implemented for Aspire 13.4 (Windows executables). | ||
| **Issue:** [microsoft/aspire#16317](https://github.com/microsoft/aspire/issues/16317) | ||
| **DCP integration:** [microsoft/dcp#133](https://github.com/microsoft/dcp/pull/133) | ||
|
|
||
| ## Goal | ||
|
|
||
| Let an Aspire AppHost author opt any executable or container resource into | ||
| interactive terminal access: | ||
|
|
||
| ```csharp | ||
| builder.AddProject<Projects.MyAgent>("agent") | ||
| .WithReplicas(2) | ||
| .WithTerminal(); | ||
| ``` | ||
|
|
||
| The dashboard then renders an xterm.js terminal per replica, and the CLI | ||
| exposes the same session as `aspire terminal agent --replica 0`. | ||
|
|
||
| ## Process topology | ||
|
|
||
| ```text | ||
| ┌────────────────────────────┐ | ||
| │ AppHost (dotnet run) │ | ||
| │ - Aspire.Hosting │ | ||
| │ - DCP control plane │ | ||
| │ - per-replica: │ | ||
| │ Aspire.TerminalHost │ (1 process per replica) | ||
| └─────────────┬──────────────┘ | ||
| │ spawn | ||
| ▼ | ||
| ┌──────────────────────┐ ┌───────────────────────┐ ┌────────────────────────┐ | ||
| │ DCP-launched │ PTY │ TerminalHost │ HMP v1 UDS │ Consumers │ | ||
| │ replica process │ ─────────────▶ │ (Hex1b HMP v1 broker) │ ─────────────▶ │ - Dashboard │ | ||
| │ (executable, repl…) │ ◀───── stdin ─ │ │ ◀───── input ─ │ /api/terminal proxy │ | ||
| └──────────────────────┘ └───────────────────────┘ │ - aspire CLI │ | ||
| └────────────────────────┘ | ||
| ``` | ||
|
|
||
| Three actors and three socket roles: | ||
|
|
||
| | Actor | Socket | Direction | Lifetime | | ||
| |----------------|---------------------|----------------------------------|----------| | ||
| | **DCP** | `producerUdsPath` | DCP → host (PTY bytes + control) | Per replica | | ||
| | **TerminalHost** | `consumerUdsPath` | host → consumers (broadcast) | Per replica | | ||
| | **TerminalHost** | `controlUdsPath` | AppHost → host (lifecycle, stats)| Per replica | | ||
|
|
||
| The producer/consumer split lets multiple consumers (dashboard + multiple CLI | ||
| sessions) attach simultaneously without coupling DCP to consumer counts. | ||
|
|
||
| ## Wire protocol | ||
|
|
||
| We do **not** define a custom protocol. The terminal traffic uses | ||
| [Hex1b](https://github.com/dotnet/hex1b)'s `HMP v1` (Hex Multiplex Protocol, | ||
| version 1), which already handles: | ||
|
|
||
| - VT byte streaming with backpressure | ||
| - Resize requests in both directions | ||
| - Hello/StateSync replay so a late-attaching consumer sees the current | ||
| scrollback | ||
| - Connection lifecycle (close, disconnect, reconnect) | ||
| - Authenticated stream factory hooks (we only use Unix-socket transport | ||
| today) | ||
|
|
||
| The `Hmp1WorkloadAdapter` is what the AppHost-side terminal host uses to | ||
| multiplex DCP's PTY traffic to the consumer-facing listener; the | ||
| `Hmp1PresentationAdapter` is what consumers (Dashboard WebSocket proxy and | ||
| the CLI) use to attach. | ||
|
|
||
| ## Property contract (gRPC `ResourceService` snapshots) | ||
|
|
||
| When `WithTerminal()` is applied to a resource, every replica snapshot | ||
| emitted by the dashboard service carries four properties: | ||
|
|
||
| | Key | Sensitivity | Meaning | | ||
| |---------------------------|-----------------|----------------------------------------------| | ||
| | `terminal.enabled` | non-sensitive | Marker. `"true"` when the replica has a PTY. | | ||
| | `terminal.replicaIndex` | non-sensitive | 0-based stable index from `DcpInstancesAnnotation`. | | ||
| | `terminal.replicaCount` | non-sensitive | Total replicas for the parent resource. | | ||
| | `terminal.consumerUdsPath`| **sensitive** | The local UDS that consumers connect to. | | ||
|
|
||
| The consumer UDS path is marked `IsSensitive=true` so the dashboard UI masks | ||
| the value in the property list. The path still rides the gRPC stream because | ||
| the dashboard's WebSocket proxy needs it server-side to resolve | ||
| `?resource=&replica=` query parameters into a real socket; the path is never | ||
| echoed back to the browser. | ||
|
|
||
| ## Dashboard `/api/terminal` WebSocket endpoint | ||
|
|
||
| Authenticated (`RequireAuthorization(FrontendAuthorizationDefaults.PolicyName)`) | ||
| endpoint at `/api/terminal?resource=<displayName>&replica=<index>`. | ||
|
|
||
| `TerminalWebSocketProxy` resolves the connection entirely server-side: | ||
|
|
||
| 1. `ITerminalConnectionResolver.ConnectAsync(resourceName, replicaIndex, ct)` | ||
| walks `IDashboardClient.GetResources()`, matches by `DisplayName` + | ||
| `TryGetTerminalReplicaInfo`, and connects via | ||
| `Hmp1Transports.ConnectUnixSocket(consumerUdsPath, ct)`. | ||
| 2. The proxy wraps the resulting stream in `Hmp1WorkloadAdapter` and runs | ||
| two pumps: | ||
| - **Inbound (browser → producer):** binary frames are forwarded as HMP v1 | ||
| `Input` (keystrokes); text frames are parsed as JSON resize control | ||
| messages (`{"type":"resize","cols":N,"rows":N}`). | ||
| - **Outbound (producer → browser):** VT bytes from the producer become | ||
| binary WebSocket frames; resize hints from the producer become JSON | ||
| text frames. | ||
| 3. Frame type — not content — distinguishes keystroke from control. This | ||
| keeps the proxy's parser cheap and avoids ambiguity around binary input | ||
| that happens to look like JSON. | ||
| 4. Multi-fragment WS reads are reassembled in `ReassembledFrame` using | ||
| `ArrayPool<byte>`. | ||
|
|
||
| The browser never sees `consumerUdsPath` and cannot induce the dashboard | ||
| to connect to an arbitrary local socket — it can only ask for | ||
| `(resource, replica)` pairs that are present in the resource snapshot | ||
| stream. | ||
|
|
||
| ## CLI | ||
|
|
||
| `aspire terminal <resource> [--replica N]` (`Aspire.Cli/Commands/TerminalCommand.cs`) | ||
| opens its own `Hmp1PresentationAdapter` against the consumer UDS path | ||
| returned by `IBackchannel.GetTerminalInfoAsync(resource, replica)` and | ||
| renders frames into the host terminal via Hex1b's `Hex1bTerminal`. When the | ||
| resource has more than one replica and the CLI is interactive, it prompts | ||
| for a selection; in non-interactive mode the `--replica` flag is required. | ||
|
|
||
| ## DCP integration | ||
|
|
||
| For each replica of a `WithTerminal()` resource, DCP allocates a pseudo-terminal | ||
| when the executable (or container) spec carries a populated `terminal` block: | ||
|
|
||
| ```json | ||
| { | ||
| "terminal": { | ||
| "udsPath": "/run/user/1000/aspire/trmnl/<run-id>/<resource>-<idx>/producer.sock", | ||
| "socketMode": "connect", | ||
| "cols": 120, | ||
| "rows": 30 | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| `socketMode: "connect"` tells DCP to dial the named UDS (the TerminalHost | ||
| process owns the listener). The dimensions are the initial PTY size; both | ||
| sides exchange resize frames over HMP afterwards. | ||
|
|
||
| Desktop PTY support is implemented across all three platforms (Unix98 `/dev/ptmx` on Linux and macOS; ConPTY on Windows). Container PTYs are tracked | ||
| as a Phase 3 follow-up on the parent issue. | ||
|
|
||
| ## Files of interest | ||
|
|
||
| | Concern | File | | ||
| |--------------------------------------|---------------------------------------------------------------------| | ||
| | Public API entry point | `src/Aspire.Hosting/TerminalResourceBuilderExtensions.cs` | | ||
| | Per-resource hidden host resource | `src/Aspire.Hosting/ApplicationModel/TerminalHostResource.cs` | | ||
| | DCP wire-up | `src/Aspire.Hosting/Dcp/ExecutableCreator.cs` | | ||
| | Backchannel `GetTerminalInfoAsync` | `src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelRpcTarget.cs` | | ||
| | Snapshot stamping | `src/Aspire.Hosting/Dashboard/DashboardServiceData.cs` | | ||
| | TerminalHost process | `src/Aspire.TerminalHost/` | | ||
| | CLI command | `src/Aspire.Cli/Commands/TerminalCommand.cs` | | ||
| | Dashboard WebSocket proxy | `src/Aspire.Dashboard/Terminal/TerminalWebSocketProxy.cs` | | ||
| | Dashboard resolver | `src/Aspire.Dashboard/Terminal/DefaultTerminalConnectionResolver.cs`| | ||
| | `TerminalView` (xterm.js host) | `src/Aspire.Dashboard/Components/Controls/TerminalView.razor.*` | | ||
| | Property keys | `src/Shared/Model/KnownProperties.cs` (`Terminal.*`) | | ||
| | Playground sample | `playground/Terminals/Terminals.AppHost/AppHost.cs` | | ||
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.