diff --git a/UniGetUI.iss b/UniGetUI.iss index 2692996635..50cf1f48ab 100644 --- a/UniGetUI.iss +++ b/UniGetUI.iss @@ -1,7 +1,7 @@ ; Script generated by the Inno Setup Script Wizard. ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! -#define MyAppVersion "3.1.2" +#define MyAppVersion "3.1.4-beta1" #define MyAppName "UniGetUI (formerly WingetUI)" #define MyAppPublisher "Martí Climent" #define MyAppURL "https://github.com/marticliment/UniGetUI" @@ -23,7 +23,7 @@ AppPublisher={#MyAppPublisher} AppPublisherURL="https://www.marticliment.com/unigetui/" AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} -VersionInfoVersion=3.1.2.0 +VersionInfoVersion=3.1.4.0 DefaultDirName="{autopf64}\UniGetUI" DisableProgramGroupPage=yes DisableDirPage=no diff --git a/scripts/BuildNumber b/scripts/BuildNumber new file mode 100644 index 0000000000..d7765fe47e --- /dev/null +++ b/scripts/BuildNumber @@ -0,0 +1 @@ +70 \ No newline at end of file diff --git a/scripts/apply_versions.py b/scripts/apply_versions.py index 1d29718d66..42e821ffb2 100644 --- a/scripts/apply_versions.py +++ b/scripts/apply_versions.py @@ -4,14 +4,26 @@ os.chdir(os.path.join(os.path.dirname(__file__), "..")) # move to root project try: - floatval = input("Enter version code (X.XXX) : ") + # floatval = input("Enter version code (X.XXX) : ") + # versionCode = float(floatval) + versionName = str(input("Enter version name (string) : ")) - if floatval == "": + if versionName == "": print("Version changer script aborted") exit() + + BuildNumber = -1 + c = "" + if os.path.exists("scripts/BuildNumber"): + with open("scripts/BuildNumber", "r") as f: + c = f.read() + + BuildNumber = int(c) if c != "" else int(input("Build number file was empty. Insert (integer) build number: "))-1 + print(f"Build number set to {BuildNumber+1}") + with open("scripts/BuildNumber", "w") as f: + f.write(str(BuildNumber+1)) + - versionCode = float(floatval) - versionName = str(input("Enter version name (string) : ")) versionISS = str(input("Enter version (X.X.X.X) : ")) def fileReplaceLinesWith(filename: str, list: dict[str, str], encoding="utf-8"): @@ -32,7 +44,7 @@ def fileReplaceLinesWith(filename: str, list: dict[str, str], encoding="utf-8"): fileReplaceLinesWith("src/UniGetUI.Core.Data/CoreData.cs", { " public const string VersionName = ": f" \"{versionName}\"; // Do not modify this line, use file scripts/apply_versions.py\n", - " public const double VersionNumber = ": f" {versionCode}; // Do not modify this line, use file scripts/apply_versions.py\n", + " public const int BuildNumber = ": f" {BuildNumber+1}; // Do not modify this line, use file scripts/apply_versions.py\n", }, encoding="utf-8-sig") fileReplaceLinesWith("src/SharedAssemblyInfo.cs", { diff --git a/scripts/translation_utils.py b/scripts/translation_utils.py index 1faec50a2d..4df41371d3 100644 --- a/scripts/translation_utils.py +++ b/scripts/translation_utils.py @@ -50,6 +50,8 @@ def get_all_strings(): r'<[a-zA-Z0-9]+:ButtonCard' + MAIN_WILDCARD + r'+Text=["\'].+["\']' + MAIN_WILDCARD + r'*\/?>': lambda match: match.split(" Text=\"")[1].split("\"")[0].encode('raw_unicode_escape').decode('unicode_escape'), r'<[a-zA-Z0-9]+:ButtonCard' + MAIN_WILDCARD + r'+ButtonText=["\'].+["\']' + MAIN_WILDCARD + r'*\/?>': lambda match: match.split(" ButtonText=\"")[1].split("\"")[0].encode('raw_unicode_escape').decode('unicode_escape'), r'<[a-zA-Z0-9]+:CheckboxCard' + MAIN_WILDCARD + r'+Text=["\'].+["\']' + MAIN_WILDCARD + r'*\/?>': lambda match: match.split(" Text=\"")[1].split("\"")[0].encode('raw_unicode_escape').decode('unicode_escape'), + r'<[a-zA-Z0-9]+:CheckboxButtonCard' + MAIN_WILDCARD + r'+CheckboxText=["\'].+["\']' + MAIN_WILDCARD + r'*\/?>': lambda match: match.split(" Text=\"")[1].split("\"")[0].encode('raw_unicode_escape').decode('unicode_escape'), + r'<[a-zA-Z0-9]+:CheckboxButtonCard' + MAIN_WILDCARD + r'+ButtonText=["\'].+["\']' + MAIN_WILDCARD + r'*\/?>': lambda match: match.split(" Text=\"")[1].split("\"")[0].encode('raw_unicode_escape').decode('unicode_escape'), r'<[a-zA-Z0-9]+:ComboboxCard' + MAIN_WILDCARD + r'+Text=["\'].+["\']' + MAIN_WILDCARD + r'*\/?>': lambda match: match.split(" Text=\"")[1].split("\"")[0].encode('raw_unicode_escape').decode('unicode_escape'), r'<[a-zA-Z0-9]+:BetterMenuItem' + MAIN_WILDCARD + r'+Text=["\'].+["\']' + MAIN_WILDCARD + r'*\/?>': lambda match: match.split(" Text=\"")[1].split("\"")[0].encode('raw_unicode_escape').decode('unicode_escape'), r'<[a-zA-Z0-9]+:NavButton' + MAIN_WILDCARD + r'+Text=["\'].+["\']' + MAIN_WILDCARD + r'*\/?>': lambda match: match.split(" Text=\"")[1].split("\"")[0].encode('raw_unicode_escape').decode('unicode_escape'), diff --git a/src/SharedAssemblyInfo.cs b/src/SharedAssemblyInfo.cs index 875020225d..5b6f6a6296 100644 --- a/src/SharedAssemblyInfo.cs +++ b/src/SharedAssemblyInfo.cs @@ -6,7 +6,7 @@ [assembly: AssemblyTitle("UniGetUI")] [assembly: AssemblyDefaultAlias("UniGetUI")] [assembly: AssemblyCopyright("2024, Martí Climent")] -[assembly: AssemblyVersion("3.1.2.0")] -[assembly: AssemblyFileVersion("3.1.2.0")] -[assembly: AssemblyInformationalVersion("3.1.2")] +[assembly: AssemblyVersion("3.1.4.0")] +[assembly: AssemblyFileVersion("3.1.4.0")] +[assembly: AssemblyInformationalVersion("3.1.4-beta1")] [assembly: SupportedOSPlatform("windows10.0.19041")] diff --git a/src/UniGetUI.Core.Data.Tests/CoreTests.cs b/src/UniGetUI.Core.Data.Tests/CoreTests.cs index c49d0b56df..8a9f31509e 100644 --- a/src/UniGetUI.Core.Data.Tests/CoreTests.cs +++ b/src/UniGetUI.Core.Data.Tests/CoreTests.cs @@ -23,12 +23,9 @@ public void CheckDirectoryAttributes(string directory) public void CheckOtherAttributes() { Assert.NotEmpty(CoreData.VersionName); - Assert.NotEqual(0, CoreData.VersionNumber); + Assert.NotEqual(0, CoreData.BuildNumber); Assert.True(File.Exists(CoreData.IgnoredUpdatesDatabaseFile), "The Ignored Updates database file does not exist, but it should have been created automatically."); - int notif_3 = CoreData.UpdatesAvailableNotificationTag; - int notif_4 = CoreData.UpdatesAvailableNotificationTag; - Assert.True(notif_3 == notif_4, "The UpdatesAvailableNotificationId must be always the same"); Assert.NotEqual(0, CoreData.UpdatesAvailableNotificationTag); Assert.True(Directory.Exists(CoreData.UniGetUIExecutableDirectory), "Directory where the executable is located does not exist"); diff --git a/src/UniGetUI.Core.Data/CoreData.cs b/src/UniGetUI.Core.Data/CoreData.cs index 96c35c711c..3eb033b815 100644 --- a/src/UniGetUI.Core.Data/CoreData.cs +++ b/src/UniGetUI.Core.Data/CoreData.cs @@ -47,7 +47,7 @@ private static int GetCodePage() } public const string VersionName = "3.1.4-beta1"; // Do not modify this line, use file scripts/apply_versions.py - public const double VersionNumber = 3.14; // Do not modify this line, use file scripts/apply_versions.py + public const int BuildNumber = 70; // Do not modify this line, use file scripts/apply_versions.py public const string UserAgentString = $"UniGetUI/{VersionName} (https://marticliment.com/unigetui/; contact@marticliment.com)"; @@ -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.Core.Tools/Tools.cs b/src/UniGetUI.Core.Tools/Tools.cs index 7d7ff1e2c6..9e21d427b4 100644 --- a/src/UniGetUI.Core.Tools/Tools.cs +++ b/src/UniGetUI.Core.Tools/Tools.cs @@ -2,9 +2,11 @@ using System.Diagnostics; using System.Globalization; using System.Net; +using System.Net.NetworkInformation; using System.Security.Cryptography; using System.Security.Principal; using System.Text; +using UniGetUI.Core.Classes; using UniGetUI.Core.Data; using UniGetUI.Core.Language; using UniGetUI.Core.Logging; @@ -542,5 +544,43 @@ public static ProcessStartInfo UpdateEnvironmentVariables(ProcessStartInfo info) } return info; } + + + /// + /// Pings the update server and 3 well-known sites to check for internet availability + /// + public static async Task WaitForInternetConnection() + => await (await TaskRecycler.RunOrAttachAsync(_waitForInternetConnection)); + + public static async Task _waitForInternetConnection() + { + Logger.Debug("Checking for internet connectivity. Pinging google.com, microsoft.com, couldflare.com and marticliment.com"); + string[] hosts = ["google.com", "microsoft.com", "cloudflare.com", "marticliment.com"]; + while (true) + { + foreach (var host in hosts) + { + using (var pinger = new Ping()) + { + try + { + PingReply reply = await pinger.SendPingAsync(host, 10); + if (reply.Status is IPStatus.Success) + { + Logger.Debug($"{host} responded successfully to ping, internet connection was validated."); + return; + } + + Logger.Debug($"Could not ping {host}!"); + } + catch (Exception ex) + { + Logger.Debug($"Could not ping {host} with error {ex.Message}. Are you connected to the internet?"); + } + } + } + await Task.Delay(TimeSpan.FromSeconds(5)); + } + } } } diff --git a/src/UniGetUI.Interface.BackgroundApi/BackgroundApi.cs b/src/UniGetUI.Interface.BackgroundApi/BackgroundApi.cs index fdcefa9fc4..2b3fc55c57 100644 --- a/src/UniGetUI.Interface.BackgroundApi/BackgroundApi.cs +++ b/src/UniGetUI.Interface.BackgroundApi/BackgroundApi.cs @@ -174,7 +174,7 @@ public void BuildV1WidgetsApi() return 401; } - return CoreData.VersionNumber.ToString(); + return CoreData.BuildNumber.ToString(); }); // Return found updates diff --git a/src/UniGetUI.Interface.Enums/Enums.cs b/src/UniGetUI.Interface.Enums/Enums.cs index c76b7f8fd6..2992c8f12f 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 1dc0c939af..5021f12741 100644 --- a/src/UniGetUI/App.xaml.cs +++ b/src/UniGetUI/App.xaml.cs @@ -228,12 +228,6 @@ private async Task LoadComponentsAsync() { try { - // Run other initializations asynchronously - if (!Settings.Get("DisableAutoUpdateWingetUI")) - { - UpdateUniGetUIIfPossible(); - } - IconDatabase.InitializeInstance(); IconDatabase.Instance.LoadIconAndScreenshotsDatabase(); @@ -404,147 +398,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 0000000000..adec375c8f --- /dev/null +++ b/src/UniGetUI/AutoUpdater.cs @@ -0,0 +1,352 @@ +using System.Diagnostics; +using System.Globalization; +using System.Net.NetworkInformation; +using System.Security.Cryptography; +using H.NotifyIcon; +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"; + //------------------------------------------------------------------------------------------------------------------ + public static bool ReleaseLockForAutoupdate_Notification; + public static bool ReleaseLockForAutoupdate_Window; + public static bool ReleaseLockForAutoupdate_UpdateBanner; + public static bool UpdateReadyToBeInstalled { get; private set; } + + public static async Task UpdateCheckLoop(Window window, InfoBar banner) + { + if (Settings.Get("DisableAutoUpdateWingetUI")) + { + Logger.Warn("User has disabled updates"); + return; + } + + bool IsFirstLaunch = true; + Window = window; + Banner = banner; + + await CoreTools.WaitForInternetConnection(); + while (true) + { + // User could have disabled updates on runtime + if (Settings.Get("DisableAutoUpdateWingetUI")) + { + Logger.Warn("User has disabled updates"); + return; + } + bool updateSucceeded = await CheckAndInstallUpdates(window, banner, false, IsFirstLaunch); + IsFirstLaunch = false; + await Task.Delay(TimeSpan.FromMinutes(updateSucceeded ? 60 : 10)); + } + } + + /// + /// 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; + bool WasCheckingForUpdates = true; + + 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) + { + WasCheckingForUpdates = false; + InstallerDownloadUrl = InstallerDownloadUrl.Replace("$TAG", LatestVersion); + + Logger.Info($"An update to UniGetUI version {LatestVersion} is available"); + string InstallerPath = Path.Join(CoreData.UniGetUIDataDirectory, "UniGetUI 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) + { + Logger.Error("An error occurred while checking for updates: "); + Logger.Error(e); + // We don't want an error popping if updates can't + if(Verbose || !WasCheckingForUpdates) 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 >= 3) + { + int LatestVersion = int.Parse(UpdateResponse[0].Replace("\n", "").Replace("\r", "").Trim()); + string InstallerHash = UpdateResponse[1].Replace("\n", "").Replace("\r", "").Trim(); + string VersionName = UpdateResponse[2].Replace("\n", "").Replace("\r", "").Trim(); + Logger.Debug($"Got response from endpoint: ({LatestVersion}, {VersionName}, {InstallerHash})"); + return (LatestVersion > CoreData.BuildNumber, 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; + } + } + + /// + /// Downloads the given installer to the given location + /// + 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); + result.EnsureSuccessStatusCode(); + 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."); + UpdateReadyToBeInstalled = true; + ReleaseLockForAutoupdate_Window = false; + ReleaseLockForAutoupdate_Notification = false; + ReleaseLockForAutoupdate_UpdateBanner = false; + + // Check if the user has disabled updates + if (Settings.Get("DisableAutoUpdateWingetUI")) + { + Banner.IsOpen = false; + Logger.Warn("User disabled updates!"); + return true; + } + + Window.DispatcherQueue.TryEnqueue(() => + { + // Set the banner to Restart UniGetUI to update + var UpdateNowButton = new Button { Content = CoreTools.Translate("Update now") }; + UpdateNowButton.Click += (_, _) => ReleaseLockForAutoupdate_UpdateBanner = true; + 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); + + }); + + if (AutoLaunch && !Window.Visible) + { + Logger.Debug("AutoLaunch is enabled and the Window is hidden, launching installer..."); + } + else + { + Logger.Debug("Waiting for mainWindow to be closed or for user to trigger the update from the notification..."); + while ( + !ReleaseLockForAutoupdate_Window && + !ReleaseLockForAutoupdate_Notification && + !ReleaseLockForAutoupdate_UpdateBanner) + { + await Task.Delay(100); + } + Logger.Debug("Autoupdater lock released, launching installer..."); + } + + if (Settings.Get("DisableAutoUpdateWingetUI")) + { + Logger.Warn("User has disabled updates"); + return true; + } + + await LaunchInstallerAndQuit(installerLocation); + return true; + } + + /// + /// Launches the installer located on the installerLocation argument and quits UniGetUI + /// + private static async Task LaunchInstallerAndQuit(string installerLocation) + { + Logger.Debug("Launching the updater..."); + Process p = new() + { + StartInfo = new() + { + FileName = installerLocation, + Arguments = "/SILENT /SUPPRESSMSGBOXES /NORESTART /SP-", + UseShellExecute = true, + CreateNoWindow = true, + } + }; + p.Start(); + ShowMessage_ThreadSafe( + CoreTools.Translate("UniGetUI is being updated..."), + CoreTools.Translate("This may take a minute or two"), + InfoBarSeverity.Informational, + false + ); + await p.WaitForExitAsync(); + ShowMessage_ThreadSafe( + CoreTools.Translate("Something went wrong while launching the updater."), + CoreTools.Translate("Please try again later"), + InfoBarSeverity.Error, + true + ); + } + + private static void ShowMessage_ThreadSafe(string Title, string Message, InfoBarSeverity MessageSeverity, bool BannerClosable, Button? ActionButton = null) + { + try + { + 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; + } + catch (Exception ex) + { + Logger.Error(ex); + } + + } +} diff --git a/src/UniGetUI/Controls/SettingsWidgets/CheckboxButtonCard.cs b/src/UniGetUI/Controls/SettingsWidgets/CheckboxButtonCard.cs new file mode 100644 index 0000000000..3fee7e56f7 --- /dev/null +++ b/src/UniGetUI/Controls/SettingsWidgets/CheckboxButtonCard.cs @@ -0,0 +1,87 @@ +using CommunityToolkit.WinUI.Controls; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using UniGetUI.Core.SettingsEngine; +using UniGetUI.Core.Tools; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace UniGetUI.Interface.Widgets +{ + public sealed class CheckboxButtonCard : SettingsCard + { + public CheckBox CheckBox; + public Button Button; + private bool IS_INVERTED; + + private string setting_name = ""; + public string SettingName + { + set { + setting_name = value; + IS_INVERTED = value.StartsWith("Disable"); + CheckBox.IsChecked = Settings.Get(setting_name) ^ IS_INVERTED ^ ForceInversion; + Button.IsEnabled = (CheckBox.IsChecked ?? false) || _buttonAlwaysOn ; + } + } + + public bool ForceInversion { get; set; } + + public bool Checked + { + get => CheckBox.IsChecked ?? false; + } + public event EventHandler? StateChanged; + public new event EventHandler? Click; + + public string CheckboxText + { + set => CheckBox.Content = CoreTools.Translate(value); + } + + public string ButtonText + { + set => Button.Content = CoreTools.Translate(value); + } + + private bool _buttonAlwaysOn; + public bool ButtonAlwaysOn + { + set => _buttonAlwaysOn = value; + } + + + public CheckboxButtonCard() + { + Button = new Button(); + CheckBox = new CheckBox(); + IS_INVERTED = false; + + //ContentAlignment = ContentAlignment.Left; + //HorizontalAlignment = HorizontalAlignment.Stretch; + + DefaultStyleKey = typeof(CheckboxCard); + Description = CheckBox; + Content = Button; + CheckBox.HorizontalAlignment = HorizontalAlignment.Stretch; + CheckBox.Checked += (_, _) => + { + Settings.Set(setting_name, true ^ IS_INVERTED ^ ForceInversion); + StateChanged?.Invoke(this, EventArgs.Empty); + Button.IsEnabled = true; + }; + + CheckBox.Unchecked += (_, _) => + { + Settings.Set(setting_name, false ^ IS_INVERTED ^ ForceInversion); + StateChanged?.Invoke(this, EventArgs.Empty); + Button.IsEnabled = _buttonAlwaysOn; + }; + + + Button.MinWidth = 200; + Button.Click += (s, e) => Click?.Invoke(s, e); + } + } +} diff --git a/src/UniGetUI/EntryPoint.cs b/src/UniGetUI/EntryPoint.cs index 8a96f4ee86..71702ada15 100644 --- a/src/UniGetUI/EntryPoint.cs +++ b/src/UniGetUI/EntryPoint.cs @@ -55,7 +55,7 @@ Welcome to UniGetUI Version {CoreData.VersionName} Logger.ImportantInfo(textart); Logger.ImportantInfo(" "); - Logger.ImportantInfo($"Version Code: {CoreData.VersionNumber}"); + Logger.ImportantInfo($"Build {CoreData.BuildNumber}"); Logger.ImportantInfo($"Encoding Code Page set to {CoreData.CODE_PAGE}"); // WinRT single-instance fancy stuff diff --git a/src/UniGetUI/MainWindow.xaml.cs b/src/UniGetUI/MainWindow.xaml.cs index 6583710772..27a6c0e277 100644 --- a/src/UniGetUI/MainWindow.xaml.cs +++ b/src/UniGetUI/MainWindow.xaml.cs @@ -110,6 +110,8 @@ public MainWindow() { ParametersToProcess.Enqueue(arg); } + + _ = AutoUpdater.UpdateCheckLoop(this, UpdatesBanner); } public void HandleNotificationActivation(AppNotificationActivatedEventArgs args) @@ -130,10 +132,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); @@ -144,8 +150,9 @@ public void HandleNotificationActivation(AppNotificationActivatedEventArgs args) /// public async void HandleClosingEvent(AppWindow sender, AppWindowClosingEventArgs args) { + AutoUpdater.ReleaseLockForAutoupdate_Window = true; SaveGeometry(Force: true); - if (!Settings.Get("DisableSystemTray")) + if (!Settings.Get("DisableSystemTray") || AutoUpdater.UpdateReadyToBeInstalled) { args.Cancel = true; try diff --git a/src/UniGetUI/Pages/SettingsPage.xaml b/src/UniGetUI/Pages/SettingsPage.xaml index a4a21f1150..1ca315a095 100644 --- a/src/UniGetUI/Pages/SettingsPage.xaml +++ b/src/UniGetUI/Pages/SettingsPage.xaml @@ -75,9 +75,15 @@ Click="OpenWelcomeWizard" IsEnabled="False" /--> - + ProjectDebugger - ModernWindow (Unpackaged) + UniGetUI (Unpackaged) diff --git a/src/UniGetUI/app.manifest b/src/UniGetUI/app.manifest index c899a29bb2..70ca760360 100644 --- a/src/UniGetUI/app.manifest +++ b/src/UniGetUI/app.manifest @@ -2,7 +2,7 @@