Ursachen (App startete aus bin/ bzw. per Doppelklick ohne UI): 1. appsettings.Local.json (MySQL-Connection) wurde NICHT ins Output kopiert und nur relativ zum Arbeitsverzeichnis geladen -> leere Connection -> Pomelo fiel auf localhost:3306 zurueck -> "Connect Timeout expired". 2. ServerVersion.AutoDetect(conn) oeffnet beim Options-Bau eine blockierende DB-Verbindung -> haengt/crasht den Start, wenn die DB nicht erreichbar ist. 3. CopyTradingEngine.StartAsync lud den MarketCache ungeschuetzt -> DB-Fehler riss AppHost.Start() ab, bevor die UI erschien. Fixes: - csproj: appsettings.Local.json mit ins Output kopieren (CopyToOutputDirectory). - Program: UseContentRoot(AppContext.BaseDirectory) -> Config wird immer neben der EXE gesucht (Main + Smoke-Test). - ServerVersion fest gepinnt: DatabaseServerVersion.Value = MariaDB 11.8.6 (wie am Server erkannt); AddCorePersistence + CopyTradingModule nutzen sie statt AutoDetect. Kein blockierender Connect mehr beim Start. - CopyTradingEngine-Preload in try/catch: DB-Fehler bricht den Start nicht mehr ab. - Neuer Diagnose-CLI --db-version (gibt @@version aus). Verifiziert: --smoke-ui aus fremdem Arbeitsverzeichnis laeuft gruen (3 Accounts / 32 Trader hydriert, alle Views + Launcher OK). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
377 lines
13 KiB
C#
377 lines
13 KiB
C#
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.Streaming;
|
||
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<IPolyTraderModule>
|
||
{
|
||
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<DatabaseOptions>(context.Configuration.GetSection(DatabaseOptions.SectionName));
|
||
services.AddCorePersistence(databaseOptions);
|
||
services.AddSingleton<IBlockchainWssClientFactory, AlchemyWssClientFactory>();
|
||
services.AddSingleton((IServiceProvider sp) => ServerSettings.Load("server_settings.xml"));
|
||
services.AddSingleton<TradingState>();
|
||
services.AddSingleton(delegate(IServiceProvider sp)
|
||
{
|
||
TerminalLogger requiredService2 = sp.GetRequiredService<TerminalLogger>();
|
||
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<TerminalLogger>();
|
||
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<TerminalLogger>();
|
||
services.AddSingleton<MullvadVpnService>();
|
||
services.AddSingleton<ThreemaService>();
|
||
services.AddSingleton<JobManager>();
|
||
|
||
// MUSS als erster HostedService laufen: hydriert den State, bevor die
|
||
// Trading-Services gegen einen leeren State anlaufen.
|
||
services.AddHostedService<StartupHydrationService>();
|
||
|
||
// Module registrieren ihre eigenen Services/Channels/State selbst.
|
||
foreach (var module in modules)
|
||
{
|
||
services.AddSingleton(module);
|
||
module.RegisterServices(services, context.Configuration);
|
||
}
|
||
|
||
services.AddHostedService<AlchemyWebsocketService>();
|
||
services.AddHostedService<PersistenceService>();
|
||
services.AddHostedService<MarketSyncService>();
|
||
services.AddHostedService<MasterTraderAnalyticsJob>();
|
||
services.AddHostedService<PolymarketWssClient>();
|
||
services.AddHostedService((IServiceProvider sp) => sp.GetRequiredService<MullvadVpnService>());
|
||
services.AddHostedService((IServiceProvider sp) => sp.GetRequiredService<ThreemaService>());
|
||
services.AddSingleton<PolyTraderSharp.Ui.ShellUiHost>();
|
||
services.AddTransient<PolyTraderSharp.Ui.LauncherForm>();
|
||
}).Build();
|
||
|
||
try
|
||
{
|
||
// Trade-Nummerierung fortsetzen: höchste bestehende TradeId aus dem Log lesen.
|
||
var copyState = AppHost.Services.GetRequiredService<CopyTradingState>();
|
||
var tradeLog = AppHost.Services.GetRequiredService<ICopyTradeLogRepository>();
|
||
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<PolyTraderSharp.Ui.ShellUiHost>();
|
||
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<TerminalLogger>());
|
||
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<ThreemaService>(),
|
||
viewServices.GetRequiredService<MullvadVpnService>(),
|
||
viewServices.GetRequiredService<TerminalLogger>(),
|
||
viewServices.GetRequiredService<PolyTrader.Core.Persistence.IAccountRepository>(),
|
||
viewServices.GetRequiredService<TradingState>());
|
||
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<JobManager>());
|
||
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<PolyTrader.Core.Persistence.ITradeLogRepository>(),
|
||
viewServices.GetRequiredService<TradingState>());
|
||
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<PolyTraderSharp.Ui.LauncherForm>();
|
||
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}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
private static int RunSmokeUi()
|
||
{
|
||
ApplicationConfiguration.Initialize();
|
||
|
||
var modules = new System.Collections.Generic.List<IPolyTraderModule> { 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<DatabaseOptions>(context.Configuration.GetSection(DatabaseOptions.SectionName));
|
||
services.AddCorePersistence(databaseOptions);
|
||
services.AddSingleton<TerminalLogger>();
|
||
services.AddSingleton<TradingState>();
|
||
services.AddSingleton<StartupHydrationService>();
|
||
|
||
foreach (var module in modules)
|
||
{
|
||
services.AddSingleton(module);
|
||
module.RegisterServices(services, context.Configuration);
|
||
}
|
||
|
||
services.AddSingleton<PolyTraderSharp.Ui.ShellUiHost>();
|
||
services.AddTransient<PolyTraderSharp.Ui.LauncherForm>();
|
||
}).Build();
|
||
|
||
// State aus MySQL laden (Accounts/Trader/Settings), ohne die BackgroundServices zu starten.
|
||
try
|
||
{
|
||
host.Services.GetRequiredService<StartupHydrationService>()
|
||
.StartAsync(default).GetAwaiter().GetResult();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"[WARN] Hydration übersprungen: {ex.Message}");
|
||
}
|
||
|
||
var uiHost = host.Services.GetRequiredService<PolyTraderSharp.Ui.ShellUiHost>();
|
||
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<PolyTraderSharp.Ui.LauncherForm>();
|
||
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;
|
||
}
|
||
}
|