File: TcpEndpointProbesService.cs
Web Access
Project: src\src\Libraries\Microsoft.Extensions.Diagnostics.Probes\Microsoft.Extensions.Diagnostics.Probes.csproj (Microsoft.Extensions.Diagnostics.Probes)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
 
namespace Microsoft.Extensions.Diagnostics.Probes;
 
/// <summary>
/// Opens a TCP port if the service is healthy and closes it otherwise.
/// </summary>
internal sealed class TcpEndpointProbesService : BackgroundService
{
    internal TimeProvider TimeProvider { get; set; } = TimeProvider.System;
 
    private readonly ILogger<TcpEndpointProbesService> _logger;
    private readonly HealthCheckService _healthCheckService;
    private readonly TcpEndpointProbesOptions _options;
#pragma warning disable CA2213 // 'TcpEndpointProbesService' contains field '_listener' that is of IDisposable type 'TcpListener'
    private readonly TcpListener _listener;
#pragma warning restore CA2213
 
    public TcpEndpointProbesService(ILogger<TcpEndpointProbesService> logger, HealthCheckService healthCheckService, TcpEndpointProbesOptions options)
    {
        _logger = logger;
        _healthCheckService = healthCheckService;
        _options = options;
 
        _listener = new TcpListener(IPAddress.Any, _options.TcpPort);
    }
 
    internal async Task UpdateHealthStatusAsync(CancellationToken cancellationToken)
    {
        try
        {
            var report = await _healthCheckService.CheckHealthAsync(_options.FilterChecks, cancellationToken).ConfigureAwait(false);
            if (report.Status == HealthStatus.Healthy)
            {
                if (!_listener.Server.IsBound)
                {
                    _listener.Start(_options.MaxPendingConnections);
                    _ = Task.Run(() => OpenTcpAsync(cancellationToken), cancellationToken);
                }
            }
            else
            {
                _listener.Stop();
            }
        }
        catch (SocketException ex)
        {
            _logger.SocketExceptionCaughtTcpEndpoint(ex);
        }
    }
 
    /// <summary>
    /// Executes the health checks in the <see cref="HealthCheckService"/> and opens the registered TCP port if the service is healthy and closes it otherwise.
    /// </summary>
    /// <param name="stoppingToken">The <see cref="CancellationToken"/>.</param>
    /// <returns>Task.</returns>
    protected async override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await Task.Yield();
 
        while (!stoppingToken.IsCancellationRequested)
        {
            await UpdateHealthStatusAsync(stoppingToken).ConfigureAwait(false);
            await TimeProvider.Delay(_options.HealthAssessmentPeriod, stoppingToken).ConfigureAwait(false);
        }
 
        _listener.Stop();
    }
 
    [SuppressMessage("Blocker Bug", "S2190:Recursion should not be infinite", Justification = "runs in background")]
    private async Task OpenTcpAsync(CancellationToken cancellationToken)
    {
        while (true)
        {
#if NET6_0_OR_GREATER
            using var client = await _listener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(false);
#else
            using var client = await _listener.AcceptTcpClientAsync().ConfigureAwait(false);
#endif
        }
    }
}