diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 7361353..6a8d6e1 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -49,6 +49,7 @@ namespace Bloxstrap private LaunchMode _launchMode; private string _launchCommandLine = App.LaunchSettings.RobloxLaunchArgs; + private Version? _latestVersion = null; private string _latestVersionGuid = null!; private string _latestVersionDirectory = null!; private PackageManifest _versionPackageManifest = null!; @@ -70,6 +71,9 @@ namespace Bloxstrap public IBootstrapperDialog? Dialog = null; public bool IsStudioLaunch => _launchMode != LaunchMode.Player; + + public string MutexName { get; set; } = "Bloxstrap-Bootstrapper"; + public bool QuitIfMutexExists { get; set; } = false; #endregion #region Core @@ -198,22 +202,24 @@ namespace Bloxstrap // ensure only one instance of the bootstrapper is running at the time // so that we don't have stuff like two updates happening simultaneously - bool mutexExists = false; + bool mutexExists = Utilities.DoesMutexExist(MutexName); - try + if (mutexExists) { - Mutex.OpenExisting("Bloxstrap-Bootstrapper").Close(); - App.Logger.WriteLine(LOG_IDENT, "Bloxstrap-Bootstrapper mutex exists, waiting..."); - SetStatus(Strings.Bootstrapper_Status_WaitingOtherInstances); - mutexExists = true; - } - catch (Exception) - { - // no mutex exists + if (!QuitIfMutexExists) + { + App.Logger.WriteLine(LOG_IDENT, $"{MutexName} mutex exists, waiting..."); + SetStatus(Strings.Bootstrapper_Status_WaitingOtherInstances); + } + else + { + App.Logger.WriteLine(LOG_IDENT, $"{MutexName} mutex exists, exiting!"); + return; + } } // wait for mutex to be released if it's not yet - await using var mutex = new AsyncMutex(false, "Bloxstrap-Bootstrapper"); + await using var mutex = new AsyncMutex(false, MutexName); await mutex.AcquireAsync(_cancelTokenSource.Token); _mutex = mutex; @@ -237,12 +243,35 @@ namespace Bloxstrap } } + CleanupVersionsFolder(); // cleanup after background updater + bool allModificationsApplied = true; if (!_noConnection) { if (AppData.State.VersionGuid != _latestVersionGuid || _mustUpgrade) - await UpgradeRoblox(); + { + bool backgroundUpdaterMutexOpen = Utilities.DoesMutexExist("Bloxstrap-BackgroundUpdater"); + if (App.LaunchSettings.BackgroundUpdaterFlag.Active) + backgroundUpdaterMutexOpen = false; // we want to actually update lol + + App.Logger.WriteLine(LOG_IDENT, $"Background updater running: {backgroundUpdaterMutexOpen}"); + + if (backgroundUpdaterMutexOpen && _mustUpgrade) + { + // I am Forced Upgrade, killer of Background Updates + Utilities.KillBackgroundUpdater(); + backgroundUpdaterMutexOpen = false; + } + + if (!backgroundUpdaterMutexOpen) + { + if (IsEligibleForBackgroundUpdate()) + StartBackgroundUpdater(); + else + await UpgradeRoblox(); + } + } if (_cancelTokenSource.IsCancellationRequested) return; @@ -339,11 +368,13 @@ namespace Bloxstrap key.SetValueSafe("www.roblox.com", Deployment.IsDefaultChannel ? "" : Deployment.Channel); _latestVersionGuid = clientVersion.VersionGuid; + _latestVersion = Utilities.ParseVersionSafe(clientVersion.Version); } else { App.Logger.WriteLine(LOG_IDENT, $"Version set to {App.LaunchSettings.VersionFlag.Data} from arguments"); _latestVersionGuid = App.LaunchSettings.VersionFlag.Data; + // we can't determine the version } _latestVersionDirectory = Path.Combine(Paths.Versions, _latestVersionGuid); @@ -366,6 +397,31 @@ namespace Bloxstrap } } + private bool IsEligibleForBackgroundUpdate() + { + // TODO: storage check + + if (App.LaunchSettings.BackgroundUpdaterFlag.Active || _mustUpgrade || IsStudioLaunch) + return false; + + if (_latestVersion == default) // todo: check if this even works + return false; + + Version? currentVersion = Utilities.GetRobloxVersion(AppData); + if (currentVersion == default) + return false; + + // always normally upgrade for downgrades + if (currentVersion.Minor > _latestVersion.Minor) + return false; + + // only background update if we're: + // - one major update behind + // - the same major update + int diff = _latestVersion.Minor - currentVersion.Minor; + return diff == 0 || diff == 1; + } + private void StartRoblox() { const string LOG_IDENT = "Bootstrapper::StartRoblox"; @@ -718,6 +774,12 @@ namespace Bloxstrap { const string LOG_IDENT = "Bootstrapper::CleanupVersionsFolder"; + if (App.LaunchSettings.BackgroundUpdaterFlag.Active) + { + App.Logger.WriteLine(LOG_IDENT, "Background updater tried to cleanup, stopping!"); + return; + } + foreach (string dir in Directory.GetDirectories(Paths.Versions)) { string dirName = Path.GetFileName(dir); @@ -801,11 +863,11 @@ namespace Bloxstrap _isInstalling = true; // make sure nothing is running before continuing upgrade - if (!IsStudioLaunch) // TODO: wait for studio processes to close before updating to prevent data loss + if (!App.LaunchSettings.BackgroundUpdaterFlag.Active && !IsStudioLaunch) // TODO: wait for studio processes to close before updating to prevent data loss KillRobloxPlayers(); // get a fully clean install - if (Directory.Exists(_latestVersionDirectory)) + if (!App.LaunchSettings.BackgroundUpdaterFlag.Active && Directory.Exists(_latestVersionDirectory)) { try { @@ -1002,6 +1064,21 @@ namespace Bloxstrap _isInstalling = false; } + private static void StartBackgroundUpdater() + { + const string LOG_IDENT = "Bootstrapper::StartBackgroundUpdater"; + + if (Utilities.DoesMutexExist("Bloxstrap-BackgroundUpdater")) + { + App.Logger.WriteLine(LOG_IDENT, "Background updater already running"); + return; + } + + App.Logger.WriteLine(LOG_IDENT, "Starting background updater"); + + Process.Start(Paths.Process, "-backgroundupdater"); + } + private async Task ApplyModifications() { const string LOG_IDENT = "Bootstrapper::ApplyModifications"; diff --git a/Bloxstrap/LaunchHandler.cs b/Bloxstrap/LaunchHandler.cs index 2dda0c9..f5da7e7 100644 --- a/Bloxstrap/LaunchHandler.cs +++ b/Bloxstrap/LaunchHandler.cs @@ -4,6 +4,7 @@ using Windows.Win32; using Windows.Win32.Foundation; using Bloxstrap.UI.Elements.Dialogs; +using Bloxstrap.Enums; namespace Bloxstrap { @@ -58,6 +59,11 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, "Opening watcher"); LaunchWatcher(); } + else if (App.LaunchSettings.BackgroundUpdaterFlag.Active) + { + App.Logger.WriteLine(LOG_IDENT, "Opening background updater"); + LaunchBackgroundUpdater(); + } else if (App.LaunchSettings.RobloxLaunchMode != LaunchMode.None) { App.Logger.WriteLine(LOG_IDENT, $"Opening bootstrapper ({App.LaunchSettings.RobloxLaunchMode})"); @@ -295,5 +301,51 @@ namespace Bloxstrap App.Terminate(); }); } + + public static void LaunchBackgroundUpdater() + { + const string LOG_IDENT = "LaunchHandler::LaunchBackgroundUpdater"; + + // Activate some LaunchFlags we need + App.LaunchSettings.QuietFlag.Active = true; + App.LaunchSettings.NoLaunchFlag.Active = true; + + App.Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper"); + App.Bootstrapper = new Bootstrapper(LaunchMode.Player) + { + MutexName = "Bloxstrap-BackgroundUpdaterKillEvent", + QuitIfMutexExists = true + }; + + CancellationTokenSource cts = new CancellationTokenSource(); + + Task.Run(() => + { + App.Logger.WriteLine(LOG_IDENT, "Started event waiter"); + using (EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.AutoReset, "Bloxstrap-BackgroundUpdater")) + handle.WaitOne(); + + App.Logger.WriteLine(LOG_IDENT, "Received close event, killing it all!"); + App.Bootstrapper.Cancel(); + }, cts.Token); + + Task.Run(App.Bootstrapper.Run).ContinueWith(t => + { + App.Logger.WriteLine(LOG_IDENT, "Bootstrapper task has finished"); + cts.Cancel(); // stop event waiter + + if (t.IsFaulted) + { + App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the bootstrapper"); + + if (t.Exception is not null) + App.FinalizeExceptionHandling(t.Exception); + } + + App.Terminate(); + }); + + App.Logger.WriteLine(LOG_IDENT, "Exiting"); + } } } diff --git a/Bloxstrap/LaunchSettings.cs b/Bloxstrap/LaunchSettings.cs index bd4c36a..e0a5051 100644 --- a/Bloxstrap/LaunchSettings.cs +++ b/Bloxstrap/LaunchSettings.cs @@ -12,31 +12,33 @@ namespace Bloxstrap { public class LaunchSettings { - public LaunchFlag MenuFlag { get; } = new("preferences,menu,settings"); + public LaunchFlag MenuFlag { get; } = new("preferences,menu,settings"); - public LaunchFlag WatcherFlag { get; } = new("watcher"); + public LaunchFlag WatcherFlag { get; } = new("watcher"); - public LaunchFlag QuietFlag { get; } = new("quiet"); + public LaunchFlag BackgroundUpdaterFlag { get; } = new("backgroundupdater"); - public LaunchFlag UninstallFlag { get; } = new("uninstall"); + public LaunchFlag QuietFlag { get; } = new("quiet"); - public LaunchFlag NoLaunchFlag { get; } = new("nolaunch"); + public LaunchFlag UninstallFlag { get; } = new("uninstall"); + + public LaunchFlag NoLaunchFlag { get; } = new("nolaunch"); - public LaunchFlag TestModeFlag { get; } = new("testmode"); + public LaunchFlag TestModeFlag { get; } = new("testmode"); - public LaunchFlag NoGPUFlag { get; } = new("nogpu"); + public LaunchFlag NoGPUFlag { get; } = new("nogpu"); - public LaunchFlag UpgradeFlag { get; } = new("upgrade"); + public LaunchFlag UpgradeFlag { get; } = new("upgrade"); - public LaunchFlag PlayerFlag { get; } = new("player"); + public LaunchFlag PlayerFlag { get; } = new("player"); - public LaunchFlag StudioFlag { get; } = new("studio"); + public LaunchFlag StudioFlag { get; } = new("studio"); - public LaunchFlag VersionFlag { get; } = new("version"); + public LaunchFlag VersionFlag { get; } = new("version"); - public LaunchFlag ChannelFlag { get; } = new("channel"); + public LaunchFlag ChannelFlag { get; } = new("channel"); - public LaunchFlag ForceFlag { get; } = new("force"); + public LaunchFlag ForceFlag { get; } = new("force"); #if DEBUG public bool BypassUpdateCheck => true; diff --git a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs index 5afcc43..7a89d97 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs @@ -104,7 +104,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper public ByfronDialog() { - string version = Utilities.GetRobloxVersion(Bootstrapper?.IsStudioLaunch ?? false); + string version = Utilities.GetRobloxVersionStr(Bootstrapper?.IsStudioLaunch ?? false); _viewModel = new ByfronDialogViewModel(this, version); DataContext = _viewModel; Title = App.Settings.Prop.BootstrapperTitle; diff --git a/Bloxstrap/Utilities.cs b/Bloxstrap/Utilities.cs index eeea1d6..0a27315 100644 --- a/Bloxstrap/Utilities.cs +++ b/Bloxstrap/Utilities.cs @@ -75,10 +75,24 @@ namespace Bloxstrap } } - public static string GetRobloxVersion(bool studio) + /// + /// Parses the input version string and prints if fails + /// + public static Version? ParseVersionSafe(string versionStr) { - IAppData data = studio ? new RobloxStudioData() : new RobloxPlayerData(); + const string LOG_IDENT = "Utilities::ParseVersionSafe"; + if (!Version.TryParse(versionStr, out Version? version)) + { + App.Logger.WriteLine(LOG_IDENT, $"Failed to convert {versionStr} to a valid Version type."); + return version; + } + + return version; + } + + public static string GetRobloxVersionStr(IAppData data) + { string playerLocation = data.ExecutablePath; if (!File.Exists(playerLocation)) @@ -92,6 +106,19 @@ namespace Bloxstrap return versionInfo.ProductVersion.Replace(", ", "."); } + public static string GetRobloxVersionStr(bool studio) + { + IAppData data = studio ? new RobloxStudioData() : new RobloxPlayerData(); + + return GetRobloxVersionStr(data); + } + + public static Version? GetRobloxVersion(IAppData data) + { + string str = GetRobloxVersionStr(data); + return ParseVersionSafe(str); + } + public static Process[] GetProcessesSafe() { const string LOG_IDENT = "Utilities::GetProcessesSafe"; @@ -107,5 +134,24 @@ namespace Bloxstrap return Array.Empty(); // can we retry? } } + + public static bool DoesMutexExist(string name) + { + try + { + Mutex.OpenExisting(name).Close(); + return true; + } + catch + { + return false; + } + } + + public static void KillBackgroundUpdater() + { + using EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.AutoReset, "Bloxstrap-BackgroundUpdaterKillEvent"); + handle.Set(); + } } }