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 partial class App : Application
{ {
public const string ProjectName = "Bloxstrap"; public const string ProjectName = "Bloxstrap";
public const string ProjectOwner = "pizzaboxer";
public const string ProjectRepository = "pizzaboxer/bloxstrap"; 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 RobloxPlayerAppName = "RobloxPlayerBeta";
public const string RobloxStudioAppName = "RobloxStudioBeta"; public const string RobloxStudioAppName = "RobloxStudioBeta";
@ -103,6 +107,27 @@ namespace Bloxstrap
Terminate(ErrorCode.ERROR_INSTALL_FAILURE); 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) protected override void OnStartup(StartupEventArgs e)
{ {
const string LOG_IDENT = "App::OnStartup"; const string LOG_IDENT = "App::OnStartup";
@ -221,7 +246,7 @@ namespace Bloxstrap
Locale.Set(Settings.Prop.Locale); Locale.Set(Settings.Prop.Locale);
#if !DEBUG #if !DEBUG
if (!LaunchSettings.UninstallFlag.Active) if (!LaunchSettings.BypassUpdateCheck)
Installer.HandleUpgrade(); Installer.HandleUpgrade();
#endif #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 System.Windows.Forms;
using Microsoft.Win32; using Microsoft.Win32;
@ -152,9 +165,14 @@ namespace Bloxstrap
await RobloxDeployment.GetInfo(RobloxDeployment.DefaultChannel); await RobloxDeployment.GetInfo(RobloxDeployment.DefaultChannel);
#if !DEBUG #if !DEBUG || DEBUG_UPDATER
if (App.Settings.Prop.CheckForUpdates) if (App.Settings.Prop.CheckForUpdates && !App.LaunchSettings.UpgradeFlag.Active)
await CheckForUpdates(); {
bool updatePresent = await CheckForUpdates();
if (updatePresent)
return;
}
#endif #endif
// ensure only one instance of the bootstrapper is running at the time // ensure only one instance of the bootstrapper is running at the time
@ -405,7 +423,7 @@ namespace Bloxstrap
App.Terminate(ErrorCode.ERROR_CANCELLED); App.Terminate(ErrorCode.ERROR_CANCELLED);
} }
#endregion #endregion
#region App Install #region App Install
public void RegisterProgramSize() public void RegisterProgramSize()
@ -449,53 +467,56 @@ namespace Bloxstrap
#endif #endif
} }
private async Task CheckForUpdates() private async Task<bool> CheckForUpdates()
{ {
const string LOG_IDENT = "Bootstrapper::CheckForUpdates"; const string LOG_IDENT = "Bootstrapper::CheckForUpdates";
// don't update if there's another instance running (likely running in the background) // 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"); 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 if (releaseInfo is null)
{ return false;
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;
}
var versionComparison = Utilities.CompareVersions(App.Version, releaseInfo.TagName); 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 // 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"); App.Logger.WriteLine(LOG_IDENT, "No updates found");
return; return false;
} }
string version = releaseInfo.TagName;
#else
string version = App.Version;
#endif
SetStatus(Strings.Bootstrapper_Status_UpgradingBloxstrap); SetStatus(Strings.Bootstrapper_Status_UpgradingBloxstrap);
try try
{ {
// 64-bit is always the first option #if DEBUG_UPDATER
GithubReleaseAsset asset = releaseInfo.Assets[0]; string downloadLocation = Path.Combine(Paths.TempUpdates, "Bloxstrap.exe");
string downloadLocation = Path.Combine(Paths.LocalAppData, "Temp", asset.Name);
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}..."); App.Logger.WriteLine(LOG_IDENT, $"Downloading {releaseInfo.TagName}...");
@ -503,25 +524,35 @@ namespace Bloxstrap
{ {
var response = await App.HttpClient.GetAsync(asset.BrowserDownloadUrl); 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); await response.Content.CopyToAsync(fileStream);
} }
#endif
App.Logger.WriteLine(LOG_IDENT, $"Starting {releaseInfo.TagName}..."); App.Logger.WriteLine(LOG_IDENT, $"Starting {version}...");
ProcessStartInfo startInfo = new() ProcessStartInfo startInfo = new()
{ {
FileName = downloadLocation, FileName = downloadLocation,
}; };
startInfo.ArgumentList.Add("-upgrade");
foreach (string arg in App.LaunchSettings.Args) foreach (string arg in App.LaunchSettings.Args)
startInfo.ArgumentList.Add(arg); 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(); App.Settings.Save();
new InterProcessLock("AutoUpdater");
Process.Start(startInfo); Process.Start(startInfo);
App.Terminate(); return true;
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -529,10 +560,14 @@ namespace Bloxstrap
App.Logger.WriteException(LOG_IDENT, ex); App.Logger.WriteException(LOG_IDENT, ex);
Frontend.ShowMessageBox( Frontend.ShowMessageBox(
string.Format(Strings.Bootstrapper_AutoUpdateFailed, releaseInfo.TagName), string.Format(Strings.Bootstrapper_AutoUpdateFailed, version),
MessageBoxImage.Information MessageBoxImage.Information
); );
Utilities.ShellExecute(App.ProjectDownloadLink);
} }
return false;
} }
#endregion #endregion
@ -851,6 +886,7 @@ namespace Bloxstrap
foreach (FontFace fontFace in fontFamilyData.Faces) foreach (FontFace fontFace in fontFamilyData.Faces)
fontFace.AssetId = "rbxasset://fonts/CustomFont.ttf"; fontFace.AssetId = "rbxasset://fonts/CustomFont.ttf";
// TODO: writing on every launch is not necessary
File.WriteAllText(modFilepath, JsonSerializer.Serialize(fontFamilyData, new JsonSerializerOptions { WriteIndented = true })); 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 // 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 // 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 // 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) foreach (string fileLocation in App.State.Prop.ModManifest)
{ {
if (modFolderFiles.Contains(fileLocation)) if (modFolderFiles.Contains(fileLocation))

View File

@ -1,9 +1,4 @@
using System.DirectoryServices; using System.Windows;
using System.Reflection;
using System.Reflection.Metadata.Ecma335;
using System.Windows;
using System.Windows.Media.Animation;
using Bloxstrap.Resources;
using Microsoft.Win32; using Microsoft.Win32;
namespace Bloxstrap namespace Bloxstrap
@ -50,12 +45,13 @@ namespace Bloxstrap
uninstallKey.SetValue("InstallLocation", Paths.Base); uninstallKey.SetValue("InstallLocation", Paths.Base);
uninstallKey.SetValue("NoRepair", 1); uninstallKey.SetValue("NoRepair", 1);
uninstallKey.SetValue("Publisher", "pizzaboxer"); uninstallKey.SetValue("Publisher", App.ProjectOwner);
uninstallKey.SetValue("ModifyPath", $"\"{Paths.Application}\" -settings"); uninstallKey.SetValue("ModifyPath", $"\"{Paths.Application}\" -settings");
uninstallKey.SetValue("QuietUninstallString", $"\"{Paths.Application}\" -uninstall -quiet"); uninstallKey.SetValue("QuietUninstallString", $"\"{Paths.Application}\" -uninstall -quiet");
uninstallKey.SetValue("UninstallString", $"\"{Paths.Application}\" -uninstall"); uninstallKey.SetValue("UninstallString", $"\"{Paths.Application}\" -uninstall");
uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{App.ProjectRepository}"); uninstallKey.SetValue("HelpLink", App.ProjectHelpLink);
uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{App.ProjectRepository}/releases/latest"); uninstallKey.SetValue("URLInfoAbout", App.ProjectSupportLink);
uninstallKey.SetValue("URLUpdateInfo", App.ProjectDownloadLink);
} }
// only register player, for the scenario where the user installs bloxstrap, closes it, // only register player, for the scenario where the user installs bloxstrap, closes it,
@ -331,8 +327,9 @@ namespace Bloxstrap
return; return;
// 2.0.0 downloads updates to <BaseFolder>/Updates so lol // 2.0.0 downloads updates to <BaseFolder>/Updates so lol
// TODO: 2.8.0 will download them to <Temp>/Bloxstrap/Updates bool isAutoUpgrade = App.LaunchSettings.UpgradeFlag.Active
bool isAutoUpgrade = Paths.Process.StartsWith(Path.Combine(Paths.Base, "Updates")) || Paths.Process.StartsWith(Path.Combine(Paths.LocalAppData, "Temp")); || Paths.Process.StartsWith(Path.Combine(Paths.Base, "Updates"))
|| Paths.Process.StartsWith(Paths.Temp);
var existingVer = FileVersionInfo.GetVersionInfo(Paths.Application).ProductVersion; var existingVer = FileVersionInfo.GetVersionInfo(Paths.Application).ProductVersion;
var currentVer = FileVersionInfo.GetVersionInfo(Paths.Process).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 // 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( var result = Frontend.ShowMessageBox(
Strings.InstallChecker_VersionDifferentThanInstalled, Strings.InstallChecker_VersionDifferentThanInstalled,
@ -365,41 +362,38 @@ namespace Bloxstrap
return; return;
} }
App.Logger.WriteLine(LOG_IDENT, "Doing upgrade");
Filesystem.AssertReadOnly(Paths.Application); Filesystem.AssertReadOnly(Paths.Application);
// TODO: make this use a mutex somehow using (var ipl = new InterProcessLock("AutoUpdater", TimeSpan.FromSeconds(5)))
// 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)
{ {
attempts++; if (!ipl.IsAcquired)
try
{ {
File.Delete(Paths.Application); App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not obtain singleton mutex)");
break; return;
}
catch (Exception)
{
if (attempts == 1)
App.Logger.WriteLine(LOG_IDENT, "Waiting for write permissions to update version");
Thread.Sleep(500);
} }
} }
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; return;
} }
File.Copy(Paths.Process, Paths.Application);
using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey)) using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey))
{ {
uninstallKey.SetValue("DisplayVersion", App.Version); 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 // update migrations

View File

@ -28,6 +28,8 @@ namespace Bloxstrap
public LaunchFlag StudioFlag { get; } = new("studio"); public LaunchFlag StudioFlag { get; } = new("studio");
public bool BypassUpdateCheck => UninstallFlag.Active || WatcherFlag.Active;
public LaunchMode RobloxLaunchMode { get; set; } = LaunchMode.None; public LaunchMode RobloxLaunchMode { get; set; } = LaunchMode.None;
public string RobloxLaunchArgs { get; private set; } = ""; public string RobloxLaunchArgs { get; private set; } = "";
@ -37,7 +39,7 @@ namespace Bloxstrap
/// </summary> /// </summary>
public string[] Args { get; private set; } public string[] Args { get; private set; }
private Dictionary<string, LaunchFlag> _flagMap = new(); private readonly Dictionary<string, LaunchFlag> _flagMap = new();
public LaunchSettings(string[] args) public LaunchSettings(string[] args)
{ {
@ -68,7 +70,7 @@ namespace Bloxstrap
string identifier = arg[1..]; string identifier = arg[1..];
if (_flagMap[identifier] is not LaunchFlag flag) if (!_flagMap.TryGetValue(identifier, out LaunchFlag? flag) || flag is null)
continue; continue;
flag.Active = true; flag.Active = true;

View File

@ -4,6 +4,7 @@
{ {
// note that these are directories that aren't tethered to the basedirectory // note that these are directories that aren't tethered to the basedirectory
// so these can safely be called before initialization // 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 UserProfile => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
public static string LocalAppData => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); public static string LocalAppData => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
public static string Desktop => Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); public static string Desktop => Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
@ -12,6 +13,9 @@
public static string Process => Environment.ProcessPath!; 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 Base { get; private set; } = "";
public static string Downloads { get; private set; } = ""; public static string Downloads { get; private set; } = "";
public static string Logs { get; private set; } = ""; public static string Logs { get; private set; } = "";

View File

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

View File

@ -124,7 +124,7 @@
<value>lookup failed</value> <value>lookup failed</value>
</data> </data>
<data name="Bootstrapper.AutoUpdateFailed" xml:space="preserve"> <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>
<data name="Bootstrapper.ConfirmLaunch" xml:space="preserve"> <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> <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: case MessageBoxImage.Warning:
iconFilename = "Warning"; iconFilename = "Warning";
sound = SystemSounds.Asterisk; sound = SystemSounds.Exclamation;
break; break;
case MessageBoxImage.Information: case MessageBoxImage.Information:

View File

@ -5,7 +5,6 @@ using Bloxstrap.UI.Elements.About;
namespace Bloxstrap.UI.ViewModels.Installer 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 class LaunchMenuViewModel
{ {
public string Version => string.Format(Strings.Menu_About_Version, App.Version); public string Version => string.Format(Strings.Menu_About_Version, App.Version);

View File

@ -19,32 +19,15 @@
// called by codebehind on page load // called by codebehind on page load
public async void DoChecks() 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 if (releaseInfo is not null)
GithubRelease? releaseInfo = null;
try
{ {
releaseInfo = await Http.GetJson<GithubRelease>($"https://api.github.com/repos/{App.ProjectRepository}/releases/latest"); if (Utilities.CompareVersions(App.Version, releaseInfo.TagName) == VersionComparison.LessThan)
if (releaseInfo is null || releaseInfo.Assets is null)
{ {
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; CanContinue = true;