From 2791cb0b2ed5187b898d7c95cb887acbb329d80a Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Fri, 30 Aug 2024 13:29:51 +0100 Subject: [PATCH] Refactor automatic updater + fix install details + fix launch flag parser + fix temp directory Automatic updater now relies on the -upgrade flag specifically being set and uses a mutex for coordinating the process Temp directory is now obtained appropriately (should fix exceptions relating to it?) Installation details are now reconfigured on every upgrade Specifying a nonexistant flag would insta-crash the app Also, the message box was making the wrong sound for the warning icon --- Bloxstrap/App.xaml.cs | 27 ++++- Bloxstrap/Bootstrapper.cs | 112 ++++++++++++------ Bloxstrap/Installer.cs | 60 +++++----- Bloxstrap/LaunchSettings.cs | 6 +- Bloxstrap/Paths.cs | 4 + Bloxstrap/Resources/Strings.Designer.cs | 2 +- Bloxstrap/Resources/Strings.resx | 2 +- .../Elements/Dialogs/FluentMessageBox.xaml.cs | 2 +- .../ViewModels/Dialogs/LaunchMenuViewModel.cs | 1 - .../ViewModels/Installer/WelcomeViewModel.cs | 27 +---- 10 files changed, 144 insertions(+), 99 deletions(-) diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index 695a0f1..3a02b2e 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -15,7 +15,11 @@ namespace Bloxstrap public partial class App : Application { public const string ProjectName = "Bloxstrap"; + public const string ProjectOwner = "pizzaboxer"; public const string ProjectRepository = "pizzaboxer/bloxstrap"; + public const string ProjectDownloadLink = "https://bloxstrap.pizzaboxer.xyz"; + public const string ProjectHelpLink = "https://github.com/pizzaboxer/bloxstrap/wiki"; + public const string ProjectSupportLink = "https://github.com/pizzaboxer/bloxstrap/issues/new"; public const string RobloxPlayerAppName = "RobloxPlayerBeta"; public const string RobloxStudioAppName = "RobloxStudioBeta"; @@ -103,6 +107,27 @@ namespace Bloxstrap Terminate(ErrorCode.ERROR_INSTALL_FAILURE); } + public static async Task GetLatestRelease() + { + const string LOG_IDENT = "App::GetLatestRelease"; + + GithubRelease? releaseInfo = null; + + try + { + releaseInfo = await Http.GetJson($"https://api.github.com/repos/{ProjectRepository}/releases/latest"); + + if (releaseInfo is null || releaseInfo.Assets is null) + Logger.WriteLine(LOG_IDENT, "Encountered invalid data"); + } + catch (Exception ex) + { + Logger.WriteException(LOG_IDENT, ex); + } + + return releaseInfo; + } + protected override void OnStartup(StartupEventArgs e) { const string LOG_IDENT = "App::OnStartup"; @@ -221,7 +246,7 @@ namespace Bloxstrap Locale.Set(Settings.Prop.Locale); #if !DEBUG - if (!LaunchSettings.UninstallFlag.Active) + if (!LaunchSettings.BypassUpdateCheck) Installer.HandleUpgrade(); #endif diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 969966b..16c7c84 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -1,4 +1,17 @@ -using System.Windows; +// To debug the automatic updater: +// - Uncomment the definition below +// - Publish the executable +// - Launch the executable (click no when it asks you to upgrade) +// - Launch Roblox (for testing web launches, run it from the command prompt) +// - To re-test the same executable, delete it from the installation folder + +// #define DEBUG_UPDATER + +#if DEBUG_UPDATER +#warning "Automatic updater debugging is enabled" +#endif + +using System.Windows; using System.Windows.Forms; using Microsoft.Win32; @@ -152,9 +165,14 @@ namespace Bloxstrap await RobloxDeployment.GetInfo(RobloxDeployment.DefaultChannel); -#if !DEBUG - if (App.Settings.Prop.CheckForUpdates) - await CheckForUpdates(); +#if !DEBUG || DEBUG_UPDATER + if (App.Settings.Prop.CheckForUpdates && !App.LaunchSettings.UpgradeFlag.Active) + { + bool updatePresent = await CheckForUpdates(); + + if (updatePresent) + return; + } #endif // ensure only one instance of the bootstrapper is running at the time @@ -405,7 +423,7 @@ namespace Bloxstrap App.Terminate(ErrorCode.ERROR_CANCELLED); } - #endregion +#endregion #region App Install public void RegisterProgramSize() @@ -449,53 +467,56 @@ namespace Bloxstrap #endif } - private async Task CheckForUpdates() + private async Task CheckForUpdates() { const string LOG_IDENT = "Bootstrapper::CheckForUpdates"; // don't update if there's another instance running (likely running in the background) - if (Process.GetProcessesByName(App.ProjectName).Count() > 1) + // i don't like this, but there isn't much better way of doing it /shrug + if (Process.GetProcessesByName(App.ProjectName).Length > 1) { App.Logger.WriteLine(LOG_IDENT, $"More than one Bloxstrap instance running, aborting update check"); - return; + return false; } - App.Logger.WriteLine(LOG_IDENT, $"Checking for updates..."); + App.Logger.WriteLine(LOG_IDENT, "Checking for updates..."); - GithubRelease? releaseInfo; +#if !DEBUG_UPDATER + var releaseInfo = await App.GetLatestRelease(); - try - { - releaseInfo = await Http.GetJson($"https://api.github.com/repos/{App.ProjectRepository}/releases/latest"); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, $"Failed to fetch releases: {ex}"); - return; - } - - if (releaseInfo is null || releaseInfo.Assets is null) - { - App.Logger.WriteLine(LOG_IDENT, $"No updates found"); - return; - } + if (releaseInfo is null) + return false; var versionComparison = Utilities.CompareVersions(App.Version, releaseInfo.TagName); // check if we aren't using a deployed build, so we can update to one if a new version comes out - if (versionComparison == VersionComparison.Equal && App.IsProductionBuild || versionComparison == VersionComparison.GreaterThan) + if (App.IsProductionBuild && versionComparison == VersionComparison.Equal || versionComparison == VersionComparison.GreaterThan) { - App.Logger.WriteLine(LOG_IDENT, $"No updates found"); - return; + App.Logger.WriteLine(LOG_IDENT, "No updates found"); + return false; } + string version = releaseInfo.TagName; +#else + string version = App.Version; +#endif + SetStatus(Strings.Bootstrapper_Status_UpgradingBloxstrap); - + try { - // 64-bit is always the first option - GithubReleaseAsset asset = releaseInfo.Assets[0]; - string downloadLocation = Path.Combine(Paths.LocalAppData, "Temp", asset.Name); +#if DEBUG_UPDATER + string downloadLocation = Path.Combine(Paths.TempUpdates, "Bloxstrap.exe"); + + Directory.CreateDirectory(Paths.TempUpdates); + + File.Copy(Paths.Process, downloadLocation, true); +#else + var asset = releaseInfo.Assets![0]; + + string downloadLocation = Path.Combine(Paths.TempUpdates, asset.Name); + + Directory.CreateDirectory(Paths.TempUpdates); App.Logger.WriteLine(LOG_IDENT, $"Downloading {releaseInfo.TagName}..."); @@ -503,25 +524,35 @@ namespace Bloxstrap { var response = await App.HttpClient.GetAsync(asset.BrowserDownloadUrl); - await using var fileStream = new FileStream(downloadLocation, FileMode.CreateNew); + await using var fileStream = new FileStream(downloadLocation, FileMode.OpenOrCreate, FileAccess.Write); await response.Content.CopyToAsync(fileStream); } +#endif - App.Logger.WriteLine(LOG_IDENT, $"Starting {releaseInfo.TagName}..."); + App.Logger.WriteLine(LOG_IDENT, $"Starting {version}..."); ProcessStartInfo startInfo = new() { FileName = downloadLocation, }; + startInfo.ArgumentList.Add("-upgrade"); + foreach (string arg in App.LaunchSettings.Args) startInfo.ArgumentList.Add(arg); - + + if (_launchMode == LaunchMode.Player && !startInfo.ArgumentList.Contains("-player")) + startInfo.ArgumentList.Add("-player"); + else if (_launchMode == LaunchMode.Studio && !startInfo.ArgumentList.Contains("-studio")) + startInfo.ArgumentList.Add("-studio"); + App.Settings.Save(); + + new InterProcessLock("AutoUpdater"); Process.Start(startInfo); - App.Terminate(); + return true; } catch (Exception ex) { @@ -529,10 +560,14 @@ namespace Bloxstrap App.Logger.WriteException(LOG_IDENT, ex); Frontend.ShowMessageBox( - string.Format(Strings.Bootstrapper_AutoUpdateFailed, releaseInfo.TagName), + string.Format(Strings.Bootstrapper_AutoUpdateFailed, version), MessageBoxImage.Information ); + + Utilities.ShellExecute(App.ProjectDownloadLink); } + + return false; } #endregion @@ -851,6 +886,7 @@ namespace Bloxstrap foreach (FontFace fontFace in fontFamilyData.Faces) fontFace.AssetId = "rbxasset://fonts/CustomFont.ttf"; + // TODO: writing on every launch is not necessary File.WriteAllText(modFilepath, JsonSerializer.Serialize(fontFamilyData, new JsonSerializerOptions { WriteIndented = true })); } @@ -902,6 +938,8 @@ namespace Bloxstrap // the manifest is primarily here to keep track of what files have been // deleted from the modifications folder, so that we know when to restore the original files from the downloaded packages // now check for files that have been deleted from the mod folder according to the manifest + + // TODO: this needs to extract the files from packages in bulk, this is way too slow foreach (string fileLocation in App.State.Prop.ModManifest) { if (modFolderFiles.Contains(fileLocation)) diff --git a/Bloxstrap/Installer.cs b/Bloxstrap/Installer.cs index 849a834..2e841fd 100644 --- a/Bloxstrap/Installer.cs +++ b/Bloxstrap/Installer.cs @@ -1,9 +1,4 @@ -using System.DirectoryServices; -using System.Reflection; -using System.Reflection.Metadata.Ecma335; -using System.Windows; -using System.Windows.Media.Animation; -using Bloxstrap.Resources; +using System.Windows; using Microsoft.Win32; namespace Bloxstrap @@ -50,12 +45,13 @@ namespace Bloxstrap uninstallKey.SetValue("InstallLocation", Paths.Base); uninstallKey.SetValue("NoRepair", 1); - uninstallKey.SetValue("Publisher", "pizzaboxer"); + uninstallKey.SetValue("Publisher", App.ProjectOwner); uninstallKey.SetValue("ModifyPath", $"\"{Paths.Application}\" -settings"); uninstallKey.SetValue("QuietUninstallString", $"\"{Paths.Application}\" -uninstall -quiet"); uninstallKey.SetValue("UninstallString", $"\"{Paths.Application}\" -uninstall"); - uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{App.ProjectRepository}"); - uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{App.ProjectRepository}/releases/latest"); + uninstallKey.SetValue("HelpLink", App.ProjectHelpLink); + uninstallKey.SetValue("URLInfoAbout", App.ProjectSupportLink); + uninstallKey.SetValue("URLUpdateInfo", App.ProjectDownloadLink); } // only register player, for the scenario where the user installs bloxstrap, closes it, @@ -331,8 +327,9 @@ namespace Bloxstrap return; // 2.0.0 downloads updates to /Updates so lol - // TODO: 2.8.0 will download them to /Bloxstrap/Updates - bool isAutoUpgrade = Paths.Process.StartsWith(Path.Combine(Paths.Base, "Updates")) || Paths.Process.StartsWith(Path.Combine(Paths.LocalAppData, "Temp")); + bool isAutoUpgrade = App.LaunchSettings.UpgradeFlag.Active + || Paths.Process.StartsWith(Path.Combine(Paths.Base, "Updates")) + || Paths.Process.StartsWith(Paths.Temp); var existingVer = FileVersionInfo.GetVersionInfo(Paths.Application).ProductVersion; var currentVer = FileVersionInfo.GetVersionInfo(Paths.Process).ProductVersion; @@ -353,7 +350,7 @@ namespace Bloxstrap } // silently upgrade version if the command line flag is set or if we're launching from an auto update - if (!App.LaunchSettings.UpgradeFlag.Active && !isAutoUpgrade) + if (!isAutoUpgrade) { var result = Frontend.ShowMessageBox( Strings.InstallChecker_VersionDifferentThanInstalled, @@ -365,41 +362,38 @@ namespace Bloxstrap return; } + App.Logger.WriteLine(LOG_IDENT, "Doing upgrade"); + Filesystem.AssertReadOnly(Paths.Application); - // TODO: make this use a mutex somehow - // yes, this is EXTREMELY hacky, but the updater process that launched the - // new version may still be open and so we have to wait for it to close - int attempts = 0; - while (attempts < 10) + using (var ipl = new InterProcessLock("AutoUpdater", TimeSpan.FromSeconds(5))) { - attempts++; - - try + if (!ipl.IsAcquired) { - File.Delete(Paths.Application); - break; - } - catch (Exception) - { - if (attempts == 1) - App.Logger.WriteLine(LOG_IDENT, "Waiting for write permissions to update version"); - - Thread.Sleep(500); + App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not obtain singleton mutex)"); + return; } } - if (attempts == 10) + try { - App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not get write permissions after 5 seconds)"); + File.Copy(Paths.Process, Paths.Application, true); + } + catch (Exception ex) + { + App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not replace executable)"); + App.Logger.WriteException(LOG_IDENT, ex); return; } - File.Copy(Paths.Process, Paths.Application); - using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey)) { uninstallKey.SetValue("DisplayVersion", App.Version); + + uninstallKey.SetValue("Publisher", App.ProjectOwner); + uninstallKey.SetValue("HelpLink", App.ProjectHelpLink); + uninstallKey.SetValue("URLInfoAbout", App.ProjectSupportLink); + uninstallKey.SetValue("URLUpdateInfo", App.ProjectDownloadLink); } // update migrations diff --git a/Bloxstrap/LaunchSettings.cs b/Bloxstrap/LaunchSettings.cs index 046fd9a..25a24fa 100644 --- a/Bloxstrap/LaunchSettings.cs +++ b/Bloxstrap/LaunchSettings.cs @@ -28,6 +28,8 @@ namespace Bloxstrap public LaunchFlag StudioFlag { get; } = new("studio"); + public bool BypassUpdateCheck => UninstallFlag.Active || WatcherFlag.Active; + public LaunchMode RobloxLaunchMode { get; set; } = LaunchMode.None; public string RobloxLaunchArgs { get; private set; } = ""; @@ -37,7 +39,7 @@ namespace Bloxstrap /// public string[] Args { get; private set; } - private Dictionary _flagMap = new(); + private readonly Dictionary _flagMap = new(); public LaunchSettings(string[] args) { @@ -68,7 +70,7 @@ namespace Bloxstrap string identifier = arg[1..]; - if (_flagMap[identifier] is not LaunchFlag flag) + if (!_flagMap.TryGetValue(identifier, out LaunchFlag? flag) || flag is null) continue; flag.Active = true; diff --git a/Bloxstrap/Paths.cs b/Bloxstrap/Paths.cs index cd04cc8..43d4a1c 100644 --- a/Bloxstrap/Paths.cs +++ b/Bloxstrap/Paths.cs @@ -4,6 +4,7 @@ { // note that these are directories that aren't tethered to the basedirectory // so these can safely be called before initialization + public static string Temp => Path.Combine(Path.GetTempPath(), App.ProjectName); public static string UserProfile => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); public static string LocalAppData => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); public static string Desktop => Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); @@ -12,6 +13,9 @@ public static string Process => Environment.ProcessPath!; + public static string TempUpdates => Path.Combine(Temp, "Updates"); + public static string TempLogs => Path.Combine(Temp, "Logs"); + public static string Base { get; private set; } = ""; public static string Downloads { get; private set; } = ""; public static string Logs { get; private set; } = ""; diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs index 28c8fc2..0b754ca 100644 --- a/Bloxstrap/Resources/Strings.Designer.cs +++ b/Bloxstrap/Resources/Strings.Designer.cs @@ -106,7 +106,7 @@ namespace Bloxstrap.Resources { } /// - /// Looks up a localized string similar to Bloxstrap was unable to auto-update to {0}. Please update it manually by downloading and running the latest release from the GitHub page.. + /// Looks up a localized string similar to Bloxstrap was unable to automatically update to version {0}. Please update it manually by downloading and running it from the website.. /// public static string Bootstrapper_AutoUpdateFailed { get { diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx index 6f57c25..8f12701 100644 --- a/Bloxstrap/Resources/Strings.resx +++ b/Bloxstrap/Resources/Strings.resx @@ -124,7 +124,7 @@ lookup failed - Bloxstrap was unable to auto-update to {0}. Please update it manually by downloading and running the latest release from the GitHub page. + Bloxstrap was unable to automatically update to version {0}. Please update it manually by downloading and running it from the website. Roblox is currently running, and launching another instance will close it. Are you sure you want to continue launching? diff --git a/Bloxstrap/UI/Elements/Dialogs/FluentMessageBox.xaml.cs b/Bloxstrap/UI/Elements/Dialogs/FluentMessageBox.xaml.cs index 30d5dfc..47a4c39 100644 --- a/Bloxstrap/UI/Elements/Dialogs/FluentMessageBox.xaml.cs +++ b/Bloxstrap/UI/Elements/Dialogs/FluentMessageBox.xaml.cs @@ -41,7 +41,7 @@ namespace Bloxstrap.UI.Elements.Dialogs case MessageBoxImage.Warning: iconFilename = "Warning"; - sound = SystemSounds.Asterisk; + sound = SystemSounds.Exclamation; break; case MessageBoxImage.Information: diff --git a/Bloxstrap/UI/ViewModels/Dialogs/LaunchMenuViewModel.cs b/Bloxstrap/UI/ViewModels/Dialogs/LaunchMenuViewModel.cs index 0680998..25962a9 100644 --- a/Bloxstrap/UI/ViewModels/Dialogs/LaunchMenuViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Dialogs/LaunchMenuViewModel.cs @@ -5,7 +5,6 @@ using Bloxstrap.UI.Elements.About; namespace Bloxstrap.UI.ViewModels.Installer { - // TODO: have it so it shows "Launch Roblox"/"Install and Launch Roblox" depending on state of /App/ folder public class LaunchMenuViewModel { public string Version => string.Format(Strings.Menu_About_Version, App.Version); diff --git a/Bloxstrap/UI/ViewModels/Installer/WelcomeViewModel.cs b/Bloxstrap/UI/ViewModels/Installer/WelcomeViewModel.cs index 5ba85f3..a903659 100644 --- a/Bloxstrap/UI/ViewModels/Installer/WelcomeViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Installer/WelcomeViewModel.cs @@ -19,32 +19,15 @@ // called by codebehind on page load public async void DoChecks() { - const string LOG_IDENT = "WelcomeViewModel::DoChecks"; + var releaseInfo = await App.GetLatestRelease(); - // TODO: move into unified function that bootstrapper can use too - GithubRelease? releaseInfo = null; - - try + if (releaseInfo is not null) { - releaseInfo = await Http.GetJson($"https://api.github.com/repos/{App.ProjectRepository}/releases/latest"); - - if (releaseInfo is null || releaseInfo.Assets is null) + if (Utilities.CompareVersions(App.Version, releaseInfo.TagName) == VersionComparison.LessThan) { - App.Logger.WriteLine(LOG_IDENT, $"Encountered invalid data when fetching GitHub releases"); + VersionNotice = String.Format(Strings.Installer_Welcome_UpdateNotice, App.Version, releaseInfo.TagName.Replace("v", "")); + OnPropertyChanged(nameof(VersionNotice)); } - else - { - if (Utilities.CompareVersions(App.Version, releaseInfo.TagName) == VersionComparison.LessThan) - { - VersionNotice = String.Format(Resources.Strings.Installer_Welcome_UpdateNotice, App.Version, releaseInfo.TagName.Replace("v", "")); - OnPropertyChanged(nameof(VersionNotice)); - } - } - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, $"Error occurred when fetching GitHub releases"); - App.Logger.WriteException(LOG_IDENT, ex); } CanContinue = true;