From 7890fa380a4a669ed2d020df5acdab9d7a68fcb1 Mon Sep 17 00:00:00 2001 From: rjindael Date: Tue, 1 Aug 2023 22:59:28 -0700 Subject: [PATCH] feat: add uninstall code --- Kiseki.Launcher.Windows/Launcher.cs | 76 +++++++++++++++++++++++-- Kiseki.Launcher.Windows/Program.cs | 6 ++ Kiseki.Launcher.Windows/Protocol.cs | 12 ++-- Kiseki.Launcher.Windows/Win32.cs | 15 ++++- Kiseki.Launcher/Controller.cs | 2 + Kiseki.Launcher/Helpers/Http.cs | 1 + Kiseki.Launcher/Interfaces/ILauncher.cs | 1 + Kiseki.Launcher/Interfaces/IProtocol.cs | 4 +- Kiseki.Launcher/Web.cs | 6 +- 9 files changed, 103 insertions(+), 20 deletions(-) diff --git a/Kiseki.Launcher.Windows/Launcher.cs b/Kiseki.Launcher.Windows/Launcher.cs index 78ec55e..07ecc67 100644 --- a/Kiseki.Launcher.Windows/Launcher.cs +++ b/Kiseki.Launcher.Windows/Launcher.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Reflection; using Microsoft.Win32; @@ -15,14 +16,69 @@ namespace Kiseki.Launcher.Windows Directory.CreateDirectory(Directories.Base); } - // TODO: Implement this - public static void Register() + public static void Uninstall(bool quiet = false) { - using (RegistryKey applicationKey = Registry.CurrentUser.CreateSubKey($@"Software\{Constants.PROJECT_NAME}")) + DialogResult answer = quiet ? DialogResult.Yes : MessageBox.Show($"Are you sure you want to uninstall {Constants.PROJECT_NAME}?", Constants.PROJECT_NAME, MessageBoxButtons.YesNo, MessageBoxIcon.Warning); + + if (answer != DialogResult.Yes) + Environment.Exit((int)Win32.ErrorCode.ERROR_CANCELLED); + + // Close active processes + if (Process.GetProcessesByName($"{Constants.PROJECT_NAME}.Player").Any() || Process.GetProcessesByName($"{Constants.PROJECT_NAME}.Studio").Any()) { - applicationKey.SetValue("InstallLocation", Directories.Base); + answer = quiet ? DialogResult.Yes : MessageBox.Show($"Kiseki is currently running. Would you like to close Kiseki now?", Constants.PROJECT_NAME, MessageBoxButtons.YesNo, MessageBoxIcon.Warning); + + if (answer != DialogResult.Yes) + Environment.Exit((int)Win32.ErrorCode.ERROR_CANCELLED); + + try + { + foreach (Process process in Process.GetProcessesByName($"{Constants.PROJECT_NAME}.Player")) + process.Kill(); + + foreach (Process process in Process.GetProcessesByName($"{Constants.PROJECT_NAME}.Studio")) + process.Kill(); + } + catch + { + Environment.Exit((int)Win32.ErrorCode.ERROR_INTERNAL_ERROR); + } } + // Delete all files + Directory.Delete(Directories.Logs, true); + Directory.Delete(Directories.Versions, true); + Directory.Delete(Directories.License); + + // Cleanup our registry entries + Unregister(); + Protocol.Unregister(); + + answer = quiet ? DialogResult.OK : MessageBox.Show($"Sucessfully uninstalled {Constants.PROJECT_NAME}!", Constants.PROJECT_NAME, MessageBoxButtons.OK, MessageBoxIcon.Information); + if (answer == DialogResult.OK || answer == DialogResult.Cancel) + { + string command = $"del /Q \"{Directories.Application}\""; + + if (Directory.GetFiles(Directories.Base, "*", SearchOption.AllDirectories).Length == 1) + { + // We're the only file in the directory, so we can delete the entire directory + command += $" && rmdir \"{Directories.Base}\""; + } + + Process.Start(new ProcessStartInfo() + { + FileName = "cmd.exe", + Arguments = $"/c timeout 5 && {command}", + UseShellExecute = true, + WindowStyle = ProcessWindowStyle.Hidden + }); + + Environment.Exit((int)Win32.ErrorCode.ERROR_SUCCESS); + } + } + + public static void Register() + { using RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{Constants.PROJECT_NAME}"); uninstallKey.SetValue("DisplayIcon", $"{Directories.Application},0"); @@ -42,10 +98,18 @@ namespace Kiseki.Launcher.Windows uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{Constants.PROJECT_REPOSITORY}/releases/latest"); } - // TODO: Implement this public static void Unregister() { - Registry.CurrentUser.DeleteSubKey($@"Software\{Constants.PROJECT_NAME}"); + try + { + Registry.CurrentUser.DeleteSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{Constants.PROJECT_NAME}"); + } + catch + { +#if DEBUG + throw; +#endif + } } #endregion diff --git a/Kiseki.Launcher.Windows/Program.cs b/Kiseki.Launcher.Windows/Program.cs index 45f2f9d..7cfc367 100644 --- a/Kiseki.Launcher.Windows/Program.cs +++ b/Kiseki.Launcher.Windows/Program.cs @@ -47,6 +47,12 @@ namespace Kiseki.Launcher.Windows return; } + if (args[0] == "uninstall") + { + Launcher.Uninstall(args[0] == "-quiet"); + return; + } + ApplicationConfiguration.Initialize(); Application.Run(new MainWindow(args[0])); } diff --git a/Kiseki.Launcher.Windows/Protocol.cs b/Kiseki.Launcher.Windows/Protocol.cs index b6d81ec..cdc11a5 100644 --- a/Kiseki.Launcher.Windows/Protocol.cs +++ b/Kiseki.Launcher.Windows/Protocol.cs @@ -6,17 +6,17 @@ namespace Kiseki.Launcher.Windows { public const string PROTOCOL_KEY = "kiseki"; - public void Register(string key, string name, string handler) + public static void Register() { - string arguments = $"\"{handler}\" \"%1\""; + string arguments = $"\"{Directories.Application}\" \"%1\""; - RegistryKey uriKey = Registry.CurrentUser.CreateSubKey(@$"Software\Classes\{key}"); + RegistryKey uriKey = Registry.CurrentUser.CreateSubKey(@$"Software\Classes\{PROTOCOL_KEY}"); RegistryKey uriIconKey = uriKey.CreateSubKey("DefaultIcon"); RegistryKey uriCommandKey = uriKey.CreateSubKey(@"shell\open\command"); if (uriKey.GetValue("") is null) { - uriKey.SetValue("", $"URL: {name} Protocol"); + uriKey.SetValue("", $"URL: {Constants.PROJECT_NAME} Protocol"); uriKey.SetValue("URL Protocol", ""); } @@ -30,9 +30,9 @@ namespace Kiseki.Launcher.Windows uriCommandKey.Close(); } - public void Unregister(string key) + public static void Unregister() { - Registry.CurrentUser.DeleteSubKeyTree(@$"Software\Classes\{key}"); + Registry.CurrentUser.DeleteSubKeyTree(@$"Software\Classes\{PROTOCOL_KEY}"); } } } \ No newline at end of file diff --git a/Kiseki.Launcher.Windows/Win32.cs b/Kiseki.Launcher.Windows/Win32.cs index 39934a5..91e1942 100644 --- a/Kiseki.Launcher.Windows/Win32.cs +++ b/Kiseki.Launcher.Windows/Win32.cs @@ -4,13 +4,24 @@ namespace Kiseki.Launcher.Windows { public static class Win32 { - [DllImport("shell32", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] - private static extern string SHGetKnownFolderPath([MarshalAs(UnmanagedType.LPStruct)] Guid rfid, uint dwFlags, nint hToken = default); + // https://learn.microsoft.com/en-us/windows/win32/msi/error-codes + // https://i-logic.com/serial/errorcodes.htm + public enum ErrorCode + { + ERROR_SUCCESS = 0, + ERROR_INSTALL_USEREXIT = 1602, + ERROR_INSTALL_FAILURE = 1603, + ERROR_CANCELLED = 1223, + ERROR_INTERNAL_ERROR = 1359 + } // https://www.codeproject.com/Articles/878605/Getting-All-Special-Folders-in-NET public static string GetDownloadsPath() { return SHGetKnownFolderPath(new("374DE290-123F-4565-9164-39C4925E467B"), 0); } + + [DllImport("shell32", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] + private static extern string SHGetKnownFolderPath([MarshalAs(UnmanagedType.LPStruct)] Guid rfid, uint dwFlags, nint hToken = default); } } \ No newline at end of file diff --git a/Kiseki.Launcher/Controller.cs b/Kiseki.Launcher/Controller.cs index 9f3531f..5c4fcaf 100644 --- a/Kiseki.Launcher/Controller.cs +++ b/Kiseki.Launcher/Controller.cs @@ -49,6 +49,7 @@ namespace Kiseki.Launcher { PageHeadingChange("Downloading Kiseki..."); ProgressBarStateChange(ProgressBarState.Normal); + marquee = false; } @@ -60,6 +61,7 @@ namespace Kiseki.Launcher await Task.Delay(2800); yield return 4; + await Task.Delay(200); } diff --git a/Kiseki.Launcher/Helpers/Http.cs b/Kiseki.Launcher/Helpers/Http.cs index 72009a8..4ecd706 100644 --- a/Kiseki.Launcher/Helpers/Http.cs +++ b/Kiseki.Launcher/Helpers/Http.cs @@ -9,6 +9,7 @@ namespace Kiseki.Launcher.Helpers try { string json = Web.HttpClient.GetStringAsync(url).Result; + return JsonSerializer.Deserialize(json); } catch diff --git a/Kiseki.Launcher/Interfaces/ILauncher.cs b/Kiseki.Launcher/Interfaces/ILauncher.cs index 91cb9f6..619a85c 100644 --- a/Kiseki.Launcher/Interfaces/ILauncher.cs +++ b/Kiseki.Launcher/Interfaces/ILauncher.cs @@ -3,6 +3,7 @@ namespace Kiseki.Launcher public interface ILauncher { static abstract void Install(); + static abstract void Uninstall(bool quiet = false); static abstract void Register(); static abstract void Unregister(); } diff --git a/Kiseki.Launcher/Interfaces/IProtocol.cs b/Kiseki.Launcher/Interfaces/IProtocol.cs index a21ed2a..7ba76d9 100644 --- a/Kiseki.Launcher/Interfaces/IProtocol.cs +++ b/Kiseki.Launcher/Interfaces/IProtocol.cs @@ -2,7 +2,7 @@ namespace Kiseki.Launcher { public interface IProtocol { - void Register(string key, string name, string handler); - void Unregister(string key); + static abstract void Register(); + static abstract void Unregister(); } } \ No newline at end of file diff --git a/Kiseki.Launcher/Web.cs b/Kiseki.Launcher/Web.cs index 27e47c7..8c45a94 100644 --- a/Kiseki.Launcher/Web.cs +++ b/Kiseki.Launcher/Web.cs @@ -22,12 +22,11 @@ namespace Kiseki.Launcher CurrentUrl = IsInMaintenance ? $"{MAINTENANCE_DOMAIN}.{BASE_URL}" : BASE_URL; int response = CheckHealth(); + if (response != RESPONSE_SUCCESS) { if (response == RESPONSE_MAINTENANCE) - { IsInMaintenance = true; - } return false; } @@ -55,10 +54,9 @@ namespace Kiseki.Launcher HttpClient.DefaultRequestHeaders.Clear(); var headers = JsonSerializer.Deserialize>(license)!; + for (int i = 0; i < headers.Count; i++) - { HttpClient.DefaultRequestHeaders.Add(headers.ElementAt(i).Key, headers.ElementAt(i).Value); - } } catch {