mirror of
https://github.com/bloxstraplabs/bloxstrap.git
synced 2025-04-18 16:41:36 -07:00
Move activity watcher to separate process (#810)
this was done to: - ensure robloxplayerbeta launches as an orphaned process - help alleviate problems with multiple instances - alleviate problems with the notifyicon causing blocking conflicts on the bootstrapper ui thread - help reduce functional dependency on the bootstrapper, makes it less monolithic and more maintainable ive always wanted to do this for a long while, but have always put it off because of how painful it would be this may genuinely be the most painful refactoring i've ever had to do, but after 2 days, i managed to do it, and it works great!
This commit is contained in:
parent
cf45d9c808
commit
fd290f9ff7
@ -8,6 +8,7 @@ using Microsoft.Win32;
|
||||
using Bloxstrap.Models.SettingTasks.Base;
|
||||
using Bloxstrap.UI.Elements.About.Pages;
|
||||
using Bloxstrap.UI.Elements.About;
|
||||
using System;
|
||||
|
||||
namespace Bloxstrap
|
||||
{
|
||||
@ -37,8 +38,6 @@ namespace Bloxstrap
|
||||
|
||||
public static readonly MD5 MD5Provider = MD5.Create();
|
||||
|
||||
public static NotifyIconWrapper? NotifyIcon { get; set; }
|
||||
|
||||
public static readonly Logger Logger = new();
|
||||
|
||||
public static readonly Dictionary<string, BaseTask> PendingSettingTasks = new();
|
||||
@ -55,19 +54,23 @@ namespace Bloxstrap
|
||||
)
|
||||
);
|
||||
|
||||
#if RELEASE
|
||||
private static bool _showingExceptionDialog = false;
|
||||
#endif
|
||||
|
||||
private static bool _terminating = false;
|
||||
|
||||
public static void Terminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS)
|
||||
{
|
||||
if (_terminating)
|
||||
return;
|
||||
|
||||
int exitCodeNum = (int)exitCode;
|
||||
|
||||
Logger.WriteLine("App::Terminate", $"Terminating with exit code {exitCodeNum} ({exitCode})");
|
||||
|
||||
NotifyIcon?.Dispose();
|
||||
Current.Dispatcher.Invoke(() => Current.Shutdown(exitCodeNum));
|
||||
// Environment.Exit(exitCodeNum);
|
||||
|
||||
Environment.Exit(exitCodeNum);
|
||||
_terminating = true;
|
||||
}
|
||||
|
||||
void GlobalExceptionHandler(object sender, DispatcherUnhandledExceptionEventArgs e)
|
||||
@ -79,24 +82,28 @@ namespace Bloxstrap
|
||||
FinalizeExceptionHandling(e.Exception);
|
||||
}
|
||||
|
||||
public static void FinalizeExceptionHandling(Exception exception, bool log = true)
|
||||
public static void FinalizeExceptionHandling(AggregateException ex)
|
||||
{
|
||||
foreach (var innerEx in ex.InnerExceptions)
|
||||
Logger.WriteException("App::FinalizeExceptionHandling", innerEx);
|
||||
|
||||
FinalizeExceptionHandling(ex.GetBaseException(), false);
|
||||
}
|
||||
|
||||
public static void FinalizeExceptionHandling(Exception ex, bool log = true)
|
||||
{
|
||||
if (log)
|
||||
Logger.WriteException("App::FinalizeExceptionHandling", exception);
|
||||
Logger.WriteException("App::FinalizeExceptionHandling", ex);
|
||||
|
||||
#if DEBUG
|
||||
throw exception;
|
||||
#else
|
||||
if (_showingExceptionDialog)
|
||||
return;
|
||||
|
||||
_showingExceptionDialog = true;
|
||||
|
||||
if (!LaunchSettings.QuietFlag.Active)
|
||||
Frontend.ShowExceptionDialog(exception);
|
||||
Frontend.ShowExceptionDialog(ex);
|
||||
|
||||
Terminate(ErrorCode.ERROR_INSTALL_FAILURE);
|
||||
#endif
|
||||
}
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
@ -208,10 +215,6 @@ namespace Bloxstrap
|
||||
State.Load();
|
||||
FastFlags.Load();
|
||||
|
||||
// we can only parse them now as settings need
|
||||
// to be loaded first to know what our channel is
|
||||
// LaunchSettings.ParseRoblox();
|
||||
|
||||
if (!Locale.SupportedLocales.ContainsKey(Settings.Prop.Locale))
|
||||
{
|
||||
Settings.Prop.Locale = "nil";
|
||||
@ -228,7 +231,7 @@ namespace Bloxstrap
|
||||
LaunchHandler.ProcessLaunchArgs();
|
||||
}
|
||||
|
||||
Terminate();
|
||||
// you must *explicitly* call terminate when everything is done, it won't be called implicitly
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,6 @@ using System.Windows.Forms;
|
||||
|
||||
using Microsoft.Win32;
|
||||
|
||||
using Bloxstrap.Integrations;
|
||||
using Bloxstrap.Resources;
|
||||
using Bloxstrap.AppData;
|
||||
|
||||
namespace Bloxstrap
|
||||
@ -289,9 +287,6 @@ namespace Bloxstrap
|
||||
_launchCommandLine = _launchCommandLine.Replace("robloxLocale:en_us", $"robloxLocale:{match.Groups[1].Value}", StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
// whether we should wait for roblox to exit to handle stuff in the background or clean up after roblox closes
|
||||
bool shouldWait = false;
|
||||
|
||||
var startInfo = new ProcessStartInfo()
|
||||
{
|
||||
FileName = _playerLocation,
|
||||
@ -308,19 +303,16 @@ namespace Bloxstrap
|
||||
|
||||
// v2.2.0 - byfron will trip if we keep a process handle open for over a minute, so we're doing this now
|
||||
int gameClientPid;
|
||||
using (Process gameClient = Process.Start(startInfo)!)
|
||||
using (var gameClient = Process.Start(startInfo)!)
|
||||
{
|
||||
gameClientPid = gameClient.Id;
|
||||
}
|
||||
|
||||
List<Process?> autocloseProcesses = new();
|
||||
ActivityWatcher? activityWatcher = null;
|
||||
DiscordRichPresence? richPresence = null;
|
||||
|
||||
App.Logger.WriteLine(LOG_IDENT, $"Started Roblox (PID {gameClientPid})");
|
||||
|
||||
using (var startEvent = new SystemEvent(AppData.StartEvent))
|
||||
{
|
||||
// TODO: get rid of this
|
||||
bool startEventFired = await startEvent.WaitForEvent();
|
||||
|
||||
startEvent.Close();
|
||||
@ -330,40 +322,14 @@ namespace Bloxstrap
|
||||
return;
|
||||
}
|
||||
|
||||
if (App.Settings.Prop.EnableActivityTracking && _launchMode == LaunchMode.Player)
|
||||
App.NotifyIcon?.SetProcessId(gameClientPid);
|
||||
|
||||
if (App.Settings.Prop.EnableActivityTracking)
|
||||
{
|
||||
activityWatcher = new(gameClientPid);
|
||||
shouldWait = true;
|
||||
|
||||
App.NotifyIcon?.SetActivityWatcher(activityWatcher);
|
||||
|
||||
if (App.Settings.Prop.UseDisableAppPatch)
|
||||
{
|
||||
activityWatcher.OnAppClose += (_, _) =>
|
||||
{
|
||||
App.Logger.WriteLine(LOG_IDENT, "Received desktop app exit, closing Roblox");
|
||||
using var process = Process.GetProcessById(gameClientPid);
|
||||
process.CloseMainWindow();
|
||||
};
|
||||
}
|
||||
|
||||
if (App.Settings.Prop.UseDiscordRichPresence)
|
||||
{
|
||||
App.Logger.WriteLine(LOG_IDENT, "Using Discord Rich Presence");
|
||||
richPresence = new(activityWatcher);
|
||||
|
||||
App.NotifyIcon?.SetRichPresenceHandler(richPresence);
|
||||
}
|
||||
}
|
||||
var autoclosePids = new List<int>();
|
||||
|
||||
// launch custom integrations now
|
||||
foreach (CustomIntegration integration in App.Settings.Prop.CustomIntegrations)
|
||||
foreach (var integration in App.Settings.Prop.CustomIntegrations)
|
||||
{
|
||||
App.Logger.WriteLine(LOG_IDENT, $"Launching custom integration '{integration.Name}' ({integration.Location} {integration.LaunchArgs} - autoclose is {integration.AutoClose})");
|
||||
|
||||
int pid = 0;
|
||||
try
|
||||
{
|
||||
var process = Process.Start(new ProcessStartInfo
|
||||
@ -372,48 +338,34 @@ namespace Bloxstrap
|
||||
Arguments = integration.LaunchArgs.Replace("\r\n", " "),
|
||||
WorkingDirectory = Path.GetDirectoryName(integration.Location),
|
||||
UseShellExecute = true
|
||||
});
|
||||
})!;
|
||||
|
||||
if (integration.AutoClose)
|
||||
{
|
||||
shouldWait = true;
|
||||
autocloseProcesses.Add(process);
|
||||
}
|
||||
pid = process.Id;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
App.Logger.WriteLine(LOG_IDENT, $"Failed to launch integration '{integration.Name}'!");
|
||||
App.Logger.WriteLine(LOG_IDENT, $"{ex.Message}");
|
||||
}
|
||||
|
||||
if (integration.AutoClose && pid != 0)
|
||||
autoclosePids.Add(pid);
|
||||
}
|
||||
|
||||
using (var proclock = new InterProcessLock("Watcher"))
|
||||
{
|
||||
string args = gameClientPid.ToString();
|
||||
|
||||
if (autoclosePids.Any())
|
||||
args += $";{String.Join(',', autoclosePids)}";
|
||||
|
||||
if (proclock.IsAcquired)
|
||||
Process.Start(Paths.Process, $"-watcher \"{args}\"");
|
||||
}
|
||||
|
||||
// event fired, wait for 3 seconds then close
|
||||
await Task.Delay(3000);
|
||||
Dialog?.CloseBootstrapper();
|
||||
|
||||
// keep bloxstrap open in the background if needed
|
||||
if (!shouldWait)
|
||||
return;
|
||||
|
||||
activityWatcher?.StartWatcher();
|
||||
|
||||
App.Logger.WriteLine(LOG_IDENT, "Waiting for Roblox to close");
|
||||
|
||||
while (Utilities.GetProcessesSafe().Any(x => x.Id == gameClientPid))
|
||||
await Task.Delay(1000);
|
||||
|
||||
App.Logger.WriteLine(LOG_IDENT, $"Roblox has exited");
|
||||
|
||||
richPresence?.Dispose();
|
||||
|
||||
foreach (var process in autocloseProcesses)
|
||||
{
|
||||
if (process is null || process.HasExited)
|
||||
continue;
|
||||
|
||||
App.Logger.WriteLine(LOG_IDENT, $"Autoclosing process '{process.ProcessName}' (PID {process.Id})");
|
||||
process.Kill();
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelInstall()
|
||||
|
@ -19,7 +19,6 @@
|
||||
private const string GameJoinedEntryPattern = @"serverId: ([0-9\.]+)\|[0-9]+";
|
||||
private const string GameMessageEntryPattern = @"\[BloxstrapRPC\] (.*)";
|
||||
|
||||
private int _gameClientPid;
|
||||
private int _logEntriesRead = 0;
|
||||
private bool _teleportMarker = false;
|
||||
private bool _reservedTeleportMarker = false;
|
||||
@ -27,6 +26,7 @@
|
||||
public event EventHandler<string>? OnLogEntry;
|
||||
public event EventHandler? OnGameJoin;
|
||||
public event EventHandler? OnGameLeave;
|
||||
public event EventHandler? OnLogOpen;
|
||||
public event EventHandler? OnAppClose;
|
||||
public event EventHandler<Message>? OnRPCMessage;
|
||||
|
||||
@ -47,14 +47,9 @@
|
||||
|
||||
public bool IsDisposed = false;
|
||||
|
||||
public ActivityWatcher(int gameClientPid)
|
||||
public async void Start()
|
||||
{
|
||||
_gameClientPid = gameClientPid;
|
||||
}
|
||||
|
||||
public async void StartWatcher()
|
||||
{
|
||||
const string LOG_IDENT = "ActivityWatcher::StartWatcher";
|
||||
const string LOG_IDENT = "ActivityWatcher::Start";
|
||||
|
||||
// okay, here's the process:
|
||||
//
|
||||
@ -84,23 +79,26 @@
|
||||
{
|
||||
logFileInfo = new DirectoryInfo(logDirectory)
|
||||
.GetFiles()
|
||||
.Where(x => x.CreationTime <= DateTime.Now)
|
||||
.Where(x => x.Name.Contains("Player", StringComparison.OrdinalIgnoreCase) && x.CreationTime <= DateTime.Now)
|
||||
.OrderByDescending(x => x.CreationTime)
|
||||
.First();
|
||||
|
||||
if (logFileInfo.CreationTime.AddSeconds(15) > DateTime.Now)
|
||||
break;
|
||||
|
||||
// TODO: report failure after 10 seconds of no log file
|
||||
App.Logger.WriteLine(LOG_IDENT, $"Could not find recent enough log file, waiting... (newest is {logFileInfo.Name})");
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
|
||||
OnLogOpen?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
LogLocation = logFileInfo.FullName;
|
||||
FileStream logFileStream = logFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
App.Logger.WriteLine(LOG_IDENT, $"Opened {LogLocation}");
|
||||
|
||||
AutoResetEvent logUpdatedEvent = new(false);
|
||||
FileSystemWatcher logWatcher = new()
|
||||
var logUpdatedEvent = new AutoResetEvent(false);
|
||||
var logWatcher = new FileSystemWatcher()
|
||||
{
|
||||
Path = logDirectory,
|
||||
Filter = Path.GetFileName(logFileInfo.FullName),
|
||||
@ -108,7 +106,7 @@
|
||||
};
|
||||
logWatcher.Changed += (s, e) => logUpdatedEvent.Set();
|
||||
|
||||
using StreamReader sr = new(logFileStream);
|
||||
using var sr = new StreamReader(logFileStream);
|
||||
|
||||
while (!IsDisposed)
|
||||
{
|
||||
@ -117,13 +115,13 @@
|
||||
if (log is null)
|
||||
logUpdatedEvent.WaitOne(250);
|
||||
else
|
||||
ExamineLogEntry(log);
|
||||
ReadLogEntry(log);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExamineLogEntry(string entry)
|
||||
private void ReadLogEntry(string entry)
|
||||
{
|
||||
const string LOG_IDENT = "ActivityWatcher::ExamineLogEntry";
|
||||
const string LOG_IDENT = "ActivityWatcher::ReadLogEntry";
|
||||
|
||||
OnLogEntry?.Invoke(this, entry);
|
||||
|
||||
@ -302,7 +300,7 @@
|
||||
var ipInfo = await Http.GetJson<IPInfoResponse>($"https://ipinfo.io/{ActivityMachineAddress}/json");
|
||||
|
||||
if (ipInfo is null)
|
||||
return $"? ({Resources.Strings.ActivityTracker_LookupFailed})";
|
||||
return $"? ({Strings.ActivityTracker_LookupFailed})";
|
||||
|
||||
if (string.IsNullOrEmpty(ipInfo.Country))
|
||||
location = "?";
|
||||
@ -312,7 +310,7 @@
|
||||
location = $"{ipInfo.City}, {ipInfo.Region}, {ipInfo.Country}";
|
||||
|
||||
if (!ActivityInGame)
|
||||
return $"? ({Resources.Strings.ActivityTracker_LeftGame})";
|
||||
return $"? ({Strings.ActivityTracker_LeftGame})";
|
||||
|
||||
GeolocationCache[ActivityMachineAddress] = location;
|
||||
|
||||
@ -323,7 +321,7 @@
|
||||
App.Logger.WriteLine(LOG_IDENT, $"Failed to get server location for {ActivityMachineAddress}");
|
||||
App.Logger.WriteException(LOG_IDENT, ex);
|
||||
|
||||
return $"? ({Resources.Strings.ActivityTracker_LookupFailed})";
|
||||
return $"? ({Strings.ActivityTracker_LookupFailed})";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -194,6 +194,8 @@ namespace Bloxstrap.Integrations
|
||||
|
||||
App.Logger.WriteLine(LOG_IDENT, $"Setting presence for Place ID {placeId}");
|
||||
|
||||
// TODO: move this to its own function under the activity watcher?
|
||||
// TODO: show error if information cannot be queried instead of silently failing
|
||||
var universeIdResponse = await Http.GetJson<UniverseIdResponse>($"https://apis.roblox.com/universes/v1/places/{placeId}/universe");
|
||||
if (universeIdResponse is null)
|
||||
{
|
||||
@ -282,6 +284,7 @@ namespace Bloxstrap.Integrations
|
||||
// this is used for configuration from BloxstrapRPC
|
||||
_currentPresenceCopy = _currentPresence.Clone();
|
||||
|
||||
// TODO: use queue for stashing messages
|
||||
if (_stashedRPCMessage is not null)
|
||||
{
|
||||
App.Logger.WriteLine(LOG_IDENT, "Found stashed RPC message, invoking presence set command now");
|
||||
|
@ -1,11 +1,11 @@
|
||||
using System.Windows;
|
||||
|
||||
using Bloxstrap.UI.Elements.Dialogs;
|
||||
|
||||
using Microsoft.Win32;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
|
||||
using Bloxstrap.UI.Elements.Dialogs;
|
||||
|
||||
namespace Bloxstrap
|
||||
{
|
||||
public static class LaunchHandler
|
||||
@ -19,6 +19,7 @@ namespace Bloxstrap
|
||||
break;
|
||||
|
||||
case NextAction.LaunchRoblox:
|
||||
App.LaunchSettings.RobloxLaunchMode = LaunchMode.Player;
|
||||
LaunchRoblox();
|
||||
break;
|
||||
|
||||
@ -85,7 +86,7 @@ namespace Bloxstrap
|
||||
|
||||
ProcessNextAction(installer.CloseAction, !installer.Finished);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static void LaunchUninstaller()
|
||||
@ -120,6 +121,8 @@ namespace Bloxstrap
|
||||
Installer.DoUninstall(keepData);
|
||||
|
||||
Frontend.ShowMessageBox(Strings.Bootstrapper_SuccessfullyUninstalled, MessageBoxImage.Information);
|
||||
|
||||
App.Terminate();
|
||||
}
|
||||
|
||||
public static void LaunchSettings()
|
||||
@ -131,12 +134,12 @@ namespace Bloxstrap
|
||||
if (interlock.IsAcquired)
|
||||
{
|
||||
bool showAlreadyRunningWarning = Process.GetProcessesByName(App.ProjectName).Length > 1;
|
||||
new UI.Elements.Settings.MainWindow(showAlreadyRunningWarning).ShowDialog();
|
||||
new UI.Elements.Settings.MainWindow(showAlreadyRunningWarning).Show();
|
||||
}
|
||||
else
|
||||
{
|
||||
App.Logger.WriteLine(LOG_IDENT, $"Found an already existing menu window");
|
||||
|
||||
|
||||
var process = Utilities.GetProcessesSafe().Where(x => x.MainWindowTitle == Strings.Menu_Title).FirstOrDefault();
|
||||
|
||||
if (process is not null)
|
||||
@ -156,7 +159,6 @@ namespace Bloxstrap
|
||||
{
|
||||
const string LOG_IDENT = "LaunchHandler::LaunchRoblox";
|
||||
|
||||
|
||||
if (!File.Exists(Path.Combine(Paths.System, "mfplat.dll")))
|
||||
{
|
||||
Frontend.ShowMessageBox(Strings.Bootstrapper_WMFNotFound, MessageBoxImage.Error);
|
||||
@ -191,8 +193,6 @@ namespace Bloxstrap
|
||||
}
|
||||
}
|
||||
|
||||
App.NotifyIcon = new();
|
||||
|
||||
// start bootstrapper and show the bootstrapper modal if we're not running silently
|
||||
App.Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper");
|
||||
var bootstrapper = new Bootstrapper(installWebView2);
|
||||
@ -206,45 +206,53 @@ namespace Bloxstrap
|
||||
dialog.Bootstrapper = bootstrapper;
|
||||
}
|
||||
|
||||
Task bootstrapperTask = Task.Run(async () => await bootstrapper.Run()).ContinueWith(t =>
|
||||
Task.Run(bootstrapper.Run).ContinueWith(t =>
|
||||
{
|
||||
App.Logger.WriteLine(LOG_IDENT, "Bootstrapper task has finished");
|
||||
|
||||
// notifyicon is blocking main thread, must be disposed here
|
||||
App.NotifyIcon?.Dispose();
|
||||
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the bootstrapper");
|
||||
|
||||
if (t.Exception is null)
|
||||
return;
|
||||
if (t.Exception is not null)
|
||||
App.FinalizeExceptionHandling(t.Exception, false);
|
||||
}
|
||||
|
||||
App.Logger.WriteException(LOG_IDENT, t.Exception);
|
||||
|
||||
Exception exception = t.Exception;
|
||||
|
||||
#if !DEBUG
|
||||
if (t.Exception.GetType().ToString() == "System.AggregateException")
|
||||
exception = t.Exception.InnerException!;
|
||||
#endif
|
||||
|
||||
App.FinalizeExceptionHandling(exception, false);
|
||||
App.Terminate();
|
||||
});
|
||||
|
||||
// this ordering is very important as all wpf windows are shown as modal dialogs, mess it up and you'll end up blocking input to one of them
|
||||
dialog?.ShowBootstrapper();
|
||||
|
||||
if (!App.LaunchSettings.NoLaunchFlag.Active && App.Settings.Prop.EnableActivityTracking)
|
||||
App.NotifyIcon?.InitializeContextMenu();
|
||||
|
||||
App.Logger.WriteLine(LOG_IDENT, "Waiting for bootstrapper task to finish");
|
||||
|
||||
bootstrapperTask.Wait();
|
||||
}
|
||||
|
||||
public static void LaunchWatcher()
|
||||
{
|
||||
const string LOG_IDENT = "LaunchHandler::LaunchWatcher";
|
||||
|
||||
// this whole topology is a bit confusing, bear with me:
|
||||
// main thread: strictly UI only, handles showing of the notification area icon, context menu, server details dialog
|
||||
// - server information task: queries server location, invoked if either the explorer notification is shown or the server details dialog is opened
|
||||
// - discord rpc thread: handles rpc connection with discord
|
||||
// - discord rich presence tasks: handles querying and displaying of game information, invoked on activity watcher events
|
||||
// - watcher task: runs activity watcher + waiting for roblox to close, terminates when it has
|
||||
|
||||
var watcher = new Watcher();
|
||||
|
||||
Task.Run(watcher.Run).ContinueWith(t =>
|
||||
{
|
||||
App.Logger.WriteLine(LOG_IDENT, "Watcher task has finished");
|
||||
|
||||
watcher.Dispose();
|
||||
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the watcher");
|
||||
|
||||
if (t.Exception is not null)
|
||||
App.FinalizeExceptionHandling(t.Exception);
|
||||
}
|
||||
|
||||
App.Terminate();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ namespace Bloxstrap
|
||||
|
||||
public LaunchFlag StudioFlag { get; } = new("studio");
|
||||
|
||||
public LaunchMode RobloxLaunchMode { get; private set; } = LaunchMode.None;
|
||||
public LaunchMode RobloxLaunchMode { get; set; } = LaunchMode.None;
|
||||
|
||||
public string RobloxLaunchArgs { get; private set; } = "";
|
||||
|
||||
|
@ -26,6 +26,10 @@
|
||||
"Bloxstrap (Studio Launch)": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "-studio"
|
||||
},
|
||||
"Bloxstrap (Watcher)": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "-watcher"
|
||||
}
|
||||
}
|
||||
}
|
@ -61,7 +61,7 @@
|
||||
</Grid>
|
||||
</MenuItem.Header>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="CloseRobloxMenuItem" Visibility="Collapsed" Click="CloseRobloxMenuItem_Click">
|
||||
<MenuItem Click="CloseRobloxMenuItem_Click">
|
||||
<MenuItem.Header>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
@ -73,7 +73,7 @@
|
||||
</Grid>
|
||||
</MenuItem.Header>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="LogTracerMenuItem" Click="LogTracerMenuItem_Click">
|
||||
<MenuItem x:Name="LogTracerMenuItem" Visibility="Collapsed" Click="LogTracerMenuItem_Click">
|
||||
<MenuItem.Header>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
|
@ -22,32 +22,28 @@ namespace Bloxstrap.UI.Elements.ContextMenu
|
||||
{
|
||||
// i wouldve gladly done this as mvvm but turns out that data binding just does not work with menuitems for some reason so idk this sucks
|
||||
|
||||
private readonly ActivityWatcher? _activityWatcher;
|
||||
private readonly DiscordRichPresence? _richPresenceHandler;
|
||||
private readonly Watcher _watcher;
|
||||
|
||||
private ActivityWatcher? _activityWatcher => _watcher.ActivityWatcher;
|
||||
|
||||
private ServerInformation? _serverInformationWindow;
|
||||
private int? _processId;
|
||||
|
||||
public MenuContainer(ActivityWatcher? activityWatcher, DiscordRichPresence? richPresenceHandler, int? processId)
|
||||
public MenuContainer(Watcher watcher)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_activityWatcher = activityWatcher;
|
||||
_richPresenceHandler = richPresenceHandler;
|
||||
_processId = processId;
|
||||
_watcher = watcher;
|
||||
|
||||
if (_activityWatcher is not null)
|
||||
{
|
||||
_activityWatcher.OnLogOpen += ActivityWatcher_OnLogOpen;
|
||||
_activityWatcher.OnGameJoin += ActivityWatcher_OnGameJoin;
|
||||
_activityWatcher.OnGameLeave += ActivityWatcher_OnGameLeave;
|
||||
}
|
||||
|
||||
if (_richPresenceHandler is not null)
|
||||
if (_watcher.RichPresence is not null)
|
||||
RichPresenceMenuItem.Visibility = Visibility.Visible;
|
||||
|
||||
if (_processId is not null)
|
||||
CloseRobloxMenuItem.Visibility = Visibility.Visible;
|
||||
|
||||
VersionTextBlock.Text = $"{App.ProjectName} v{App.Version}";
|
||||
}
|
||||
|
||||
@ -55,7 +51,7 @@ namespace Bloxstrap.UI.Elements.ContextMenu
|
||||
{
|
||||
if (_serverInformationWindow is null)
|
||||
{
|
||||
_serverInformationWindow = new ServerInformation(_activityWatcher!);
|
||||
_serverInformationWindow = new ServerInformation(_watcher);
|
||||
_serverInformationWindow.Closed += (_, _) => _serverInformationWindow = null;
|
||||
}
|
||||
|
||||
@ -65,17 +61,23 @@ namespace Bloxstrap.UI.Elements.ContextMenu
|
||||
_serverInformationWindow.Activate();
|
||||
}
|
||||
|
||||
private void ActivityWatcher_OnGameJoin(object? sender, EventArgs e)
|
||||
public void ActivityWatcher_OnLogOpen(object? sender, EventArgs e) =>
|
||||
Dispatcher.Invoke(() => LogTracerMenuItem.Visibility = Visibility.Visible);
|
||||
|
||||
public void ActivityWatcher_OnGameJoin(object? sender, EventArgs e)
|
||||
{
|
||||
if (_activityWatcher is null)
|
||||
return;
|
||||
|
||||
Dispatcher.Invoke(() => {
|
||||
if (_activityWatcher?.ActivityServerType == ServerType.Public)
|
||||
if (_activityWatcher.ActivityServerType == ServerType.Public)
|
||||
InviteDeeplinkMenuItem.Visibility = Visibility.Visible;
|
||||
|
||||
ServerDetailsMenuItem.Visibility = Visibility.Visible;
|
||||
});
|
||||
}
|
||||
|
||||
private void ActivityWatcher_OnGameLeave(object? sender, EventArgs e)
|
||||
public void ActivityWatcher_OnGameLeave(object? sender, EventArgs e)
|
||||
{
|
||||
Dispatcher.Invoke(() => {
|
||||
InviteDeeplinkMenuItem.Visibility = Visibility.Collapsed;
|
||||
@ -100,7 +102,7 @@ namespace Bloxstrap.UI.Elements.ContextMenu
|
||||
|
||||
private void Window_Closed(object sender, EventArgs e) => App.Logger.WriteLine("MenuContainer::Window_Closed", "Context menu container closed");
|
||||
|
||||
private void RichPresenceMenuItem_Click(object sender, RoutedEventArgs e) => _richPresenceHandler?.SetVisibility(((MenuItem)sender).IsChecked);
|
||||
private void RichPresenceMenuItem_Click(object sender, RoutedEventArgs e) => _watcher.RichPresence?.SetVisibility(((MenuItem)sender).IsChecked);
|
||||
|
||||
private void InviteDeeplinkMenuItem_Click(object sender, RoutedEventArgs e) => Clipboard.SetDataObject($"roblox://experiences/start?placeId={_activityWatcher?.ActivityPlaceId}&gameInstanceId={_activityWatcher?.ActivityJobId}");
|
||||
|
||||
@ -110,13 +112,8 @@ namespace Bloxstrap.UI.Elements.ContextMenu
|
||||
{
|
||||
string? location = _activityWatcher?.LogLocation;
|
||||
|
||||
if (location is null)
|
||||
{
|
||||
Frontend.ShowMessageBox(Strings.ContextMenu_RobloxNotRunning, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
Utilities.ShellExecute(location);
|
||||
if (location is not null)
|
||||
Utilities.ShellExecute(location);
|
||||
}
|
||||
|
||||
private void CloseRobloxMenuItem_Click(object sender, RoutedEventArgs e)
|
||||
@ -130,9 +127,7 @@ namespace Bloxstrap.UI.Elements.ContextMenu
|
||||
if (result != MessageBoxResult.Yes)
|
||||
return;
|
||||
|
||||
using Process process = Process.GetProcessById((int)_processId!);
|
||||
process.Kill();
|
||||
process.Close();
|
||||
_watcher.KillRobloxProcess();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,9 +22,13 @@ namespace Bloxstrap.UI.Elements.ContextMenu
|
||||
/// </summary>
|
||||
public partial class ServerInformation
|
||||
{
|
||||
public ServerInformation(ActivityWatcher activityWatcher)
|
||||
public ServerInformation(Watcher watcher)
|
||||
{
|
||||
DataContext = new ServerInformationViewModel(this, activityWatcher);
|
||||
var viewModel = new ServerInformationViewModel(watcher);
|
||||
|
||||
viewModel.RequestCloseEvent += (_, _) => Close();
|
||||
|
||||
DataContext = viewModel;
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
|
@ -94,7 +94,7 @@
|
||||
<ui:Button Content="{x:Static resources:Strings.Menu_Save}" Appearance="Primary" Command="{Binding SaveSettingsCommand, Mode=OneWay}" />
|
||||
</StatusBarItem>
|
||||
<StatusBarItem Grid.Column="2" Padding="4,0,0,0">
|
||||
<ui:Button Content="{x:Static resources:Strings.Common_Close}" IsCancel="True" />
|
||||
<ui:Button Content="{x:Static resources:Strings.Common_Close}" Command="{Binding CloseWindowCommand, Mode=OneWay}" />
|
||||
</StatusBarItem>
|
||||
</StatusBar>
|
||||
</Grid>
|
||||
|
@ -17,7 +17,9 @@ namespace Bloxstrap.UI.Elements.Settings
|
||||
public MainWindow(bool showAlreadyRunningWarning)
|
||||
{
|
||||
var viewModel = new MainWindowViewModel();
|
||||
|
||||
viewModel.RequestSaveNoticeEvent += (_, _) => SettingsSavedSnackbar.Show();
|
||||
viewModel.RequestCloseWindowEvent += (_, _) => Close();
|
||||
|
||||
DataContext = viewModel;
|
||||
|
||||
@ -64,6 +66,9 @@ namespace Bloxstrap.UI.Elements.Settings
|
||||
if (result != MessageBoxResult.Yes)
|
||||
e.Cancel = true;
|
||||
}
|
||||
|
||||
if (!e.Cancel)
|
||||
App.Terminate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bloxstrap.Integrations;
|
||||
using Bloxstrap.UI.Elements.About;
|
||||
using Bloxstrap.UI.Elements.ContextMenu;
|
||||
|
||||
namespace Bloxstrap.UI
|
||||
@ -10,18 +11,21 @@ namespace Bloxstrap.UI
|
||||
private bool _disposing = false;
|
||||
|
||||
private readonly System.Windows.Forms.NotifyIcon _notifyIcon;
|
||||
private MenuContainer? _menuContainer;
|
||||
|
||||
private ActivityWatcher? _activityWatcher;
|
||||
private DiscordRichPresence? _richPresenceHandler;
|
||||
private int? _processId;
|
||||
private readonly MenuContainer _menuContainer;
|
||||
|
||||
private readonly Watcher _watcher;
|
||||
|
||||
private ActivityWatcher? _activityWatcher => _watcher.ActivityWatcher;
|
||||
|
||||
EventHandler? _alertClickHandler;
|
||||
|
||||
public NotifyIconWrapper()
|
||||
public NotifyIconWrapper(Watcher watcher)
|
||||
{
|
||||
App.Logger.WriteLine("NotifyIconWrapper::NotifyIconWrapper", "Initializing notification area icon");
|
||||
|
||||
_watcher = watcher;
|
||||
|
||||
_notifyIcon = new()
|
||||
{
|
||||
Icon = Properties.Resources.IconBloxstrap,
|
||||
@ -30,52 +34,18 @@ namespace Bloxstrap.UI
|
||||
};
|
||||
|
||||
_notifyIcon.MouseClick += MouseClickEventHandler;
|
||||
}
|
||||
|
||||
#region Handler registers
|
||||
public void SetRichPresenceHandler(DiscordRichPresence richPresenceHandler)
|
||||
{
|
||||
if (_richPresenceHandler is not null)
|
||||
return;
|
||||
|
||||
_richPresenceHandler = richPresenceHandler;
|
||||
}
|
||||
|
||||
public void SetActivityWatcher(ActivityWatcher activityWatcher)
|
||||
{
|
||||
if (_activityWatcher is not null)
|
||||
return;
|
||||
_activityWatcher.OnGameJoin += OnGameJoin;
|
||||
|
||||
_activityWatcher = activityWatcher;
|
||||
|
||||
if (App.Settings.Prop.ShowServerDetails)
|
||||
_activityWatcher.OnGameJoin += (_, _) => Task.Run(OnGameJoin);
|
||||
_menuContainer = new(_watcher);
|
||||
_menuContainer.Show();
|
||||
}
|
||||
|
||||
public void SetProcessId(int processId)
|
||||
{
|
||||
if (_processId is not null)
|
||||
return;
|
||||
|
||||
_processId = processId;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Context menu
|
||||
public void InitializeContextMenu()
|
||||
{
|
||||
if (_menuContainer is not null || _disposing)
|
||||
return;
|
||||
|
||||
App.Logger.WriteLine("NotifyIconWrapper::InitializeContextMenu", "Initializing context menu");
|
||||
|
||||
_menuContainer = new(_activityWatcher, _richPresenceHandler, _processId);
|
||||
_menuContainer.ShowDialog();
|
||||
}
|
||||
|
||||
public void MouseClickEventHandler(object? sender, System.Windows.Forms.MouseEventArgs e)
|
||||
{
|
||||
if (e.Button != System.Windows.Forms.MouseButtons.Right || _menuContainer is null)
|
||||
if (e.Button != System.Windows.Forms.MouseButtons.Right)
|
||||
return;
|
||||
|
||||
_menuContainer.Activate();
|
||||
@ -84,9 +54,12 @@ namespace Bloxstrap.UI
|
||||
#endregion
|
||||
|
||||
#region Activity handlers
|
||||
public async void OnGameJoin()
|
||||
public async void OnGameJoin(object? sender, EventArgs e)
|
||||
{
|
||||
string serverLocation = await _activityWatcher!.GetServerLocation();
|
||||
if (_activityWatcher is null)
|
||||
return;
|
||||
|
||||
string serverLocation = await _activityWatcher.GetServerLocation();
|
||||
string title = _activityWatcher.ActivityServerType switch
|
||||
{
|
||||
ServerType.Public => Strings.ContextMenu_ServerInformation_Notification_Title_Public,
|
||||
@ -99,7 +72,7 @@ namespace Bloxstrap.UI
|
||||
title,
|
||||
String.Format(Strings.ContextMenu_ServerInformation_Notification_Text, serverLocation),
|
||||
10,
|
||||
(_, _) => _menuContainer?.ShowServerInformationWindow()
|
||||
(_, _) => _menuContainer.ShowServerInformationWindow()
|
||||
);
|
||||
}
|
||||
#endregion
|
||||
@ -151,9 +124,8 @@ namespace Bloxstrap.UI
|
||||
|
||||
App.Logger.WriteLine("NotifyIconWrapper::Dispose", "Disposing NotifyIcon");
|
||||
|
||||
_menuContainer?.Dispatcher.Invoke(_menuContainer.Close);
|
||||
_notifyIcon?.Dispose();
|
||||
|
||||
_menuContainer.Dispatcher.Invoke(_menuContainer.Close);
|
||||
_notifyIcon.Dispose();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
@ -7,20 +7,23 @@ namespace Bloxstrap.UI.ViewModels.ContextMenu
|
||||
{
|
||||
internal class ServerInformationViewModel : NotifyPropertyChangedViewModel
|
||||
{
|
||||
private readonly Window _window;
|
||||
private readonly ActivityWatcher _activityWatcher;
|
||||
|
||||
public string InstanceId => _activityWatcher.ActivityJobId;
|
||||
public string ServerType => Resources.Strings.ResourceManager.GetStringSafe($"Enums.ServerType.{_activityWatcher.ActivityServerType}");
|
||||
public string ServerLocation { get; private set; } = Resources.Strings.ContextMenu_ServerInformation_Loading;
|
||||
|
||||
public string ServerType => Strings.ResourceManager.GetStringSafe($"Enums.ServerType.{_activityWatcher.ActivityServerType}");
|
||||
|
||||
public string ServerLocation { get; private set; } = Strings.ContextMenu_ServerInformation_Loading;
|
||||
|
||||
public ICommand CopyInstanceIdCommand => new RelayCommand(CopyInstanceId);
|
||||
public ICommand CloseWindowCommand => new RelayCommand(_window.Close);
|
||||
|
||||
public ServerInformationViewModel(Window window, ActivityWatcher activityWatcher)
|
||||
public ICommand CloseWindowCommand => new RelayCommand(RequestClose);
|
||||
|
||||
public EventHandler? RequestCloseEvent;
|
||||
|
||||
public ServerInformationViewModel(Watcher watcher)
|
||||
{
|
||||
_window = window;
|
||||
_activityWatcher = activityWatcher;
|
||||
_activityWatcher = watcher.ActivityWatcher!;
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
@ -30,5 +33,7 @@ namespace Bloxstrap.UI.ViewModels.ContextMenu
|
||||
}
|
||||
|
||||
private void CopyInstanceId() => Clipboard.SetDataObject(InstanceId);
|
||||
|
||||
private void RequestClose() => RequestCloseEvent?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,6 @@ namespace Bloxstrap.UI.ViewModels.Installer
|
||||
|
||||
private void LaunchRoblox() => CloseWindowRequest?.Invoke(this, NextAction.LaunchRoblox);
|
||||
|
||||
private void LaunchAbout() => new MainWindow().Show();
|
||||
private void LaunchAbout() => new MainWindow().ShowDialog();
|
||||
}
|
||||
}
|
||||
|
@ -9,11 +9,17 @@ namespace Bloxstrap.UI.ViewModels.Settings
|
||||
public ICommand OpenAboutCommand => new RelayCommand(OpenAbout);
|
||||
|
||||
public ICommand SaveSettingsCommand => new RelayCommand(SaveSettings);
|
||||
|
||||
public ICommand CloseWindowCommand => new RelayCommand(CloseWindow);
|
||||
|
||||
public EventHandler? RequestSaveNoticeEvent;
|
||||
|
||||
public EventHandler? RequestCloseWindowEvent;
|
||||
|
||||
private void OpenAbout() => new MainWindow().ShowDialog();
|
||||
|
||||
private void CloseWindow() => RequestCloseWindowEvent?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
private void SaveSettings()
|
||||
{
|
||||
const string LOG_IDENT = "MainWindowViewModel::SaveSettings";
|
||||
@ -35,7 +41,7 @@ namespace Bloxstrap.UI.ViewModels.Settings
|
||||
|
||||
App.PendingSettingTasks.Clear();
|
||||
|
||||
RequestSaveNoticeEvent?.Invoke(this, new EventArgs());
|
||||
RequestSaveNoticeEvent?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
147
Bloxstrap/Watcher.cs
Normal file
147
Bloxstrap/Watcher.cs
Normal file
@ -0,0 +1,147 @@
|
||||
using Bloxstrap.Integrations;
|
||||
using System.CodeDom;
|
||||
using System.Security.Permissions;
|
||||
|
||||
namespace Bloxstrap
|
||||
{
|
||||
public class Watcher : IDisposable
|
||||
{
|
||||
private int _gameClientPid = 0;
|
||||
|
||||
private readonly InterProcessLock _lock = new("Watcher");
|
||||
|
||||
private readonly List<int> _autoclosePids = new();
|
||||
|
||||
private readonly NotifyIconWrapper? _notifyIcon;
|
||||
|
||||
public readonly ActivityWatcher? ActivityWatcher;
|
||||
|
||||
public readonly DiscordRichPresence? RichPresence;
|
||||
|
||||
public Watcher()
|
||||
{
|
||||
const string LOG_IDENT = "Watcher";
|
||||
|
||||
if (!_lock.IsAcquired)
|
||||
{
|
||||
App.Logger.WriteLine(LOG_IDENT, "Watcher instance already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
string? watcherData = App.LaunchSettings.WatcherFlag.Data;
|
||||
|
||||
#if DEBUG
|
||||
if (String.IsNullOrEmpty(watcherData))
|
||||
{
|
||||
string path = Path.Combine(Paths.Versions, App.State.Prop.PlayerVersionGuid, "RobloxPlayerBeta.exe");
|
||||
using var gameClientProcess = Process.Start(path);
|
||||
_gameClientPid = gameClientProcess.Id;
|
||||
}
|
||||
#else
|
||||
if (String.IsNullOrEmpty(watcherData))
|
||||
throw new Exception("Watcher data not specified");
|
||||
#endif
|
||||
|
||||
if (!String.IsNullOrEmpty(watcherData) && _gameClientPid == 0)
|
||||
{
|
||||
var split = watcherData.Split(';');
|
||||
|
||||
if (split.Length == 0)
|
||||
_ = int.TryParse(watcherData, out _gameClientPid);
|
||||
|
||||
if (split.Length >= 1)
|
||||
_ = int.TryParse(split[0], out _gameClientPid);
|
||||
|
||||
if (split.Length >= 2)
|
||||
{
|
||||
foreach (string strPid in split[0].Split(';'))
|
||||
{
|
||||
if (int.TryParse(strPid, out int pid) && pid != 0)
|
||||
_autoclosePids.Add(pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_gameClientPid == 0)
|
||||
throw new Exception("Watcher data is invalid");
|
||||
|
||||
if (App.Settings.Prop.EnableActivityTracking)
|
||||
{
|
||||
ActivityWatcher = new();
|
||||
|
||||
if (App.Settings.Prop.UseDisableAppPatch)
|
||||
{
|
||||
ActivityWatcher.OnAppClose += (_, _) =>
|
||||
{
|
||||
App.Logger.WriteLine(LOG_IDENT, "Received desktop app exit, closing Roblox");
|
||||
using var process = Process.GetProcessById(_gameClientPid);
|
||||
process.CloseMainWindow();
|
||||
};
|
||||
}
|
||||
|
||||
if (App.Settings.Prop.UseDiscordRichPresence)
|
||||
RichPresence = new(ActivityWatcher);
|
||||
}
|
||||
|
||||
_notifyIcon = new(this);
|
||||
}
|
||||
|
||||
public void KillRobloxProcess() => KillProcess(_gameClientPid);
|
||||
|
||||
public void KillProcess(int pid)
|
||||
{
|
||||
using var process = Process.GetProcessById(pid);
|
||||
|
||||
App.Logger.WriteLine("Watcher::KillProcess", $"Killing process '{process.ProcessName}' (PID {process.Id})");
|
||||
|
||||
if (process.HasExited)
|
||||
{
|
||||
App.Logger.WriteLine("Watcher::KillProcess", $"PID {process.Id} has already exited");
|
||||
return;
|
||||
}
|
||||
|
||||
process.Kill();
|
||||
process.Close();
|
||||
}
|
||||
|
||||
public void CloseProcess(int pid)
|
||||
{
|
||||
using var process = Process.GetProcessById(pid);
|
||||
|
||||
App.Logger.WriteLine("Watcher::CloseProcess", $"Closing process '{process.ProcessName}' (PID {process.Id})");
|
||||
|
||||
if (process.HasExited)
|
||||
{
|
||||
App.Logger.WriteLine("Watcher::CloseProcess", $"PID {process.Id} has already exited");
|
||||
return;
|
||||
}
|
||||
|
||||
process.CloseMainWindow();
|
||||
process.Close();
|
||||
}
|
||||
|
||||
public async Task Run()
|
||||
{
|
||||
if (!_lock.IsAcquired)
|
||||
return;
|
||||
|
||||
ActivityWatcher?.Start();
|
||||
|
||||
while (Utilities.GetProcessesSafe().Any(x => x.Id == _gameClientPid))
|
||||
await Task.Delay(1000);
|
||||
|
||||
foreach (int pid in _autoclosePids)
|
||||
CloseProcess(pid);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
App.Logger.WriteLine("Watcher::Dispose", "Disposing Watcher");
|
||||
|
||||
_notifyIcon?.Dispose();
|
||||
RichPresence?.Dispose();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user