Files
DeskCore/Setup/Installer.cs
T
M_ViperandClaude Opus 5.5 77bb3e6b9d DeskCore 1.0.0
Desktop-Icons in eigene Kategorien sortieren: Windows-11-Design (hell/dunkel),
Desktop-Boxen, automatisches Einsortieren nach Regeln, Rückgängig, Suche,
Infobereich mit Tastenkürzel, Sicherung als ZIP und Installer ohne Adminrechte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 20:19:32 +02:00

369 lines
14 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 Microsoft.Win32;
namespace DeskCoreSetup;
/// <summary>Installation/Deinstallation – nur für den aktuellen Benutzer, ohne Administratorrechte.</summary>
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}";
}
}
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");
/// <summary>Ordner einer vorhandenen Installation (oder null).</summary>
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)
{
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 = Environment.ProcessPath!;
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";
/// <summary>Ordner einer alten Installation unter dem früheren Namen "DeskHub" – nur unsere eigene.</summary>
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(entireProcessTree: true);
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.Contains(exe, StringComparison.OrdinalIgnoreCase))
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 = Environment.ProcessPath!;
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 ----------
/// <summary>Laufendes DeskCore beenden (das X versteckt es nur im Infobereich).</summary>
private static void CloseRunningApp()
{
foreach (var p in Process.GetProcessesByName("DeskCore"))
{
try
{
p.Kill(entireProcessTree: true);
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.");
}
}
}
/// <summary>
/// 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.
/// </summary>
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 { }
}
/// <summary>Beiseitegelegte Dateien früherer Installationen wegräumen (sofern nicht mehr in Benutzung).</summary>
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",
};
}