| File: ApplicationModel\AspireTerminal.cs | Web Access |
| Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; namespace Aspire.Hosting.ApplicationModel; /// <summary> /// An Aspire-owned terminal handle for dashboard interaction and automation from AppHost code. /// </summary> /// <remarks> /// <para> /// Obtain a handle from <see cref="TerminalService.CreateTerminal(TerminalLaunchOptions)"/> or /// <see cref="TerminalService.TryGetTerminal"/>. Handles cannot be constructed or extended by callers; /// Aspire manages their registration and connection to the underlying terminal implementation. /// </para> /// <para> /// What disposal means depends on <see cref="Owner"/>. For <see cref="TerminalOwner.AppHost"/> the AppHost /// owns the workload, so disposing stops it and removes the terminal from the dashboard; whoever creates /// such a terminal owns it and must dispose it, and showing one in an interaction does not transfer that /// ownership, so the terminal survives the dialog it was displayed in. For /// <see cref="TerminalOwner.Resource"/> the workload belongs to the resource, so disposing only releases /// Aspire's handle and leaves the workload running. /// </para> /// <para> /// When an AppHost-owned workload ends, its terminal remains in the dashboard until disposed, but no longer /// accepts input or automation. Reopening an ended terminal displays its ended state rather than replaying output. /// </para> /// </remarks> [Experimental(TerminalDiagnostics.DiagnosticId, UrlFormat = TerminalDiagnostics.UrlFormat)] public sealed class AspireTerminal : IAsyncDisposable { internal AspireTerminal(ITerminalBackend backend) { Backend = backend; } internal ITerminalBackend Backend { get; } /// <summary> /// Gets the opaque identifier used to address this terminal over the dashboard connection. /// </summary> public string Id => Backend.Id; /// <summary> /// Gets the title shown on the terminal's dock tab. /// </summary> public string Title => Backend.Title; /// <summary> /// Gets the process that owns this terminal's workload. /// </summary> public TerminalOwner Owner => Backend.Owner; /// <summary> /// Gets where this terminal is displayed in the dashboard. /// </summary> public TerminalPlacement Placement => Backend.Placement; /// <summary> /// Starts the terminal's workload if it is not already running. /// </summary> /// <remarks> /// <para> /// Starting is the caller's decision, not the dashboard's and not the interaction service's. Call this to /// have the workload running before anyone is looking at it — a terminal that is already running when a /// dialog opens shows its scrollback immediately, and automation can drive a terminal that is never /// displayed at all. /// </para> /// <para> /// This is idempotent and does not block: it schedules the workload rather than waiting for it to produce /// output. Use <see cref="WaitForTextAsync"/> to wait for the workload to reach a known state. It is also /// a no-op for <see cref="TerminalOwner.Resource"/> terminals, whose workload is started by the resource. /// </para> /// </remarks> /// <exception cref="InvalidOperationException">The terminal has already stopped.</exception> public void Start() => Backend.Start(); /// <summary> /// Reveals the terminal dock in every connected dashboard and switches to this terminal's tab. /// </summary> /// <remarks> /// Only meaningful for <see cref="TerminalPlacement.Dock"/> terminals. Terminals in a dialog are revealed /// by that dialog, so this is a no-op for them. /// </remarks> public void Show() => Backend.Show(); /// <summary> /// Sends text to the terminal's workload as though it had been typed. /// </summary> /// <param name="text">The text to send.</param> /// <param name="cancellationToken">Cancellation token.</param> /// <returns>A task representing the input operation.</returns> /// <exception cref="ArgumentNullException"><paramref name="text"/> is <see langword="null"/>.</exception> /// <exception cref="InvalidOperationException">The AppHost-owned terminal has already stopped.</exception> /// <exception cref="OperationCanceledException">The input operation was canceled by <paramref name="cancellationToken"/>.</exception> public Task SendTextAsync(string text, CancellationToken cancellationToken = default) => Backend.SendTextAsync(text, cancellationToken); /// <summary> /// Sends a single key, optionally combined with modifiers, to the terminal's workload. /// </summary> /// <remarks> /// Use the named values on <see cref="AspireTerminalKey"/> and combine them with /// <see cref="AspireTerminalKey.Ctrl"/>, <see cref="AspireTerminalKey.Shift"/>, and /// <see cref="AspireTerminalKey.Alt"/>. For example, <c>AspireTerminalKey.Ctrl(AspireTerminalKey.R)</c> /// sends Control+R. Use <see cref="SendTextAsync"/> for arbitrary text rather than individual keys. /// Key encoding follows the terminal's current input mode; a key does not represent a physical /// key-down or key-up event. /// </remarks> /// <param name="key">The key to send.</param> /// <param name="cancellationToken">Cancellation token.</param> /// <returns>A task representing the input operation.</returns> /// <exception cref="ArgumentException"><paramref name="key"/> is an uninitialized value.</exception> /// <exception cref="InvalidOperationException">The AppHost-owned terminal has already stopped.</exception> /// <exception cref="OperationCanceledException">The input operation was canceled by <paramref name="cancellationToken"/>.</exception> public Task SendKeyAsync(AspireTerminalKey key, CancellationToken cancellationToken = default) { // Reject invalid keys before the backend can start a workload or connect to a resource terminal. key.Validate(nameof(key)); return Backend.SendKeyAsync(key, cancellationToken); } /// <summary> /// Waits until <paramref name="text"/> appears on the terminal screen. /// </summary> /// <param name="text">The text to wait for.</param> /// <param name="timeout">How long to wait before giving up. Defaults to 30 seconds.</param> /// <param name="cancellationToken">Cancellation token.</param> /// <returns>A task that completes when the text appears on the terminal screen.</returns> /// <exception cref="ArgumentNullException"><paramref name="text"/> is <see langword="null"/>.</exception> /// <exception cref="TimeoutException">The text did not appear before <paramref name="timeout"/> elapsed.</exception> /// <exception cref="InvalidOperationException">The AppHost-owned terminal has already stopped.</exception> public Task WaitForTextAsync(string text, TimeSpan? timeout = null, CancellationToken cancellationToken = default) => Backend.WaitForTextAsync(text, timeout, cancellationToken); /// <summary> /// Gets the current contents of the terminal screen, with lines separated by newlines. /// </summary> /// <returns>The current terminal screen text.</returns> /// <exception cref="InvalidOperationException">The AppHost-owned terminal has already stopped.</exception> public string GetScreenText() => Backend.GetScreenText(); /// <summary> /// Releases the handle, stopping the workload only when it is owned by the AppHost. /// </summary> /// <returns>A task representing the terminal cleanup operation.</returns> public ValueTask DisposeAsync() => Backend.DisposeAsync(); }