diff --git a/LibreHardwareMonitor/Utilities/HttpServer.cs b/LibreHardwareMonitor/Utilities/HttpServer.cs index 8f3db35..b1bca71 100644 --- a/LibreHardwareMonitor/Utilities/HttpServer.cs +++ b/LibreHardwareMonitor/Utilities/HttpServer.cs @@ -18,6 +18,7 @@ using System.Reflection; using System.Security.Cryptography; using System.Text; using System.Threading; +using System.Threading.Tasks; using System.Web; using LibreHardwareMonitor.Hardware; using LibreHardwareMonitor.UI; @@ -29,7 +30,8 @@ public class HttpServer private readonly HttpListener _listener; private readonly Node _root; private readonly IElement _rootElement; - private Thread _listenerThread; + private Task _listenerTask; + private CancellationTokenSource _cts; public HttpServer(Node node, IElement rootElement, string ip, int port, bool authEnabled = false, string userName = "", string password = "") { @@ -57,7 +59,11 @@ public class HttpServer return; StopHttpListener(); - _listener.Abort(); + try + { + _listener?.Abort(); + } + catch { } } public bool AuthEnabled { get; set; } @@ -117,11 +123,8 @@ public class HttpServer _listener.AuthenticationSchemes = AuthEnabled ? AuthenticationSchemes.Basic : AuthenticationSchemes.Anonymous; _listener.Start(); - if (_listenerThread == null) - { - _listenerThread = new Thread(HandleRequests); - _listenerThread.Start(); - } + _cts = new CancellationTokenSource(); + _listenerTask = Task.Run(() => ProcessRequestsAsync(_cts.Token)); } catch (Exception) { @@ -138,13 +141,14 @@ public class HttpServer try { - _listenerThread?.Abort(); - _listener.Stop(); - _listenerThread = null; + _cts?.Cancel(); + _listenerTask?.Wait(TimeSpan.FromSeconds(5)); // Graceful wait + _listener?.Stop(); + _cts?.Dispose(); } catch (HttpListenerException) { } - catch (ThreadAbortException) + catch (OperationCanceledException) { } catch (NullReferenceException) { } @@ -154,12 +158,33 @@ public class HttpServer return true; } - private void HandleRequests() + private async Task ProcessRequestsAsync(CancellationToken cancellationToken) { - while (_listener.IsListening) + while (_listener.IsListening && !cancellationToken.IsCancellationRequested) { - IAsyncResult context = _listener.BeginGetContext(ListenerCallback, _listener); - context.AsyncWaitHandle.WaitOne(); + try + { + var context = await _listener.GetContextAsync(); + _ = Task.Run(() => HandleContextAsync(context), cancellationToken); + } + catch (HttpListenerException ex) when (ex.ErrorCode == 50) + { + // Handle Windows update bug (e.g., 2025-10 Cumulative Update): retry after delay + System.Diagnostics.Debug.WriteLine($"HttpListener error (code {ex.ErrorCode}): {ex.Message}. Retrying in 5 seconds."); + await Task.Delay(5000, cancellationToken); + } + catch (ObjectDisposedException) + { + break; // Listener stopped + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Unexpected HttpListener error: {ex.Message}"); + } } } @@ -193,7 +218,6 @@ public class HttpServer return null; } - public void SetSensorControlValue(SensorNode sNode, string value) { if (sNode.Sensor.Control == null) @@ -308,43 +332,23 @@ public class HttpServer return System.Text.Json.JsonSerializer.Serialize(result); } - private void ListenerCallback(IAsyncResult result) + private async Task HandleContextAsync(HttpListenerContext context) { - HttpListener listener = (HttpListener)result.AsyncState; - if (listener == null || !listener.IsListening) - return; - - // Call EndGetContext to complete the asynchronous operation. - HttpListenerContext context; - try - { - context = listener.EndGetContext(result); - } - catch (Exception) - { - return; - } - HttpListenerRequest request = context.Request; - - bool authenticated; + bool authenticated = true; if (AuthEnabled) { try { HttpListenerBasicIdentity identity = (HttpListenerBasicIdentity)context.User.Identity; - authenticated = (identity.Name == UserName) & (ComputeSHA256(identity.Password) == Password); + authenticated = (identity.Name == UserName) && (ComputeSHA256(identity.Password) == PasswordSHA256); } catch { authenticated = false; } } - else - { - authenticated = true; - } if (authenticated) { @@ -353,17 +357,7 @@ public class HttpServer case "POST": { string postResult = HandlePostRequest(request); - - Stream output = context.Response.OutputStream; - byte[] utfBytes = Encoding.UTF8.GetBytes(postResult); - - context.Response.AddHeader("Cache-Control", "no-cache"); - context.Response.ContentLength64 = utfBytes.Length; - context.Response.ContentType = "application/json"; - - output.Write(utfBytes, 0, utfBytes.Length); - output.Close(); - + await SendResponseAsync(context.Response, postResult, "application/json"); break; } case "GET": @@ -372,15 +366,13 @@ public class HttpServer if (requestedFile == "data.json") { - SendJson(context.Response, request); + await SendJsonAsync(context.Response, request); return; } if (requestedFile.Contains("images_icon")) { - ServeResourceImage(context.Response, - requestedFile.Replace("images_icon/", string.Empty)); - + await ServeResourceImageAsync(context.Response, requestedFile.Replace("images_icon/", string.Empty)); return; } @@ -388,7 +380,7 @@ public class HttpServer { var sensorResult = new Dictionary(); HandleSensorRequest(request, sensorResult); - SendJsonSensor(context.Response, sensorResult); + await SendJsonSensorAsync(context.Response, sensorResult); return; } @@ -399,7 +391,7 @@ public class HttpServer sensor.ResetMin(); sensor.ResetMax(); })); - SendJson(context.Response, request); + await SendJsonAsync(context.Response, request); return; } @@ -409,8 +401,7 @@ public class HttpServer string[] splits = requestedFile.Split('.'); string ext = splits[splits.Length - 1]; - ServeResourceFile(context.Response, "Web." + requestedFile.Replace('/', '.'), ext); - + await ServeResourceFileAsync(context.Response, "Web." + requestedFile.Replace('/', '.'), ext); break; } default: @@ -431,12 +422,7 @@ public class HttpServer

401 Unauthorized

Authorization required. "; - byte[] buffer = Encoding.UTF8.GetBytes(responseString); - context.Response.ContentLength64 = buffer.Length; - context.Response.StatusCode = 401; - Stream output = context.Response.OutputStream; - output.Write(buffer, 0, buffer.Length); - output.Close(); + await SendResponseAsync(context.Response, responseString, "text/html"); } try @@ -448,8 +434,7 @@ public class HttpServer // client closed connection before the content was sent } } - - private void ServeResourceFile(HttpListenerResponse response, string name, string ext) + private async Task ServeResourceFileAsync(HttpListenerResponse response, string name, string ext) { // resource names do not support the hyphen name = "LibreHardwareMonitor.Resources." + @@ -468,15 +453,14 @@ public class HttpServer byte[] buffer = new byte[512 * 1024]; try { - Stream output = response.OutputStream; int len; - while ((len = stream.Read(buffer, 0, buffer.Length)) > 0) + while ((len = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0) { - output.Write(buffer, 0, len); + await response.OutputStream.WriteAsync(buffer, 0, len); } - output.Flush(); - output.Close(); + await response.OutputStream.FlushAsync(); + response.OutputStream.Close(); response.Close(); } catch (HttpListenerException) @@ -492,7 +476,7 @@ public class HttpServer response.Close(); } - private void ServeResourceImage(HttpListenerResponse response, string name) + private async Task ServeResourceImageAsync(HttpListenerResponse response, string name) { name = "LibreHardwareMonitor.Resources." + name; @@ -504,23 +488,19 @@ public class HttpServer { using Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(names[i]); - Image image = Image.FromStream(stream); + using Image image = Image.FromStream(stream); response.ContentType = "image/png"; try { - Stream output = response.OutputStream; - using (MemoryStream ms = new()) - { - image.Save(ms, ImageFormat.Png); - ms.WriteTo(output); - } - - output.Close(); + using var ms = new MemoryStream(); + image.Save(ms, ImageFormat.Png); + byte[] buffer = ms.ToArray(); + await response.OutputStream.WriteAsync(buffer, 0, buffer.Length); + response.OutputStream.Close(); } catch (HttpListenerException) { } - image.Dispose(); response.Close(); return; } @@ -530,7 +510,7 @@ public class HttpServer response.Close(); } - private void SendJson(HttpListenerResponse response, HttpListenerRequest request = null) + private async Task SendJsonAsync(HttpListenerResponse response, HttpListenerRequest request = null) { Dictionary json = new(); @@ -550,34 +530,32 @@ public class HttpServer bool acceptGzip; try { - acceptGzip = (request != null) && (request.Headers["Accept-Encoding"].ToLower().IndexOf("gzip", StringComparison.OrdinalIgnoreCase) >= 0); + acceptGzip = (request != null) && (request.Headers["Accept-Encoding"].IndexOf("gzip", StringComparison.OrdinalIgnoreCase) >= 0); } catch { acceptGzip = false; } - if (acceptGzip) - response.AddHeader("Content-Encoding", "gzip"); - response.AddHeader("Cache-Control", "no-cache"); response.AddHeader("Access-Control-Allow-Origin", "*"); response.ContentType = "application/json"; + try { if (acceptGzip) { + response.AddHeader("Content-Encoding", "gzip"); using var ms = new MemoryStream(); using (var zip = new GZipStream(ms, CompressionMode.Compress, true)) - zip.Write(buffer, 0, buffer.Length); + await zip.WriteAsync(buffer, 0, buffer.Length); buffer = ms.ToArray(); } response.ContentLength64 = buffer.Length; - Stream output = response.OutputStream; - output.Write(buffer, 0, buffer.Length); - output.Close(); + await response.OutputStream.WriteAsync(buffer, 0, buffer.Length); + response.OutputStream.Close(); } catch (HttpListenerException) { } @@ -585,7 +563,7 @@ public class HttpServer response.Close(); } - private void SendJsonSensor(HttpListenerResponse response, Dictionary sensorData) + private async Task SendJsonSensorAsync(HttpListenerResponse response, Dictionary sensorData) { // Convert the JObject to a JSON string string responseContent = System.Text.Json.JsonSerializer.Serialize(sensorData); @@ -600,9 +578,8 @@ public class HttpServer try { response.ContentLength64 = buffer.Length; - Stream output = response.OutputStream; - output.Write(buffer, 0, buffer.Length); - output.Close(); + await response.OutputStream.WriteAsync(buffer, 0, buffer.Length); + response.OutputStream.Close(); } catch (HttpListenerException) { } @@ -633,7 +610,7 @@ public class HttpServer jsonNode["Value"] = sensorNode.Value; jsonNode["Max"] = sensorNode.Max; - // Unformatted values for external systems to have consistent readings, e.g. Throughput will always be measured in B/s + // Unformatted values for external systems to have consistent readings, e.g. Throughput will always be measured in B/s jsonNode["RawMin"] = string.Format(sensorNode.Format, sensorNode.Sensor.Min); jsonNode["RawValue"] = string.Format(sensorNode.Format, sensorNode.Sensor.Value); jsonNode["RawMax"] = string.Format(sensorNode.Format, sensorNode.Sensor.Max); @@ -685,7 +662,6 @@ public class HttpServer default: return "application/octet-stream"; } } - private static string GetHardwareImageFile(HardwareNode hn) { switch (hn.Hardware.HardwareType) @@ -769,6 +745,26 @@ public class HttpServer return; StopHttpListener(); - _listener.Abort(); + try + { + _listener?.Abort(); + } + catch { } } + + private static async Task SendResponseAsync(HttpListenerResponse response, string content, string contentType) + { + byte[] buffer = Encoding.UTF8.GetBytes(content); + response.ContentType = contentType; + response.ContentLength64 = buffer.Length; + + try + { + await response.OutputStream.WriteAsync(buffer, 0, buffer.Length); + response.OutputStream.Close(); + } + catch (HttpListenerException) + { } + } + }