mirror of
https://github.com/bloxstraplabs/bloxstrap.git
synced 2025-04-21 10:01:27 -07:00
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:
parent
467f55c4b2
commit
b5250e29dc
@ -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();
|
||||
|
@ -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}...");
|
||||
|
||||
|
101
Bloxstrap/Helpers/AsyncMutex.cs
Normal file
101
Bloxstrap/Helpers/AsyncMutex.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 });
|
||||
|
Loading…
Reference in New Issue
Block a user