diff --git a/src/UniGetUI.Core.Data/CoreData.cs b/src/UniGetUI.Core.Data/CoreData.cs index 96c35c711..4aa2a5845 100644 --- a/src/UniGetUI.Core.Data/CoreData.cs +++ b/src/UniGetUI.Core.Data/CoreData.cs @@ -169,6 +169,7 @@ public static string IgnoredUpdatesDatabaseFile /// The ID of the notification that is used to inform the user that updates are available /// public const int UpdatesAvailableNotificationTag = 1234; + public const int UniGetUICanBeUpdated = 1235; /// diff --git a/src/UniGetUI.Interface.Enums/Enums.cs b/src/UniGetUI.Interface.Enums/Enums.cs index c76b7f8fd..2992c8f12 100644 --- a/src/UniGetUI.Interface.Enums/Enums.cs +++ b/src/UniGetUI.Interface.Enums/Enums.cs @@ -91,5 +91,6 @@ public class NotificationArguments public const string Show = "openUniGetUI"; public const string ShowOnUpdatesTab = "openUniGetUIOnUpdatesTab"; public const string UpdateAllPackages = "updateAll"; + public const string ReleaseSelfUpdateLock = "releaseSelfUpdateLock"; } } diff --git a/src/UniGetUI/App.xaml.cs b/src/UniGetUI/App.xaml.cs index 1dc0c939a..253b9f08f 100644 --- a/src/UniGetUI/App.xaml.cs +++ b/src/UniGetUI/App.xaml.cs @@ -404,147 +404,6 @@ public async void DisposeAndQuit(int outputCode = 0) Environment.Exit(outputCode); } - private async void UpdateUniGetUIIfPossible(int round = 0) - { - InfoBar? banner = null; - try - { - Logger.Debug("Starting update check"); - - string fileContents; - - using (HttpClient client = new(CoreData.GenericHttpClientParameters)) - { - client.Timeout = TimeSpan.FromSeconds(600); - client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreData.UserAgentString); - fileContents = await client.GetStringAsync("https://www.marticliment.com/versions/unigetui.ver"); - } - - if (!fileContents.Contains("///")) - { - throw new FormatException("The updates file does not follow the FloatVersion///Sha256Hash format"); - } - - float LatestVersion = float.Parse(fileContents.Split("///")[0].Replace("\n", "").Trim(), CultureInfo.InvariantCulture); - string InstallerHash = fileContents.Split("///")[1].Replace("\n", "").Trim().ToLower(); - - if (LatestVersion > CoreData.VersionNumber) - { - Logger.Info("Updates found, downloading installer..."); - Logger.Info("Current version: " + CoreData.VersionNumber.ToString(CultureInfo.InvariantCulture)); - Logger.Info("Latest version : " + LatestVersion.ToString(CultureInfo.InvariantCulture)); - - banner = MainWindow.UpdatesBanner; - banner.Title = CoreTools.Translate("WingetUI version {0} is being downloaded.", LatestVersion.ToString(CultureInfo.InvariantCulture)); - banner.Message = CoreTools.Translate("This may take a minute or two"); - banner.Severity = InfoBarSeverity.Informational; - banner.IsOpen = true; - banner.IsClosable = false; - - Uri DownloadUrl = new("https://github.com/marticliment/WingetUI/releases/latest/download/UniGetUI.Installer.exe"); - string InstallerPath = Path.Join(Directory.CreateTempSubdirectory().FullName, "unigetui-updater.exe"); - - using (HttpClient client = new(CoreData.GenericHttpClientParameters)) - { - client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreData.UserAgentString); - HttpResponseMessage result = await client.GetAsync(DownloadUrl); - using FileStream fs = new(InstallerPath, FileMode.CreateNew); - await result.Content.CopyToAsync(fs); - } - - string Hash = ""; - SHA256 Sha256 = SHA256.Create(); - using (FileStream stream = File.OpenRead(InstallerPath)) - { - Hash = Convert.ToHexString(Sha256.ComputeHash(stream)).ToLower(); - } - - if (Hash == InstallerHash) - { - - banner.Title = CoreTools.Translate("WingetUI {0} is ready to be installed.", LatestVersion.ToString(CultureInfo.InvariantCulture)); - banner.Message = CoreTools.Translate("The update will be installed upon closing WingetUI"); - banner.ActionButton = new Button - { - Content = CoreTools.Translate("Update now") - }; - banner.ActionButton.Click += (_, _) => { MainWindow.HideWindow(); }; - banner.Severity = InfoBarSeverity.Success; - banner.IsOpen = true; - banner.IsClosable = true; - - if (MainWindow.Visible) - { - Logger.Debug("Waiting for mainWindow to be hidden"); - } - - while (MainWindow.Visible) - { - await Task.Delay(100); - } - - if (Settings.Get("DisableAutoUpdateWingetUI")) - { - Logger.Warn("User disabled updates!"); - return; - } - - Logger.ImportantInfo("The hash matches the expected value, starting update process..."); - Process p = new(); - p.StartInfo.FileName = "cmd.exe"; - p.StartInfo.Arguments = $"/c start /B \"\" \"{InstallerPath}\" /silent"; - p.StartInfo.UseShellExecute = true; - p.StartInfo.CreateNoWindow = true; - p.Start(); - DisposeAndQuit(); - } - else - { - Logger.Error("Hash mismatch, not updating!"); - Logger.Error("Current hash : " + Hash); - Logger.Error("Expected hash: " + InstallerHash); - File.Delete(InstallerPath); - - banner.Title = CoreTools.Translate("The installer hash does not match the expected value."); - banner.Message = CoreTools.Translate("The update will not continue."); - banner.Severity = InfoBarSeverity.Error; - banner.IsOpen = true; - banner.IsClosable = true; - - await Task.Delay(3600000); // Check again in 1 hour - UpdateUniGetUIIfPossible(); - } - } - else - { - Logger.Info("UniGetUI is up to date"); - await Task.Delay(3600000); // Check again in 1 hour - UpdateUniGetUIIfPossible(); - } - } - catch (Exception e) - { - if (banner is not null) - { - banner.Title = CoreTools.Translate("An error occurred when checking for updates: "); - banner.Message = e.Message; - banner.Severity = InfoBarSeverity.Error; - banner.IsOpen = true; - banner.IsClosable = true; - } - - Logger.Error(e); - - if (round >= 3) - { - return; - } - - await Task.Delay(600000); // Try again in 10 minutes - UpdateUniGetUIIfPossible(round + 1); - } - } - public void KillAndRestart() { Process.Start(CoreData.UniGetUIExecutableFile); diff --git a/src/UniGetUI/AutoUpdater.cs b/src/UniGetUI/AutoUpdater.cs new file mode 100644 index 000000000..350170872 --- /dev/null +++ b/src/UniGetUI/AutoUpdater.cs @@ -0,0 +1,300 @@ +using System.Diagnostics; +using System.Globalization; +using System.Security.Cryptography; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.Windows.AppNotifications; +using Microsoft.Windows.AppNotifications.Builder; +using UniGetUI.Core.Data; +using UniGetUI.Core.Logging; +using UniGetUI.Core.SettingsEngine; +using UniGetUI.Core.Tools; +using UniGetUI.Interface; +using UniGetUI.Interface.Enums; +using Version = YamlDotNet.Core.Version; + +namespace UniGetUI; + +public class AutoUpdater +{ + public static Window Window = null!; + public static InfoBar Banner = null!; + //------------------------------------------------------------------------------------------------------------------ + private const string STABLE_ENDPOINT = "https://www.marticliment.com/versions/unigetui-stable.ver"; + private const string BETA_ENDPOINT = "https://www.marticliment.com/versions/unigetui-beta.ver"; + private const string STABLE_INSTALLER_URL = "https://github.com/marticliment/UniGetUI/releases/latest/download/UniGetUI.Installer.exe"; + private const string BETA_INSTALLER_URL = "https://github.com/marticliment/UniGetUI/releases/download/$TAG/UniGetUI.Installer.exe"; + //------------------------------------------------------------------------------------------------------------------ + private static bool _installerHasBeenLaunched; + public static bool ReleaseLockForAutoupdate_Notification; + public static bool AnUpdateIsAwaitingWindowClose { get; private set; } + + public static async Task UpdateCheckLoop(Window window, InfoBar banner) + { + Window = window; + Banner = banner; + + bool result = await CheckAndInstallUpdates(window, banner, false); + + // TODO: Wait for internet connection + + // Check for updates + + // Set timer to check again in X time, X = ERROR ? 10min : 120min + } + + /// + /// Performs the entire update process, and returns true/false whether the process finished successfully; + /// + public static async Task CheckAndInstallUpdates(Window window, InfoBar banner, bool Verbose, bool AutoLaunch = false) + { + Window = window; + Banner = banner; + + try + { + if (Verbose) ShowMessage_ThreadSafe( + CoreTools.Translate("We are checking for updates."), + CoreTools.Translate("Please wait"), + InfoBarSeverity.Informational, + false + ); + + // Check for updates + string UpdatesEndpoint = Settings.Get("EnableUniGetUIBeta") ? BETA_ENDPOINT : STABLE_ENDPOINT; + string InstallerDownloadUrl = Settings.Get("EnableUniGetUIBeta") ? BETA_INSTALLER_URL : STABLE_INSTALLER_URL; + var (IsUpgradable, LatestVersion, InstallerHash) = await CheckForUpdates(UpdatesEndpoint); + + if (IsUpgradable) + { + InstallerDownloadUrl = InstallerDownloadUrl.Replace("$TAG", LatestVersion); + + Logger.Info($"An update to UniGetUI version {LatestVersion} is available"); + string InstallerPath = Path.Join(CoreData.UniGetUIDataDirectory, "Updater.exe"); + + if (File.Exists(InstallerPath) + && await CheckInstallerHash(InstallerPath, InstallerHash)) + { + Logger.Info($"A cached valid installer was found, launching update process..."); + return await PrepairToLaunchInstaller(InstallerPath, LatestVersion, AutoLaunch); + } + else + { + File.Delete(InstallerPath); + } + + ShowMessage_ThreadSafe( + CoreTools.Translate("UniGetUI version {0} is being downloaded.", LatestVersion.ToString(CultureInfo.InvariantCulture)), + CoreTools.Translate("This may take a minute or two"), + InfoBarSeverity.Informational, + false); + + // Download the installer + await DownloadInstaller(InstallerDownloadUrl, InstallerPath); + + if (await CheckInstallerHash(InstallerPath, InstallerHash)) + { + Logger.Info("The downloaded installer is valid, launching update process..."); + return await PrepairToLaunchInstaller(InstallerPath, LatestVersion, AutoLaunch); + } + else + { + ShowMessage_ThreadSafe( + CoreTools.Translate("The installer authenticity could not be verified."), + CoreTools.Translate("The update process has been aborted."), + InfoBarSeverity.Error, + true); + return false; + } + } + else + { + if (Verbose) ShowMessage_ThreadSafe( + CoreTools.Translate("Great! You are on the latest version."), + CoreTools.Translate("There are no new UniGetUI versions to be installed"), + InfoBarSeverity.Success, + true + ); + return true; + } + + } + catch (Exception e) + { + ShowMessage_ThreadSafe( + CoreTools.Translate("An error occurred when checking for updates: "), + e.Message, + InfoBarSeverity.Error, + true + ); + return false; + } + } + + /// + /// Checks whether new updates are available, and returns a tuple containing: + /// - A boolean that is set to True if new updates are available + /// - The new version name + /// - The hash of the installer for the new version, as a string. + /// + private static async Task<(bool, string, string)> CheckForUpdates(string endpoint) + { + Logger.Debug($"Begin check for updates on endpoint {endpoint}"); + string[] UpdateResponse; + using (HttpClient client = new(CoreData.GenericHttpClientParameters)) + { + client.Timeout = TimeSpan.FromSeconds(600); + client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreData.UserAgentString); + UpdateResponse = (await client.GetStringAsync(endpoint)).Split("///"); + } + + if (UpdateResponse.Length >= 2) + { + double LatestVersion = double.Parse(UpdateResponse[0].Replace("\n", "").Replace("\r", "").Trim(), CultureInfo.InvariantCulture); + string InstallerHash = UpdateResponse[1].Replace("\n", "").Replace("\r", "").Trim(); + string VersionName = UpdateResponse.Length >= 3 ? UpdateResponse[2] : LatestVersion.ToString(CultureInfo.InvariantCulture); + Logger.Debug($"Got response from endpoint: ({LatestVersion}, {VersionName}, {InstallerHash})"); + return (LatestVersion > CoreData.VersionNumber, VersionName, InstallerHash); + } + + Logger.Warn($"Received update string is {UpdateResponse[0]}"); + throw new FormatException("The updates file does not follow the FloatVersion///Sha256Hash[///VersionName] format"); + } + + /// + /// Checks whether the downloaded updater matches the hash. + /// + private static async Task CheckInstallerHash(string installerLocation, string expectedHash) + { + Logger.Debug($"Checking updater hash on location {installerLocation}"); + using (FileStream stream = File.OpenRead(installerLocation)) + { + string hash = Convert.ToHexString(await SHA256.Create().ComputeHashAsync(stream)).ToLower(); + if (hash == expectedHash.ToLower()) + { + Logger.Debug($"The hashes match ({hash})"); + return true; + } + Logger.Warn($"Hash mismatch.\nExpected: {expectedHash}\nGot: {hash}"); + return false; + } + } + + private static async Task DownloadInstaller(string downloadUrl, string installerLocation) + { + Logger.Debug($"Downloading installer from {downloadUrl} to {installerLocation}"); + using (HttpClient client = new(CoreData.GenericHttpClientParameters)) + { + client.Timeout = TimeSpan.FromSeconds(600); + client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreData.UserAgentString); + HttpResponseMessage result = await client.GetAsync(downloadUrl); + using FileStream fs = new(installerLocation, FileMode.OpenOrCreate); + await result.Content.CopyToAsync(fs); + } + Logger.Debug("The download has finished successfully"); + } + + /// + /// Waits for the window to be closed if it is open and launches the updater + /// + private static async Task PrepairToLaunchInstaller(string installerLocation, string NewVersion, bool AutoLaunch) + { + Logger.Debug("Starting the process to launch the installer."); + AnUpdateIsAwaitingWindowClose = true; + + Window.DispatcherQueue.TryEnqueue(() => + { + // Set the banner to Restart UniGetUI to update + var UpdateNowButton = new Button { Content = CoreTools.Translate("Update now") }; + UpdateNowButton.Click += (_, _) => LaunchInstallerAndQuit(installerLocation); + ShowMessage_ThreadSafe( + CoreTools.Translate("UniGetUI {0} is ready to be installed.", NewVersion), + CoreTools.Translate("The update process will start after closing UniGetUI"), + InfoBarSeverity.Success, + true, + UpdateNowButton); + + // Show a toast notification + AppNotificationBuilder builder = new AppNotificationBuilder() + .SetScenario(AppNotificationScenario.Default) + .SetTag(CoreData.UniGetUICanBeUpdated.ToString()) + .AddText(CoreTools.Translate("{0} can be updated to version {1}", "UniGetUI", NewVersion)) + .SetAttributionText(CoreTools.Translate("You have currently version {0} installed", CoreData.VersionName)) + .AddArgument("action", NotificationArguments.Show) + .AddButton(new AppNotificationButton(CoreTools.Translate("Update now")) + .AddArgument("action", NotificationArguments.ReleaseSelfUpdateLock) + ); + AppNotification notification = builder.BuildNotification(); + notification.ExpiresOnReboot = true; + AppNotificationManager.Default.Show(notification); + + }); + + // Check if the user has disabled updates + if (Settings.Get("DisableAutoUpdateWingetUI")) + { + Logger.Warn("User disabled updates!"); + return true; + } + + if (AutoLaunch) + { + Logger.Debug("Waiting for mainWindow to be hidden or for user to trigger the update from the notification..."); + while (Window.Visible && !ReleaseLockForAutoupdate_Notification) await Task.Delay(100); + } + else + { + Logger.Debug("Waiting for user to trigger the update from the notification..."); + while (!ReleaseLockForAutoupdate_Notification) await Task.Delay(100); + } + LaunchInstallerAndQuit(installerLocation); + return true; + } + + /// + /// Launches the installer located on the installerLocation argument and quits UniGetUI + /// + private static void LaunchInstallerAndQuit(string installerLocation) + { + if (_installerHasBeenLaunched) + { + Logger.Warn("The installer has already been launched, something went wrong if you are seeing this."); + return; + } + + Logger.Debug("Launching the updater..."); + Process p = new() + { + StartInfo = new() + { + FileName = installerLocation, + Arguments = "/SILENT /SUPPRESSMSGBOXES /NORESTART /SP-", + UseShellExecute = true, + CreateNoWindow = true, + } + }; + p.Start(); + _installerHasBeenLaunched = true; + MainApp.Instance.DisposeAndQuit(0); + } + + private static void ShowMessage_ThreadSafe(string Title, string Message, InfoBarSeverity MessageSeverity, bool BannerClosable, Button? ActionButton = null) + { + if (Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread() is null) + { + Window.DispatcherQueue.TryEnqueue(() => ShowMessage_ThreadSafe(Title, Message, MessageSeverity, BannerClosable, ActionButton)); + return; + } + + Banner.Title = Title; + Banner.Message = Message; + Banner.Severity = MessageSeverity; + Banner.IsClosable = BannerClosable; + Banner.ActionButton = ActionButton; + Banner.IsOpen = true; + } + + + + +} diff --git a/src/UniGetUI/MainWindow.xaml.cs b/src/UniGetUI/MainWindow.xaml.cs index 658371077..c8a48d7cf 100644 --- a/src/UniGetUI/MainWindow.xaml.cs +++ b/src/UniGetUI/MainWindow.xaml.cs @@ -130,10 +130,14 @@ public void HandleNotificationActivation(AppNotificationActivatedEventArgs args) { Activate(); } + else if (action == NotificationArguments.ReleaseSelfUpdateLock) + { + AutoUpdater.ReleaseLockForAutoupdate_Notification = true; + } else { throw new ArgumentException( - "args.Argument was not set to a value present in Enums.NotificationArguments"); + $"args.Argument was not set to a value present in Enums.NotificationArguments (value is {action})"); } Logger.Debug("Notification activated: " + args.Arguments);