using System.Diagnostics; using System.Reflection; using Microsoft.Win32; namespace BSO.Launcher; /// /// Installation: die heruntergeladene .exe (BSO-Launcher-Setup.exe) ist zugleich das Setup (SetupForm). Sie kopiert sich /// ohne Admin-Rechte in den gewaehlten Ordner (Standard %LocalAppData%\Programs\BSO Launcher), legt die gewaehlten /// Verknuepfungen an, traegt sich unter "Apps & Features" ein (Deinstallieren) und startet dann den installierten Launcher. /// Der Ordner steht in der Registry (InstallLocation) - danach aktualisiert sich der Launcher dort selbst. /// public static class Installer { public const string AppName = "BSO Launcher"; const string ExeName = "BSO-Launcher.exe"; const string UninstallKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall\BSO-Launcher"; public static string DefaultDir => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", AppName); /// Ordner der bestehenden Installation (aus "Apps & Features") oder null. public static string? RegisteredDir { get { try { using var key = Registry.CurrentUser.OpenSubKey(UninstallKey); return key?.GetValue("InstallLocation") is string dir && dir.Length > 0 ? dir : null; } catch { return null; } } } public static string InstallDir => RegisteredDir ?? DefaultDir; public static string InstalledExe => Path.Combine(InstallDir, ExeName); static string Self => Environment.ProcessPath ?? Application.ExecutablePath; public static long SelfSize => new FileInfo(Self).Length; static string StartMenuShortcut => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Programs), AppName + ".lnk"); static string DesktopShortcut => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), AppName + ".lnk"); public static bool HasStartMenuShortcut => File.Exists(StartMenuShortcut); public static bool HasDesktopShortcut => File.Exists(DesktopShortcut); /// Gewaehlter Ordner -> Installationsordner: immer ein eigener Unterordner "BSO Launcher" (beim Deinstallieren /// wird nur der geloescht, nie z.B. "D:\Spiele"). public static string NormalizeDir(string chosen) { string dir = Path.GetFullPath(Environment.ExpandEnvironmentVariables(chosen.Trim().Trim('"'))); return string.Equals(Path.GetFileName(dir.TrimEnd('\\')), AppName, StringComparison.OrdinalIgnoreCase) ? dir : Path.Combine(dir, AppName); } /// Darf dieser Benutzer dort schreiben? (Programme ohne Admin-Rechte - auch fuer die Selbst-Updates noetig.) public static string? CheckWritable(string dir) { try { Directory.CreateDirectory(dir); string probe = Path.Combine(dir, ".bso-write-test"); File.WriteAllText(probe, ""); File.Delete(probe); return null; } catch (UnauthorizedAccessException) { return "Keine Schreibrechte in diesem Ordner - bitte einen Ordner waehlen, in dem du ohne Admin-Rechte schreiben darfst"; } catch (Exception e) { return "Ordner nicht nutzbar: " + e.Message; } } /// Laeuft gerade die installierte Kopie? public static bool RunningInstalled => string.Equals(Path.GetFullPath(Self), Path.GetFullPath(InstalledExe), StringComparison.OrdinalIgnoreCase); public static string? InstalledVersion() { if (!File.Exists(InstalledExe)) return null; return FileVersionInfo.GetVersionInfo(InstalledExe).ProductVersion?.Split('+')[0]; } /// Installieren (auch Update/Neu-Installation, auch in einen anderen Ordner). progress(0..1, Text). public static void Install(string dir, bool desktop, bool startMenu, Action progress) { string? oldExe = File.Exists(InstalledExe) ? InstalledExe : null; string exe = Path.Combine(dir, ExeName); progress(0.1, Lang.T("Laufenden Launcher schliessen …")); foreach (var p in Process.GetProcessesByName("BSO-Launcher")) { try { if (p.Id == Environment.ProcessId) continue; string? file = p.MainModule?.FileName; if (!string.Equals(file, exe, StringComparison.OrdinalIgnoreCase) && !string.Equals(file, oldExe, StringComparison.OrdinalIgnoreCase)) continue; p.CloseMainWindow(); if (!p.WaitForExit(3000)) p.Kill(true); } catch { } } progress(0.3, Lang.T("Dateien kopieren …")); Directory.CreateDirectory(dir); string tmp = exe + ".new"; File.Copy(Self, tmp, true); File.Move(tmp, exe, true); // Vorher woanders installiert: die alte Kopie wegraeumen (nur unsere Dateien, den Ordner nur wenn leer) if (oldExe != null && !string.Equals(oldExe, exe, StringComparison.OrdinalIgnoreCase)) RemoveFiles(Path.GetDirectoryName(oldExe)!); progress(0.6, Lang.T("Verknuepfungen anlegen …")); Register(dir); // ab hier zeigen InstallDir/InstalledExe auf den neuen Ordner if (startMenu) CreateShortcut(StartMenuShortcut); else TryDelete(StartMenuShortcut); if (desktop) CreateShortcut(DesktopShortcut); else TryDelete(DesktopShortcut); progress(1.0, Lang.T("Fertig")); } static void TryDelete(string file) { try { File.Delete(file); } catch { } } /// Unsere Dateien im Installationsordner loeschen, den Ordner nur wenn er danach leer ist. static void RemoveFiles(string dir) { foreach (string name in new[] { ExeName, ExeName + ".old", ExeName + ".new" }) TryDelete(Path.Combine(dir, name)); try { if (!Directory.EnumerateFileSystemEntries(dir).Any()) Directory.Delete(dir); } catch { } } /// Eintrag unter "Apps & Features" (bzw. dessen Version nach einem Selbst-Update). public static void Register(string? dir = null) { try { dir ??= InstallDir; string exe = Path.Combine(dir, ExeName); using var key = Registry.CurrentUser.CreateSubKey(UninstallKey); key.SetValue("DisplayName", AppName); key.SetValue("DisplayVersion", SelfUpdate.CurrentVersion); key.SetValue("Publisher", "MBM.wtf"); key.SetValue("DisplayIcon", exe + ",0"); key.SetValue("InstallLocation", dir); key.SetValue("UninstallString", $"\"{exe}\" --uninstall"); key.SetValue("QuietUninstallString", $"\"{exe}\" --uninstall --quiet"); key.SetValue("URLInfoAbout", Api.BaseUrl); key.SetValue("NoModify", 1, RegistryValueKind.DWord); key.SetValue("NoRepair", 1, RegistryValueKind.DWord); try { key.SetValue("EstimatedSize", (int)(new FileInfo(exe).Length / 1024), RegistryValueKind.DWord); } catch { } } catch { /* Eintrag ist nur Komfort */ } } static void CreateShortcut(string path) { try { Type? shellType = Type.GetTypeFromProgID("WScript.Shell"); if (shellType == null) return; dynamic shell = Activator.CreateInstance(shellType)!; dynamic link = shell.CreateShortcut(path); link.TargetPath = InstalledExe; link.WorkingDirectory = InstallDir; link.IconLocation = InstalledExe + ",0"; link.Description = "BSO - Bus Simulator Online"; link.Save(); } catch { /* ohne Verknuepfung geht es auch */ } } /// Installierte Kopie starten - mit -dev, wenn diese hier mit -dev lief. public static void StartInstalled() { var start = new ProcessStartInfo(InstalledExe) { UseShellExecute = true, WorkingDirectory = InstallDir }; if (Api.Dev) start.ArgumentList.Add(Api.DevArg); Process.Start(start); } /// Ordner/Dateien loeschen, sobald dieser Prozess beendet ist (seine .exe ist bis dahin gesperrt). Danach /// emptyDir entfernen, aber nur wenn er leer ist (der Benutzer hat den Ordner gewaehlt - fremde Dateien bleiben). static void RemoveAfterExit(string emptyDir, params string[] paths) { static string Quote(string p) => "'" + p.Replace("'", "''") + "'"; string list = string.Join(",", paths.Where(p => !string.IsNullOrWhiteSpace(p)).Select(Quote)); string script = $"Wait-Process -Id {Environment.ProcessId} -ErrorAction SilentlyContinue; " + $"foreach ($i in 1..30) {{ $left = $false; foreach ($p in @({list})) {{ " + "if (Test-Path -LiteralPath $p) { Remove-Item -LiteralPath $p -Recurse -Force -ErrorAction SilentlyContinue; " + "if (Test-Path -LiteralPath $p) { $left = $true } } }; if (-not $left) { break }; Start-Sleep -Seconds 1 }; " + $"$d = {Quote(emptyDir)}; if ((Test-Path -LiteralPath $d) -and -not (Get-ChildItem -LiteralPath $d -Force)) {{ Remove-Item -LiteralPath $d -Force }}"; var psi = new ProcessStartInfo("powershell.exe") { CreateNoWindow = true, UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden }; foreach (string a in new[] { "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", script }) psi.ArgumentList.Add(a); try { Process.Start(psi); } catch { } } /// Deinstallieren (aus "Apps & Features" oder den Einstellungen). false = abgebrochen. public static bool Uninstall(bool quiet) { if (!quiet) { var answer = MessageBox.Show( Lang.T("BSO komplett deinstallieren?\n\nDas entfernt den Launcher, BSO aus dem Bus-Simulator-2012-Ordner (dinput8.dll, BSO-Ordner) " + "und deine Anmeldung im Launcher."), AppName, MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2); if (answer != DialogResult.Yes) return false; while (Game.IsRunning()) { if (MessageBox.Show(Lang.T("Erst Bus-Simulator 2012 schliessen - solange es laeuft, sind die Dateien in Benutzung."), AppName, MessageBoxButtons.RetryCancel, MessageBoxIcon.Warning) != DialogResult.Retry) return false; } } string dir = InstallDir; foreach (var p in Process.GetProcessesByName("BSO-Launcher")) try { if (p.Id != Environment.ProcessId) p.Kill(true); } catch { } List problems = Game.RemoveBso(Settings.Load()); try { File.Delete(StartMenuShortcut); } catch { } try { File.Delete(DesktopShortcut); } catch { } try { Registry.CurrentUser.DeleteSubKeyTree(UninstallKey, false); } catch { } // Die laufende .exe laesst sich nicht selbst loeschen: aufraeumen, sobald dieser Prozess beendet ist RemoveAfterExit(dir, Path.Combine(dir, ExeName), Path.Combine(dir, ExeName + ".old"), Path.Combine(dir, ExeName + ".new"), Settings.Folder); if (!quiet) MessageBox.Show(Lang.T(problems.Count == 0 ? "BSO wurde komplett deinstalliert." : "BSO wurde deinstalliert. Einiges liess sich nicht entfernen:\n\n" + string.Join("\n", problems)), AppName, MessageBoxButtons.OK, problems.Count == 0 ? MessageBoxIcon.Information : MessageBoxIcon.Warning); return true; } public static Image? Logo() { var s = Assembly.GetExecutingAssembly().GetManifestResourceStream("ui/logo.png"); return s == null ? null : Image.FromStream(s); } }