From d244f42b49ed3f12764e32f720e6118f5c869dd6 Mon Sep 17 00:00:00 2001 From: Matt <97983689+bluepilledgreat@users.noreply.github.com> Date: Sat, 15 Mar 2025 17:42:21 +0000 Subject: [PATCH] Reintroduce multi-instance launching (#4888) --- Bloxstrap/Bootstrapper.cs | 41 +++++++++-- Bloxstrap/LaunchHandler.cs | 29 +++++++- Bloxstrap/LaunchSettings.cs | 30 ++++---- Bloxstrap/Models/Persistable/Settings.cs | 1 + Bloxstrap/MultiInstanceWatcher.cs | 68 +++++++++++++++++++ Bloxstrap/Resources/Strings.Designer.cs | 18 +++++ Bloxstrap/Resources/Strings.resx | 6 ++ .../Settings/Pages/IntegrationsPage.xaml | 8 +++ .../Settings/IntegrationsViewModel.cs | 6 ++ 9 files changed, 185 insertions(+), 22 deletions(-) create mode 100644 Bloxstrap/MultiInstanceWatcher.cs diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index f680e00..af3e614 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -471,21 +471,48 @@ namespace Bloxstrap } } + private static void LaunchMultiInstanceWatcher() + { + const string LOG_IDENT = "Bootstrapper::LaunchMultiInstanceWatcher"; + + if (Utilities.DoesMutexExist("ROBLOX_singletonMutex")) + { + App.Logger.WriteLine(LOG_IDENT, "Roblox singleton mutex already exists"); + return; + } + + using EventWaitHandle initEventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, "Bloxstrap-MultiInstanceWatcherInitialisationFinished"); + Process.Start(Paths.Process, "-multiinstancewatcher"); + + bool initSuccess = initEventHandle.WaitOne(TimeSpan.FromSeconds(2)); + if (initSuccess) + App.Logger.WriteLine(LOG_IDENT, "Initialisation finished signalled, continuing."); + else + App.Logger.WriteLine(LOG_IDENT, "Did not receive the initialisation finished signal, continuing."); + } + private void StartRoblox() { const string LOG_IDENT = "Bootstrapper::StartRoblox"; SetStatus(Strings.Bootstrapper_Status_Starting); - if (_launchMode == LaunchMode.Player && App.Settings.Prop.ForceRobloxLanguage) + if (_launchMode == LaunchMode.Player) { - var match = Regex.Match(_launchCommandLine, "gameLocale:([a-z_]+)", RegexOptions.CultureInvariant); + // this needs to be done before roblox launches + if (App.Settings.Prop.MultiInstanceLaunching) + LaunchMultiInstanceWatcher(); - if (match.Groups.Count == 2) - _launchCommandLine = _launchCommandLine.Replace( - "robloxLocale:en_us", - $"robloxLocale:{match.Groups[1].Value}", - StringComparison.OrdinalIgnoreCase); + if (App.Settings.Prop.ForceRobloxLanguage) + { + var match = Regex.Match(_launchCommandLine, "gameLocale:([a-z_]+)", RegexOptions.CultureInvariant); + + if (match.Groups.Count == 2) + _launchCommandLine = _launchCommandLine.Replace( + "robloxLocale:en_us", + $"robloxLocale:{match.Groups[1].Value}", + StringComparison.OrdinalIgnoreCase); + } } var startInfo = new ProcessStartInfo() diff --git a/Bloxstrap/LaunchHandler.cs b/Bloxstrap/LaunchHandler.cs index 1eee8b2..4171b83 100644 --- a/Bloxstrap/LaunchHandler.cs +++ b/Bloxstrap/LaunchHandler.cs @@ -59,6 +59,11 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, "Opening watcher"); LaunchWatcher(); } + else if (App.LaunchSettings.MultiInstanceWatcherFlag.Active) + { + App.Logger.WriteLine(LOG_IDENT, "Opening multi-instance watcher"); + LaunchMultiInstanceWatcher(); + } else if (App.LaunchSettings.BackgroundUpdaterFlag.Active) { App.Logger.WriteLine(LOG_IDENT, "Opening background updater"); @@ -223,7 +228,7 @@ namespace Bloxstrap App.Terminate(ErrorCode.ERROR_FILE_NOT_FOUND); } - if (App.Settings.Prop.ConfirmLaunches && Mutex.TryOpenExisting("ROBLOX_singletonMutex", out var _)) + if (App.Settings.Prop.ConfirmLaunches && Mutex.TryOpenExisting("ROBLOX_singletonMutex", out var _) && !App.Settings.Prop.MultiInstanceLaunching) { // this currently doesn't work very well since it relies on checking the existence of the singleton mutex // which often hangs around for a few seconds after the window closes @@ -302,6 +307,28 @@ namespace Bloxstrap }); } + public static void LaunchMultiInstanceWatcher() + { + const string LOG_IDENT = "LaunchHandler::LaunchMultiInstanceWatcher"; + + App.Logger.WriteLine(LOG_IDENT, "Starting multi-instance watcher"); + + Task.Run(MultiInstanceWatcher.Run).ContinueWith(t => + { + App.Logger.WriteLine(LOG_IDENT, "Multi instance watcher task has finished"); + + if (t.IsFaulted) + { + App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the multi-instance watcher"); + + if (t.Exception is not null) + App.FinalizeExceptionHandling(t.Exception); + } + + App.Terminate(); + }); + } + public static void LaunchBackgroundUpdater() { const string LOG_IDENT = "LaunchHandler::LaunchBackgroundUpdater"; diff --git a/Bloxstrap/LaunchSettings.cs b/Bloxstrap/LaunchSettings.cs index e0a5051..e7e8543 100644 --- a/Bloxstrap/LaunchSettings.cs +++ b/Bloxstrap/LaunchSettings.cs @@ -12,33 +12,35 @@ 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 BackgroundUpdaterFlag { get; } = new("backgroundupdater"); + public LaunchFlag MultiInstanceWatcherFlag { get; } = new("multiinstancewatcher"); - 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/Models/Persistable/Settings.cs b/Bloxstrap/Models/Persistable/Settings.cs index 0f6bb86..eb9c273 100644 --- a/Bloxstrap/Models/Persistable/Settings.cs +++ b/Bloxstrap/Models/Persistable/Settings.cs @@ -11,6 +11,7 @@ namespace Bloxstrap.Models.Persistable public string BootstrapperIconCustomLocation { get; set; } = ""; public Theme Theme { get; set; } = Theme.Default; public bool CheckForUpdates { get; set; } = true; + public bool MultiInstanceLaunching { get; set; } = false; public bool ConfirmLaunches { get; set; } = false; public string Locale { get; set; } = "nil"; public bool ForceRobloxLanguage { get; set; } = false; diff --git a/Bloxstrap/MultiInstanceWatcher.cs b/Bloxstrap/MultiInstanceWatcher.cs new file mode 100644 index 0000000..8799490 --- /dev/null +++ b/Bloxstrap/MultiInstanceWatcher.cs @@ -0,0 +1,68 @@ +namespace Bloxstrap +{ + internal static class MultiInstanceWatcher + { + private static int GetOpenProcessesCount() + { + const string LOG_IDENT = "MultiInstanceWatcher::GetOpenProcessesCount"; + + try + { + // prevent any possible race conditions by checking for bloxstrap processes too + int count = Process.GetProcesses().Count(x => x.ProcessName is "RobloxPlayerBeta" or "Bloxstrap"); + count -= 1; // ignore the current process + return count; + } + catch (Exception ex) + { + // everything process related can error at any time + App.Logger.WriteException(LOG_IDENT, ex); + return -1; + } + } + + private static void FireInitialisedEvent() + { + using EventWaitHandle initEventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, "Bloxstrap-MultiInstanceWatcherInitialisationFinished"); + initEventHandle.Set(); + } + + public static void Run() + { + const string LOG_IDENT = "MultiInstanceWatcher::Run"; + + // try to get the mutex + bool acquiredMutex; + using Mutex mutex = new Mutex(false, "ROBLOX_singletonMutex"); + try + { + acquiredMutex = mutex.WaitOne(0); + } + catch (AbandonedMutexException) + { + acquiredMutex = true; + } + + if (!acquiredMutex) + { + App.Logger.WriteLine(LOG_IDENT, "Client singleton mutex is already acquired"); + FireInitialisedEvent(); + return; + } + + App.Logger.WriteLine(LOG_IDENT, "Acquired mutex!"); + FireInitialisedEvent(); + + // watch for alive processes + int count; + do + { + Thread.Sleep(5000); + count = GetOpenProcessesCount(); + } + while (count == -1 || count > 0); // redo if -1 (one of the Process apis failed) + + App.Logger.WriteLine(LOG_IDENT, "All Roblox related processes have closed, exiting!"); + } + } +} diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs index 848b724..f61475b 100644 --- a/Bloxstrap/Resources/Strings.Designer.cs +++ b/Bloxstrap/Resources/Strings.Designer.cs @@ -3425,6 +3425,24 @@ namespace Bloxstrap.Resources { } } + /// + /// Looks up a localized string similar to Allows for having more than one Roblox game client instance open simultaneously.. + /// + public static string Menu_Integrations_MultiInstanceLaunching_Description { + get { + return ResourceManager.GetString("Menu.Integrations.MultiInstanceLaunching.Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Allow multi-instance launching. + /// + public static string Menu_Integrations_MultiInstanceLaunching_Title { + get { + return ResourceManager.GetString("Menu.Integrations.MultiInstanceLaunching.Title", resourceCulture); + } + } + /// /// Looks up a localized string similar to When in-game, you'll be able to see where your server is located via [ipinfo.io]({0}).. /// diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx index f0355ae..2e9abaa 100644 --- a/Bloxstrap/Resources/Strings.resx +++ b/Bloxstrap/Resources/Strings.resx @@ -1487,4 +1487,10 @@ Defaulting to {1}. Custom Theme {0} {0} is a string (e.g. '1', '1-1234') + + Allow multi-instance launching + + + Allows for having more than one Roblox game client instance open simultaneously. + \ No newline at end of file diff --git a/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml b/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml index 0e391bb..0cb02e6 100644 --- a/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml +++ b/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml @@ -66,6 +66,14 @@ + + + + + + diff --git a/Bloxstrap/UI/ViewModels/Settings/IntegrationsViewModel.cs b/Bloxstrap/UI/ViewModels/Settings/IntegrationsViewModel.cs index d66789d..4aed82e 100644 --- a/Bloxstrap/UI/ViewModels/Settings/IntegrationsViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Settings/IntegrationsViewModel.cs @@ -125,6 +125,12 @@ namespace Bloxstrap.UI.ViewModels.Settings set => App.Settings.Prop.UseDisableAppPatch = value; } + public bool MultiInstanceLaunchingEnabled + { + get => App.Settings.Prop.MultiInstanceLaunching; + set => App.Settings.Prop.MultiInstanceLaunching = value; + } + public ObservableCollection CustomIntegrations { get => App.Settings.Prop.CustomIntegrations;