From b5250e29dcbc7705f0719116920a55210ba24f75 Mon Sep 17 00:00:00 2001 From: pizzaboxer <41478239+pizzaboxer@users.noreply.github.com> Date: Sun, 19 Feb 2023 00:37:33 +0000 Subject: [PATCH] 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 --- Bloxstrap/App.xaml.cs | 23 ++-- Bloxstrap/Bootstrapper.cs | 99 +++++++++++------ Bloxstrap/Helpers/AsyncMutex.cs | 101 ++++++++++++++++++ .../Helpers/Integrations/RbxFpsUnlocker.cs | 4 + Bloxstrap/Helpers/Integrations/ReShade.cs | 6 ++ Bloxstrap/Helpers/Utilities.cs | 11 ++ 6 files changed, 204 insertions(+), 40 deletions(-) create mode 100644 Bloxstrap/Helpers/AsyncMutex.cs diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index 453edfe..dfeaf49 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -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) { + Mutex mutex; + + try + { + 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 (Process.GetProcessesByName(ProjectName).Length > 1) - { - ShowMessageBox($"{ProjectName} is currently running. Please close any currently open Bloxstrap or Roblox window before opening the menu.", MessageBoxImage.Error); - Environment.Exit(0); - } + 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(); diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 1eaafbe..cd6ec41 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -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; @@ -133,13 +163,17 @@ namespace Bloxstrap CheckInstall(); 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 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}..."); diff --git a/Bloxstrap/Helpers/AsyncMutex.cs b/Bloxstrap/Helpers/AsyncMutex.cs new file mode 100644 index 0000000..3691582 --- /dev/null +++ b/Bloxstrap/Helpers/AsyncMutex.cs @@ -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(); + } + } +} diff --git a/Bloxstrap/Helpers/Integrations/RbxFpsUnlocker.cs b/Bloxstrap/Helpers/Integrations/RbxFpsUnlocker.cs index 504614d..d353bfe 100644 --- a/Bloxstrap/Helpers/Integrations/RbxFpsUnlocker.cs +++ b/Bloxstrap/Helpers/Integrations/RbxFpsUnlocker.cs @@ -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(); diff --git a/Bloxstrap/Helpers/Integrations/ReShade.cs b/Bloxstrap/Helpers/Integrations/ReShade.cs index efe0951..eacc91d 100644 --- a/Bloxstrap/Helpers/Integrations/ReShade.cs +++ b/Bloxstrap/Helpers/Integrations/ReShade.cs @@ -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 diff --git a/Bloxstrap/Helpers/Utilities.cs b/Bloxstrap/Helpers/Utilities.cs index b6237df..5b22a84 100644 --- a/Bloxstrap/Helpers/Utilities.cs +++ b/Bloxstrap/Helpers/Utilities.cs @@ -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 });