reintroduce multi-instance launching

This commit is contained in:
bluepilledgreat 2025-03-15 17:36:09 +00:00
parent 49fd8eb2d2
commit 4a1068713e
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() private void StartRoblox()
{ {
const string LOG_IDENT = "Bootstrapper::StartRoblox"; const string LOG_IDENT = "Bootstrapper::StartRoblox";
SetStatus(Strings.Bootstrapper_Status_Starting); 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) if (App.Settings.Prop.ForceRobloxLanguage)
_launchCommandLine = _launchCommandLine.Replace( {
"robloxLocale:en_us", var match = Regex.Match(_launchCommandLine, "gameLocale:([a-z_]+)", RegexOptions.CultureInvariant);
$"robloxLocale:{match.Groups[1].Value}",
StringComparison.OrdinalIgnoreCase); if (match.Groups.Count == 2)
_launchCommandLine = _launchCommandLine.Replace(
"robloxLocale:en_us",
$"robloxLocale:{match.Groups[1].Value}",
StringComparison.OrdinalIgnoreCase);
}
} }
var startInfo = new ProcessStartInfo() var startInfo = new ProcessStartInfo()

View File

@ -59,6 +59,11 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, "Opening watcher"); App.Logger.WriteLine(LOG_IDENT, "Opening watcher");
LaunchWatcher(); LaunchWatcher();
} }
else if (App.LaunchSettings.MultiInstanceWatcherFlag.Active)
{
App.Logger.WriteLine(LOG_IDENT, "Opening multi-instance watcher");
LaunchMultiInstanceWatcher();
}
else if (App.LaunchSettings.BackgroundUpdaterFlag.Active) else if (App.LaunchSettings.BackgroundUpdaterFlag.Active)
{ {
App.Logger.WriteLine(LOG_IDENT, "Opening background updater"); App.Logger.WriteLine(LOG_IDENT, "Opening background updater");
@ -223,7 +228,7 @@ namespace Bloxstrap
App.Terminate(ErrorCode.ERROR_FILE_NOT_FOUND); 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 // 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 // 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() public static void LaunchBackgroundUpdater()
{ {
const string LOG_IDENT = "LaunchHandler::LaunchBackgroundUpdater"; const string LOG_IDENT = "LaunchHandler::LaunchBackgroundUpdater";

View File

@ -12,33 +12,35 @@ namespace Bloxstrap
{ {
public class LaunchSettings 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 TestModeFlag { get; } = new("testmode"); public LaunchFlag NoLaunchFlag { get; } = new("nolaunch");
public LaunchFlag NoGPUFlag { get; } = new("nogpu"); public LaunchFlag TestModeFlag { get; } = new("testmode");
public LaunchFlag UpgradeFlag { get; } = new("upgrade"); public LaunchFlag NoGPUFlag { get; } = new("nogpu");
public LaunchFlag PlayerFlag { get; } = new("player"); public LaunchFlag UpgradeFlag { get; } = new("upgrade");
public LaunchFlag StudioFlag { get; } = new("studio"); public LaunchFlag PlayerFlag { get; } = new("player");
public LaunchFlag VersionFlag { get; } = new("version"); public LaunchFlag StudioFlag { get; } = new("studio");
public LaunchFlag ChannelFlag { get; } = new("channel"); public LaunchFlag VersionFlag { get; } = new("version");
public LaunchFlag ForceFlag { get; } = new("force"); public LaunchFlag ChannelFlag { get; } = new("channel");
public LaunchFlag ForceFlag { get; } = new("force");
#if DEBUG #if DEBUG
public bool BypassUpdateCheck => true; public bool BypassUpdateCheck => true;

View File

@ -11,6 +11,7 @@ namespace Bloxstrap.Models.Persistable
public string BootstrapperIconCustomLocation { get; set; } = ""; public string BootstrapperIconCustomLocation { get; set; } = "";
public Theme Theme { get; set; } = Theme.Default; public Theme Theme { get; set; } = Theme.Default;
public bool CheckForUpdates { get; set; } = true; public bool CheckForUpdates { get; set; } = true;
public bool MultiInstanceLaunching { get; set; } = false;
public bool ConfirmLaunches { get; set; } = false; public bool ConfirmLaunches { get; set; } = false;
public string Locale { get; set; } = "nil"; public string Locale { get; set; } = "nil";
public bool ForceRobloxLanguage { get; set; } = false; 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> /// <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}).. /// 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> /// </summary>

View File

@ -1487,4 +1487,10 @@ Defaulting to {1}.</value>
<value>Custom Theme {0}</value> <value>Custom Theme {0}</value>
<comment>{0} is a string (e.g. '1', '1-1234')</comment> <comment>{0} is a string (e.g. '1', '1-1234')</comment>
</data> </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> </root>

View File

@ -66,6 +66,14 @@
<ui:ToggleSwitch IsChecked="{Binding DiscordAccountOnProfile, Mode=TwoWay}" /> <ui:ToggleSwitch IsChecked="{Binding DiscordAccountOnProfile, Mode=TwoWay}" />
</controls:OptionControl> </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_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}" /> <TextBlock Text="{x:Static resources:Strings.Menu_Integrations_Custom_Description}" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<Grid Margin="0,8,0,0"> <Grid Margin="0,8,0,0">

View File

@ -125,6 +125,12 @@ namespace Bloxstrap.UI.ViewModels.Settings
set => App.Settings.Prop.UseDisableAppPatch = value; set => App.Settings.Prop.UseDisableAppPatch = value;
} }
public bool MultiInstanceLaunchingEnabled
{
get => App.Settings.Prop.MultiInstanceLaunching;
set => App.Settings.Prop.MultiInstanceLaunching = value;
}
public ObservableCollection<CustomIntegration> CustomIntegrations public ObservableCollection<CustomIntegration> CustomIntegrations
{ {
get => App.Settings.Prop.CustomIntegrations; get => App.Settings.Prop.CustomIntegrations;