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); } } } }