232 lines
7.3 KiB
C#
232 lines
7.3 KiB
C#
|
|
using Laservall.Solidworks.Server.Handlers;
|
||
|
|
using System;
|
||
|
|
using System.Diagnostics;
|
||
|
|
using System.Net;
|
||
|
|
using System.Threading;
|
||
|
|
using System.Threading.Tasks;
|
||
|
|
|
||
|
|
namespace Laservall.Solidworks.Server
|
||
|
|
{
|
||
|
|
internal sealed class BomHttpServer : IDisposable
|
||
|
|
{
|
||
|
|
private HttpListener _listener;
|
||
|
|
private CancellationTokenSource _cts;
|
||
|
|
private Task _listenTask;
|
||
|
|
private readonly RequestRouter _router;
|
||
|
|
private readonly AuthTokenMiddleware _auth;
|
||
|
|
private readonly HeartbeatMonitor _heartbeat;
|
||
|
|
private readonly IBomDataProvider _dataProvider;
|
||
|
|
private readonly BomStreamHandler _bomStreamHandler;
|
||
|
|
private readonly BomDataHandler _bomDataHandler;
|
||
|
|
private readonly SaveHandler _saveHandler;
|
||
|
|
private readonly ExportHandler _exportHandler;
|
||
|
|
private readonly SettingsHandler _settingsHandler;
|
||
|
|
private int _port;
|
||
|
|
private bool _disposed;
|
||
|
|
|
||
|
|
public int Port => _port;
|
||
|
|
public string BaseUrl => $"http://127.0.0.1:{_port}";
|
||
|
|
public string BrowserUrl => $"{BaseUrl}/#token={_auth.Token}";
|
||
|
|
public bool IsRunning => _listener != null && _listener.IsListening;
|
||
|
|
|
||
|
|
public BomHttpServer(IBomDataProvider dataProvider)
|
||
|
|
{
|
||
|
|
_dataProvider = dataProvider;
|
||
|
|
_auth = new AuthTokenMiddleware();
|
||
|
|
_router = new RequestRouter();
|
||
|
|
_heartbeat = new HeartbeatMonitor(TimeSpan.FromSeconds(30), OnHeartbeatTimeout);
|
||
|
|
|
||
|
|
_bomStreamHandler = new BomStreamHandler(dataProvider);
|
||
|
|
_bomDataHandler = new BomDataHandler(dataProvider);
|
||
|
|
_saveHandler = new SaveHandler(dataProvider);
|
||
|
|
_exportHandler = new ExportHandler(() => _bomDataHandler.LastLoadResult ?? _bomStreamHandler.LastLoadResult, dataProvider);
|
||
|
|
_settingsHandler = new SettingsHandler(dataProvider);
|
||
|
|
|
||
|
|
RegisterRoutes();
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Start()
|
||
|
|
{
|
||
|
|
if (IsRunning) return;
|
||
|
|
|
||
|
|
_port = PortFinder.FindAvailablePort();
|
||
|
|
_cts = new CancellationTokenSource();
|
||
|
|
|
||
|
|
_listener = new HttpListener();
|
||
|
|
_listener.Prefixes.Add($"http://127.0.0.1:{_port}/");
|
||
|
|
_listener.Start();
|
||
|
|
|
||
|
|
_heartbeat.Start();
|
||
|
|
_listenTask = Task.Run(() => AcceptLoop(_cts.Token));
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Stop()
|
||
|
|
{
|
||
|
|
if (!IsRunning) return;
|
||
|
|
|
||
|
|
_heartbeat.Stop();
|
||
|
|
_cts?.Cancel();
|
||
|
|
|
||
|
|
try { _listener?.Stop(); } catch { }
|
||
|
|
try { _listener?.Close(); } catch { }
|
||
|
|
|
||
|
|
_listener = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
public void OpenInBrowser()
|
||
|
|
{
|
||
|
|
if (!IsRunning) return;
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
Process.Start(new ProcessStartInfo
|
||
|
|
{
|
||
|
|
FileName = BrowserUrl,
|
||
|
|
UseShellExecute = true
|
||
|
|
});
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void RegisterRoutes()
|
||
|
|
{
|
||
|
|
_router.Get("/", async (ctx, ct) =>
|
||
|
|
{
|
||
|
|
await StaticFileHandler.ServeIndex(ctx, _auth.Token, ct);
|
||
|
|
});
|
||
|
|
|
||
|
|
_router.Get("/api/ping", async (ctx, ct) =>
|
||
|
|
{
|
||
|
|
_heartbeat.Ping();
|
||
|
|
ctx.Response.StatusCode = 200;
|
||
|
|
ctx.Response.ContentType = "application/json; charset=utf-8";
|
||
|
|
byte[] body = System.Text.Encoding.UTF8.GetBytes("{\"ok\":true}");
|
||
|
|
ctx.Response.ContentLength64 = body.Length;
|
||
|
|
await ctx.Response.OutputStream.WriteAsync(body, 0, body.Length, ct);
|
||
|
|
ctx.Response.Close();
|
||
|
|
});
|
||
|
|
|
||
|
|
_router.Get("/api/bom/stream", async (ctx, ct) =>
|
||
|
|
{
|
||
|
|
await _bomStreamHandler.HandleStream(ctx, ct);
|
||
|
|
});
|
||
|
|
|
||
|
|
_router.Get("/api/bom/data", async (ctx, ct) =>
|
||
|
|
{
|
||
|
|
await _bomDataHandler.HandleGetData(ctx, ct);
|
||
|
|
});
|
||
|
|
|
||
|
|
_router.Post("/api/bom/save", async (ctx, ct) =>
|
||
|
|
{
|
||
|
|
await _saveHandler.HandleSave(ctx, ct);
|
||
|
|
});
|
||
|
|
|
||
|
|
_router.Get("/api/bom/export", async (ctx, ct) =>
|
||
|
|
{
|
||
|
|
await _exportHandler.HandleExport(ctx, ct);
|
||
|
|
});
|
||
|
|
|
||
|
|
_router.Get("/api/settings", async (ctx, ct) =>
|
||
|
|
{
|
||
|
|
await _settingsHandler.HandleGet(ctx, ct);
|
||
|
|
});
|
||
|
|
|
||
|
|
_router.Post("/api/settings", async (ctx, ct) =>
|
||
|
|
{
|
||
|
|
await _settingsHandler.HandlePost(ctx, ct);
|
||
|
|
});
|
||
|
|
|
||
|
|
_router.Post("/api/shutdown", async (ctx, ct) =>
|
||
|
|
{
|
||
|
|
ctx.Response.StatusCode = 200;
|
||
|
|
ctx.Response.ContentType = "application/json; charset=utf-8";
|
||
|
|
byte[] body = System.Text.Encoding.UTF8.GetBytes("{\"ok\":true}");
|
||
|
|
ctx.Response.ContentLength64 = body.Length;
|
||
|
|
await ctx.Response.OutputStream.WriteAsync(body, 0, body.Length, ct);
|
||
|
|
ctx.Response.Close();
|
||
|
|
|
||
|
|
// Delay stop so response is sent first
|
||
|
|
_ = Task.Delay(200).ContinueWith(_ => Stop());
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task AcceptLoop(CancellationToken ct)
|
||
|
|
{
|
||
|
|
while (!ct.IsCancellationRequested)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var context = await _listener.GetContextAsync();
|
||
|
|
_ = HandleRequestAsync(context, ct);
|
||
|
|
}
|
||
|
|
catch (HttpListenerException) when (ct.IsCancellationRequested)
|
||
|
|
{
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
catch (ObjectDisposedException)
|
||
|
|
{
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task HandleRequestAsync(HttpListenerContext context, CancellationToken ct)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
SetCorsHeaders(context.Response);
|
||
|
|
|
||
|
|
if (context.Request.HttpMethod == "OPTIONS")
|
||
|
|
{
|
||
|
|
context.Response.StatusCode = 204;
|
||
|
|
context.Response.Close();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!_auth.IsAuthorized(context.Request))
|
||
|
|
{
|
||
|
|
AuthTokenMiddleware.RejectUnauthorized(context.Response);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
await _router.RouteAsync(context, ct);
|
||
|
|
}
|
||
|
|
catch (Exception)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
context.Response.StatusCode = 500;
|
||
|
|
context.Response.Close();
|
||
|
|
}
|
||
|
|
catch { }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void SetCorsHeaders(HttpListenerResponse response)
|
||
|
|
{
|
||
|
|
response.Headers.Set("Access-Control-Allow-Origin", "*");
|
||
|
|
response.Headers.Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||
|
|
response.Headers.Set("Access-Control-Allow-Headers", "Authorization, Content-Type");
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnHeartbeatTimeout()
|
||
|
|
{
|
||
|
|
Stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Dispose()
|
||
|
|
{
|
||
|
|
if (_disposed) return;
|
||
|
|
_disposed = true;
|
||
|
|
Stop();
|
||
|
|
_cts?.Dispose();
|
||
|
|
_heartbeat?.Dispose();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|