using System; using System.Linq; using System.Net.Http; using System.Threading.Channels; using System.Windows.Forms; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using PolyTrader.Core.Configuration; using PolyTrader.Core.DependencyInjection; using PolyTrader.Core.Modularity; using PolyTrader.Modules.CopyTrading; using PolyTrader.Modules.CopyTrading.Persistence; using PolyTraderSharp.Models; using PolyTraderSharp.Services; namespace PolyTraderSharp; internal static class Program { public static IHost? AppHost { get; private set; } [STAThread] private static void Main(string[] args) { // Config-Migration aus mongoexport-JSON (Ordner, Default "MongoDB") -> MySQL. if (args.Length > 0 && string.Equals(args[0], "--migrate-json", StringComparison.OrdinalIgnoreCase)) { var folder = args.Length > 1 ? args[1] : "MongoDB"; RunConfigMigrationFromJson(folder); return; } // Readback-Verifikation der migrierten Config aus MySQL. Kein UI-Start. if (args.Length > 0 && string.Equals(args[0], "--verify-mysql", StringComparison.OrdinalIgnoreCase)) { RunVerifyMySql(); return; } // Headless-Smoke-Test der UI: konstruiert jede View + den Launcher (ohne Message-Loop // und ohne Trading-/WSS-Services). Fängt Laufzeit-Konstruktionsfehler ab. Kein Fenster. if (args.Length > 0 && string.Equals(args[0], "--smoke-ui", StringComparison.OrdinalIgnoreCase)) { Environment.ExitCode = RunSmokeUi(); return; } // Diagnose: gibt die MySQL-Serverversion aus (für das ServerVersion-Pinning). Kein UI. if (args.Length > 0 && string.Equals(args[0], "--db-version", StringComparison.OrdinalIgnoreCase)) { RunDbVersion(); return; } ApplicationConfiguration.Initialize(); var modules = new System.Collections.Generic.List { new CopyTradingModule() }; AppHost = Host.CreateDefaultBuilder() // Config immer neben der EXE suchen (nicht im Arbeitsverzeichnis), damit die App // auch beim Start aus bin/ oder per Doppelklick ihre appsettings findet. .UseContentRoot(AppContext.BaseDirectory) // Host.CreateDefaultBuilder lädt appsettings.Local.json NICHT (nur appsettings.json // + appsettings.{Environment}.json). Die gitignorierte Local-Datei hält aber die // MySQL-Connection – daher hier explizit ergänzen, sonst bleibt sie leer. .ConfigureAppConfiguration((context, config) => config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false)) .ConfigureServices(delegate(HostBuilderContext context, IServiceCollection services) { var databaseOptions = new DatabaseOptions { MySqlConnectionString = context.Configuration["Database:MySqlConnectionString"] ?? string.Empty }; services.Configure(context.Configuration.GetSection(DatabaseOptions.SectionName)); services.AddCorePersistence(databaseOptions); services.AddSingleton((IServiceProvider sp) => ServerSettings.Load("server_settings.xml")); services.AddSingleton(); services.AddSingleton(delegate(IServiceProvider sp) { TerminalLogger requiredService2 = sp.GetRequiredService(); var httpHandler = new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(2), MaxConnectionsPerServer = 100 }; return new PolymarketApiService(requiredService2, new HttpClient(httpHandler) { DefaultRequestHeaders = { { "User-Agent", "py_clob_client" }, { "Accept", "*/*" } } }); }); services.AddSingleton(delegate(IServiceProvider sp) { TerminalLogger requiredService2 = sp.GetRequiredService(); var httpHandler = new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(2), MaxConnectionsPerServer = 100 }; return new PolymarketClobClient(requiredService2, new HttpClient(httpHandler) { DefaultRequestHeaders = { { "User-Agent", "py_clob_client" }, { "Accept", "*/*" } } }); }); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // MUSS als erster HostedService laufen: hydriert den State, bevor die // Trading-Services gegen einen leeren State anlaufen. services.AddHostedService(); // Module registrieren ihre eigenen Services/Channels/State selbst. foreach (var module in modules) { services.AddSingleton(module); module.RegisterServices(services, context.Configuration); } services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); services.AddHostedService((IServiceProvider sp) => sp.GetRequiredService()); services.AddHostedService((IServiceProvider sp) => sp.GetRequiredService()); services.AddSingleton(); services.AddTransient(); }).Build(); try { // Trade-Nummerierung fortsetzen: höchste bestehende TradeId aus dem Log lesen. var copyState = AppHost.Services.GetRequiredService(); var tradeLog = AppHost.Services.GetRequiredService(); var allTrades = tradeLog.Find(_ => true); if (allTrades.Count > 0) copyState.TotalCopyTrades = allTrades.Max(t => t.TradeId); } catch { } AppHost.Start(); // Shell-Views registrieren (Core-App-Views; Module folgen via module.RegisterUi). var uiHost = AppHost.Services.GetRequiredService(); var viewServices = AppHost.Services; uiHost.RegisterView(new PolyTrader.Core.Modularity.ModuleView { Id = "core.terminal", Title = "Terminal", Group = "Core", Order = 10, CreateForm = () => { var view = new PolyTraderSharp.Ui.Views.TerminalView(); view.Initialize(viewServices.GetRequiredService()); return view; } }); uiHost.RegisterView(new PolyTrader.Core.Modularity.ModuleView { Id = "core.settings", Title = "Server Settings", Group = "Core", Order = 20, CreateForm = () => { var view = new PolyTraderSharp.Ui.Views.SettingsView(); view.Initialize( viewServices.GetRequiredService(), viewServices.GetRequiredService(), viewServices.GetRequiredService(), viewServices.GetRequiredService(), viewServices.GetRequiredService()); return view; } }); uiHost.RegisterView(new PolyTrader.Core.Modularity.ModuleView { Id = "core.jobs", Title = "Server Jobs", Group = "Core", Order = 30, CreateForm = () => { var view = new PolyTraderSharp.Ui.Views.JobsView(); view.Initialize(viewServices.GetRequiredService()); return view; } }); uiHost.RegisterView(new PolyTrader.Core.Modularity.ModuleView { Id = "core.dashboard", Title = "Dashboard", Group = "Core", Order = 5, CreateForm = () => { var view = new PolyTraderSharp.Ui.Views.DashboardView(); view.Initialize( viewServices.GetRequiredService(), viewServices.GetRequiredService()); return view; } }); // Modul-UI registrieren (Module steuern ihre Views selbst bei). foreach (var module in modules) { module.RegisterUi(uiHost, viewServices); } var launcher = AppHost.Services.GetRequiredService(); Application.Run(launcher); AppHost.StopAsync().GetAwaiter().GetResult(); } private static void RunConfigMigrationFromJson(string folder) { var config = new Microsoft.Extensions.Configuration.ConfigurationBuilder() .SetBasePath(System.IO.Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: true) .AddJsonFile("appsettings.Local.json", optional: true) .AddEnvironmentVariables() .Build(); var mySql = config["Database:MySqlConnectionString"] ?? string.Empty; if (string.IsNullOrWhiteSpace(mySql)) { Console.WriteLine("FEHLER: Database:MySqlConnectionString fehlt (appsettings.Local.json)."); return; } if (!System.IO.Path.IsPathRooted(folder)) folder = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), folder); Services.ConfigMigrator.RunFromJson(folder, mySql); } private static void RunVerifyMySql() { var config = new Microsoft.Extensions.Configuration.ConfigurationBuilder() .SetBasePath(System.IO.Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: true) .AddJsonFile("appsettings.Local.json", optional: true) .AddEnvironmentVariables() .Build(); var mySql = config["Database:MySqlConnectionString"] ?? string.Empty; if (string.IsNullOrWhiteSpace(mySql)) { Console.WriteLine("FEHLER: Database:MySqlConnectionString fehlt (appsettings.Local.json)."); return; } Services.ConfigMigrator.VerifyMySql(mySql); } private static void RunDbVersion() { var config = new Microsoft.Extensions.Configuration.ConfigurationBuilder() .SetBasePath(System.IO.Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: true) .AddJsonFile("appsettings.Local.json", optional: true) .AddEnvironmentVariables() .Build(); var conn = config["Database:MySqlConnectionString"] ?? string.Empty; if (string.IsNullOrWhiteSpace(conn)) { Console.WriteLine("FEHLER: Database:MySqlConnectionString fehlt (appsettings.Local.json)."); return; } try { using var c = new MySqlConnector.MySqlConnection(conn); c.Open(); Console.WriteLine($"ServerVersion: {c.ServerVersion}"); } catch (Exception ex) { Console.WriteLine($"FEHLER: {ex.GetType().Name}: {ex.Message}"); } } /// /// Headless-Smoke-Test: baut einen minimalen Host (Persistenz + State + Modul-Registrierung /// + Shell/Launcher), hydriert den State aus MySQL und konstruiert jede registrierte View /// sowie den Launcher – ohne Message-Loop und ohne Trading-/WSS-Services zu starten. /// Gibt 0 zurück, wenn alles fehlerfrei konstruiert, sonst die Fehleranzahl. /// private static int RunSmokeUi() { ApplicationConfiguration.Initialize(); var modules = new System.Collections.Generic.List { new CopyTradingModule() }; using var host = Host.CreateDefaultBuilder() .UseContentRoot(AppContext.BaseDirectory) .ConfigureAppConfiguration((context, config) => config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false)) .ConfigureServices((context, services) => { var databaseOptions = new DatabaseOptions { MySqlConnectionString = context.Configuration["Database:MySqlConnectionString"] ?? string.Empty }; services.Configure(context.Configuration.GetSection(DatabaseOptions.SectionName)); services.AddCorePersistence(databaseOptions); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); foreach (var module in modules) { services.AddSingleton(module); module.RegisterServices(services, context.Configuration); } services.AddSingleton(); services.AddTransient(); }).Build(); // State aus MySQL laden (Accounts/Trader/Settings), ohne die BackgroundServices zu starten. try { host.Services.GetRequiredService() .StartAsync(default).GetAwaiter().GetResult(); } catch (Exception ex) { Console.WriteLine($"[WARN] Hydration übersprungen: {ex.Message}"); } var uiHost = host.Services.GetRequiredService(); foreach (var module in modules) module.RegisterUi(uiHost, host.Services); int failures = 0; Console.WriteLine("=== Smoke-UI: View-Konstruktion ==="); foreach (var view in uiHost.Views) { try { using var form = view.CreateForm(); Console.WriteLine($"[OK] {view.Id} ({view.Title})"); } catch (Exception ex) { failures++; Console.WriteLine($"[FEHLER] {view.Id}: {ex.GetType().Name}: {ex.Message}"); } } try { using var launcher = host.Services.GetRequiredService(); Console.WriteLine("[OK] LauncherForm konstruiert"); } catch (Exception ex) { failures++; Console.WriteLine($"[FEHLER] LauncherForm: {ex.GetType().Name}: {ex.Message}"); } Console.WriteLine(failures == 0 ? "=== Smoke-UI OK ===" : $"=== Smoke-UI: {failures} Fehler ==="); return failures; } }