Implement better support for multiprocess handling

mutexes are created before starting the menu or bootstrapper to ensure only one instance of them are running
also menu can be opened without having to close bloxstrap background processes
also fixed roblox singleton mutex not being created properly
its 12 am and im fucking tired
This commit is contained in:
pizzaboxer 2023-02-19 00:37:33 +00:00
parent 467f55c4b2
commit b5250e29dc
6 changed files with 204 additions and 40 deletions

View File

@ -1,10 +1,10 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net.Http;
using System.Net;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.Win32;
@ -173,12 +173,23 @@ namespace Bloxstrap
if (IsMenuLaunch)
{
#if !DEBUG
if (Process.GetProcessesByName(ProjectName).Length > 1)
Mutex mutex;
try
{
ShowMessageBox($"{ProjectName} is currently running. Please close any currently open Bloxstrap or Roblox window before opening the menu.", MessageBoxImage.Error);
Environment.Exit(0);
mutex = Mutex.OpenExisting("Bloxstrap_MenuMutex");
Logger.WriteLine("[App::OnStartup] Bloxstrap_MenuMutex mutex exists, aborting menu launch...");
Terminate();
}
catch
{
// no mutex exists, continue to opening preferences menu
mutex = new(true, "Bloxstrap_MenuMutex");
}
#if !DEBUG
if (Utilities.GetProcessCount(ProjectName) > 1)
ShowMessageBox($"{ProjectName} is currently running, likely as a background Roblox process. Please note that not all your changes will immediately apply until you close all currently open Roblox instances.", MessageBoxImage.Information);
#endif
new MainWindow().ShowDialog();

View File

@ -113,15 +113,45 @@ namespace Bloxstrap
await CheckForUpdates();
#endif
// 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;
try
{
Mutex.OpenExisting("Bloxstrap_BootstrapperMutex").Close();
App.Logger.WriteLine("[Bootstrapper::Run] Bloxstrap_BootstrapperMutex mutex exists, waiting...");
mutexExists = true;
}
catch
{
// no mutex exists
}
// wait for mutex to be released if it's not yet
await using AsyncMutex mutex = new("Bloxstrap_BootstrapperMutex");
await mutex.AcquireAsync(_cancelTokenSource.Token);
// reload our configs since they've likely changed by now
if (mutexExists)
{
App.Settings.Load();
App.State.Load();
}
await CheckLatestVersion();
CheckInstallMigration();
// if bloxstrap is installing for the first time but is running, prompt to close roblox
// if roblox needs updating but is running, ignore update for now
if (!Directory.Exists(_versionFolder) && CheckIfRunning(true) || App.State.Prop.VersionGuid != _versionGuid && !CheckIfRunning(false))
// if roblox needs updating but is running and we have multiple instances open, ignore update for now
if (App.IsFirstRun || _versionGuid != App.State.Prop.VersionGuid && Utilities.GetProcessCount("RobloxPlayerBeta") == 0)
await InstallLatestVersion();
// last time the version folder was set, it was set to the latest version guid
// but if we skipped updating because roblox is already running, we want it to be set to our current version
_versionFolder = Path.Combine(Directories.Versions, App.State.Prop.VersionGuid);
if (App.IsFirstRun)
App.ShouldSaveConfigs = true;
@ -134,12 +164,16 @@ namespace Bloxstrap
await RbxFpsUnlocker.CheckInstall();
// at this point we've finished updating our configs
App.Settings.Save();
App.State.Save();
App.ShouldSaveConfigs = false;
await mutex.ReleaseAsync();
if (App.IsFirstRun && App.IsNoLaunch)
Dialog?.ShowSuccess($"{App.ProjectName} has successfully installed");
else if (!App.IsNoLaunch)
else if (!App.IsNoLaunch && !_cancelFired)
await StartRoblox();
}
@ -265,32 +299,20 @@ namespace Bloxstrap
App.Logger.WriteLine("[Bootstrapper::CheckInstallMigration] Finished migrating install location!");
}
private bool CheckIfRunning(bool shutdown)
private bool ShutdownIfRobloxRunning()
{
App.Logger.WriteLine($"[Bootstrapper::CheckIfRunning] Checking if Roblox is running... (shutdown={shutdown})");
App.Logger.WriteLine($"[Bootstrapper::ShutdownIfRobloxRunning] Checking if Roblox is running...");
Process[] processes = Process.GetProcessesByName("RobloxPlayerBeta");
if (processes.Length == 0)
{
App.Logger.WriteLine($"[Bootstrapper::CheckIfRunning] Roblox is not running");
if (Utilities.GetProcessCount("RobloxPlayerBeta") == 0)
return false;
}
App.Logger.WriteLine($"[Bootstrapper::CheckIfRunning] Roblox is running, found {processes.Length} process(es)");
if (!shutdown)
return true;
App.Logger.WriteLine($"[Bootstrapper::CheckIfRunning] Attempting to shutdown Roblox...");
App.Logger.WriteLine($"[Bootstrapper::ShutdownIfRobloxRunning] Attempting to shutdown Roblox...");
Dialog?.PromptShutdown();
try
{
// try/catch just in case process was closed before prompt was answered
foreach (Process process in processes)
foreach (Process process in Process.GetProcessesByName("RobloxPlayerBeta"))
{
process.CloseMainWindow();
process.Close();
@ -298,10 +320,10 @@ namespace Bloxstrap
}
catch (Exception ex)
{
App.Logger.WriteLine($"[Bootstrapper::CheckIfRunning] Failed to close process! {ex}");
App.Logger.WriteLine($"[Bootstrapper::ShutdownIfRobloxRunning] Failed to close process! {ex}");
}
App.Logger.WriteLine($"[Bootstrapper::CheckIfRunning] All Roblox processes closed");
App.Logger.WriteLine($"[Bootstrapper::ShutdownIfRobloxRunning] All Roblox processes closed");
return true;
}
@ -327,10 +349,27 @@ namespace Bloxstrap
// whether we should wait for roblox to exit to handle stuff in the background or clean up after roblox closes
bool shouldWait = false;
Mutex singletonMutex;
if (App.Settings.Prop.MultiInstanceLaunching)
{
App.Logger.WriteLine("[Bootstrapper::StartRoblox] Creating singleton mutex");
// this might be a bit problematic since this mutex will be released when the first launched instance is closed...
try
{
singletonMutex = Mutex.OpenExisting("ROBLOX_singletonMutex");
App.Logger.WriteLine("[Bootstrapper::StartRoblox] Warning - singleton mutex already exists");
}
catch
{
singletonMutex = new Mutex(true, "ROBLOX_singletonMutex");
}
shouldWait = true;
}
Process gameClient = Process.Start(Path.Combine(_versionFolder, "RobloxPlayerBeta.exe"), _launchCommandLine);
List<Process> autocloseProcesses = new();
DiscordRichPresence? richPresence = null;
Mutex? singletonMutex = null;
App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Started Roblox (PID {gameClient.Id})");
@ -370,14 +409,6 @@ namespace Bloxstrap
shouldWait = true;
}
if (App.Settings.Prop.MultiInstanceLaunching)
{
App.Logger.WriteLine("[Bootstrapper::StartRoblox] Creating singleton mutex");
// this might be a bit problematic since this mutex will be released when the first launched instance is closed...
singletonMutex = new Mutex(true, "ROBLOX_singletonMutex");
shouldWait = true;
}
// launch custom integrations now
foreach (CustomIntegration integration in App.Settings.Prop.CustomIntegrations)
{
@ -548,7 +579,7 @@ namespace Bloxstrap
private void Uninstall()
{
CheckIfRunning(true);
ShutdownIfRobloxRunning();
SetStatus($"Uninstalling {App.ProjectName}...");

View File

@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Bloxstrap.Helpers
{
// https://gist.github.com/dfederm/35c729f6218834b764fa04c219181e4e
public sealed class AsyncMutex : IAsyncDisposable
{
private readonly string _name;
private Task? _mutexTask;
private ManualResetEventSlim? _releaseEvent;
private CancellationTokenSource? _cancellationTokenSource;
public AsyncMutex(string name)
{
_name = name;
}
public Task AcquireAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
TaskCompletionSource taskCompletionSource = new();
_releaseEvent = new ManualResetEventSlim();
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
// Putting all mutex manipulation in its own task as it doesn't work in async contexts
// Note: this task should not throw.
_mutexTask = Task.Factory.StartNew(
state =>
{
try
{
CancellationToken cancellationToken = _cancellationTokenSource.Token;
using var mutex = new Mutex(false, _name);
try
{
// Wait for either the mutex to be acquired, or cancellation
if (WaitHandle.WaitAny(new[] { mutex, cancellationToken.WaitHandle }) != 0)
{
taskCompletionSource.SetCanceled(cancellationToken);
return;
}
}
catch (AbandonedMutexException)
{
// Abandoned by another process, we acquired it.
}
taskCompletionSource.SetResult();
// Wait until the release call
_releaseEvent.Wait();
mutex.ReleaseMutex();
}
catch (OperationCanceledException)
{
taskCompletionSource.TrySetCanceled(cancellationToken);
}
catch (Exception ex)
{
taskCompletionSource.TrySetException(ex);
}
},
state: null,
cancellationToken,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
return taskCompletionSource.Task;
}
public async Task ReleaseAsync()
{
_releaseEvent?.Set();
if (_mutexTask != null)
{
await _mutexTask;
}
}
public async ValueTask DisposeAsync()
{
// Ensure the mutex task stops waiting for any acquire
_cancellationTokenSource?.Cancel();
// Ensure the mutex is released
await ReleaseAsync();
_releaseEvent?.Dispose();
_cancellationTokenSource?.Dispose();
}
}
}

View File

@ -62,6 +62,10 @@ namespace Bloxstrap.Helpers.Integrations
if (!App.Settings.Prop.RFUEnabled)
{
// don't delete rbxfpsunlocker if rbxfpsunlocker and roblox is currently running
if (Utilities.GetProcessCount(ApplicationName) > 0 && Utilities.GetProcessCount("RobloxPlayerBeta") > 0)
return;
App.State.Prop.RbxFpsUnlockerVersion = "";
App.State.Save();

View File

@ -325,6 +325,9 @@ namespace Bloxstrap.Helpers.Integrations
if (!App.Settings.Prop.UseReShadeExtraviPresets && !String.IsNullOrEmpty(App.State.Prop.ExtraviReShadePresetsVersion))
{
if (Utilities.GetProcessCount("RobloxPlayerBeta") > 0)
return;
UninstallExtraviPresets();
App.State.Prop.ExtraviReShadePresetsVersion = "";
@ -333,6 +336,9 @@ namespace Bloxstrap.Helpers.Integrations
if (!App.Settings.Prop.UseReShade)
{
if (Utilities.GetProcessCount("RobloxPlayerBeta") > 0)
return;
App.Logger.WriteLine("[ReShade::CheckModifications] ReShade is not enabled");
// we should already be uninstalled

View File

@ -26,6 +26,17 @@ namespace Bloxstrap.Helpers
return -1;
}
public static int GetProcessCount(string processName)
{
App.Logger.WriteLine($"[Utilities::CheckIfProcessRunning] Checking if '{processName}' is running...");
Process[] processes = Process.GetProcessesByName("RobloxPlayerBeta");
App.Logger.WriteLine($"[Utilities::CheckIfProcessRunning] Found {processes.Length} process(es) running for '{processName}'");
return processes.Length;
}
public static void OpenWebsite(string website)
{
Process.Start(new ProcessStartInfo { FileName = website, UseShellExecute = true });