Files
EngineeringSync 04ae8a0aae Initial commit: EngineeringSync v1.0.0
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>
2026-03-26 21:52:26 +01:00

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();