LmxProxy is no longer needed. Moved the entire lmxproxy/ workspace, DCL adapter files, and related docs to deprecated/. Removed LmxProxy registration from DataConnectionFactory, project reference from DCL, protocol option from UI, and cleaned up all requirement docs.
216 lines
7.3 KiB
C#
216 lines
7.3 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// HTTP status server providing an HTML dashboard, JSON API, and health endpoint.
|
|
/// </summary>
|
|
public class StatusWebServer : IDisposable
|
|
{
|
|
private static readonly ILogger Logger = Log.ForContext<StatusWebServer>();
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|