Files
M_ViperandClaude Opus 5.5 1ae6e1abc2 Fortschritt, .NET-Prüfung, Updates, Fenster merken, Box-Funktionen
- Große Kopien laufen im Hintergrund mit Fortschritt und Abbrechen;
  abgebrochene Kopien werden aufgeräumt, das Original bleibt unverändert
- Sichern/Wiederherstellen mit Fortschritt, keine halben ZIP-Dateien
- Installer auf .NET Framework 4.8: erkennt fehlende .NET 9 Desktop Runtime
  und installiert sie auf Wunsch (Microsoft-Signatur wird geprüft)
- Update-Hinweis über Gitea-Releases, Update mit automatischem Neustart
- Fenstergröße und -position werden gemerkt
- Desktop-Boxen: umbenennen, löschen, verschieben, herausziehen
- Fenster und Boxen kehren auf sichtbare Bildschirme zurück
- README ergänzt

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

389 lines
15 KiB
C#
Raw Permalink 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}";
}
}
/// <summary>Pfad dieses Setups (auch als Deinstallationsprogramm).</summary>
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");
/// <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,
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";
/// <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();
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 ----------
/// <summary>Rechnet Fortschritt 0–1 in einen Teilbereich um.</summary>
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));
}
/// <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();
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",
};
}