diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index 695a0f1b..3a02b2ed 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 @@ public static void FinalizeExceptionHandling(Exception ex, bool log = true) 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 @@ protected override void OnStartup(StartupEventArgs e) 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 969966b6..16c7c843 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 @@ public async Task Run() 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 @@ public void CancelInstall() App.Terminate(ErrorCode.ERROR_CANCELLED); } - #endregion +#endregion #region App Install public void RegisterProgramSize() @@ -449,53 +467,56 @@ public static void CheckInstall() #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 @@ private async Task CheckForUpdates() { 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 @@ private async Task CheckForUpdates() 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 @@ private async Task ApplyModifications() 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 @@ private async Task ApplyModifications() // 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 849a834b..2e841fda 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 @@ public void DoInstall() 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 @@ public static void HandleUpgrade() 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 @@ public static void HandleUpgrade() } // 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 @@ public static void HandleUpgrade() 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 + { + File.Copy(Paths.Process, Paths.Application, true); + } + catch (Exception ex) { - App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not get write permissions after 5 seconds)"); + 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 046fd9ae..25a24fad 100644 --- a/Bloxstrap/LaunchSettings.cs +++ b/Bloxstrap/LaunchSettings.cs @@ -28,6 +28,8 @@ public class LaunchSettings 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 @@ public class LaunchSettings /// public string[] Args { get; private set; } - private Dictionary _flagMap = new(); + private readonly Dictionary _flagMap = new(); public LaunchSettings(string[] args) { @@ -68,7 +70,7 @@ public LaunchSettings(string[] args) 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 cd04cc83..43d4a1c0 100644 --- a/Bloxstrap/Paths.cs +++ b/Bloxstrap/Paths.cs @@ -4,6 +4,7 @@ static class Paths { // 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 @@ static class Paths 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 28c8fc2b..0b754caf 100644 --- a/Bloxstrap/Resources/Strings.Designer.cs +++ b/Bloxstrap/Resources/Strings.Designer.cs @@ -106,7 +106,7 @@ public static string ActivityTracker_LookupFailed { } /// - /// 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 6f57c25d..8f12701b 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 30d5dfcf..47a4c398 100644 --- a/Bloxstrap/UI/Elements/Dialogs/FluentMessageBox.xaml.cs +++ b/Bloxstrap/UI/Elements/Dialogs/FluentMessageBox.xaml.cs @@ -41,7 +41,7 @@ public FluentMessageBox(string message, MessageBoxImage image, MessageBoxButton 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 06809989..25962a93 100644 --- a/Bloxstrap/UI/ViewModels/Dialogs/LaunchMenuViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Dialogs/LaunchMenuViewModel.cs @@ -5,7 +5,6 @@ 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 5ba85f34..a9036596 100644 --- a/Bloxstrap/UI/ViewModels/Installer/WelcomeViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Installer/WelcomeViewModel.cs @@ -19,33 +19,16 @@ public class WelcomeViewModel : NotifyPropertyChangedViewModel // 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) - { - App.Logger.WriteLine(LOG_IDENT, $"Encountered invalid data when fetching GitHub releases"); - } - else + if (Utilities.CompareVersions(App.Version, releaseInfo.TagName) == VersionComparison.LessThan) { - 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)); - } + VersionNotice = String.Format(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; OnPropertyChanged(nameof(CanContinue));