File: Projects\IAppHostServerProject.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Cli.Configuration;
using Aspire.Cli.DotNet;
using Aspire.Cli.Processes;
using Aspire.Cli.Utils;
 
namespace Aspire.Cli.Projects;
 
/// <summary>
/// Result of preparing an AppHost server for running.
/// </summary>
/// <param name="Success">Whether preparation succeeded.</param>
/// <param name="Output">Build/preparation output for display on failure.</param>
/// <param name="ChannelName">The NuGet channel used (SDK mode only, null for bundle mode).</param>
/// <param name="NeedsCodeGeneration">Whether code generation is needed for the guest language.</param>
internal sealed record AppHostServerPrepareResult(
    bool Success,
    OutputCollector? Output,
    string? ChannelName = null,
    bool NeedsCodeGeneration = false);
 
/// <summary>
/// Result of <see cref="IAppHostServerProject.RunAsync"/> — a launched AppHost server process plus the
/// captured output.
/// </summary>
/// <param name="SocketPath">RPC socket the server is publishing on.</param>
/// <param name="OutputCollector">Captured stdout/stderr for failure display.</param>
/// <param name="Execution">
/// The started <see cref="IProcessExecution"/> that owns the server child. Callers observe state
/// (<see cref="IProcessExecution.HasExited"/>, <see cref="IProcessExecution.ExitCode"/>,
/// <see cref="IProcessExecution.ProcessId"/>), drive its lifetime via
/// <see cref="IProcessExecution.WaitForExitAsync(CancellationToken)"/> (which runs the shared
/// shutdown ladder on cancellation), and dispose it via
/// <see cref="System.IAsyncDisposable.DisposeAsync"/>. The execution encapsulates the isolated
/// Windows spawn quirk (the underlying Process is obtained via <see cref="System.Diagnostics.Process.GetProcessById(int)"/>),
/// so its status getters are reliable on every path — see https://github.com/dotnet/runtime/issues/45003.
/// </param>
internal sealed record AppHostServerRunResult(
    string SocketPath,
    OutputCollector OutputCollector,
    IProcessExecution Execution);
 
/// <summary>
/// Controls how <see cref="IAppHostServerProject.RunAsync"/> spawns and tears down the server child.
/// The default (all-null / false) preserves today's force-kill-on-cancel behavior for the non-Run
/// callers (SDK gen, scaffolding, publish, dump). The run path supplies the graceful infrastructure.
/// </summary>
/// <param name="IsolateConsole">
/// When <see langword="true"/>, on Windows the server is spawned via <see cref="IsolatedProcess"/>
/// into its own hidden console (CREATE_NEW_CONSOLE | SW_HIDE) so a graceful shutdown can
/// <c>AttachConsole</c> + post <c>CTRL_C_EVENT</c> against the server without also signalling the CLI.
/// On Unix the spawn is effectively the same as today's path.
/// </param>
/// <param name="KillOnParentExit">
/// When <see langword="true"/>, on Windows the server is bound to the process-wide
/// <see cref="WindowsConsoleProcessJob"/> kill-on-close safety net.
/// </param>
/// <param name="GracefulShutdownSignaler">
/// Issues the graceful shutdown signal during the shared ladder, or <see langword="null"/> to fall
/// back to force-kill on cancellation.
/// </param>
/// <param name="ShutdownService">
/// The command-level graceful window bounding the ladder, or <see langword="null"/> to fall back to
/// force-kill on cancellation.
/// </param>
internal sealed record AppHostServerRunControl(
    bool IsolateConsole = false,
    bool KillOnParentExit = false,
    IProcessTreeGracefulShutdownSignaler? GracefulShutdownSignaler = null,
    IGracefulShutdownWindow? ShutdownService = null);
 
/// <summary>
/// Represents an AppHost server that can be prepared and run.
/// This abstraction allows for different implementations:
/// - SDK mode: dynamically generates and builds a .NET project
/// - Bundle mode: uses a pre-built server from the Aspire bundle
/// </summary>
internal interface IAppHostServerProject
{
    /// <summary>
    /// Gets the path to the user's app (the polyglot apphost directory).
    /// </summary>
    string AppDirectoryPath { get; }
 
    /// <summary>
    /// Prepares the AppHost server for running.
    /// For SDK mode: creates project files and builds the project.
    /// For bundle mode: restores integration packages from NuGet.
    /// </summary>
    /// <param name="sdkVersion">The Aspire SDK version to use.</param>
    /// <param name="integrations">The integration references (NuGet packages and/or project references) required by the app host.</param>
    /// <param name="requestedChannel">The package channel to use for this prepare operation, or <see langword="null" /> to use the project configuration.</param>
    /// <param name="packageSourceOverride">Optional package source to prefer for Aspire package restore.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The preparation result indicating success/failure and any output.</returns>
    Task<AppHostServerPrepareResult> PrepareAsync(
        string sdkVersion,
        IEnumerable<IntegrationReference> integrations,
        string? requestedChannel = null,
        string? packageSourceOverride = null,
        CancellationToken cancellationToken = default);
 
    /// <summary>
    /// Runs the AppHost server process.
    /// </summary>
    /// <remarks>
    /// Implementations should return promptly once the child process has been spawned and its
    /// identity (socket path, PID) is available; long-running initialization should happen after the
    /// returned task completes. <see cref="AppHostServerSession"/> awaits this call while holding its
    /// start gate, and <c>DisposeAsync</c> acquires the same gate — so any latency here directly
    /// delays how quickly a concurrent dispose can begin tearing the session down.
    /// </remarks>
    /// <param name="hostPid">The host process ID (CLI) for orphan detection.</param>
    /// <param name="environmentVariables">Environment variables to pass to the server.</param>
    /// <param name="additionalArgs">Additional command-line arguments.</param>
    /// <param name="debug">Whether to enable debug logging.</param>
    /// <param name="runControl">
    /// Console-isolation + graceful-shutdown wiring for the spawn. <see langword="null"/>
    /// preserves force-kill-on-cancel semantics for non-Run callers (SDK gen, scaffolding,
    /// publish, dump). The run path passes a populated <see cref="AppHostServerRunControl"/>.
    /// </param>
    /// <returns>A task producing the launched server process execution and its captured output.</returns>
    Task<AppHostServerRunResult> RunAsync(
        int hostPid,
        IReadOnlyDictionary<string, string>? environmentVariables,
        string[]? additionalArgs,
        bool debug,
        AppHostServerRunControl? runControl);
 
    /// <summary>
    /// Gets a unique identifier path for this AppHost, used for running instance detection.
    /// For SDK mode: returns the generated project file path.
    /// For prebuilt mode: returns the app path.
    /// </summary>
    /// <returns>A path that uniquely identifies this AppHost.</returns>
    string GetInstanceIdentifier();
}