Skip to content

Commit

Permalink
Refactor automatic updater + fix install details + fix launch flag pa…
Browse files Browse the repository at this point in the history
…rser + 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
  • Loading branch information
pizzaboxer committed Aug 30, 2024
1 parent f747f40 commit 2791cb0
Show file tree
Hide file tree
Showing 10 changed files with 144 additions and 99 deletions.
27 changes: 26 additions & 1 deletion Bloxstrap/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -103,6 +107,27 @@ public static void FinalizeExceptionHandling(Exception ex, bool log = true)
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";
Expand Down Expand Up @@ -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

Expand Down
112 changes: 75 additions & 37 deletions Bloxstrap/Bootstrapper.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -405,7 +423,7 @@ public void CancelInstall()

App.Terminate(ErrorCode.ERROR_CANCELLED);
}
#endregion
#endregion

#region App Install
public void RegisterProgramSize()
Expand Down Expand Up @@ -449,90 +467,107 @@ public static void CheckInstall()
#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}...");

if (!File.Exists(downloadLocation))
{
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)
{
App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the auto-updater");
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

Expand Down Expand Up @@ -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 }));
}

Expand Down Expand Up @@ -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))
Expand Down
60 changes: 27 additions & 33 deletions Bloxstrap/Installer.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -331,8 +327,9 @@ public static void HandleUpgrade()
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;
Expand All @@ -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,
Expand All @@ -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
Expand Down
6 changes: 4 additions & 2 deletions Bloxstrap/LaunchSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; } = "";
Expand All @@ -37,7 +39,7 @@ public class LaunchSettings
/// </summary>
public string[] Args { get; private set; }

private Dictionary<string, LaunchFlag> _flagMap = new();
private readonly Dictionary<string, LaunchFlag> _flagMap = new();

public LaunchSettings(string[] args)
{
Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 2791cb0

Please sign in to comment.