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;