UI for key update
This commit is contained in:
55
EpsonPrintService/ConfigurationLoader.cs
Normal file
55
EpsonPrintService/ConfigurationLoader.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using Inspectron.Epson.PrintServer.ConfigurationSources;
|
||||||
|
|
||||||
|
namespace EpsonPrintService;
|
||||||
|
|
||||||
|
public record ConfigLoadResult(bool Success, EpsonPrintServiceConfiguration? Config, string? Error);
|
||||||
|
|
||||||
|
public static class ConfigurationLoader
|
||||||
|
{
|
||||||
|
public static ConfigLoadResult TryLoadFromBase64(string base64Content)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var decoded = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(base64Content.Trim()));
|
||||||
|
var parts = decoded.Split(';');
|
||||||
|
if (parts.Length != 3)
|
||||||
|
return new ConfigLoadResult(false, null, $"Invalid configuration format. Expected 'apiUrl;restaurantId;apiKey', got {parts.Length} parts.");
|
||||||
|
|
||||||
|
var config = new EpsonPrintServiceConfiguration
|
||||||
|
{
|
||||||
|
ApiUrl = parts[0],
|
||||||
|
RestaurantId = parts[1],
|
||||||
|
ApiKey = parts[2]
|
||||||
|
};
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(config.ApiUrl) || string.IsNullOrWhiteSpace(config.RestaurantId) || string.IsNullOrWhiteSpace(config.ApiKey))
|
||||||
|
return new ConfigLoadResult(false, null, "Configuration contains empty values.");
|
||||||
|
|
||||||
|
return new ConfigLoadResult(true, config, null);
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
return new ConfigLoadResult(false, null, "Invalid base64 content.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ConfigLoadResult TryLoadFromFile()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var configPath = ConfigurationPaths.GetConfigPath();
|
||||||
|
if (!File.Exists(configPath))
|
||||||
|
return new ConfigLoadResult(false, null, $"Configuration file not found: {configPath}");
|
||||||
|
|
||||||
|
var base64Line = File.ReadAllText(configPath).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(base64Line))
|
||||||
|
return new ConfigLoadResult(false, null, "Configuration file is empty.");
|
||||||
|
|
||||||
|
return TryLoadFromBase64(base64Line);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new ConfigLoadResult(false, null, $"Failed to read configuration: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
127
EpsonPrintService/HtmlPageBuilder.cs
Normal file
127
EpsonPrintService/HtmlPageBuilder.cs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
using Inspectron.Epson.PrintServer.ConfigurationSources;
|
||||||
|
|
||||||
|
namespace EpsonPrintService;
|
||||||
|
|
||||||
|
public static class HtmlPageBuilder
|
||||||
|
{
|
||||||
|
public static string BuildConfigPage(
|
||||||
|
EpsonPrintServiceConfiguration? currentConfig,
|
||||||
|
bool printServerRunning,
|
||||||
|
UsbKeyResult? usbResult,
|
||||||
|
string? errorMessage = null)
|
||||||
|
{
|
||||||
|
var statusColor = printServerRunning ? "#2e7d32" : "#e65100";
|
||||||
|
var statusText = printServerRunning ? "Running" : "Not Configured";
|
||||||
|
|
||||||
|
return $@"<!DOCTYPE html>
|
||||||
|
<html lang=""en"">
|
||||||
|
<head>
|
||||||
|
<meta charset=""UTF-8"">
|
||||||
|
<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
|
||||||
|
<title>Epson Print Server</title>
|
||||||
|
<style>
|
||||||
|
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 600px; margin: 40px auto; padding: 0 20px; background: #f5f5f5; color: #333; }}
|
||||||
|
h1 {{ font-size: 1.4em; }}
|
||||||
|
.card {{ background: #fff; border-radius: 8px; padding: 20px; margin: 16px 0; box-shadow: 0 1px 3px rgba(0,0,0,0.12); }}
|
||||||
|
.status {{ display: inline-block; padding: 4px 12px; border-radius: 12px; color: #fff; font-weight: bold; font-size: 0.9em; background: {statusColor}; }}
|
||||||
|
.info {{ color: #666; font-size: 0.9em; }}
|
||||||
|
.info strong {{ color: #333; }}
|
||||||
|
.error {{ background: #ffebee; border-left: 4px solid #c62828; padding: 12px 16px; border-radius: 4px; margin: 12px 0; color: #c62828; }}
|
||||||
|
.usb {{ background: #e3f2fd; border-left: 4px solid #1565c0; padding: 12px 16px; border-radius: 4px; margin: 12px 0; }}
|
||||||
|
.usb strong {{ color: #1565c0; }}
|
||||||
|
.btn {{ display: inline-block; padding: 10px 24px; border: none; border-radius: 6px; font-size: 1em; cursor: pointer; text-decoration: none; color: #fff; }}
|
||||||
|
.btn-primary {{ background: #1565c0; }}
|
||||||
|
.btn-primary:hover {{ background: #0d47a1; }}
|
||||||
|
.btn-secondary {{ background: #757575; }}
|
||||||
|
.btn-secondary:hover {{ background: #616161; }}
|
||||||
|
.actions {{ margin-top: 16px; display: flex; gap: 10px; align-items: center; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Epson Print Server</h1>
|
||||||
|
|
||||||
|
<div class=""card"">
|
||||||
|
<p>Status: <span class=""status"">{statusText}</span></p>
|
||||||
|
{(currentConfig != null ? $@"
|
||||||
|
<div class=""info"">
|
||||||
|
<p><strong>Restaurant ID:</strong> {Escape(currentConfig.RestaurantId)}</p>
|
||||||
|
<p><strong>API URL:</strong> {Escape(currentConfig.ApiUrl ?? "(not set)")}</p>
|
||||||
|
</div>" : "")}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(errorMessage != null ? $@"<div class=""error"">{Escape(errorMessage)}</div>" : "")}
|
||||||
|
|
||||||
|
{BuildUsbSection(usbResult)}
|
||||||
|
|
||||||
|
<div class=""actions"">
|
||||||
|
<a href=""/"" class=""btn btn-secondary"">Refresh</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildUsbSection(UsbKeyResult? usbResult)
|
||||||
|
{
|
||||||
|
if (usbResult == null)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
if (usbResult.Error != null && !usbResult.Found)
|
||||||
|
return $@"<div class=""card""><p class=""info"">{Escape(usbResult.Error)}</p></div>";
|
||||||
|
|
||||||
|
if (!usbResult.Found)
|
||||||
|
return @"<div class=""card""><p class=""info"">No USB drive with <strong>print_server_key.txt</strong> found. Insert a USB drive and press Refresh.</p></div>";
|
||||||
|
|
||||||
|
if (usbResult.Error != null)
|
||||||
|
return $@"<div class=""error"">USB key found but invalid: {Escape(usbResult.Error)}</div>";
|
||||||
|
|
||||||
|
return $@"
|
||||||
|
<div class=""card"">
|
||||||
|
<div class=""usb"">
|
||||||
|
<strong>USB Key Found</strong>
|
||||||
|
<p class=""info""><strong>Restaurant ID:</strong> {Escape(usbResult.DecodedRestaurantId ?? "?")}</p>
|
||||||
|
<p class=""info""><strong>API URL:</strong> {Escape(usbResult.DecodedApiUrl ?? "?")}</p>
|
||||||
|
</div>
|
||||||
|
<form method=""POST"" action=""/update-key"" style=""margin-top: 12px;"">
|
||||||
|
<button type=""submit"" class=""btn btn-primary"">Update Key</button>
|
||||||
|
</form>
|
||||||
|
</div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string BuildSuccessPage(EpsonPrintServiceConfiguration newConfig)
|
||||||
|
{
|
||||||
|
return $@"<!DOCTYPE html>
|
||||||
|
<html lang=""en"">
|
||||||
|
<head>
|
||||||
|
<meta charset=""UTF-8"">
|
||||||
|
<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
|
||||||
|
<meta http-equiv=""refresh"" content=""5"">
|
||||||
|
<title>Configuration Updated</title>
|
||||||
|
<style>
|
||||||
|
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 600px; margin: 40px auto; padding: 0 20px; background: #f5f5f5; color: #333; }}
|
||||||
|
h1 {{ font-size: 1.4em; color: #2e7d32; }}
|
||||||
|
.card {{ background: #fff; border-radius: 8px; padding: 20px; margin: 16px 0; box-shadow: 0 1px 3px rgba(0,0,0,0.12); }}
|
||||||
|
.success {{ background: #e8f5e9; border-left: 4px solid #2e7d32; padding: 12px 16px; border-radius: 4px; }}
|
||||||
|
.info {{ color: #666; font-size: 0.9em; }}
|
||||||
|
.info strong {{ color: #333; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Configuration Updated!</h1>
|
||||||
|
<div class=""card"">
|
||||||
|
<div class=""success"">
|
||||||
|
<p>The configuration has been saved. The service will restart automatically.</p>
|
||||||
|
</div>
|
||||||
|
<div class=""info"" style=""margin-top: 12px;"">
|
||||||
|
<p><strong>Restaurant ID:</strong> {Escape(newConfig.RestaurantId)}</p>
|
||||||
|
<p><strong>API URL:</strong> {Escape(newConfig.ApiUrl ?? "(not set)")}</p>
|
||||||
|
</div>
|
||||||
|
<p class=""info"" style=""margin-top: 12px;"">This page will refresh in 5 seconds...</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Escape(string value) =>
|
||||||
|
System.Net.WebUtility.HtmlEncode(value);
|
||||||
|
}
|
||||||
78
EpsonPrintService/PrintServerBootstrapper.cs
Normal file
78
EpsonPrintService/PrintServerBootstrapper.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using Inspectron.Epson;
|
||||||
|
using Inspectron.Epson.PrintServer;
|
||||||
|
using Inspectron.Epson.PrintServer.ConfigurationSources;
|
||||||
|
using Inspectron.Epson.PrintServer.JobSources;
|
||||||
|
using Inspectron.Epson.PrintServer.JobStatusReporters;
|
||||||
|
using Inspectron.Epson.PrintServer.PrinterAssinment;
|
||||||
|
using Inspectron.Epson.PrintServer.Printers;
|
||||||
|
using Inspectron.Epson.PrintServer.Printers.Utils;
|
||||||
|
using Inspectron.Epson.PrintServer.PrintServices;
|
||||||
|
using Inspectron.Epson.Queue;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Ninject;
|
||||||
|
|
||||||
|
namespace EpsonPrintService;
|
||||||
|
|
||||||
|
public class PrintServerBootstrapper
|
||||||
|
{
|
||||||
|
private readonly EpsonPrintServiceConfiguration _config;
|
||||||
|
private readonly CancellationTokenSource _cts;
|
||||||
|
private StandardKernel? _kernel;
|
||||||
|
|
||||||
|
public bool IsRunning { get; private set; }
|
||||||
|
|
||||||
|
public PrintServerBootstrapper(EpsonPrintServiceConfiguration config, CancellationTokenSource cts)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_cts = cts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartAsync()
|
||||||
|
{
|
||||||
|
_kernel = new StandardKernel();
|
||||||
|
|
||||||
|
_kernel.Bind<IDiscoveryService>().To<DiscoveryService>().InSingletonScope();
|
||||||
|
_kernel.Bind<HttpClient>().ToConstant(new HttpClient { Timeout = TimeSpan.FromSeconds(30) }).InSingletonScope();
|
||||||
|
_kernel.Bind<EpsonPrintServiceConfiguration>().ToConstant(_config);
|
||||||
|
_kernel.Bind<IPrinterFactory>().To<EpsonPrinterFactory>();
|
||||||
|
_kernel.Bind<IPrintJobSource, SignalRPrintJobSource>().To<SignalRPrintJobSource>().InSingletonScope();
|
||||||
|
_kernel.Bind<IPrintService>().To<Inspectron.Epson.PrintServer.PrintServices.EpsonPrintService>();
|
||||||
|
_kernel.Bind<ILogger>().ToMethod(ctx =>
|
||||||
|
{
|
||||||
|
var loggerFactory = LoggerFactory.Create(builder =>
|
||||||
|
{
|
||||||
|
builder.AddConsole();
|
||||||
|
builder.SetMinimumLevel(LogLevel.Trace);
|
||||||
|
});
|
||||||
|
return loggerFactory.CreateLogger("EpsonPrintService");
|
||||||
|
});
|
||||||
|
_kernel.Bind<IWrapperPrinterFactory>().To<PrinterWrapperFactory>().InSingletonScope();
|
||||||
|
_kernel.Bind<IPrinterConfigurationSource>().To<FixedConfigurationSource>();
|
||||||
|
_kernel.Bind<IReceiptConverterFactory>().To<ReceiptConverterFactory>().InSingletonScope();
|
||||||
|
_kernel.Bind<IJobStatusReporter>().To<JamesStatusReporter>();
|
||||||
|
_kernel.Bind<PrintServer>().ToSelf().InSingletonScope();
|
||||||
|
_kernel.Bind<IDiscoveredPrintersReceiver>().To<JamesDiscoveredPrintersReceiver>().InSingletonScope();
|
||||||
|
_kernel.Bind<PrinterDiscoveryBackgroundTask>().ToSelf().InSingletonScope();
|
||||||
|
_kernel.Bind<HeartbeatBackgroundTask>().ToSelf().InSingletonScope();
|
||||||
|
|
||||||
|
var printLoop = _kernel.Get<PrintLoop>();
|
||||||
|
var discoveryTask = _kernel.Get<PrinterDiscoveryBackgroundTask>();
|
||||||
|
var heartbeatTask = _kernel.Get<HeartbeatBackgroundTask>();
|
||||||
|
var jobSourceTask = _kernel.Get<IPrintJobSource>();
|
||||||
|
|
||||||
|
// Start background tasks
|
||||||
|
_ = discoveryTask.StartAsync(_cts.Token);
|
||||||
|
_ = heartbeatTask.StartAsync(_cts.Token);
|
||||||
|
|
||||||
|
// Delay for a moment to allow discovery to find printers
|
||||||
|
Console.WriteLine("Waiting for printer discovery...");
|
||||||
|
await Task.Delay(3000);
|
||||||
|
|
||||||
|
_ = jobSourceTask.StartAsync();
|
||||||
|
|
||||||
|
await printLoop.StartAsync();
|
||||||
|
|
||||||
|
IsRunning = true;
|
||||||
|
Console.WriteLine("Print server started.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,95 +1,22 @@
|
|||||||
using Inspectron.Epson;
|
|
||||||
using Microsoft.AspNetCore.SignalR.Client;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Ninject;
|
|
||||||
using System.Threading.Channels;
|
|
||||||
using Inspectron.Epson.PrintServer;
|
|
||||||
using Inspectron.Epson.PrintServer.ConfigurationSources;
|
|
||||||
using Inspectron.Epson.PrintServer.JobSources;
|
|
||||||
using Inspectron.Epson.PrintServer.PrinterAssinment;
|
|
||||||
using Inspectron.Epson.PrintServer.Printers;
|
|
||||||
using Inspectron.Epson.PrintServer.Printers.Utils;
|
|
||||||
using Inspectron.Epson.PrintServer.PrintServices;
|
|
||||||
using EpsonPrintService;
|
using EpsonPrintService;
|
||||||
using Inspectron.Epson.PrintServer.JobStatusReporters;
|
using Inspectron.Epson.PrintServer.ConfigurationSources;
|
||||||
using Inspectron.Epson.Queue;
|
|
||||||
|
|
||||||
StandardKernel kernel = new StandardKernel();
|
// Load configuration (may be absent on first boot)
|
||||||
|
var configResult = ConfigurationLoader.TryLoadFromFile();
|
||||||
|
EpsonPrintServiceConfiguration? activeConfig = null;
|
||||||
|
|
||||||
|
if (configResult.Success)
|
||||||
var configPath = ConfigurationPaths.GetConfigPath();
|
|
||||||
Console.WriteLine($"Loading configuration from: {configPath}");
|
|
||||||
|
|
||||||
var base64Line = File.ReadAllText(configPath).Trim();
|
|
||||||
var decodedConfig = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(base64Line));
|
|
||||||
var configParts = decodedConfig.Split(';');
|
|
||||||
if (configParts.Length != 3)
|
|
||||||
throw new InvalidOperationException($"Invalid configuration format. Expected 'apiUrl;restaurantId;apiKey', got {configParts.Length} parts.");
|
|
||||||
|
|
||||||
var config = new EpsonPrintServiceConfiguration
|
|
||||||
{
|
{
|
||||||
ApiUrl = configParts[0],
|
activeConfig = configResult.Config;
|
||||||
RestaurantId = configParts[1],
|
Console.WriteLine($"Configuration loaded.");
|
||||||
ApiKey = configParts[2]
|
Console.WriteLine($"Restaurant: {activeConfig!.RestaurantId}");
|
||||||
};
|
Console.WriteLine($"API URL: {activeConfig.ApiUrl}");
|
||||||
|
}
|
||||||
Console.WriteLine($"Restaurant: {config.RestaurantId}");
|
else
|
||||||
Console.WriteLine($"API URL: {config.ApiUrl}");
|
|
||||||
|
|
||||||
kernel.Bind<IDiscoveryService>().To<DiscoveryService>().InSingletonScope();
|
|
||||||
|
|
||||||
//var discovery = new PreconfiguredDiscoveryService();
|
|
||||||
//discovery.AddPrinter("192.168.50.176", "TM-T30III", port: 9100);
|
|
||||||
//discovery.AddPrinter("192.168.50.60", "TM-T30III", port: 9100);
|
|
||||||
//discovery.AddPrinter("192.168.50.61", "TM-T30III", port: 9100);
|
|
||||||
//discovery.AddPrinter("192.168.50.80", "TM-U220II", port: 9100);
|
|
||||||
//kernel.Bind<IDiscoveryService>().ToConstant(discovery);
|
|
||||||
|
|
||||||
kernel.Bind<HttpClient>().ToConstant(new HttpClient { Timeout = TimeSpan.FromSeconds(30) }).InSingletonScope();
|
|
||||||
|
|
||||||
|
|
||||||
kernel.Bind<EpsonPrintServiceConfiguration>().ToConstant(config);
|
|
||||||
//kernel.Bind<IPrinterFactory>().ToMethod(sp=>new HtmlPrinterFactory("TM-U220II.html", paperWidth: 258,type:0x0d, logger:sp.Kernel.Get<ILogger>()));
|
|
||||||
//kernel.Bind<IPrinterFactory>().ToMethod(sp=>new HtmlPrinterFactory("TM-T30III.html",logger:sp.Kernel.Get<ILogger>()));
|
|
||||||
kernel.Bind<IPrinterFactory>().To<EpsonPrinterFactory>();
|
|
||||||
|
|
||||||
kernel.Bind<IPrintJobSource, SignalRPrintJobSource>().To<SignalRPrintJobSource>().InSingletonScope();
|
|
||||||
//kernel.Bind<IPrintJobSource>().To<HttpPollingPrintJobSource>().InSingletonScope();
|
|
||||||
|
|
||||||
kernel.Bind<IPrintService>().To<Inspectron.Epson.PrintServer.PrintServices.EpsonPrintService>();
|
|
||||||
kernel.Bind<ILogger>().ToMethod(ctx =>
|
|
||||||
{
|
{
|
||||||
var loggerFactory = LoggerFactory.Create(builder =>
|
Console.WriteLine($"No valid configuration: {configResult.Error}");
|
||||||
{
|
Console.WriteLine("Running in configuration-only mode. Navigate to http://<this-ip> to configure.");
|
||||||
builder.AddConsole();
|
}
|
||||||
builder.SetMinimumLevel(LogLevel.Trace);
|
|
||||||
});
|
|
||||||
return loggerFactory.CreateLogger("EpsonPrintService");
|
|
||||||
});
|
|
||||||
|
|
||||||
kernel.Bind<IWrapperPrinterFactory>().To<PrinterWrapperFactory>().InSingletonScope();
|
|
||||||
kernel.Bind<IPrinterConfigurationSource>().To<FixedConfigurationSource>();
|
|
||||||
kernel.Bind<IReceiptConverterFactory>().To<ReceiptConverterFactory>().InSingletonScope();
|
|
||||||
kernel.Bind<IJobStatusReporter>().To<JamesStatusReporter>();
|
|
||||||
kernel.Bind<PrintServer>().ToSelf().InSingletonScope();
|
|
||||||
kernel.Bind<IDiscoveredPrintersReceiver>().To<JamesDiscoveredPrintersReceiver>().InSingletonScope();
|
|
||||||
kernel.Bind<PrinterDiscoveryBackgroundTask>().ToSelf().InSingletonScope();
|
|
||||||
kernel.Bind<HeartbeatBackgroundTask>().ToSelf().InSingletonScope();
|
|
||||||
|
|
||||||
|
|
||||||
var printLoop = kernel.Get<PrintLoop>();
|
|
||||||
var printServer = kernel.Get<PrintServer>();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var discoveryTask = kernel.Get<PrinterDiscoveryBackgroundTask>();
|
|
||||||
var heartbeatTask = kernel.Get<HeartbeatBackgroundTask>();
|
|
||||||
var jobSourceTask = kernel.Get<IPrintJobSource>();
|
|
||||||
//printServer.RegisterPrinter("10.0.20.12");
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var cts = new CancellationTokenSource();
|
var cts = new CancellationTokenSource();
|
||||||
Console.CancelKeyPress += (sender, e) =>
|
Console.CancelKeyPress += (sender, e) =>
|
||||||
@@ -99,18 +26,21 @@ Console.CancelKeyPress += (sender, e) =>
|
|||||||
cts.Cancel();
|
cts.Cancel();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start background tasks
|
// Track print server state for the web UI
|
||||||
_ = discoveryTask.StartAsync(cts.Token);
|
PrintServerBootstrapper? bootstrapper = null;
|
||||||
_ = heartbeatTask.StartAsync(cts.Token);
|
|
||||||
|
|
||||||
|
// Start web UI (always runs)
|
||||||
|
var httpServer = new SimpleHttpServer(
|
||||||
|
getConfig: () => activeConfig,
|
||||||
|
getIsRunning: () => bootstrapper?.IsRunning ?? false);
|
||||||
|
_ = Task.Run(() => httpServer.StartAsync(cts.Token));
|
||||||
|
|
||||||
// delay for a moment to allow discovery to find printers
|
// Start print server if configured
|
||||||
Console.WriteLine("Waiting for printer discovery...");
|
if (activeConfig != null)
|
||||||
await Task.Delay(3000);
|
{
|
||||||
|
bootstrapper = new PrintServerBootstrapper(activeConfig, cts);
|
||||||
_ = jobSourceTask.StartAsync();
|
await bootstrapper.StartAsync();
|
||||||
|
}
|
||||||
await printLoop.StartAsync();
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -120,4 +50,3 @@ catch (TaskCanceledException)
|
|||||||
{
|
{
|
||||||
// Expected when Ctrl+C is pressed
|
// Expected when Ctrl+C is pressed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
136
EpsonPrintService/SimpleHttpServer.cs
Normal file
136
EpsonPrintService/SimpleHttpServer.cs
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using Inspectron.Epson.PrintServer.ConfigurationSources;
|
||||||
|
|
||||||
|
namespace EpsonPrintService;
|
||||||
|
|
||||||
|
public class SimpleHttpServer
|
||||||
|
{
|
||||||
|
private readonly HttpListener _listener;
|
||||||
|
private readonly Func<EpsonPrintServiceConfiguration?> _getConfig;
|
||||||
|
private readonly Func<bool> _getIsRunning;
|
||||||
|
|
||||||
|
public SimpleHttpServer(Func<EpsonPrintServiceConfiguration?> getConfig, Func<bool> getIsRunning)
|
||||||
|
{
|
||||||
|
_listener = new HttpListener();
|
||||||
|
_listener.Prefixes.Add("http://+:80/");
|
||||||
|
_getConfig = getConfig;
|
||||||
|
_getIsRunning = getIsRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_listener.Start();
|
||||||
|
Console.WriteLine("Web UI listening on http://+:80/");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var context = await _listener.GetContextAsync();
|
||||||
|
_ = Task.Run(() => HandleRequestAsync(context), cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (HttpListenerException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// Expected on shutdown
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listener.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleRequestAsync(HttpListenerContext context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = context.Request;
|
||||||
|
var response = context.Response;
|
||||||
|
var path = request.Url?.AbsolutePath ?? "/";
|
||||||
|
var method = request.HttpMethod;
|
||||||
|
|
||||||
|
string html;
|
||||||
|
|
||||||
|
if (method == "GET" && path == "/")
|
||||||
|
{
|
||||||
|
html = await BuildIndexPageAsync();
|
||||||
|
}
|
||||||
|
else if (method == "POST" && path == "/update-key")
|
||||||
|
{
|
||||||
|
html = await HandleUpdateKeyAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
response.StatusCode = 404;
|
||||||
|
html = "<html><body><h1>Not Found</h1></body></html>";
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = Encoding.UTF8.GetBytes(html);
|
||||||
|
response.ContentType = "text/html; charset=utf-8";
|
||||||
|
response.ContentLength64 = buffer.Length;
|
||||||
|
await response.OutputStream.WriteAsync(buffer);
|
||||||
|
response.Close();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"HTTP error: {ex.Message}");
|
||||||
|
try { context.Response.Close(); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> BuildIndexPageAsync()
|
||||||
|
{
|
||||||
|
var usbResult = await UsbKeyScanner.ScanForKeyAsync();
|
||||||
|
return HtmlPageBuilder.BuildConfigPage(_getConfig(), _getIsRunning(), usbResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> HandleUpdateKeyAsync()
|
||||||
|
{
|
||||||
|
var usbResult = await UsbKeyScanner.ScanForKeyAsync();
|
||||||
|
|
||||||
|
if (!usbResult.Found || usbResult.Base64Content == null)
|
||||||
|
{
|
||||||
|
return HtmlPageBuilder.BuildConfigPage(
|
||||||
|
_getConfig(), _getIsRunning(), usbResult,
|
||||||
|
errorMessage: "No valid USB key found. Please insert a USB drive and try again.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate again
|
||||||
|
var validation = ConfigurationLoader.TryLoadFromBase64(usbResult.Base64Content);
|
||||||
|
if (!validation.Success)
|
||||||
|
{
|
||||||
|
return HtmlPageBuilder.BuildConfigPage(
|
||||||
|
_getConfig(), _getIsRunning(), usbResult,
|
||||||
|
errorMessage: $"Key validation failed: {validation.Error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write config file
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var configPath = ConfigurationPaths.GetConfigPath();
|
||||||
|
var configDir = Path.GetDirectoryName(configPath);
|
||||||
|
if (!string.IsNullOrEmpty(configDir) && !Directory.Exists(configDir))
|
||||||
|
Directory.CreateDirectory(configDir);
|
||||||
|
|
||||||
|
await File.WriteAllTextAsync(configPath, usbResult.Base64Content);
|
||||||
|
Console.WriteLine($"Configuration updated at {configPath}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return HtmlPageBuilder.BuildConfigPage(
|
||||||
|
_getConfig(), _getIsRunning(), usbResult,
|
||||||
|
errorMessage: $"Failed to write configuration: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule exit so systemd restarts us with new config
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await Task.Delay(500);
|
||||||
|
Console.WriteLine("Exiting for restart with new configuration...");
|
||||||
|
Environment.Exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
return HtmlPageBuilder.BuildSuccessPage(validation.Config!);
|
||||||
|
}
|
||||||
|
}
|
||||||
137
EpsonPrintService/UsbKeyScanner.cs
Normal file
137
EpsonPrintService/UsbKeyScanner.cs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace EpsonPrintService;
|
||||||
|
|
||||||
|
public record UsbKeyResult(bool Found, string? Base64Content, string? DecodedApiUrl, string? DecodedRestaurantId, string? Error);
|
||||||
|
|
||||||
|
public static class UsbKeyScanner
|
||||||
|
{
|
||||||
|
private const string KeyFileName = "print_server_key.txt";
|
||||||
|
|
||||||
|
public static async Task<UsbKeyResult> ScanForKeyAsync()
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
return new UsbKeyResult(false, null, null, null, "USB scanning is only supported on Linux.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var lsblkJson = await RunCommandAsync("lsblk", "-J -o NAME,RM,MOUNTPOINT,TYPE");
|
||||||
|
if (lsblkJson == null)
|
||||||
|
return new UsbKeyResult(false, null, null, null, "Failed to run lsblk.");
|
||||||
|
|
||||||
|
var doc = JsonDocument.Parse(lsblkJson);
|
||||||
|
var devices = doc.RootElement.GetProperty("blockdevices");
|
||||||
|
|
||||||
|
foreach (var device in devices.EnumerateArray())
|
||||||
|
{
|
||||||
|
var result = await ScanDeviceAsync(device);
|
||||||
|
if (result != null)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
// Check children (partitions)
|
||||||
|
if (device.TryGetProperty("children", out var children))
|
||||||
|
{
|
||||||
|
foreach (var child in children.EnumerateArray())
|
||||||
|
{
|
||||||
|
result = await ScanDeviceAsync(child);
|
||||||
|
if (result != null)
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UsbKeyResult(false, null, null, null, null);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new UsbKeyResult(false, null, null, null, $"USB scan failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<UsbKeyResult?> ScanDeviceAsync(JsonElement device)
|
||||||
|
{
|
||||||
|
var isRemovable = device.TryGetProperty("rm", out var rm) && (rm.ValueKind == JsonValueKind.True || (rm.ValueKind == JsonValueKind.Number && rm.GetInt32() == 1));
|
||||||
|
if (!isRemovable)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var type = device.TryGetProperty("type", out var t) ? t.GetString() : null;
|
||||||
|
if (type != "part")
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var name = device.TryGetProperty("name", out var n) ? n.GetString() : null;
|
||||||
|
if (name == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var mountpoint = device.TryGetProperty("mountpoint", out var mp) && mp.ValueKind == JsonValueKind.String ? mp.GetString() : null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(mountpoint))
|
||||||
|
{
|
||||||
|
return CheckMountpoint(mountpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to mount and check
|
||||||
|
var tempMount = $"/tmp/usb_scan_{name}";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(tempMount);
|
||||||
|
var mountResult = await RunCommandAsync("mount", $"-o ro /dev/{name} {tempMount}");
|
||||||
|
if (mountResult == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return CheckMountpoint(tempMount);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { await RunCommandAsync("umount", tempMount); } catch { }
|
||||||
|
try { Directory.Delete(tempMount); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UsbKeyResult? CheckMountpoint(string mountpoint)
|
||||||
|
{
|
||||||
|
var keyPath = Path.Combine(mountpoint, KeyFileName);
|
||||||
|
if (!File.Exists(keyPath))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var content = File.ReadAllText(keyPath).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(content))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var validation = ConfigurationLoader.TryLoadFromBase64(content);
|
||||||
|
if (!validation.Success)
|
||||||
|
return new UsbKeyResult(true, content, null, null, $"Key file found but invalid: {validation.Error}");
|
||||||
|
|
||||||
|
return new UsbKeyResult(true, content, validation.Config!.ApiUrl, validation.Config.RestaurantId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string?> RunCommandAsync(string command, string arguments)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var process = new Process
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = command,
|
||||||
|
Arguments = arguments,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
var output = await process.StandardOutput.ReadToEndAsync();
|
||||||
|
await process.WaitForExitAsync();
|
||||||
|
|
||||||
|
return process.ExitCode == 0 ? output : null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user