* HttpServer.cs: fix #1855 Changed HTTP server to asynchronous tasks. * HttpServer.cs: reformat * HttpServer.cs: fix
This commit is contained in:
@@ -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<string, object>();
|
||||
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
|
||||
<BODY><H4>401 Unauthorized</H4>
|
||||
Authorization required.</BODY></HTML> ";
|
||||
|
||||
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<string, object> 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<string, object> sensorData)
|
||||
private async Task SendJsonSensorAsync(HttpListenerResponse response, Dictionary<string, object> 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)
|
||||
{ }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user