using System; 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.Status { /// /// HTTP status server providing an HTML dashboard, JSON API, and health endpoint. /// public class StatusWebServer : IDisposable { private static readonly ILogger Logger = Log.ForContext(); private readonly WebServerConfiguration _configuration; private readonly StatusReportService _statusReportService; private HttpListener? _httpListener; private CancellationTokenSource? _cancellationTokenSource; private Task? _listenerTask; private bool _disposed; public StatusWebServer(WebServerConfiguration configuration, StatusReportService statusReportService) { _configuration = configuration; _statusReportService = statusReportService; } public bool Start() { if (!_configuration.Enabled) { Logger.Information("Status web server is disabled"); return true; } try { _httpListener = new HttpListener(); var 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 on {Prefix}", prefix); return true; } catch (Exception ex) { Logger.Error(ex, "Failed to start status web server"); return false; } } public bool Stop() { if (!_configuration.Enabled || _httpListener == null) return true; try { _cancellationTokenSource?.Cancel(); if (_listenerTask != null) { _listenerTask.Wait(TimeSpan.FromSeconds(5)); } _httpListener.Stop(); _httpListener.Close(); Logger.Information("Status web server stopped"); return true; } catch (Exception ex) { Logger.Error(ex, "Error stopping status web server"); return false; } } public void Dispose() { if (_disposed) return; _disposed = true; Stop(); _cancellationTokenSource?.Dispose(); if (_httpListener != null) { ((IDisposable)_httpListener).Dispose(); } } private async Task HandleRequestsAsync(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested && _httpListener != null && _httpListener.IsListening) { try { var context = await _httpListener.GetContextAsync(); _ = Task.Run(() => HandleRequestAsync(context)); } catch (ObjectDisposedException) { // Expected during shutdown break; } catch (HttpListenerException ex) when (ex.ErrorCode == 995) { // ERROR_OPERATION_ABORTED — expected during shutdown break; } catch (Exception ex) { Logger.Error(ex, "Error accepting HTTP request"); await Task.Delay(1000, cancellationToken).ConfigureAwait(false); } } } private async Task HandleRequestAsync(HttpListenerContext context) { try { if (context.Request.HttpMethod != "GET") { context.Response.StatusCode = 405; await WriteResponseAsync(context.Response, "Method Not Allowed", "text/plain"); return; } var path = context.Request.Url?.AbsolutePath?.ToLowerInvariant() ?? "/"; switch (path) { case "/": await HandleStatusPageAsync(context.Response); break; case "/api/status": await HandleStatusApiAsync(context.Response); break; case "/api/health": await HandleHealthApiAsync(context.Response); break; default: context.Response.StatusCode = 404; await WriteResponseAsync(context.Response, "Not Found", "text/plain"); break; } } catch (Exception ex) { Logger.Error(ex, "Error handling HTTP request"); try { context.Response.StatusCode = 500; await WriteResponseAsync(context.Response, "Internal Server Error", "text/plain"); } catch { // Ignore errors writing error response } } } private async Task HandleStatusPageAsync(HttpListenerResponse response) { var html = await _statusReportService.GenerateHtmlReportAsync(); await WriteResponseAsync(response, html, "text/html; charset=utf-8"); } private async Task HandleStatusApiAsync(HttpListenerResponse response) { var json = await _statusReportService.GenerateJsonReportAsync(); await WriteResponseAsync(response, json, "application/json; charset=utf-8"); } private async Task HandleHealthApiAsync(HttpListenerResponse response) { var isHealthy = await _statusReportService.IsHealthyAsync(); if (isHealthy) { response.StatusCode = 200; await WriteResponseAsync(response, "OK", "text/plain"); } else { response.StatusCode = 503; await WriteResponseAsync(response, "UNHEALTHY", "text/plain"); } } 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"); var buffer = Encoding.UTF8.GetBytes(content); response.ContentLength64 = buffer.Length; await response.OutputStream.WriteAsync(buffer, 0, buffer.Length); response.OutputStream.Close(); } } }