File: Backchannel\BackchannelService.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.Net.Sockets;
using Aspire.Hosting.Backchannel;
using Aspire.Hosting.Eventing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using StreamJsonRpc;
 
namespace Aspire.Hosting.Cli;
 
internal sealed class BackchannelService(
    ILogger<BackchannelService> logger,
    IConfiguration configuration,
    AppHostRpcTarget appHostRpcTarget,
    IDistributedApplicationEventing eventing,
    IServiceProvider serviceProvider)
    : BackgroundService
{
    private JsonRpc? _rpc;
    private Socket? _serverSocket;
    private string? _socketPath;
 
    public bool IsBackchannelExpected => configuration.GetValue<string>(KnownConfigNames.UnixSocketPath) is {};
 
    private readonly TaskCompletionSource _backchannelConnectedTcs = new();
 
    public Task BackchannelConnected => _backchannelConnectedTcs.Task;
 
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            var unixSocketPath = configuration.GetValue<string>(KnownConfigNames.UnixSocketPath);
 
            if (string.IsNullOrEmpty(unixSocketPath))
            {
                logger.LogDebug("Backchannel socket path was not specified.");
                return;
            }
 
            _socketPath = unixSocketPath;
 
            // Delete existing socket file if it exists (stale from previous run)
            if (File.Exists(unixSocketPath))
            {
                logger.LogDebug("Deleting existing socket file: {SocketPath}", unixSocketPath);
                File.Delete(unixSocketPath);
            }
 
            logger.LogDebug("Listening for backchannel connection on socket path: {SocketPath}", unixSocketPath);
            var serverSocket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
            _serverSocket = serverSocket;
            var endpoint = new UnixDomainSocketEndPoint(unixSocketPath);
            serverSocket.Bind(endpoint);
            serverSocket.Listen();
 
            var backchannelReadyEvent = new BackchannelReadyEvent(serviceProvider, unixSocketPath);
            await eventing.PublishAsync(
                backchannelReadyEvent,
                EventDispatchBehavior.NonBlockingConcurrent,
                stoppingToken).ConfigureAwait(false);
 
            var clientSocket = await serverSocket.AcceptAsync(stoppingToken).ConfigureAwait(false);
            var stream = new NetworkStream(clientSocket, true);
            var rpc = JsonRpc.Attach(stream, appHostRpcTarget);
            _rpc = rpc;
 
            // NOTE: The PipelineExecutor will await this TCS
            //       when a backchannel is expected, and will not stop
            //       the application itself - it will instead wait for
            //       the CLI to stop the application explicitly.
            _backchannelConnectedTcs.SetResult();
 
            var backchannelConnectedEvent = new BackchannelConnectedEvent(serviceProvider, unixSocketPath);
            await eventing.PublishAsync(
                backchannelConnectedEvent,
                EventDispatchBehavior.NonBlockingConcurrent,
                stoppingToken).ConfigureAwait(false);
        }
        catch (TaskCanceledException ex)
        {
            // This exception is expected when the service is shut down whilst waiting for
            // a socket and just means that we don't need to wait anymore.
            logger.LogDebug("Backchannel service was cancelled: {Message}", ex.Message);
            return;
        }
    }
 
    public override void Dispose()
    {
        // Dispose the RPC connection
        _rpc?.Dispose();
        _rpc = null;
 
        // Close and dispose the server socket
        if (_serverSocket is not null)
        {
            try
            {
                _serverSocket.Close();
                _serverSocket.Dispose();
            }
            catch
            {
                // Ignore errors during socket cleanup
            }
            _serverSocket = null;
        }
 
        // Delete the socket file to allow rebinding
        if (_socketPath is not null && File.Exists(_socketPath))
        {
            try
            {
                File.Delete(_socketPath);
                logger.LogDebug("Deleted backchannel socket file: {SocketPath}", _socketPath);
            }
            catch
            {
                // Ignore errors during file cleanup
            }
        }
 
        base.Dispose();
    }
}