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.
316 lines
11 KiB
C#
316 lines
11 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// HTTP web server that serves status information for the LmxProxy service
|
|
/// </summary>
|
|
public class StatusWebServer : IDisposable
|
|
{
|
|
private static readonly ILogger Logger = Log.ForContext<StatusWebServer>();
|
|
|
|
private readonly WebServerConfiguration _configuration;
|
|
private readonly StatusReportService _statusReportService;
|
|
private CancellationTokenSource? _cancellationTokenSource;
|
|
private bool _disposed;
|
|
private HttpListener? _httpListener;
|
|
private Task? _listenerTask;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the StatusWebServer class
|
|
/// </summary>
|
|
/// <param name="configuration">Web server configuration</param>
|
|
/// <param name="statusReportService">Service for collecting status information</param>
|
|
public StatusWebServer(WebServerConfiguration configuration, StatusReportService statusReportService)
|
|
{
|
|
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
|
_statusReportService = statusReportService ?? throw new ArgumentNullException(nameof(statusReportService));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disposes the web server and releases resources
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_disposed = true;
|
|
|
|
Stop();
|
|
|
|
_cancellationTokenSource?.Dispose();
|
|
_httpListener?.Close();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the HTTP web server
|
|
/// </summary>
|
|
/// <returns>True if started successfully, false otherwise</returns>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops the HTTP web server
|
|
/// </summary>
|
|
/// <returns>True if stopped successfully, false otherwise</returns>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Main request handling loop
|
|
/// </summary>
|
|
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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles a single HTTP request
|
|
/// </summary>
|
|
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");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the main status page (HTML)
|
|
/// </summary>
|
|
private async Task HandleStatusPageAsync(HttpListenerResponse response)
|
|
{
|
|
string statusHtml = await _statusReportService.GenerateHtmlReportAsync();
|
|
await WriteResponseAsync(response, statusHtml, "text/html; charset=utf-8");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the status API endpoint (JSON)
|
|
/// </summary>
|
|
private async Task HandleStatusApiAsync(HttpListenerResponse response)
|
|
{
|
|
string statusJson = await _statusReportService.GenerateJsonReportAsync();
|
|
await WriteResponseAsync(response, statusJson, "application/json; charset=utf-8");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the health API endpoint (simple text)
|
|
/// </summary>
|
|
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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a response to the HTTP context
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|