diff --git a/desktop/.gitignore b/desktop/.gitignore new file mode 100644 index 00000000..b9470778 --- /dev/null +++ b/desktop/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/desktop/LICENSE b/desktop/LICENSE new file mode 100644 index 00000000..8bb63b85 --- /dev/null +++ b/desktop/LICENSE @@ -0,0 +1,19 @@ +DeskHub Desktop – Nutzungshinweise + +Copyright (c) M_Viper. Alle Rechte vorbehalten. + +Diese Anwendung ist ein Desktop-Client für die DeskHub-Support-Software und +richtet sich an Mitarbeiter, die ein Konto auf einer DeskHub-Installation haben. + +1. Nutzung + Die Anwendung darf nur von berechtigten Mitarbeitern der jeweiligen + DeskHub-Installation verwendet werden. + +2. Zugangsdaten + Die Anmeldung wird als widerrufbares Token verschlüsselt auf diesem Computer + gespeichert. Auf gemeinsam genutzten Computern bitte nach der Arbeit abmelden. + +3. Keine Gewährleistung + Die Software wird ohne jegliche Gewährleistung bereitgestellt. Der Autor + haftet nicht für Schäden, die aus der Nutzung oder Unmöglichkeit der Nutzung + der Software entstehen. diff --git a/desktop/installer/App.xaml b/desktop/installer/App.xaml new file mode 100644 index 00000000..5b7206f2 --- /dev/null +++ b/desktop/installer/App.xaml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + diff --git a/desktop/installer/App.xaml.cs b/desktop/installer/App.xaml.cs new file mode 100644 index 00000000..c904d74c --- /dev/null +++ b/desktop/installer/App.xaml.cs @@ -0,0 +1,97 @@ +using System.IO; +using System.Windows; +using System.Windows.Media; +using Microsoft.Win32; + +namespace DeskHubSetup; + +public partial class App : Application +{ + private static Mutex? _single; + + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + bool has(string arg) => e.Args.Any(a => a.Equals(arg, StringComparison.OrdinalIgnoreCase)); + + // Das Deinstallationsprogramm ist das Setup ohne angehängte App → ohne Anhang immer Deinstallation + bool uninstall = has("--uninstall") || !Payload.Exists; + bool silent = has("/S"); + bool update = has("--update"); + bool launch = has("/launch"); + var dirArg = e.Args.FirstOrDefault(a => a.StartsWith("/D=", StringComparison.OrdinalIgnoreCase))?.Substring(3); + + _single = new Mutex(true, "DeskHub-Setup", out bool first); + if (!first) + { + Shutdown(0); + return; + } + + // Stille Installation/Deinstallation für Skripte: DeskHub-Setup.exe /S [/D=Ordner] [/launch] bzw. --uninstall /S + if (silent) + { + var progress = new Progress<(double, string)>(_ => { }); + try + { + if (uninstall) Installer.Uninstall(removeData: false, progress); + else + { + var dir = dirArg ?? Installer.InstalledDir ?? Installer.DefaultDir; + Installer.Install(dir, desktopLink: Installer.InstalledDir is null || Installer.DesktopLinkExists, + progress, waitForExit: true); + if (launch) System.Diagnostics.Process.Start(Path.Combine(dir, Installer.ExeName)); + } + Shutdown(0); + } + catch (Exception ex) + { + // ohne Oberfläche: Fehler für Skripte nachvollziehbar ablegen + try { File.WriteAllText(Path.Combine(Path.GetTempPath(), "DeskHub-Setup.log"), $"[{DateTime.Now}] {ex}"); } catch { } + Shutdown(1); + } + return; + } + + ApplyTheme(); + var mode = uninstall ? SetupMode.Uninstall : update ? SetupMode.Update : SetupMode.Install; + new MainWindow(mode).Show(); + } + + protected override void OnExit(ExitEventArgs e) + { + try { _single?.ReleaseMutex(); } catch { } + base.OnExit(e); + } + + /// Hell/Dunkel wie in Windows eingestellt. + private void ApplyTheme() + { + bool light = ReadDword(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", "AppsUseLightTheme") != 0; + Resources.MergedDictionaries[0] = new ResourceDictionary + { + Source = new Uri($"pack://application:,,,/Themes/Colors.{(light ? "Light" : "Dark")}.xaml"), + }; + if (!light) + { + // im Dunkeln etwas heller, damit Text und Fortschrittsbalken gut lesbar bleiben + var a = Color.FromRgb(0x4A, 0x86, 0xEA); + Resources["AccentBrush"] = new SolidColorBrush(a); + Resources["AccentHoverBrush"] = new SolidColorBrush(Color.FromRgb(0x5E, 0x94, 0xEE)); + Resources["AccentSubtleBrush"] = new SolidColorBrush(Color.FromArgb(0x30, a.R, a.G, a.B)); + Resources["AccentSubtleStrokeBrush"] = new SolidColorBrush(Color.FromArgb(0x60, a.R, a.G, a.B)); + } + } + + private static int ReadDword(string key, string name) + { + try + { + using var k = Registry.CurrentUser.OpenSubKey(key); + return k?.GetValue(name) is int i ? i : -1; + } + catch { return -1; } + } +} + +public enum SetupMode { Install, Update, Uninstall } diff --git a/desktop/installer/DeskHubSetup.csproj b/desktop/installer/DeskHubSetup.csproj new file mode 100644 index 00000000..c250c988 --- /dev/null +++ b/desktop/installer/DeskHubSetup.csproj @@ -0,0 +1,49 @@ + + + WinExe + + net48 + true + true + latest + enable + enable + AnyCPU + false + false + DeskHub-Setup + DeskHubSetup + ..\src\assets\icon.ico + app.manifest + DeskHub Setup + DeskHub Setup + Installiert DeskHub + M_Viper + M_Viper + Copyright (c) M_Viper. Alle Rechte vorbehalten. + de-DE + + 0.0.0 + + + + + + + + + + + + + + + + + + diff --git a/desktop/installer/Installer.cs b/desktop/installer/Installer.cs new file mode 100644 index 00000000..ac39fefc --- /dev/null +++ b/desktop/installer/Installer.cs @@ -0,0 +1,508 @@ +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Reflection; +using System.Text.RegularExpressions; +using Microsoft.Win32; + +namespace DeskHubSetup; + +/// Installation/Deinstallation – nur für den aktuellen Benutzer, ohne Administratorrechte. +public static class Installer +{ + public const string AppName = "DeskHub"; + public const string Publisher = "M_Viper"; + public const string ExeName = "DeskHub.exe"; + // Gleicher Name wie beim früheren NSIS-Setup: die App erkennt daran eine Setup-Installation + // und bietet Updates als Setup an (src/updater.js → hasUninstaller). + public const string UninstallerName = "Uninstall DeskHub.exe"; + // wie "appId" in package.json – gleiche Taskleisten-/Benachrichtigungs-Zuordnung wie bisher + public const string AppUserModelId = "de.deskhub.desktop"; + + // Schlüssel vom alten electron-builder-Setup übernommen → in "Apps" bleibt es ein einziger Eintrag + private const string KeyId = "7c1d9a52-3b6e-4f0a-9d84-5e2a1b6f3c10"; + private const string UninstallKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall\" + KeyId; + private const string LegacyAppKey = @"Software\" + KeyId; + private const string RunKey = @"Software\Microsoft\Windows\CurrentVersion\Run"; + private const string FileListName = "install-files.txt"; + + public static string Version + { + get + { + var info = typeof(Installer).Assembly.GetCustomAttribute()?.InformationalVersion; + if (!string.IsNullOrEmpty(info)) return info!.Split('+')[0]; + var v = typeof(Installer).Assembly.GetName().Version!; + return $"{v.Major}.{v.Minor}.{v.Build}"; + } + } + + /// Pfad dieses Setups (bzw. des Deinstallationsprogramms). + public static string SelfPath => Assembly.GetExecutingAssembly().Location; + + public static string DefaultDir => + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", AppName); + + /// App-Daten: Anmeldung (Token) und Einstellungen. + public static string DataDir => + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "deskhub"); + + 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"); + + // ---------- vorhandene Installation ---------- + + private static string? RegValue(string name) + { + using var k = Registry.CurrentUser.OpenSubKey(UninstallKey); + return k?.GetValue(name) as string; + } + + /// Ordner einer vorhandenen Installation (dieses Setups oder des alten NSIS-Setups), oder null. + public static string? InstalledDir + { + get + { + var dir = RegValue("InstallLocation"); + if (string.IsNullOrWhiteSpace(dir)) + { + // altes NSIS-Setup trägt keinen InstallLocation ein → aus dem Deinstallationsbefehl ablesen + var m = Regex.Match(RegValue("UninstallString") ?? "", "^\"([^\"]+)\""); + dir = m.Success ? Path.GetDirectoryName(m.Groups[1].Value) : null; + } + return dir is not null && File.Exists(Path.Combine(dir, ExeName)) ? dir : null; + } + } + + /// Stammt die vorhandene Installation noch vom alten NSIS-Setup? + public static bool IsLegacyInstall => + InstalledDir is not null && (RegValue("UninstallString") ?? "").IndexOf("--uninstall", StringComparison.OrdinalIgnoreCase) < 0; + + public static string? InstalledVersion => RegValue("DisplayVersion"); + + public static bool DesktopLinkExists => File.Exists(DesktopLink); + + public static bool IsRunning(string? dir) => AppProcesses(dir, InstalledDir).Any(); + + public static string LicenseText + { + get + { + using var s = typeof(Installer).Assembly.GetManifestResourceStream("LICENSE.txt"); + if (s is null) return ""; + using var r = new StreamReader(s); + return r.ReadToEnd().Trim(); + } + } + + // ---------- Installieren ---------- + + /// nach einem Update aus der App: ihr erst Zeit geben, sich zu beenden + public static void Install(string dir, bool desktopLink, IProgress<(double, string)> progress, bool waitForExit = false) + { + if (!Environment.Is64BitOperatingSystem) + throw new InvalidOperationException("DeskHub benötigt ein 64-Bit-Windows (Windows 10 oder 11)."); + + var oldDir = InstalledDir; + progress.Report((0.02, "DeskHub wird beendet …")); + CloseRunningApp(waitForExit ? 20000 : 0, dir, oldDir); + + using var zip = Payload.OpenArchive(); + var topLevel = TopLevelNames(zip); + + progress.Report((0.05, "Alte Version wird entfernt …")); + Directory.CreateDirectory(dir); + RemoveAppFiles(dir, topLevel); + if (oldDir is not null && !SamePath(oldDir, dir)) + { + // Installation wurde in einen anderen Ordner verlegt → alten Ordner aufräumen + var oldExe = Path.Combine(oldDir, ExeName); + DeleteIfPointsTo(StartMenuLink, oldExe); + DeleteIfPointsTo(DesktopLink, oldExe); + RemoveAppFiles(oldDir, topLevel); + try { Directory.Delete(oldDir); } catch { } // nur wenn leer + } + + Extract(zip, dir, p => progress.Report((0.08 + p * 0.8, "Dateien werden kopiert …"))); + + progress.Report((0.9, "Deinstallationsprogramm wird angelegt …")); + var exe = Path.Combine(dir, ExeName); + var uninstaller = Path.Combine(dir, UninstallerName); + if (!SamePath(SelfPath, uninstaller)) + { + MoveAside(uninstaller); + CopyStub(uninstaller); + } + File.WriteAllLines(Path.Combine(dir, FileListName), topLevel.Concat([UninstallerName, FileListName])); + + progress.Report((0.94, "Verknüpfungen werden angelegt …")); + Shortcut.Create(StartMenuLink, exe, "DeskHub Mitarbeiter-Client", AppUserModelId); + if (desktopLink) Shortcut.Create(DesktopLink, exe, "DeskHub Mitarbeiter-Client", AppUserModelId); + else DeleteIfPointsTo(DesktopLink, exe); + + progress.Report((0.97, "Windows wird eingerichtet …")); + Registry.CurrentUser.DeleteSubKeyTree(UninstallKey, throwOnMissingSubKey: false); // Reste des alten Setups + Registry.CurrentUser.DeleteSubKeyTree(LegacyAppKey, throwOnMissingSubKey: false); + using (var k = Registry.CurrentUser.CreateSubKey(UninstallKey)) + { + long size = DirSize(dir) / 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://github.com/"); + k.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd")); + k.SetValue("EstimatedSize", (int)Math.Min(int.MaxValue, size), RegistryValueKind.DWord); + k.SetValue("NoModify", 1, RegistryValueKind.DWord); + k.SetValue("NoRepair", 1, RegistryValueKind.DWord); + } + + progress.Report((0.98, "Installation wird abgeschlossen …")); + Vorwaermen(exe); + + progress.Report((1.0, "Fertig")); + } + + /// + /// Startet DeskHub einmal kurz mit "--prewarm" – die App beendet sich sofort wieder (siehe src/main.js). + /// Windows Defender prüft eine neue, unbekannte EXE beim ersten Start mehrere Sekunden lang; das passiert + /// so während des Fortschrittsbalkens und nicht erst beim ersten echten Start. + /// + private static void Vorwaermen(string exe) + { + try + { + using var p = Process.Start(new ProcessStartInfo(exe, "--prewarm") + { + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = Path.GetDirectoryName(exe)!, + }); + if (p != null && !p.WaitForExit(30000)) { try { p.Kill(); } catch { } } + } + catch { /* nur eine Beschleunigung */ } + } + + private static List TopLevelNames(ZipArchive zip) => + zip.Entries + .Select(e => e.FullName.Replace('\\', '/').Split('/')[0]) + .Where(n => n.Length > 0) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + private static void Extract(ZipArchive zip, string dir, Action report) + { + var root = Path.GetFullPath(dir).TrimEnd('\\') + "\\"; + long total = Math.Max(1, zip.Entries.Sum(e => e.Length)), done = 0; + var buffer = new byte[1 << 20]; + int lastPercent = -1; + + foreach (var entry in zip.Entries) + { + var target = Path.GetFullPath(Path.Combine(dir, entry.FullName)); + if (!target.StartsWith(root, StringComparison.OrdinalIgnoreCase)) continue; // nie außerhalb des Ordners schreiben + if (entry.Name.Length == 0) + { + Directory.CreateDirectory(target); + continue; + } + Directory.CreateDirectory(Path.GetDirectoryName(target)!); + long before = done; + Retry(target, () => + { + done = before; + MoveAside(target); + using var src = entry.Open(); + using var dst = File.Create(target); + int n; + while ((n = src.Read(buffer, 0, buffer.Length)) > 0) + { + dst.Write(buffer, 0, n); + done += n; + int percent = (int)(done * 100 / total); + if (percent != lastPercent) + { + lastPercent = percent; + report(Math.Min(1.0, (double)done / total)); + } + } + }); + } + report(1.0); + } + + /// Setup ohne angehängte App als Deinstallationsprogramm ablegen (nur ein paar hundert KB). + private static void CopyStub(string path) + { + long length = Payload.StubLength; + using var src = File.OpenRead(SelfPath); + using var dst = File.Create(path); + var buffer = new byte[81920]; + long left = length; + while (left > 0) + { + int n = src.Read(buffer, 0, (int)Math.Min(buffer.Length, left)); + if (n <= 0) break; + dst.Write(buffer, 0, n); + left -= n; + } + } + + // ---------- Deinstallieren ---------- + + 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, "DeskHub wird beendet …")); + CloseRunningApp(0, dir); + + progress.Report((0.3, "Verknüpfungen werden entfernt …")); + DeleteIfPointsTo(StartMenuLink, exe); + DeleteIfPointsTo(DesktopLink, exe); + + progress.Report((0.45, "Windows-Einträge werden entfernt …")); + RemoveAutostart(exe); + Registry.CurrentUser.DeleteSubKeyTree(UninstallKey, throwOnMissingSubKey: false); + Registry.CurrentUser.DeleteSubKeyTree(LegacyAppKey, throwOnMissingSubKey: false); + // Name/Logo für Windows-Benachrichtigungen (legt die App selbst an) + Registry.CurrentUser.DeleteSubKeyTree(@"Software\Classes\AppUserModelId\" + AppUserModelId, throwOnMissingSubKey: false); + + if (removeData && Directory.Exists(DataDir)) + { + progress.Report((0.6, "Anmeldung und Einstellungen werden in den Papierkorb gelegt …")); + try + { + Microsoft.VisualBasic.FileIO.FileSystem.DeleteDirectory(DataDir, + Microsoft.VisualBasic.FileIO.UIOption.OnlyErrorDialogs, + Microsoft.VisualBasic.FileIO.RecycleOption.SendToRecycleBin); + } + catch { } + } + + progress.Report((0.8, "Dateien werden entfernt …")); + if (dir is null) { progress.Report((1.0, "Fertig")); return; } + RemoveAppFiles(dir, null); + + // 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 (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")); + } + + /// Autostart-Einträge entfernen, die die App selbst gesetzt hat (Name vergibt Electron). + private static void RemoveAutostart(string exe) + { + if (exe.Length == 0) return; + using var run = Registry.CurrentUser.OpenSubKey(RunKey, writable: true); + if (run is null) return; + foreach (var name in run.GetValueNames()) + { + if (run.GetValue(name) is string cmd && cmd.IndexOf(exe, StringComparison.OrdinalIgnoreCase) >= 0) + run.DeleteValue(name, throwOnMissingValue: false); + } + } + + // ---------- Dateien ---------- + + /// + /// Entfernt die App-Dateien aus einem Installationsordner. Gelöscht wird nur, was nachweislich zu DeskHub gehört: + /// Einträge aus der Dateiliste der letzten Installation bzw. die Namen aus dem neuen Paket – + /// Ordner nur, wenn sie eindeutig von Electron stammen. Fremde Dateien im Ordner bleiben unangetastet. + /// + private static void RemoveAppFiles(string dir, IEnumerable? extraNames) + { + if (!Directory.Exists(dir)) return; + CleanupOldFiles(dir); + var listFile = Path.Combine(dir, FileListName); + var fromList = File.Exists(listFile) ? File.ReadAllLines(listFile).Where(l => l.Trim().Length > 0).ToList() : []; + var names = fromList.Concat(extraNames ?? []).Concat([UninstallerName, FileListName]) + .Distinct(StringComparer.OrdinalIgnoreCase); + + foreach (var name in names) + { + if (name.IndexOfAny(['\\', '/', ':']) >= 0 || name is "." or "..") continue; + var path = Path.Combine(dir, name); + if (SamePath(path, SelfPath)) continue; // das laufende Deinstallationsprogramm räumt cmd weg + + if (Directory.Exists(path)) + { + bool ours = fromList.Contains(name, StringComparer.OrdinalIgnoreCase) + || (name.Equals("resources", StringComparison.OrdinalIgnoreCase) && File.Exists(Path.Combine(path, "app.asar"))) + || (name.Equals("locales", StringComparison.OrdinalIgnoreCase) && File.Exists(Path.Combine(path, "en-US.pak"))); + if (ours) Retry(path, () => Directory.Delete(path, recursive: true)); + } + else if (File.Exists(path)) + { + Retry(path, () => MoveAside(path)); + } + } + } + + /// + /// Längster Pfad im Paket. Windows erlaubt (ohne Sonderfreigabe) nur 259 Zeichen pro Pfad – + /// der Installationsordner darf deshalb höchstens Zeichen lang sein. + /// + private static int? _longestEntry; + public static int MaxDirLength + { + get + { + if (_longestEntry is null) + { + try + { + using var zip = Payload.OpenArchive(); + _longestEntry = zip.Entries.Count == 0 ? 0 : zip.Entries.Max(e => e.FullName.Length); + } + catch { _longestEntry = 0; } + } + return 259 - 1 - _longestEntry.Value; + } + } + + private static void Retry(string path, Action action) + { + for (int attempt = 1; ; attempt++) + { + try + { + action(); + return; + } + catch (PathTooLongException ex) + { + throw new IOException($"Der Installationsordner ist zu lang. Bitte einen kürzeren Ordner wählen (höchstens {MaxDirLength} Zeichen).", ex); + } + catch (Exception ex) when ((ex is IOException || ex is UnauthorizedAccessException) && attempt < 10) + { + Thread.Sleep(500); + } + catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) + { + throw new IOException($"„{Path.GetFileName(path)}“ wird noch verwendet. Bitte DeskHub beenden " + + "(auch das Symbol im Infobereich neben der Uhr) und erneut versuchen.", ex); + } + } + } + + /// + /// Vorhandene Datei löschen oder – falls sie noch geöffnet ist (Explorer, Virenscanner) – beiseite umbenennen. + /// Umbenennen klappt unter Windows auch bei geöffneten Dateien, überschreiben nicht. + /// + private static void MoveAside(string path) + { + if (!File.Exists(path)) return; + try { File.Delete(path); return; } catch { } + File.Move(path, $"{path}.{Guid.NewGuid():N}.old"); + } + + private static void CleanupOldFiles(string dir) + { + if (!Directory.Exists(dir)) return; + foreach (var f in Directory.GetFiles(dir, "*.old", SearchOption.AllDirectories)) + try { File.Delete(f); } catch { } + } + + private static void TryDelete(string path) + { + try { if (File.Exists(path)) File.Delete(path); } catch { } + } + + private static void DeleteIfPointsTo(string link, string target) + { + if (!File.Exists(link) || target.Length == 0) return; + if (!SamePath(Shortcut.Target(link) ?? "", target)) 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); } + } + } + + // ---------- Prozesse ---------- + + private static IEnumerable AppProcesses(params string?[] dirs) + { + var roots = dirs.Where(d => !string.IsNullOrEmpty(d)).Select(d => d!).ToList(); + if (roots.Count == 0) yield break; + // App (mehrere Electron-Prozesse) + foreach (var p in Process.GetProcessesByName("DeskHub")) + { + string? file = null; + try { file = p.MainModule?.FileName; } catch { } + if (file is not null && roots.Any(r => IsUnder(file, r))) yield return p; + } + } + + private static void CloseRunningApp(int waitMs, params string?[] dirs) + { + var until = DateTime.Now.AddMilliseconds(waitMs); + while (waitMs > 0 && DateTime.Now < until && AppProcesses(dirs).Any()) + Thread.Sleep(300); + + foreach (var p in AppProcesses(dirs).ToList()) + { + try + { + p.Kill(); + p.WaitForExit(10000); + } + catch { } + } + } + + // ---------- Helfer ---------- + + private static bool SamePath(string a, string b) + { + try { return string.Equals(Path.GetFullPath(a).TrimEnd('\\'), Path.GetFullPath(b).TrimEnd('\\'), StringComparison.OrdinalIgnoreCase); } + catch { return false; } + } + + private static bool IsUnder(string path, string dir) => + Path.GetFullPath(path).StartsWith(Path.GetFullPath(dir).TrimEnd('\\') + "\\", StringComparison.OrdinalIgnoreCase); + + private static long DirSize(string dir) + { + long sum = 0; + try + { + foreach (var f in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories)) + try { sum += new FileInfo(f).Length; } catch { } + } + catch { } + return sum; + } + + public static long DataSize() => Directory.Exists(DataDir) ? DirSize(DataDir) : 0; + + 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", + }; +} diff --git a/desktop/installer/LauncherImport.cs b/desktop/installer/LauncherImport.cs new file mode 100644 index 00000000..296d2ee0 --- /dev/null +++ b/desktop/installer/LauncherImport.cs @@ -0,0 +1,16 @@ +namespace DeskHubSetup; + +/// Platzhalter: DeskHub hat keinen Import aus anderen Programmen. Der Import-Schritt des Assistenten entfällt damit automatisch. +public sealed class FoundLauncher(string name, string path, int count, string unit) +{ + public string Name { get; } = name; + public string Path { get; } = path; + public int Count { get; } = count; + public string Unit { get; } = unit; +} + +public static class LauncherImport +{ + public static List Detect() => []; + public static void WritePending(List selected) { } +} diff --git a/desktop/installer/MainWindow.xaml b/desktop/installer/MainWindow.xaml new file mode 100644 index 00000000..d07beef4 --- /dev/null +++ b/desktop/installer/MainWindow.xaml @@ -0,0 +1,392 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +