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>
305 lines
12 KiB
C#
305 lines
12 KiB
C#
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);
|
||
}
|