File: src\Shared\ContainerReplCommand.cs
Web Access
Project: src\src\Aspire.Hosting.MongoDB\Aspire.Hosting.MongoDB.csproj (Aspire.Hosting.MongoDB)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Publishing;
using Microsoft.Extensions.DependencyInjection;
 
#pragma warning disable ASPIRETERMINAL001
#pragma warning disable ASPIRECONTAINERRUNTIME001
 
namespace Aspire.Hosting;
 
internal static class ContainerReplCommand
{
    internal static IResourceBuilder<T> WithReplCommand<T>(
        this IResourceBuilder<T> builder,
        Func<CancellationToken, Task<TerminalLaunchOptions>> createOptions) where T : ContainerResource
    {
        if (!builder.ApplicationBuilder.ExecutionContext.IsRunMode)
        {
            return builder;
        }
 
        return builder.WithCommand("repl", "REPL", async context =>
        {
            context.CancellationToken.ThrowIfCancellationRequested();
            var notifications = context.Services.GetRequiredService<ResourceNotificationService>();
            if (!notifications.TryGetCurrentState(context.ResourceName, out var resourceEvent) ||
                resourceEvent.Snapshot.State?.Text != KnownResourceStates.Running)
            {
                return CommandResults.Failure("The container is not running.");
            }
 
            // The runtime ID follows container restarts and Aspire's generated name suffixes.
            var containerId = GetContainerId(resourceEvent.Snapshot);
            if (string.IsNullOrEmpty(containerId))
            {
                return CommandResults.Failure("The container ID is not available yet.");
            }
 
            var runtime = await context.Services.GetRequiredService<IContainerRuntimeResolver>()
                .ResolveAsync(context.CancellationToken).ConfigureAwait(false);
            var options = await createOptions(context.CancellationToken).ConfigureAwait(false);
            var execOptions = CreateExecOptions(options, runtime.Name.ToLowerInvariant(), containerId);
            context.CancellationToken.ThrowIfCancellationRequested();
 
            var terminals = context.Services.GetRequiredService<TerminalService>();
            // Dock terminals outlive the command and are disposed when closed or when the AppHost shuts down.
            // Docker may leave the remote client running after local terminal disposal; users should quit
            // the REPL before closing the tab. See https://github.com/moby/moby/issues/9098.
            var terminal = terminals.CreateTerminal(execOptions);
            terminal.Start();
            terminal.Show();
 
            return CommandResults.Success();
        }, new CommandOptions
        {
            Description = "Open a REPL inside the running container in the terminal dock.",
            IconName = "WindowConsole",
            UpdateState = context => context.ResourceSnapshot.State?.Text == KnownResourceStates.Running &&
                !string.IsNullOrEmpty(GetContainerId(context.ResourceSnapshot))
                    ? ResourceCommandState.Enabled
                    : ResourceCommandState.Disabled
        });
    }
 
    internal static TerminalLaunchOptions CreateExecOptions(TerminalLaunchOptions options, string runtime, string containerId)
    {
        var execOptions = new TerminalLaunchOptions
        {
            Title = options.Title,
            Executable = runtime,
            Arguments = ["exec", "-it"],
            Placement = TerminalPlacement.Dock
        };
 
        foreach (var (name, value) in options.EnvironmentVariables)
        {
            // `docker exec --env PGPASSWORD` forwards the CLI's environment without exposing the value in argv.
            execOptions.Arguments.Add("--env");
            execOptions.Arguments.Add(name);
            execOptions.EnvironmentVariables[name] = value;
        }
 
        execOptions.Arguments.Add(containerId);
        execOptions.Arguments.Add(options.Executable);
        foreach (var argument in options.Arguments)
        {
            execOptions.Arguments.Add(argument);
        }
 
        return execOptions;
    }
 
    private static string? GetContainerId(CustomResourceSnapshot snapshot) =>
        snapshot.Properties.FirstOrDefault(property => property.Name == "container.id")?.Value as string;
}