Vollständige Implementierung des EngineeringSync-Middleware-Tools: - Windows Service (Kestrel :5050) mit FileSystemWatcher + SignalR - WPF Tray-App mit PendingChanges- und Projektverwaltungs-Fenster - Setup-Wizard (8-Schritte-Installer) - SQLite/EF Core Datenschicht (WAL-Modus) - SHA-256-basiertes Debouncing (2s Fenster) - Backup-System mit konfigurierbarer Aufbewahrung Bugfixes & Verbesserungen: - BUG-1: AppDbContext OnConfiguring invertierte Bedingung behoben - BUG-2: Event-Handler-Leak in TrayApp (Fenster-Singleton-Pattern) - BUG-3: ProjectConfigChanged SignalR-Signal in allen CRUD-Endpoints - BUG-5: Rename-Sync löscht alte Datei im Simulations-Ordner - BUG-6: Doppeltes Dispose von SignalR verhindert - BUG-7: Registry-Deinstallation nur EngineeringSync-Eintrag entfernt - S1: Path-Traversal-Schutz via SafeCombine() im SyncManager - E1: FSW Buffer 64KB + automatischer Re-Scan bei Overflow - E2: Retry-Logik (3x) für gesperrte Dateien mit exponentiellem Backoff - E4: Channel.Writer.TryComplete() beim Shutdown - C2: HubMethodNames-Konstanten statt Magic Strings - E3: Pagination in Changes-API (page/pageSize Query-Parameter) - A1: Fire-and-Forget mit try/catch + Logging Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
152 lines
5.9 KiB
C#
152 lines
5.9 KiB
C#
using System.Text.Json;
|
|
using EngineeringSync.Infrastructure;
|
|
using EngineeringSync.Service.Api;
|
|
using EngineeringSync.Service.Hubs;
|
|
using EngineeringSync.Service.Services;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
builder.Host.UseWindowsService();
|
|
|
|
// Datenbankpfad: neben der .exe oder im AppData-Ordner (dev: aktuelles Verzeichnis)
|
|
var dbPath = Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
|
"EngineeringSync", "engineeringsync.db");
|
|
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
|
|
|
|
builder.Services.AddInfrastructure(dbPath);
|
|
builder.Services.AddSignalR();
|
|
builder.Services.AddSingleton<WatcherService>();
|
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<WatcherService>());
|
|
builder.Services.AddSingleton<SyncManager>();
|
|
|
|
// Kestrel nur auf localhost binden (kein öffentlicher Port)
|
|
builder.WebHost.UseUrls("http://localhost:5050");
|
|
|
|
var app = builder.Build();
|
|
|
|
// Datenbankmigrationen beim Start
|
|
await using (var scope = app.Services.CreateAsyncScope())
|
|
{
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
await db.Database.MigrateAsync();
|
|
|
|
// WAL-Modus aktivieren (muss nach dem Öffnen der DB gesetzt werden)
|
|
await db.Database.ExecuteSqlRawAsync("PRAGMA journal_mode=WAL;");
|
|
|
|
// First-Run-Config laden (falls vorhanden)
|
|
var watcher = scope.ServiceProvider.GetRequiredService<WatcherService>();
|
|
await ProcessFirstRunConfigAsync(db, watcher);
|
|
}
|
|
|
|
async Task ProcessFirstRunConfigAsync(AppDbContext db, WatcherService watcher)
|
|
{
|
|
var configPath = Path.Combine(AppContext.BaseDirectory, "firstrun-config.json");
|
|
if (!File.Exists(configPath))
|
|
return;
|
|
|
|
try
|
|
{
|
|
var json = await File.ReadAllTextAsync(configPath);
|
|
var root = JsonDocument.Parse(json);
|
|
var firstRunElement = root.RootElement.GetProperty("FirstRun");
|
|
|
|
var projectName = firstRunElement.GetProperty("ProjectName").GetString() ?? "Imported Project";
|
|
var engineeringPath = firstRunElement.GetProperty("EngineeringPath").GetString() ?? "";
|
|
var simulationPath = firstRunElement.GetProperty("SimulationPath").GetString() ?? "";
|
|
var fileExtensionsRaw = firstRunElement.GetProperty("FileExtensions").GetString() ?? "";
|
|
var watchAllFiles = firstRunElement.TryGetProperty("WatchAllFiles", out var watchAllElement)
|
|
? watchAllElement.GetBoolean()
|
|
: false;
|
|
|
|
// Wenn WatchAllFiles=true oder FileExtensions="*", dann leerer String (alle Dateien)
|
|
var fileExtensions = (watchAllFiles || fileExtensionsRaw == "*") ? "" : fileExtensionsRaw;
|
|
|
|
// Backup-Einstellungen (optional, mit Standardwerten für Rückwärtskompatibilität)
|
|
var backupEnabled = true;
|
|
string? backupPath = null;
|
|
var maxBackupsPerFile = 0;
|
|
|
|
if (root.RootElement.TryGetProperty("Backup", out var backupElement))
|
|
{
|
|
if (backupElement.TryGetProperty("BackupEnabled", out var be))
|
|
backupEnabled = be.GetBoolean();
|
|
if (backupElement.TryGetProperty("BackupPath", out var bp) &&
|
|
bp.ValueKind != JsonValueKind.Null)
|
|
backupPath = bp.GetString();
|
|
if (backupElement.TryGetProperty("MaxBackupsPerFile", out var mb))
|
|
maxBackupsPerFile = mb.GetInt32();
|
|
}
|
|
|
|
// Nur erstellen, wenn die Verzeichnisse existieren
|
|
if (!Directory.Exists(engineeringPath) || !Directory.Exists(simulationPath))
|
|
{
|
|
System.Console.WriteLine(
|
|
$"[FirstRunConfig] WARNUNG: Engineering- oder Simulations-Pfad existiert nicht. " +
|
|
$"Engineering={engineeringPath}, Simulation={simulationPath}");
|
|
return;
|
|
}
|
|
|
|
// Prüfen: Existiert bereits ein Projekt mit diesem Namen?
|
|
var existingProject = await db.Projects
|
|
.FirstOrDefaultAsync(p => p.Name == projectName);
|
|
|
|
if (existingProject != null)
|
|
{
|
|
System.Console.WriteLine(
|
|
$"[FirstRunConfig] Projekt '{projectName}' existiert bereits. Überspringe Import.");
|
|
return;
|
|
}
|
|
|
|
// Neue ProjectConfig erstellen
|
|
var project = new EngineeringSync.Domain.Entities.ProjectConfig
|
|
{
|
|
Name = projectName,
|
|
EngineeringPath = engineeringPath,
|
|
SimulationPath = simulationPath,
|
|
FileExtensions = fileExtensions,
|
|
IsActive = true,
|
|
CreatedAt = DateTime.UtcNow,
|
|
BackupEnabled = backupEnabled,
|
|
BackupPath = backupPath,
|
|
MaxBackupsPerFile = maxBackupsPerFile
|
|
};
|
|
|
|
db.Projects.Add(project);
|
|
await db.SaveChangesAsync();
|
|
|
|
System.Console.WriteLine(
|
|
$"[FirstRunConfig] Projekt '{projectName}' wurde importiert und aktiviert. " +
|
|
$"FileExtensions='{fileExtensions}'");
|
|
|
|
// Initialer Scan: Unterschiede zwischen Engineering- und Simulations-Ordner erkennen
|
|
try { await watcher.ScanExistingFilesAsync(project); }
|
|
catch (Exception ex)
|
|
{
|
|
System.Console.WriteLine(
|
|
$"[FirstRunConfig] FEHLER beim initialen Scan für '{projectName}': {ex.Message}");
|
|
}
|
|
|
|
// Konfigurationsdatei umbenennen, damit sie beim nächsten Start nicht wieder verarbeitet wird
|
|
// (WatcherService.ExecuteAsync lädt das Projekt automatisch aus der DB beim Host-Start)
|
|
var processedPath = configPath + ".processed";
|
|
if (File.Exists(processedPath))
|
|
File.Delete(processedPath);
|
|
File.Move(configPath, processedPath);
|
|
|
|
System.Console.WriteLine($"[FirstRunConfig] Konfigurationsdatei verarbeitet: {configPath} → {processedPath}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Console.WriteLine(
|
|
$"[FirstRunConfig] FEHLER beim Verarbeiten der Erstkonfiguration: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
app.MapHub<NotificationHub>("/notifications");
|
|
app.MapProjectsApi();
|
|
app.MapChangesApi();
|
|
|
|
app.Run();
|