using System.Diagnostics; using System.IO; using Microsoft.Win32; namespace DeskCoreSetup; /// Installation/Deinstallation – nur für den aktuellen Benutzer, ohne Administratorrechte. public static class Installer { public const string AppName = "DeskCore"; public const string Publisher = "M_Viper"; public const string ExeName = "DeskCore.exe"; public const string UninstallerName = "DeskCore-Uninstall.exe"; private const string UninstallKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall\DeskCore"; private const string RunKey = @"Software\Microsoft\Windows\CurrentVersion\Run"; public static string Version { get { var v = typeof(Installer).Assembly.GetName().Version!; return $"{v.Major}.{v.Minor}.{v.Build}"; } } /// Pfad dieses Setups (auch als Deinstallationsprogramm). public static string SelfPath => System.Reflection.Assembly.GetExecutingAssembly().Location; public static string DefaultDir => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", AppName); private static string StartMenuLink => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Programs), AppName + ".lnk"); private static string DesktopLink => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), AppName + ".lnk"); /// Ordner einer vorhandenen Installation (oder null). public static string? InstalledDir { get { using var k = Registry.CurrentUser.OpenSubKey(UninstallKey); return k?.GetValue("InstallLocation") as string is { } d && File.Exists(Path.Combine(d, ExeName)) ? d : null; } } public static string? InstalledVersion { get { using var k = Registry.CurrentUser.OpenSubKey(UninstallKey); return k?.GetValue("DisplayVersion") as string; } } public static bool AutostartEnabled { get { using var k = Registry.CurrentUser.OpenSubKey(RunKey); return k?.GetValue(AppName) is string; } } public static bool DesktopLinkExists => File.Exists(DesktopLink); public static string LicenseText => ReadResourceText("payload.LICENSE.txt"); // ---------- Installieren ---------- public static void Install(string dir, bool desktopLink, bool autostart, IProgress<(double, string)> progress, bool installRuntime = false) { if (!Runtime.Is64BitWindows) throw new InvalidOperationException("DeskCore benötigt ein 64-Bit-Windows (Windows 10 oder 11)."); if (installRuntime && !Runtime.IsInstalled) { // 0–60 % für die Laufzeit, Rest für DeskCore Runtime.Install((v, text) => progress.Report((v * 0.6, text))); var inner = progress; progress = new Scaled(inner, 0.6, 0.4); } progress.Report((0.05, "DeskCore wird beendet …")); CloseRunningApp(); RemoveLegacyInstall(); progress.Report((0.2, "Dateien werden kopiert …")); Directory.CreateDirectory(dir); var exe = Path.Combine(dir, ExeName); WriteResource("payload.DeskCore.exe", exe); WriteResource("payload.LICENSE.txt", Path.Combine(dir, "LICENSE.txt")); // Setup selbst als Deinstallationsprogramm ablegen var self = SelfPath; var uninstaller = Path.Combine(dir, UninstallerName); if (!string.Equals(Path.GetFullPath(self), Path.GetFullPath(uninstaller), StringComparison.OrdinalIgnoreCase)) { MoveAside(uninstaller); File.Copy(self, uninstaller, overwrite: true); } progress.Report((0.55, "Verknüpfungen werden angelegt …")); CreateShortcut(StartMenuLink, exe); if (desktopLink) CreateShortcut(DesktopLink, exe); progress.Report((0.75, "Windows wird eingerichtet …")); using (var run = Registry.CurrentUser.CreateSubKey(RunKey)) { if (autostart) run.SetValue(AppName, $"\"{exe}\" --minimized"); else run.DeleteValue(AppName, throwOnMissingValue: false); } using (var k = Registry.CurrentUser.CreateSubKey(UninstallKey)) { long size = new[] { exe, uninstaller, Path.Combine(dir, "LICENSE.txt") } .Where(File.Exists).Sum(f => new FileInfo(f).Length) / 1024; k.SetValue("DisplayName", AppName); k.SetValue("DisplayVersion", Version); k.SetValue("Publisher", Publisher); k.SetValue("DisplayIcon", $"{exe},0"); k.SetValue("InstallLocation", dir); k.SetValue("UninstallString", $"\"{uninstaller}\" --uninstall"); k.SetValue("QuietUninstallString", $"\"{uninstaller}\" --uninstall /S"); k.SetValue("URLInfoAbout", "https://m-viper.de"); k.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd")); k.SetValue("EstimatedSize", (int)size, RegistryValueKind.DWord); k.SetValue("NoModify", 1, RegistryValueKind.DWord); k.SetValue("NoRepair", 1, RegistryValueKind.DWord); } progress.Report((1.0, "Fertig")); } // ---------- Frühere Version "DeskHub" entfernen ---------- private const string LegacyName = "DeskHub"; private const string LegacyUninstallKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall\DeskHub"; /// Ordner einer alten Installation unter dem früheren Namen "DeskHub" – nur unsere eigene. public static string? LegacyInstallDir { get { using var k = Registry.CurrentUser.OpenSubKey(LegacyUninstallKey); // "DeskHub" heißt auch ein anderes Programm → nur mit unserem Herausgeber anfassen if (k?.GetValue("Publisher") as string != Publisher) return null; return k.GetValue("InstallLocation") as string; } } private static void RemoveLegacyInstall() { var dir = LegacyInstallDir; if (dir is null) return; var exe = Path.Combine(dir, LegacyName + ".exe"); // nur Prozesse beenden, die wirklich aus dem alten Ordner laufen foreach (var p in Process.GetProcessesByName(LegacyName)) { try { if (p.MainModule?.FileName is { } f && Storage_IsUnder(f, dir)) { p.Kill(); p.WaitForExit(10000); } } catch { } } DeleteIfPointsTo(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Programs), LegacyName + ".lnk"), exe); DeleteIfPointsTo(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), LegacyName + ".lnk"), exe); using (var run = Registry.CurrentUser.OpenSubKey(RunKey, writable: true)) { if (run?.GetValue(LegacyName) is string cmd && cmd.IndexOf(exe, StringComparison.OrdinalIgnoreCase) >= 0) run.DeleteValue(LegacyName, throwOnMissingValue: false); } foreach (var f in new[] { exe, Path.Combine(dir, "LICENSE.txt"), Path.Combine(dir, LegacyName + "-Uninstall.exe") }) MoveAside(f); CleanupOldFiles(dir); try { Directory.Delete(dir); } catch { } // nur wenn leer Registry.CurrentUser.DeleteSubKeyTree(LegacyUninstallKey, throwOnMissingSubKey: false); } // ---------- Deinstallieren ---------- public static readonly string[] DataDirs = [ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppName), // Kategorien Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AppName), // Einstellungen Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), AppName), // Rückgängig-Zwischenablage ]; public static void Uninstall(bool removeData, IProgress<(double, string)> progress) { // Nur im eingetragenen Installationsordner löschen – nie dort, wo das Setup zufällig liegt (z. B. Downloads) var dir = InstalledDir; var exe = dir is null ? "" : Path.Combine(dir, ExeName); progress.Report((0.1, "DeskCore wird beendet …")); CloseRunningApp(); progress.Report((0.3, "Verknüpfungen werden entfernt …")); DeleteIfPointsTo(StartMenuLink, exe); DeleteIfPointsTo(DesktopLink, exe); progress.Report((0.45, "Windows-Einträge werden entfernt …")); using (var run = Registry.CurrentUser.OpenSubKey(RunKey, writable: true)) run?.DeleteValue(AppName, throwOnMissingValue: false); Registry.CurrentUser.DeleteSubKeyTree(UninstallKey, throwOnMissingSubKey: false); if (removeData) { progress.Report((0.6, "Kategorien und Einstellungen werden in den Papierkorb gelegt …")); foreach (var d in DataDirs.Where(Directory.Exists)) { try { Microsoft.VisualBasic.FileIO.FileSystem.DeleteDirectory(d, Microsoft.VisualBasic.FileIO.UIOption.OnlyErrorDialogs, Microsoft.VisualBasic.FileIO.RecycleOption.SendToRecycleBin); } catch { } } } progress.Report((0.85, "Dateien werden entfernt …")); if (dir is null) { progress.Report((1.0, "Fertig")); return; } TryDelete(exe); TryDelete(Path.Combine(dir, "LICENSE.txt")); CleanupOldFiles(dir); // Das laufende Deinstallationsprogramm kann sich nicht selbst löschen → kurz nach dem Beenden per cmd. // rmdir ohne /s entfernt den Ordner nur, wenn er danach leer ist. var self = SelfPath; if (Storage_IsUnder(self, dir)) { Process.Start(new ProcessStartInfo("cmd.exe", $"/c ping 127.0.0.1 -n 3 > nul & del /f /q \"{self}\" & del /f /q \"{Path.Combine(dir, "*.old")}\" & rmdir \"{dir}\"") { CreateNoWindow = true, UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden, }); } else { TryDelete(Path.Combine(dir, UninstallerName)); try { Directory.Delete(dir); } catch { } // nur wenn leer } progress.Report((1.0, "Fertig")); } // ---------- Helfer ---------- /// Rechnet Fortschritt 0–1 in einen Teilbereich um. private sealed class Scaled(IProgress<(double, string)> inner, double offset, double factor) : IProgress<(double, string)> { public void Report((double, string) value) => inner.Report((offset + value.Item1 * factor, value.Item2)); } /// Laufendes DeskCore beenden (das X versteckt es nur im Infobereich). private static void CloseRunningApp() { foreach (var p in Process.GetProcessesByName("DeskCore")) { try { p.Kill(); p.WaitForExit(10000); } catch { } } } private static void WriteResource(string name, string path) { CleanupOldFiles(Path.GetDirectoryName(path)!); MoveAside(path); for (int attempt = 1; ; attempt++) { try { using var s = typeof(Installer).Assembly.GetManifestResourceStream(name) ?? throw new InvalidOperationException($"Installationsdatei {name} fehlt."); using var f = File.Create(path); s.CopyTo(f); return; } catch (IOException) when (attempt < 10) { Thread.Sleep(500); } catch (IOException) when (attempt >= 10) { throw new IOException($"„{Path.GetFileName(path)}“ wird noch verwendet. Bitte DeskCore beenden (Rechtsklick auf das Symbol im Infobereich → Beenden) und erneut versuchen."); } } } /// /// Vorhandene Datei beiseite umbenennen. Das klappt unter Windows auch dann, wenn sie noch geöffnet ist /// (z. B. vom Explorer für das Icon oder vom Virenscanner) – überschreiben würde dagegen fehlschlagen. /// private static void MoveAside(string path) { if (!File.Exists(path)) return; try { File.Delete(path); return; } catch { } try { File.Move(path, $"{path}.{Guid.NewGuid():N}.old"); } catch { } } /// Beiseitegelegte Dateien früherer Installationen wegräumen (sofern nicht mehr in Benutzung). private static void CleanupOldFiles(string dir) { if (!Directory.Exists(dir)) return; foreach (var f in Directory.GetFiles(dir, "*.old")) try { File.Delete(f); } catch { } } private static string ReadResourceText(string name) { using var s = typeof(Installer).Assembly.GetManifestResourceStream(name); if (s is null) return ""; using var r = new StreamReader(s); return r.ReadToEnd().Trim(); } private static void CreateShortcut(string link, string target) { Directory.CreateDirectory(Path.GetDirectoryName(link)!); dynamic shell = Activator.CreateInstance(Type.GetTypeFromProgID("WScript.Shell")!)!; dynamic lnk = shell.CreateShortcut(link); lnk.TargetPath = target; lnk.WorkingDirectory = Path.GetDirectoryName(target); lnk.IconLocation = $"{target},0"; lnk.Description = "Desktop-Icons in Kategorien sortieren"; lnk.Save(); } private static void DeleteIfPointsTo(string link, string target) { if (!File.Exists(link)) return; try { dynamic shell = Activator.CreateInstance(Type.GetTypeFromProgID("WScript.Shell")!)!; string t = shell.CreateShortcut(link).TargetPath; if (!string.Equals(t, target, StringComparison.OrdinalIgnoreCase)) return; } catch { return; } // Explorer/Indexdienst halten Verknüpfungen manchmal kurz fest → mehrmals versuchen for (int i = 0; i < 10 && File.Exists(link); i++) { try { File.Delete(link); } catch { Thread.Sleep(300); } } } private static void TryDelete(string path) { try { if (File.Exists(path)) File.Delete(path); } catch { } } private static bool Storage_IsUnder(string path, string dir) => (Path.GetFullPath(path)).StartsWith(Path.GetFullPath(dir).TrimEnd('\\') + "\\", StringComparison.OrdinalIgnoreCase); public static long DataSize() { long sum = 0; foreach (var d in DataDirs.Where(Directory.Exists)) { try { foreach (var f in Directory.EnumerateFiles(d, "*", SearchOption.AllDirectories)) try { sum += new FileInfo(f).Length; } catch { } } catch { } } return sum; } public static string FormatSize(long bytes) => bytes switch { < 1024 * 1024 => $"{Math.Max(1, bytes / 1024)} KB", < 1024L * 1024 * 1024 => $"{bytes / 1024.0 / 1024:0.#} MB", _ => $"{bytes / 1024.0 / 1024 / 1024:0.##} GB", }; }