Fix: Startet nicht / MySQL-Timeout ohne UI (Config- & Startup-Härtung)
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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6f478e8764
commit
f1451b547c
@@ -32,6 +32,11 @@
|
|||||||
<None Update="appsettings.json">
|
<None Update="appsettings.json">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<!-- Gitignorierte Local-Datei (MySQL-Connection) neben die EXE kopieren, damit die App
|
||||||
|
die Verbindung auch findet, wenn sie NICHT aus dem Projekt-Root gestartet wird. -->
|
||||||
|
<None Update="appsettings.Local.json" Condition="Exists('appsettings.Local.json')">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
+39
@@ -48,6 +48,13 @@ internal static class Program
|
|||||||
return;
|
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();
|
ApplicationConfiguration.Initialize();
|
||||||
|
|
||||||
var modules = new System.Collections.Generic.List<IPolyTraderModule>
|
var modules = new System.Collections.Generic.List<IPolyTraderModule>
|
||||||
@@ -56,6 +63,9 @@ internal static class Program
|
|||||||
};
|
};
|
||||||
|
|
||||||
AppHost = Host.CreateDefaultBuilder()
|
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
|
// Host.CreateDefaultBuilder lädt appsettings.Local.json NICHT (nur appsettings.json
|
||||||
// + appsettings.{Environment}.json). Die gitignorierte Local-Datei hält aber die
|
// + appsettings.{Environment}.json). Die gitignorierte Local-Datei hält aber die
|
||||||
// MySQL-Connection – daher hier explizit ergänzen, sonst bleibt sie leer.
|
// MySQL-Connection – daher hier explizit ergänzen, sonst bleibt sie leer.
|
||||||
@@ -252,6 +262,34 @@ internal static class Program
|
|||||||
Services.ConfigMigrator.VerifyMySql(mySql);
|
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>
|
/// <summary>
|
||||||
/// Headless-Smoke-Test: baut einen minimalen Host (Persistenz + State + Modul-Registrierung
|
/// Headless-Smoke-Test: baut einen minimalen Host (Persistenz + State + Modul-Registrierung
|
||||||
/// + Shell/Launcher), hydriert den State aus MySQL und konstruiert jede registrierte View
|
/// + Shell/Launcher), hydriert den State aus MySQL und konstruiert jede registrierte View
|
||||||
@@ -265,6 +303,7 @@ internal static class Program
|
|||||||
var modules = new System.Collections.Generic.List<IPolyTraderModule> { new CopyTradingModule() };
|
var modules = new System.Collections.Generic.List<IPolyTraderModule> { new CopyTradingModule() };
|
||||||
|
|
||||||
using var host = Host.CreateDefaultBuilder()
|
using var host = Host.CreateDefaultBuilder()
|
||||||
|
.UseContentRoot(AppContext.BaseDirectory)
|
||||||
.ConfigureAppConfiguration((context, config) =>
|
.ConfigureAppConfiguration((context, config) =>
|
||||||
config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false))
|
config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false))
|
||||||
.ConfigureServices((context, services) =>
|
.ConfigureServices((context, services) =>
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace PolyTrader.Core.Configuration
|
namespace PolyTrader.Core.Configuration
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -11,4 +14,15 @@ namespace PolyTrader.Core.Configuration
|
|||||||
/// <summary>MySQL-Connection-String (aus gitignorierter appsettings.Local.json).</summary>
|
/// <summary>MySQL-Connection-String (aus gitignorierter appsettings.Local.json).</summary>
|
||||||
public string MySqlConnectionString { get; set; } = string.Empty;
|
public string MySqlConnectionString { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Feste Ziel-Server-Version (MariaDB 11.8.6, wie am Server erkannt). Bewusst gepinnt statt
|
||||||
|
/// <c>ServerVersion.AutoDetect</c>: AutoDetect öffnet beim Bau der DbContext-Optionen eine
|
||||||
|
/// blockierende DB-Verbindung – ist die DB langsam/nicht erreichbar, hängt/crasht der Start,
|
||||||
|
/// bevor die UI erscheint. Mit fester Version startet die App unabhängig von der DB.
|
||||||
|
/// </summary>
|
||||||
|
public static class DatabaseServerVersion
|
||||||
|
{
|
||||||
|
public static ServerVersion Value => new MariaDbServerVersion(new Version(11, 8, 6));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace PolyTrader.Core.DependencyInjection
|
|||||||
public static IServiceCollection AddCorePersistence(this IServiceCollection services, DatabaseOptions options)
|
public static IServiceCollection AddCorePersistence(this IServiceCollection services, DatabaseOptions options)
|
||||||
{
|
{
|
||||||
var conn = options.MySqlConnectionString;
|
var conn = options.MySqlConnectionString;
|
||||||
services.AddDbContextFactory<CoreDbContext>(o => o.UseMySql(conn, ServerVersion.AutoDetect(conn)));
|
services.AddDbContextFactory<CoreDbContext>(o => o.UseMySql(conn, DatabaseServerVersion.Value));
|
||||||
|
|
||||||
services.AddSingleton<IAccountRepository, EfAccountRepository>();
|
services.AddSingleton<IAccountRepository, EfAccountRepository>();
|
||||||
services.AddSingleton<IMarketRepository, EfMarketRepository>();
|
services.AddSingleton<IMarketRepository, EfMarketRepository>();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System.Threading.Channels;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using PolyTrader.Core.Configuration;
|
||||||
using PolyTrader.Core.Modularity;
|
using PolyTrader.Core.Modularity;
|
||||||
using PolyTrader.Modules.CopyTrading.Persistence;
|
using PolyTrader.Modules.CopyTrading.Persistence;
|
||||||
using PolyTrader.Modules.CopyTrading.Persistence.Ef;
|
using PolyTrader.Modules.CopyTrading.Persistence.Ef;
|
||||||
@@ -39,7 +40,7 @@ namespace PolyTrader.Modules.CopyTrading
|
|||||||
|
|
||||||
// Modul-Persistenz: EF Core / Pomelo / MySQL (thread-safer DbContextFactory).
|
// Modul-Persistenz: EF Core / Pomelo / MySQL (thread-safer DbContextFactory).
|
||||||
var conn = configuration["Database:MySqlConnectionString"] ?? string.Empty;
|
var conn = configuration["Database:MySqlConnectionString"] ?? string.Empty;
|
||||||
services.AddDbContextFactory<CopyTradingDbContext>(o => o.UseMySql(conn, ServerVersion.AutoDetect(conn)));
|
services.AddDbContextFactory<CopyTradingDbContext>(o => o.UseMySql(conn, DatabaseServerVersion.Value));
|
||||||
|
|
||||||
services.AddSingleton<ICopyTradeLogRepository, EfCopyTradeLogRepository>();
|
services.AddSingleton<ICopyTradeLogRepository, EfCopyTradeLogRepository>();
|
||||||
services.AddSingleton<ICopyTradingAccountSettingsRepository, EfCopyTradingAccountSettingsRepository>();
|
services.AddSingleton<ICopyTradingAccountSettingsRepository, EfCopyTradingAccountSettingsRepository>();
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ namespace PolyTraderSharp.Services
|
|||||||
public override async Task StartAsync(CancellationToken cancellationToken)
|
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_logger.Info("Starte Preload des MarketCache aus der Datenbank um Flaschenhälse zu vermeiden...");
|
_logger.Info("Starte Preload des MarketCache aus der Datenbank um Flaschenhälse zu vermeiden...");
|
||||||
|
try
|
||||||
{
|
{
|
||||||
// Initialize cache for EVERYTHING in DB that is not closed!
|
// Initialize cache for EVERYTHING in DB that is not closed!
|
||||||
var activeMarkets = _marketRepo.GetActive();
|
var activeMarkets = _marketRepo.GetActive();
|
||||||
@@ -62,7 +63,7 @@ namespace PolyTraderSharp.Services
|
|||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(md.ClobTokenIds))
|
if (!string.IsNullOrEmpty(md.ClobTokenIds))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var tokenIds = System.Text.Json.JsonSerializer.Deserialize<List<string>>(md.ClobTokenIds);
|
var tokenIds = System.Text.Json.JsonSerializer.Deserialize<List<string>>(md.ClobTokenIds);
|
||||||
if (tokenIds != null)
|
if (tokenIds != null)
|
||||||
@@ -79,6 +80,12 @@ namespace PolyTraderSharp.Services
|
|||||||
}
|
}
|
||||||
_logger.Info($"MarketCache Preload abgeschlossen: {loaded} Token herangeführt.");
|
_logger.Info($"MarketCache Preload abgeschlossen: {loaded} Token herangeführt.");
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// DB nicht erreichbar o.ä.: Start NICHT abbrechen (sonst erscheint keine UI).
|
||||||
|
// Der Cache füllt sich zur Laufzeit über den MarketSync/Signale nach.
|
||||||
|
_logger.Error($"MarketCache Preload übersprungen (DB nicht erreichbar?): {ex.Message}");
|
||||||
|
}
|
||||||
await base.StartAsync(cancellationToken);
|
await base.StartAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
</root>
|
||||||
Reference in New Issue
Block a user