using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
using ZB.MOM.WW.LmxProxy.Host.Configuration;
namespace ZB.MOM.WW.LmxProxy.Host.Services
{
///
/// HTTP web server that serves status information for the LmxProxy service
///
public class StatusWebServer : IDisposable
{
private static readonly ILogger Logger = Log.ForContext();
private readonly WebServerConfiguration _configuration;
private readonly StatusReportService _statusReportService;
private CancellationTokenSource? _cancellationTokenSource;
private bool _disposed;
private HttpListener? _httpListener;
private Task? _listenerTask;
///
/// Initializes a new instance of the StatusWebServer class
///
/// Web server configuration
/// Service for collecting status information
public StatusWebServer(WebServerConfiguration configuration, StatusReportService statusReportService)
{
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
_statusReportService = statusReportService ?? throw new ArgumentNullException(nameof(statusReportService));
}
///
/// Disposes the web server and releases resources
///
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
Stop();
_cancellationTokenSource?.Dispose();
_httpListener?.Close();
}
///
/// Starts the HTTP web server
///
/// True if started successfully, false otherwise
public bool Start()
{
try
{
if (!_configuration.Enabled)
{
Logger.Information("Status web server is disabled");
return true;
}
Logger.Information("Starting status web server on port {Port}", _configuration.Port);
_httpListener = new HttpListener();
// Configure the URL prefix
string prefix = _configuration.Prefix ?? $"http://+:{_configuration.Port}/";
if (!prefix.EndsWith("/"))
{
prefix += "/";
}
_httpListener.Prefixes.Add(prefix);
_httpListener.Start();
_cancellationTokenSource = new CancellationTokenSource();
_listenerTask = Task.Run(() => HandleRequestsAsync(_cancellationTokenSource.Token));
Logger.Information("Status web server started successfully on {Prefix}", prefix);
return true;
}
catch (Exception ex)
{
Logger.Error(ex, "Failed to start status web server");
return false;
}
}
///
/// Stops the HTTP web server
///
/// True if stopped successfully, false otherwise
public bool Stop()
{
try
{
if (!_configuration.Enabled || _httpListener == null)
{
return true;
}
Logger.Information("Stopping status web server");
_cancellationTokenSource?.Cancel();
if (_listenerTask != null)
{
try
{
_listenerTask.Wait(TimeSpan.FromSeconds(5));
}
catch (Exception ex)
{
Logger.Warning(ex, "Error waiting for listener task to complete");
}
}
_httpListener?.Stop();
_httpListener?.Close();
Logger.Information("Status web server stopped successfully");
return true;
}
catch (Exception ex)
{
Logger.Error(ex, "Error stopping status web server");
return false;
}
}
///
/// Main request handling loop
///
private async Task HandleRequestsAsync(CancellationToken cancellationToken)
{
Logger.Information("Status web server listener started");
while (!cancellationToken.IsCancellationRequested && _httpListener != null && _httpListener.IsListening)
{
try
{
HttpListenerContext? context = await _httpListener.GetContextAsync();
// Handle request asynchronously without waiting
_ = Task.Run(async () =>
{
try
{
await HandleRequestAsync(context);
}
catch (Exception ex)
{
Logger.Error(ex, "Error handling HTTP request from {RemoteEndPoint}",
context.Request.RemoteEndPoint);
}
}, cancellationToken);
}
catch (ObjectDisposedException)
{
// Expected when stopping the listener
break;
}
catch (HttpListenerException ex) when (ex.ErrorCode == 995) // ERROR_OPERATION_ABORTED
{
// Expected when stopping the listener
break;
}
catch (Exception ex)
{
Logger.Error(ex, "Error in request listener loop");
// Brief delay before continuing to avoid tight error loops
try
{
await Task.Delay(1000, cancellationToken);
}
catch (OperationCanceledException)
{
break;
}
}
}
Logger.Information("Status web server listener stopped");
}
///
/// Handles a single HTTP request
///
private async Task HandleRequestAsync(HttpListenerContext context)
{
HttpListenerRequest? request = context.Request;
HttpListenerResponse response = context.Response;
try
{
Logger.Debug("Handling {Method} request to {Url} from {RemoteEndPoint}",
request.HttpMethod, request.Url?.AbsolutePath, request.RemoteEndPoint);
// Only allow GET requests
if (request.HttpMethod != "GET")
{
response.StatusCode = 405; // Method Not Allowed
response.StatusDescription = "Method Not Allowed";
await WriteResponseAsync(response, "Only GET requests are supported", "text/plain");
return;
}
string path = request.Url?.AbsolutePath?.ToLowerInvariant() ?? "/";
switch (path)
{
case "/":
await HandleStatusPageAsync(response);
break;
case "/api/status":
await HandleStatusApiAsync(response);
break;
case "/api/health":
await HandleHealthApiAsync(response);
break;
default:
response.StatusCode = 404; // Not Found
response.StatusDescription = "Not Found";
await WriteResponseAsync(response, "Resource not found", "text/plain");
break;
}
}
catch (Exception ex)
{
Logger.Error(ex, "Error handling HTTP request");
try
{
response.StatusCode = 500; // Internal Server Error
response.StatusDescription = "Internal Server Error";
await WriteResponseAsync(response, "Internal server error", "text/plain");
}
catch (Exception responseEx)
{
Logger.Error(responseEx, "Error writing error response");
}
}
finally
{
try
{
response.Close();
}
catch (Exception ex)
{
Logger.Warning(ex, "Error closing HTTP response");
}
}
}
///
/// Handles the main status page (HTML)
///
private async Task HandleStatusPageAsync(HttpListenerResponse response)
{
string statusHtml = await _statusReportService.GenerateHtmlReportAsync();
await WriteResponseAsync(response, statusHtml, "text/html; charset=utf-8");
}
///
/// Handles the status API endpoint (JSON)
///
private async Task HandleStatusApiAsync(HttpListenerResponse response)
{
string statusJson = await _statusReportService.GenerateJsonReportAsync();
await WriteResponseAsync(response, statusJson, "application/json; charset=utf-8");
}
///
/// Handles the health API endpoint (simple text)
///
private async Task HandleHealthApiAsync(HttpListenerResponse response)
{
bool isHealthy = await _statusReportService.IsHealthyAsync();
string healthText = isHealthy ? "OK" : "UNHEALTHY";
response.StatusCode = isHealthy ? 200 : 503; // Service Unavailable if unhealthy
await WriteResponseAsync(response, healthText, "text/plain");
}
///
/// Writes a response to the HTTP context
///
private static async Task WriteResponseAsync(HttpListenerResponse response, string content, string contentType)
{
response.ContentType = contentType;
response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
response.Headers.Add("Pragma", "no-cache");
response.Headers.Add("Expires", "0");
byte[] buffer = Encoding.UTF8.GetBytes(content);
response.ContentLength64 = buffer.Length;
using (Stream? output = response.OutputStream)
{
await output.WriteAsync(buffer, 0, buffer.Length);
}
}
}
}