Files
EngineeringSync/EngineeringSync.Setup/Services/InstallerService.cs
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

305 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}