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
This commit is contained in:
pizzaboxer 2024-08-30 13:29:51 +01:00
parent f747f40ca5
commit 2791cb0b2e
No known key found for this signature in database
GPG Key ID: 59D4A1DBAD0F2BA8
10 changed files with 144 additions and 99 deletions

View File

@ -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<GithubRelease?> GetLatestRelease()
{
const string LOG_IDENT = "App::GetLatestRelease";
GithubRelease? releaseInfo = null;
try
{
releaseInfo = await Http.GetJson<GithubRelease>($"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

View File

@ -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<bool> 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<GithubRelease>($"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))

View File

@ -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 <BaseFolder>/Updates so lol
// TODO: 2.8.0 will download them to <Temp>/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

View File

@ -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
/// </summary>
public string[] Args { get; private set; }
private Dictionary<string, LaunchFlag> _flagMap = new();
private readonly Dictionary<string, LaunchFlag> _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;

View File

@ -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; } = "";

View File

@ -106,7 +106,7 @@ namespace Bloxstrap.Resources {
}
/// <summary>
/// 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..
/// </summary>
public static string Bootstrapper_AutoUpdateFailed {
get {

View File

@ -124,7 +124,7 @@
<value>lookup failed</value>
</data>
<data name="Bootstrapper.AutoUpdateFailed" xml:space="preserve">
<value>Bloxstrap was unable to auto-update to {0}. Please update it manually by downloading and running the latest release from the GitHub page.</value>
<value>Bloxstrap was unable to automatically update to version {0}. Please update it manually by downloading and running it from the website.</value>
</data>
<data name="Bootstrapper.ConfirmLaunch" xml:space="preserve">
<value>Roblox is currently running, and launching another instance will close it. Are you sure you want to continue launching?</value>

View File

@ -41,7 +41,7 @@ namespace Bloxstrap.UI.Elements.Dialogs
case MessageBoxImage.Warning:
iconFilename = "Warning";
sound = SystemSounds.Asterisk;
sound = SystemSounds.Exclamation;
break;
case MessageBoxImage.Information:

View File

@ -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);

View File

@ -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<GithubRelease>($"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;