Reintroduce multi-instance launching (#4888)

This commit is contained in:
Matt 2025-03-15 17:42:21 +00:00 committed by GitHub
parent 49fd8eb2d2
commit d244f42b49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 185 additions and 22 deletions

View File

@ -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()

View File

@ -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";

View File

@ -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;

View File

@ -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;

View File

@ -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!");
}
}
}

View File

@ -3425,6 +3425,24 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Allows for having more than one Roblox game client instance open simultaneously..
/// </summary>
public static string Menu_Integrations_MultiInstanceLaunching_Description {
get {
return ResourceManager.GetString("Menu.Integrations.MultiInstanceLaunching.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Allow multi-instance launching.
/// </summary>
public static string Menu_Integrations_MultiInstanceLaunching_Title {
get {
return ResourceManager.GetString("Menu.Integrations.MultiInstanceLaunching.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to When in-game, you&apos;ll be able to see where your server is located via [ipinfo.io]({0})..
/// </summary>

View File

@ -1487,4 +1487,10 @@ Defaulting to {1}.</value>
<value>Custom Theme {0}</value>
<comment>{0} is a string (e.g. '1', '1-1234')</comment>
</data>
<data name="Menu.Integrations.MultiInstanceLaunching.Title" xml:space="preserve">
<value>Allow multi-instance launching</value>
</data>
<data name="Menu.Integrations.MultiInstanceLaunching.Description" xml:space="preserve">
<value>Allows for having more than one Roblox game client instance open simultaneously.</value>
</data>
</root>

View File

@ -66,6 +66,14 @@
<ui:ToggleSwitch IsChecked="{Binding DiscordAccountOnProfile, Mode=TwoWay}" />
</controls:OptionControl>
<TextBlock Text="{x:Static resources:Strings.Common_Miscellaneous}" FontSize="20" FontWeight="Medium" Margin="0,16,0,0" />
<controls:OptionControl
Header="{x:Static resources:Strings.Menu_Integrations_MultiInstanceLaunching_Title}"
Description="{x:Static resources:Strings.Menu_Integrations_MultiInstanceLaunching_Description}">
<ui:ToggleSwitch IsChecked="{Binding MultiInstanceLaunchingEnabled, Mode=TwoWay}" />
</controls:OptionControl>
<TextBlock Text="{x:Static resources:Strings.Menu_Integrations_Custom_Title}" FontSize="20" FontWeight="Medium" Margin="0,16,0,0" />
<TextBlock Text="{x:Static resources:Strings.Menu_Integrations_Custom_Description}" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<Grid Margin="0,8,0,0">

View File

@ -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<CustomIntegration> CustomIntegrations
{
get => App.Settings.Prop.CustomIntegrations;