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>
This commit is contained in:
EngineeringSync
2026-03-26 21:52:26 +01:00
commit 04ae8a0aae
98 changed files with 8172 additions and 0 deletions

View File

@@ -0,0 +1,304 @@
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text.Json;
using EngineeringSync.Setup.ViewModels;
using Microsoft.Win32;
namespace EngineeringSync.Setup.Services;
/// <summary>
/// Führt alle Installationsschritte aus. Gibt Fortschritt und Log-Meldungen
/// über Events zurück, damit die UI in Echtzeit aktualisiert werden kann.
/// </summary>
public class InstallerService(WizardState state)
{
public event Action<int, string>? Progress;
public event Action<string>? LogMessage;
private void Report(int percent, string step, string? log = null)
{
Progress?.Invoke(percent, step);
LogMessage?.Invoke(log ?? step);
}
/// <summary>Hauptinstallationsablauf läuft auf einem Background-Thread.</summary>
public async Task InstallAsync()
{
await Task.Run(async () =>
{
// 1. Installationsverzeichnis anlegen
Report(5, "Installationsverzeichnis anlegen...");
Directory.CreateDirectory(state.InstallPath);
await Delay();
// 2. Programmdateien kopieren
Report(15, "Programmdateien werden kopiert...");
await CopyApplicationFilesAsync();
// 3. Datenbank-Verzeichnis anlegen
Report(30, "Datenbankverzeichnis anlegen...");
var dbDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
"EngineeringSync");
Directory.CreateDirectory(dbDir);
Report(33, "Datenbankverzeichnis angelegt.", $"Datenbank-Pfad: {dbDir}");
await Delay();
// 4. Erstkonfigurations-Datei schreiben
Report(40, "Erstkonfiguration wird gespeichert...");
WriteFirstRunConfig(dbDir);
await Delay();
// 5. Bestehenden Dienst stoppen & deinstallieren (Upgrade-Szenario)
Report(50, "Vorherige Installation wird geprüft...");
await StopAndRemoveExistingServiceAsync();
await Delay();
// 6. Windows Service registrieren
Report(60, "Windows-Dienst wird registriert...");
var serviceExe = Path.Combine(state.InstallPath, "EngineeringSync.Service.exe");
await RegisterWindowsServiceAsync(serviceExe);
await Delay();
// 7. Autostart für TrayApp
if (state.AutoStartTrayApp)
{
Report(70, "TrayApp-Autostart wird konfiguriert...");
ConfigureTrayAppAutostart();
await Delay();
}
// 8. Verknüpfungen erstellen
Report(78, "Verknüpfungen werden erstellt...");
if (state.CreateDesktopShortcut) CreateShortcut(ShortcutTarget.Desktop);
if (state.CreateStartMenuEntry) CreateShortcut(ShortcutTarget.StartMenu);
await Delay();
// 9. Add/Remove Programs Eintrag
Report(85, "Deinstallations-Eintrag wird angelegt...");
RegisterUninstallEntry();
await Delay();
// 10. Dienst starten
if (state.AutoStartService)
{
Report(92, "Windows-Dienst wird gestartet...");
await StartServiceAsync();
}
Report(100, "Installation abgeschlossen.", "✓ EngineeringSync wurde erfolgreich installiert.");
});
}
// ── Hilfsmethoden ─────────────────────────────────────────────────
private async Task CopyApplicationFilesAsync()
{
var sourceDir = Path.GetFullPath(AppContext.BaseDirectory).TrimEnd(Path.DirectorySeparatorChar);
var targetDir = Path.GetFullPath(state.InstallPath).TrimEnd(Path.DirectorySeparatorChar);
// Wenn das Setup bereits aus dem Zielverzeichnis läuft (z.B. nach Inno-Setup-Installation),
// wurden die Dateien bereits kopiert nichts zu tun.
if (string.Equals(sourceDir, targetDir, StringComparison.OrdinalIgnoreCase))
{
LogMessage?.Invoke(" Dateien bereits installiert (Inno-Setup) - Kopieren wird übersprungen.");
return;
}
var patterns = new[] { "*.exe", "*.dll", "*.json", "*.pdb" };
var allFiles = patterns
.SelectMany(p => Directory.GetFiles(sourceDir, p))
.Distinct()
.ToList();
for (int i = 0; i < allFiles.Count; i++)
{
var src = allFiles[i];
var dest = Path.Combine(targetDir, Path.GetFileName(src));
File.Copy(src, dest, overwrite: true);
LogMessage?.Invoke($" Kopiert: {Path.GetFileName(src)}");
await Task.Delay(30); // Kurze Pause für UI-Responsivität
}
}
private void WriteFirstRunConfig(string dbDir)
{
var config = new
{
FirstRun = new
{
ProjectName = state.ProjectName,
EngineeringPath = state.EngineeringPath,
SimulationPath = state.SimulationPath,
FileExtensions = state.WatchAllFiles ? "*" : state.FileExtensions,
WatchAllFiles = state.WatchAllFiles
},
Backup = new // NEU
{
BackupEnabled = state.BackupEnabled,
BackupPath = state.BackupUseCustomPath ? state.BackupCustomPath : (string?)null,
MaxBackupsPerFile = state.MaxBackupsPerFile
}
};
var json = JsonSerializer.Serialize(config,
new JsonSerializerOptions { WriteIndented = true });
var configPath = Path.Combine(state.InstallPath, "firstrun-config.json");
File.WriteAllText(configPath, json);
LogMessage?.Invoke($" Konfiguration geschrieben: {configPath}");
}
private async Task StopAndRemoveExistingServiceAsync()
{
try
{
var stopResult = await RunProcessAsync("sc", "stop EngineeringSync");
if (stopResult.exitCode == 0)
{
LogMessage?.Invoke(" Bestehender Dienst gestoppt.");
await Task.Delay(1500); // Warten bis Dienst wirklich gestoppt
}
await RunProcessAsync("sc", "delete EngineeringSync");
LogMessage?.Invoke(" Bestehender Dienst entfernt.");
}
catch { /* Kein vorheriger Dienst OK */ }
}
private async Task RegisterWindowsServiceAsync(string exePath)
{
if (!File.Exists(exePath))
{
LogMessage?.Invoke($" WARNUNG: Service-EXE nicht gefunden: {exePath}");
LogMessage?.Invoke(" (Im Entwicklungsmodus Service-Registrierung übersprungen)");
return;
}
var startType = state.AutoStartService ? "auto" : "demand";
var args = $"create EngineeringSync binPath= \"{exePath}\" " +
$"start= {startType} " +
$"DisplayName= \"EngineeringSync Watcher Service\"";
var (exitCode, output, error) = await RunProcessAsync("sc", args);
if (exitCode == 0)
LogMessage?.Invoke(" ✓ Windows-Dienst registriert.");
else
{
LogMessage?.Invoke($" FEHLER bei sc create (Code {exitCode}): {error}");
throw new InvalidOperationException($"Service-Registrierung fehlgeschlagen: {error}");
}
}
private void ConfigureTrayAppAutostart()
{
var trayExe = Path.Combine(state.InstallPath, "EngineeringSync.TrayApp.exe");
if (!File.Exists(trayExe))
{
LogMessage?.Invoke(" TrayApp nicht gefunden Autostart übersprungen.");
return;
}
using var key = Registry.CurrentUser.OpenSubKey(
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", writable: true)!;
key.SetValue("EngineeringSync.TrayApp", $"\"{trayExe}\"");
LogMessage?.Invoke(" ✓ TrayApp-Autostart in Registry eingetragen.");
}
private enum ShortcutTarget { Desktop, StartMenu }
private void CreateShortcut(ShortcutTarget target)
{
var trayExe = Path.Combine(state.InstallPath, "EngineeringSync.TrayApp.exe");
var dir = target == ShortcutTarget.Desktop
? Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory)
: Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu),
"Programs", "EngineeringSync");
Directory.CreateDirectory(dir);
var linkPath = Path.Combine(dir, "EngineeringSync.lnk");
CreateWindowsShortcut(linkPath, trayExe, state.InstallPath, "EngineeringSync Tray App");
LogMessage?.Invoke($" ✓ Verknüpfung erstellt: {linkPath}");
}
private static void CreateWindowsShortcut(string linkPath, string targetPath,
string workingDir, string description)
{
// WScript.Shell COM-Interop für Verknüpfungs-Erstellung
var type = Type.GetTypeFromProgID("WScript.Shell")!;
dynamic shell = Activator.CreateInstance(type)!;
var shortcut = shell.CreateShortcut(linkPath);
shortcut.TargetPath = targetPath;
shortcut.WorkingDirectory = workingDir;
shortcut.Description = description;
shortcut.Save();
Marshal.FinalReleaseComObject(shortcut);
Marshal.FinalReleaseComObject(shell);
}
private void RegisterUninstallEntry()
{
using var key = Registry.LocalMachine.CreateSubKey(
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\EngineeringSync");
var uninstallExe = Path.Combine(state.InstallPath, "EngineeringSync.Setup.exe");
key.SetValue("DisplayName", "EngineeringSync");
key.SetValue("DisplayVersion", "1.0.0");
key.SetValue("Publisher", "EngineeringSync");
key.SetValue("InstallLocation", state.InstallPath);
key.SetValue("UninstallString", $"\"{uninstallExe}\" /uninstall");
key.SetValue("NoModify", 1, RegistryValueKind.DWord);
key.SetValue("NoRepair", 1, RegistryValueKind.DWord);
key.SetValue("EstimatedSize", 45000, RegistryValueKind.DWord);
key.SetValue("DisplayIcon", Path.Combine(state.InstallPath, "EngineeringSync.TrayApp.exe"));
LogMessage?.Invoke(" ✓ Deinstallations-Eintrag in Registry angelegt.");
}
private async Task StartServiceAsync()
{
var (exitCode, _, error) = await RunProcessAsync("sc", "start EngineeringSync");
if (exitCode == 0)
LogMessage?.Invoke(" ✓ Windows-Dienst gestartet.");
else
LogMessage?.Invoke($" WARNUNG: Dienst konnte nicht gestartet werden ({error})");
}
public void LaunchTrayApp()
{
var trayExe = Path.Combine(state.InstallPath, "EngineeringSync.TrayApp.exe");
if (File.Exists(trayExe))
Process.Start(new ProcessStartInfo(trayExe) { UseShellExecute = true });
}
private static async Task<(int exitCode, string output, string error)> RunProcessAsync(
string fileName, string arguments)
{
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};
process.Start();
var output = await process.StandardOutput.ReadToEndAsync();
var error = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
return (process.ExitCode, output.Trim(), error.Trim());
}
private static Task Delay() => Task.Delay(400);
}