using System.Diagnostics; using System.Reflection; using Microsoft.Win32; namespace BSO.Launcher; /// /// Installation wie bei Discord (wie beim MSCO Launcher): die heruntergeladene .exe installiert sich selbst ohne /// Admin-Rechte nach %LocalAppData%\Programs\BSO Launcher, legt Verknuepfungen (Desktop, Startmenue) an, traegt sich unter /// "Apps & Features" ein (Deinstallieren) und startet dann den installierten Launcher. /// 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 InstallDir => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", AppName); public static string InstalledExe => Path.Combine(InstallDir, ExeName); static string Self => Environment.ProcessPath ?? Application.ExecutablePath; static string StartMenuShortcut => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Programs), AppName + ".lnk"); static string DesktopShortcut => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), AppName + ".lnk"); /// 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]; } /// Muss installiert (oder aktualisiert) werden? false = installierte Version ist gleich/neuer. public static bool NeedsInstall() { string? installed = InstalledVersion(); return installed == null || SelfUpdate.IsNewer(SelfUpdate.CurrentVersion, installed); } /// Installieren. progress(0..1, Text). public static void Install(Action progress) { progress(0.1, "Laufenden Launcher schliessen …"); foreach (var p in Process.GetProcessesByName("BSO-Launcher")) { try { if (p.Id == Environment.ProcessId) continue; if (!string.Equals(p.MainModule?.FileName, InstalledExe, StringComparison.OrdinalIgnoreCase)) continue; p.CloseMainWindow(); if (!p.WaitForExit(3000)) p.Kill(true); } catch { } } progress(0.3, "Dateien kopieren …"); Directory.CreateDirectory(InstallDir); string tmp = InstalledExe + ".new"; File.Copy(Self, tmp, true); File.Move(tmp, InstalledExe, true); progress(0.6, "Verknuepfungen anlegen …"); CreateShortcut(StartMenuShortcut); CreateShortcut(DesktopShortcut); progress(0.8, "Registrieren …"); Register(); progress(1.0, "Fertig"); } /// Eintrag unter "Apps & Features" (bzw. dessen Version nach einem Selbst-Update). public static void Register() { try { using var key = Registry.CurrentUser.CreateSubKey(UninstallKey); key.SetValue("DisplayName", AppName); key.SetValue("DisplayVersion", SelfUpdate.CurrentVersion); key.SetValue("Publisher", "MBM.wtf"); key.SetValue("DisplayIcon", InstalledExe + ",0"); key.SetValue("InstallLocation", InstallDir); key.SetValue("UninstallString", $"\"{InstalledExe}\" --uninstall"); key.SetValue("QuietUninstallString", $"\"{InstalledExe}\" --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(InstalledExe).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 */ } } public static void StartInstalled() { Process.Start(new ProcessStartInfo(InstalledExe) { UseShellExecute = true, WorkingDirectory = InstallDir }); } /// Ordner/Dateien loeschen, sobald dieser Prozess beendet ist (seine .exe ist bis dahin gesperrt). static void RemoveAfterExit(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 }"; 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( "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("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, Settings.Folder); if (!quiet) MessageBox.Show(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); } } /// Kleines Installationsfenster (wie der Discord-Installer): Logo, Status, Fortschrittsbalken. public sealed class InstallForm : Form { readonly Label status = new(); double progress; string? error; public InstallForm() { FormBorderStyle = FormBorderStyle.None; StartPosition = FormStartPosition.CenterScreen; ClientSize = new Size(420, 260); BackColor = Color.FromArgb(0x15, 0x19, 0x22); ShowInTaskbar = true; Text = "BSO Launcher wird installiert"; DoubleBuffered = true; try { Icon = Icon.ExtractAssociatedIcon(Environment.ProcessPath ?? Application.ExecutablePath); } catch { } status.ForeColor = Color.FromArgb(0x8b, 0x95, 0xa5); status.Font = new Font("Segoe UI", 9.5f); status.TextAlign = ContentAlignment.MiddleCenter; status.SetBounds(20, 178, 380, 24); status.Text = "Vorbereiten …"; Controls.Add(status); Shown += async (_, _) => await Run(); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; using (var border = new Pen(Color.FromArgb(0x26, 0x2c, 0x38))) g.DrawRectangle(border, 0, 0, Width - 1, Height - 1); var logo = Installer.Logo(); if (logo != null) g.DrawImage(logo, (Width - 72) / 2, 34, 72, 72); using var title = new Font("Segoe UI", 15f, FontStyle.Bold); TextRenderer.DrawText(g, "BSO Launcher", title, new Rectangle(0, 116, Width, 34), Color.White, TextFormatFlags.HorizontalCenter); using var sub = new Font("Segoe UI", 9f); TextRenderer.DrawText(g, "Bus Simulator Online", sub, new Rectangle(0, 148, Width, 22), Color.FromArgb(0x8b, 0x95, 0xa5), TextFormatFlags.HorizontalCenter); // Fortschrittsbalken var bar = new Rectangle(40, 214, Width - 80, 8); using (var back = new SolidBrush(Color.FromArgb(0x23, 0x2a, 0x36))) g.FillRectangle(back, bar); if (progress > 0) { var fill = new Rectangle(bar.X, bar.Y, (int)(bar.Width * progress), bar.Height); using var brush = new System.Drawing.Drawing2D.LinearGradientBrush(bar, Color.FromArgb(0x1f, 0x6f, 0xe5), Color.FromArgb(0x5e, 0xe4, 0xff), 0f); g.FillRectangle(error != null ? Brushes.IndianRed : brush, fill); } } async Task Run() { try { await Task.Run(() => Installer.Install((p, text) => BeginInvoke(() => { progress = p; status.Text = text; Invalidate(); }))); await Task.Delay(400); Installer.StartInstalled(); Close(); } catch (Exception e) { error = e.Message; status.ForeColor = Color.FromArgb(0xff, 0x8a, 0x8d); status.Text = "Installation fehlgeschlagen: " + e.Message; Invalidate(); await Task.Delay(6000); Close(); } } }