Phase 2: Plattformneutralen Hosting-Kern extrahiert
Neues Projekt Predictalytics.Hosting nimmt auf, was bisher im windows-gebundenen WinFormsHost feststeckte, aber portabel ist: - PredictalyticsHost (aus EmbeddedWebServer): Kestrel- und Worker-Lifecycle, Wartungsaktionen, DB-Groesse. Meldet Zustandswechsel ueber StateChanged. - PredictalyticsOptions (aus AppSettings): ohne WinForms-Bezug. Die System.ComponentModel-Attribute sind plattformneutral und bleiben, damit das PropertyGrid Gruppen und Beschreibungen behaelt. - LoggingSetup (aus Program.cs): Serilog-Aufbau, Terminal-Sink als optionale Action statt fester RichTextBox. - LicenseGuard: GUI-frei. Periodische Revalidierung ueber PeriodicTimer statt WinForms-Timer, Abbruch ueber Callback statt Application.Exit. Der interaktive Dialogaufruf bleibt als LicenseGate im WinForms-Host. - WatchdogHeartbeatService unveraendert verschoben. Infrastructure: RichTextBoxSink -> DelegateSink umbenannt (war nie WinForms-abhaengig, nur missverstaendlich benannt). Einstellungen liegen jetzt unter %APPDATA%/Predictalytics bzw. ~/.config/Predictalytics statt neben der Programmdatei, mit einmaliger Uebernahme aus dem alten Ort. Das Installationsverzeichnis ist unter Linux ueblicherweise nicht beschreibbar. wwwroot wird ueber einen Content-Eintrag neben die Programmdatei kopiert; die frueheren Pfad-Heuristiken entfallen. Hosting und WinFormsHost nutzen Microsoft.NET.Sdk statt Sdk.Web: der Web-SDK globbt wwwroot automatisch als Static Web Asset und kollidiert mit dem Content-Eintrag. WebApplication kommt ueber FrameworkReference. Neu konfigurierbar (verhaltensgleiche Defaults): WebserverHost fuer die Kestrel-Bind-Adresse, DbSslMode fuer die MySQL-Verschluesselung. explorer.exe-Aufrufe durch ProcessStartInfo mit UseShellExecute ersetzt — funktioniert unter Windows und Linux. Build: 0 Fehler. Tests: 100 bestanden, 0 Fehler, 1 uebersprungen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,13 @@
|
||||
using System.Reflection;
|
||||
using Predictalytics.WinFormsHost.Services;
|
||||
using Predictalytics.Hosting;
|
||||
using Serilog;
|
||||
|
||||
namespace Predictalytics.WinFormsHost;
|
||||
|
||||
public partial class MainForm : Form
|
||||
{
|
||||
private EmbeddedWebServer? _webServer;
|
||||
private PredictalyticsHost _host = null!;
|
||||
private CancellationTokenSource? _workerCts;
|
||||
private bool _workerRunning;
|
||||
private bool _webServerRunning;
|
||||
private AppSettings _settings = null!;
|
||||
private PredictalyticsOptions _settings = null!;
|
||||
private WatchdogHeartbeatService? _watchdog;
|
||||
|
||||
/// <summary>Exposes the terminal RichTextBox for the Serilog sink.</summary>
|
||||
@@ -27,33 +24,31 @@ public partial class MainForm : Form
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after Serilog is configured. Initializes the embedded web server.
|
||||
/// Called after Serilog is configured. Initializes the application host.
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
_settings = AppSettings.Load();
|
||||
_settings = PredictalyticsOptions.Load();
|
||||
// Der Host haelt dieselbe Options-Instanz — Aenderungen im PropertyGrid
|
||||
// wirken damit ohne weitere Weitergabe beim naechsten Start.
|
||||
_host = new PredictalyticsHost(_settings);
|
||||
_host.StateChanged += () => BeginInvoke(UpdateStatusBar);
|
||||
|
||||
pg_settings.SelectedObject = _settings;
|
||||
pg_settings.PropertyValueChanged += (s, e) => {
|
||||
pg_settings.PropertyValueChanged += (s, e) =>
|
||||
{
|
||||
_settings.Save();
|
||||
if (_webServer != null)
|
||||
{
|
||||
_webServer.ConnectionString = _settings.ConnectionString;
|
||||
_webServer.DbConnectionDebug = _settings.DbConnectionDebug;
|
||||
_webServer.EgressChannelsText = _settings.EgressChannelsText;
|
||||
}
|
||||
RestartWatchdog();
|
||||
};
|
||||
|
||||
_webServer = new EmbeddedWebServer();
|
||||
_webServer.ConnectionString = _settings.ConnectionString;
|
||||
_webServer.DbConnectionDebug = _settings.DbConnectionDebug;
|
||||
_webServer.EgressChannelsText = _settings.EgressChannelsText;
|
||||
|
||||
// Build Version (Date of compilation/file creation)
|
||||
try {
|
||||
try
|
||||
{
|
||||
var buildDate = new FileInfo(this.GetType().Assembly.Location).LastWriteTime;
|
||||
label_buildVersion.Text = $"Build: {buildDate:yyyy-MM-dd HH:mm:ss}";
|
||||
} catch {
|
||||
}
|
||||
catch
|
||||
{
|
||||
label_buildVersion.Text = "Build: Unknown";
|
||||
}
|
||||
|
||||
@@ -65,7 +60,8 @@ public partial class MainForm : Form
|
||||
|
||||
Log.Information("MainForm initialized. Ready.");
|
||||
Log.Information("Press 'Start Server' to begin polling & discovery.");
|
||||
Log.Information("Press 'Start Local Webserver' to launch the WebUI on http://localhost:{Port}", _settings.WebserverPort);
|
||||
Log.Information("Press 'Start Local Webserver' to launch the WebUI on {Url}", _settings.WebserverUrl);
|
||||
Log.Information("Settings: {Path}", PredictalyticsOptions.SettingsFilePath);
|
||||
|
||||
_ = UpdateDbSizeAsync();
|
||||
var dbSizeTimer = new System.Windows.Forms.Timer { Interval = 6 * 60 * 60 * 1000 };
|
||||
@@ -100,28 +96,24 @@ public partial class MainForm : Form
|
||||
_settings.WatchdogIntervalSeconds,
|
||||
metadataProvider: () => new
|
||||
{
|
||||
workersRunning = _workerRunning,
|
||||
webserverRunning = _webServerRunning
|
||||
workersRunning = _host.WorkersRunning,
|
||||
webserverRunning = _host.WebServerRunning
|
||||
});
|
||||
_watchdog.Start();
|
||||
}
|
||||
|
||||
private async void Btn_serverstart_Click(object? sender, EventArgs e)
|
||||
{
|
||||
if (!_workerRunning)
|
||||
if (!_host.WorkersRunning)
|
||||
{
|
||||
// Start workers
|
||||
_workerCts = new CancellationTokenSource();
|
||||
_workerRunning = true;
|
||||
btn_serverstart.Text = "⏹ Stop Server";
|
||||
Log.Information("🚀 Starting background workers...");
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
_webServer!.ConnectionString = _settings.ConnectionString;
|
||||
_webServer!.DbConnectionDebug = _settings.DbConnectionDebug;
|
||||
_webServer!.EgressChannelsText = _settings.EgressChannelsText;
|
||||
await _webServer!.StartWorkersAsync(_workerCts.Token);
|
||||
await _host.StartWorkersAsync(_workerCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex) { Log.Error(ex, "Worker error"); }
|
||||
@@ -131,7 +123,6 @@ public partial class MainForm : Form
|
||||
// Stop workers
|
||||
Log.Information("⏹ Stopping background workers...");
|
||||
_workerCts?.Cancel();
|
||||
_workerRunning = false;
|
||||
btn_serverstart.Text = "▶ Start Server";
|
||||
Log.Information("Workers stopped.");
|
||||
}
|
||||
@@ -140,28 +131,25 @@ public partial class MainForm : Form
|
||||
|
||||
private async void Btn_localWebserver_Click(object? sender, EventArgs e)
|
||||
{
|
||||
if (!_webServerRunning)
|
||||
if (!_host.WebServerRunning)
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.Information("🌐 Starting embedded Kestrel webserver on http://localhost:{Port}...", _settings.WebserverPort);
|
||||
await _webServer!.StartWebServerAsync(_settings.WebserverPort);
|
||||
_webServerRunning = true;
|
||||
Log.Information("🌐 Starting embedded Kestrel webserver on {Url}...", _settings.WebserverUrl);
|
||||
await _host.StartWebServerAsync();
|
||||
btn_localWebserver.Text = "⏹ Stop Webserver";
|
||||
Log.Information("✅ WebUI available at http://localhost:{Port}", _settings.WebserverPort);
|
||||
Log.Information("📄 Swagger API docs at http://localhost:{Port}/swagger", _settings.WebserverPort);
|
||||
Log.Information("✅ WebUI available at {Url}", _settings.WebserverUrl);
|
||||
Log.Information("📄 Swagger API docs at {Url}/swagger", _settings.WebserverUrl);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to start webserver");
|
||||
_webServerRunning = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Information("⏹ Stopping webserver...");
|
||||
await _webServer!.StopWebServerAsync();
|
||||
_webServerRunning = false;
|
||||
await _host.StopWebServerAsync();
|
||||
btn_localWebserver.Text = "▶ Start Webserver";
|
||||
Log.Information("Webserver stopped.");
|
||||
}
|
||||
@@ -170,8 +158,10 @@ public partial class MainForm : Form
|
||||
|
||||
private void UpdateStatusBar()
|
||||
{
|
||||
var workerStatus = _workerRunning ? "[RUNNING] Workers" : "[STOPPED] Workers";
|
||||
var serverStatus = _webServerRunning ? $"[RUNNING] Webserver :{_settings.WebserverPort}" : "[STOPPED] Webserver";
|
||||
var workerStatus = _host.WorkersRunning ? "[RUNNING] Workers" : "[STOPPED] Workers";
|
||||
var serverStatus = _host.WebServerRunning
|
||||
? $"[RUNNING] Webserver :{_settings.WebserverPort}"
|
||||
: "[STOPPED] Webserver";
|
||||
this.Text = $"Predictalytics Analytics — {workerStatus} | {serverStatus}";
|
||||
}
|
||||
|
||||
@@ -181,7 +171,7 @@ public partial class MainForm : Form
|
||||
_watchdog?.Dispose();
|
||||
_watchdog = null;
|
||||
_workerCts?.Cancel();
|
||||
_webServer?.StopWebServerAsync().GetAwaiter().GetResult();
|
||||
_host?.StopWebServerAsync().GetAwaiter().GetResult();
|
||||
base.OnFormClosing(e);
|
||||
}
|
||||
|
||||
@@ -189,11 +179,11 @@ public partial class MainForm : Form
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start("explorer.exe", $"\"http://localhost:{_settings.WebserverPort}\"");
|
||||
OpenInShell(_settings.WebserverUrl);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Fehler beim Öffnen des Browsers");
|
||||
Log.Error(ex, "Fehler beim Öffnen des Browsers");
|
||||
MessageBox.Show("Browser konnte nicht gestartet werden.", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
@@ -202,21 +192,26 @@ public partial class MainForm : Form
|
||||
{
|
||||
try
|
||||
{
|
||||
var logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs");
|
||||
if (Directory.Exists(logPath))
|
||||
System.Diagnostics.Process.Start("explorer.exe", logPath);
|
||||
else
|
||||
System.Diagnostics.Process.Start("explorer.exe", Environment.CurrentDirectory);
|
||||
var logPath = LoggingSetup.DefaultLogDirectory;
|
||||
OpenInShell(Directory.Exists(logPath) ? logPath : Environment.CurrentDirectory);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Fehler beim Öffnen des Log-Ordners");
|
||||
Log.Error(ex, "Fehler beim Öffnen des Log-Ordners");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Oeffnet Pfad oder URL mit der Standardanwendung. UseShellExecute funktioniert
|
||||
/// unter Windows wie unter Linux (dort ueber xdg-open) — im Gegensatz zum
|
||||
/// vorherigen direkten Aufruf von explorer.exe.
|
||||
/// </summary>
|
||||
private static void OpenInShell(string target) =>
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(target) { UseShellExecute = true });
|
||||
|
||||
private async void syncMarketsaToolStripMenuItem_Click(object? sender, EventArgs e)
|
||||
{
|
||||
if (_workerRunning)
|
||||
if (_host.WorkersRunning)
|
||||
{
|
||||
MessageBox.Show("Market sync cannot be started while background workers are running.",
|
||||
"Workers Busy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
@@ -227,11 +222,11 @@ public partial class MainForm : Form
|
||||
{
|
||||
btn_syncmarkets.Enabled = false;
|
||||
Log.Information("Manual market sync triggered...");
|
||||
|
||||
|
||||
// Use a temporary CTS for this operation
|
||||
using var cts = new CancellationTokenSource();
|
||||
await _webServer!.RunSingleMarketSyncAsync(cts.Token);
|
||||
|
||||
await _host.RunSingleMarketSyncAsync(cts.Token);
|
||||
|
||||
Log.Information("Manual market sync completed successfully.");
|
||||
MessageBox.Show("Market sync completed.", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
@@ -248,7 +243,7 @@ public partial class MainForm : Form
|
||||
|
||||
private async void btn_dbUpdate_Click(object? sender, EventArgs e)
|
||||
{
|
||||
if (_workerRunning)
|
||||
if (_host.WorkersRunning)
|
||||
{
|
||||
MessageBox.Show("Database update cannot be run while background workers are running.",
|
||||
"Workers Busy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
@@ -259,7 +254,7 @@ public partial class MainForm : Form
|
||||
{
|
||||
btn_dbUpdate.Enabled = false;
|
||||
Log.Information("Manual database update triggered...");
|
||||
await _webServer!.UpdateDatabaseAsync();
|
||||
await _host.UpdateDatabaseAsync();
|
||||
Log.Information("Database updated successfully.");
|
||||
MessageBox.Show("Database update completed successfully.", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
@@ -276,7 +271,7 @@ public partial class MainForm : Form
|
||||
|
||||
private async void btn_recalcAll_Click(object? sender, EventArgs e)
|
||||
{
|
||||
if (_workerRunning)
|
||||
if (_host.WorkersRunning)
|
||||
{
|
||||
MessageBox.Show("Recalculation cannot be started while background workers are running. Stop the server first.",
|
||||
"Workers Busy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
@@ -298,7 +293,7 @@ public partial class MainForm : Form
|
||||
Log.Information("Manual full recalculation reset triggered...");
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
var summary = await _webServer!.RunRecalculateAllTradersAsync(cts.Token);
|
||||
var summary = await _host.RunRecalculateAllTradersAsync(cts.Token);
|
||||
|
||||
MessageBox.Show($"Reset complete:\n\n{summary}\n\nNow start the server to rebuild the analytics.",
|
||||
"Recalculate All Traders", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
@@ -316,29 +311,10 @@ public partial class MainForm : Form
|
||||
|
||||
private async Task UpdateDbSizeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Build the connection string
|
||||
var csBuilder = new MySqlConnector.MySqlConnectionStringBuilder(_settings.ConnectionString);
|
||||
if (string.IsNullOrWhiteSpace(csBuilder.Database))
|
||||
return; // Not ready or valid yet
|
||||
|
||||
using var conn = new MySqlConnector.MySqlConnection(_settings.ConnectionString);
|
||||
await conn.OpenAsync();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "SELECT SUM(data_length + index_length) / 1024 / 1024 FROM information_schema.tables WHERE table_schema = DATABASE();";
|
||||
|
||||
var result = await cmd.ExecuteScalarAsync();
|
||||
if (result != DBNull.Value && result != null)
|
||||
{
|
||||
var sizeMb = Convert.ToDouble(result);
|
||||
this.Invoke(() => label_dbSize.Text = $"DB Size: {sizeMb:F2} MB");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Invoke(() => label_dbSize.Text = "DB Size: Error");
|
||||
Log.Debug(ex, "Failed to fetch DB size for status bar");
|
||||
}
|
||||
var sizeMb = await _host.GetDatabaseSizeMbAsync();
|
||||
if (IsDisposed) return;
|
||||
this.Invoke(() => label_dbSize.Text = sizeMb.HasValue
|
||||
? $"DB Size: {sizeMb.Value:F2} MB"
|
||||
: "DB Size: —");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user