diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index f244c7c9..651806d8 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -8,6 +8,7 @@ using Bloxstrap.Models.SettingTasks.Base; using Bloxstrap.UI.Elements.About.Pages; using Bloxstrap.UI.Elements.About; +using System; namespace Bloxstrap { @@ -37,8 +38,6 @@ public partial class App : Application public static readonly MD5 MD5Provider = MD5.Create(); - public static NotifyIconWrapper? NotifyIcon { get; set; } - public static readonly Logger Logger = new(); public static readonly Dictionary PendingSettingTasks = new(); @@ -55,19 +54,23 @@ public partial class App : Application ) ); -#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 @@ void GlobalExceptionHandler(object sender, DispatcherUnhandledExceptionEventArgs 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 @@ protected override void OnStartup(StartupEventArgs e) 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 @@ protected override void OnStartup(StartupEventArgs e) LaunchHandler.ProcessLaunchArgs(); } - Terminate(); + // you must *explicitly* call terminate when everything is done, it won't be called implicitly } } } diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index db5b7e06..0fc6171f 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -3,8 +3,6 @@ using Microsoft.Win32; -using Bloxstrap.Integrations; -using Bloxstrap.Resources; using Bloxstrap.AppData; namespace Bloxstrap @@ -289,9 +287,6 @@ private async Task StartRoblox() _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 @@ private async Task StartRoblox() // 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 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 @@ private async Task StartRoblox() 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(); // 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 @@ private async Task StartRoblox() 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}"); } - } - // 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(); + if (integration.AutoClose && pid != 0) + autoclosePids.Add(pid); + } - foreach (var process in autocloseProcesses) + using (var proclock = new InterProcessLock("Watcher")) { - if (process is null || process.HasExited) - continue; + string args = gameClientPid.ToString(); + + if (autoclosePids.Any()) + args += $";{String.Join(',', autoclosePids)}"; - App.Logger.WriteLine(LOG_IDENT, $"Autoclosing process '{process.ProcessName}' (PID {process.Id})"); - process.Kill(); + if (proclock.IsAcquired) + Process.Start(Paths.Process, $"-watcher \"{args}\""); } + + // event fired, wait for 3 seconds then close + await Task.Delay(3000); + Dialog?.CloseBootstrapper(); } public void CancelInstall() diff --git a/Bloxstrap/Integrations/ActivityWatcher.cs b/Bloxstrap/Integrations/ActivityWatcher.cs index e15cd2da..fbca8d11 100644 --- a/Bloxstrap/Integrations/ActivityWatcher.cs +++ b/Bloxstrap/Integrations/ActivityWatcher.cs @@ -19,7 +19,6 @@ public class ActivityWatcher : IDisposable 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 class ActivityWatcher : IDisposable public event EventHandler? OnLogEntry; public event EventHandler? OnGameJoin; public event EventHandler? OnGameLeave; + public event EventHandler? OnLogOpen; public event EventHandler? OnAppClose; public event EventHandler? OnRPCMessage; @@ -47,14 +47,9 @@ public class ActivityWatcher : IDisposable 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 @@ public async void StartWatcher() { 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 @@ public async void StartWatcher() }; logWatcher.Changed += (s, e) => logUpdatedEvent.Set(); - using StreamReader sr = new(logFileStream); + using var sr = new StreamReader(logFileStream); while (!IsDisposed) { @@ -117,13 +115,13 @@ public async void StartWatcher() 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 @@ public async Task GetServerLocation() var ipInfo = await Http.GetJson($"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 @@ public async Task GetServerLocation() 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 @@ public async Task GetServerLocation() 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})"; } } diff --git a/Bloxstrap/Integrations/DiscordRichPresence.cs b/Bloxstrap/Integrations/DiscordRichPresence.cs index ab920d12..c79fe04d 100644 --- a/Bloxstrap/Integrations/DiscordRichPresence.cs +++ b/Bloxstrap/Integrations/DiscordRichPresence.cs @@ -194,6 +194,8 @@ public async Task SetCurrentGame() 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($"https://apis.roblox.com/universes/v1/places/{placeId}/universe"); if (universeIdResponse is null) { @@ -282,6 +284,7 @@ public async Task SetCurrentGame() // 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"); diff --git a/Bloxstrap/LaunchHandler.cs b/Bloxstrap/LaunchHandler.cs index 58ef4215..a7f54067 100644 --- a/Bloxstrap/LaunchHandler.cs +++ b/Bloxstrap/LaunchHandler.cs @@ -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 @@ public static void ProcessNextAction(NextAction action, bool isUnfinishedInstall break; case NextAction.LaunchRoblox: + App.LaunchSettings.RobloxLaunchMode = LaunchMode.Player; LaunchRoblox(); break; @@ -85,7 +86,7 @@ public static void LaunchInstaller() ProcessNextAction(installer.CloseAction, !installer.Finished); } - + } public static void LaunchUninstaller() @@ -120,6 +121,8 @@ public static void LaunchUninstaller() Installer.DoUninstall(keepData); Frontend.ShowMessageBox(Strings.Bootstrapper_SuccessfullyUninstalled, MessageBoxImage.Information); + + App.Terminate(); } public static void LaunchSettings() @@ -131,12 +134,12 @@ public static void LaunchSettings() 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 @@ public static void LaunchRoblox() { 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 @@ public static void LaunchRoblox() } } - 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 @@ public static void LaunchRoblox() 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); + App.Terminate(); + }); - Exception exception = t.Exception; + dialog?.ShowBootstrapper(); + } -#if !DEBUG - if (t.Exception.GetType().ToString() == "System.AggregateException") - exception = t.Exception.InnerException!; -#endif + public static void LaunchWatcher() + { + const string LOG_IDENT = "LaunchHandler::LaunchWatcher"; - App.FinalizeExceptionHandling(exception, false); - }); + // 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 - // 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(); + var watcher = new Watcher(); - if (!App.LaunchSettings.NoLaunchFlag.Active && App.Settings.Prop.EnableActivityTracking) - App.NotifyIcon?.InitializeContextMenu(); + Task.Run(watcher.Run).ContinueWith(t => + { + App.Logger.WriteLine(LOG_IDENT, "Watcher task has finished"); - App.Logger.WriteLine(LOG_IDENT, "Waiting for bootstrapper task to finish"); + watcher.Dispose(); - bootstrapperTask.Wait(); - } + if (t.IsFaulted) + { + App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the watcher"); - public static void LaunchWatcher() - { + if (t.Exception is not null) + App.FinalizeExceptionHandling(t.Exception); + } + App.Terminate(); + }); } } } diff --git a/Bloxstrap/LaunchSettings.cs b/Bloxstrap/LaunchSettings.cs index a217d094..046fd9ae 100644 --- a/Bloxstrap/LaunchSettings.cs +++ b/Bloxstrap/LaunchSettings.cs @@ -28,7 +28,7 @@ public class LaunchSettings 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; } = ""; diff --git a/Bloxstrap/Properties/launchSettings.json b/Bloxstrap/Properties/launchSettings.json index e3f8ff70..d07acf20 100644 --- a/Bloxstrap/Properties/launchSettings.json +++ b/Bloxstrap/Properties/launchSettings.json @@ -26,6 +26,10 @@ "Bloxstrap (Studio Launch)": { "commandName": "Project", "commandLineArgs": "-studio" + }, + "Bloxstrap (Watcher)": { + "commandName": "Project", + "commandLineArgs": "-watcher" } } } \ No newline at end of file diff --git a/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml b/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml index 8bf796cd..a926eb4c 100644 --- a/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml +++ b/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml @@ -61,7 +61,7 @@ - + @@ -73,7 +73,7 @@ - + diff --git a/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs b/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs index 9e34a298..c4412ba7 100644 --- a/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs +++ b/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs @@ -22,32 +22,28 @@ public partial class MenuContainer { // 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 @@ public void ShowServerInformationWindow() { if (_serverInformationWindow is null) { - _serverInformationWindow = new ServerInformation(_activityWatcher!); + _serverInformationWindow = new ServerInformation(_watcher); _serverInformationWindow.Closed += (_, _) => _serverInformationWindow = null; } @@ -65,17 +61,23 @@ public void ShowServerInformationWindow() _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 @@ private void Window_Loaded(object? sender, RoutedEventArgs e) 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 @@ private void LogTracerMenuItem_Click(object sender, RoutedEventArgs e) { 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 @@ private void CloseRobloxMenuItem_Click(object sender, RoutedEventArgs e) if (result != MessageBoxResult.Yes) return; - using Process process = Process.GetProcessById((int)_processId!); - process.Kill(); - process.Close(); + _watcher.KillRobloxProcess(); } } } diff --git a/Bloxstrap/UI/Elements/ContextMenu/ServerInformation.xaml.cs b/Bloxstrap/UI/Elements/ContextMenu/ServerInformation.xaml.cs index 5d46d85c..65b8f1eb 100644 --- a/Bloxstrap/UI/Elements/ContextMenu/ServerInformation.xaml.cs +++ b/Bloxstrap/UI/Elements/ContextMenu/ServerInformation.xaml.cs @@ -22,9 +22,13 @@ namespace Bloxstrap.UI.Elements.ContextMenu /// 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(); } } diff --git a/Bloxstrap/UI/Elements/Settings/MainWindow.xaml b/Bloxstrap/UI/Elements/Settings/MainWindow.xaml index 7012567b..b63b1a7c 100644 --- a/Bloxstrap/UI/Elements/Settings/MainWindow.xaml +++ b/Bloxstrap/UI/Elements/Settings/MainWindow.xaml @@ -94,7 +94,7 @@ - + diff --git a/Bloxstrap/UI/Elements/Settings/MainWindow.xaml.cs b/Bloxstrap/UI/Elements/Settings/MainWindow.xaml.cs index c22a7326..59bd1db3 100644 --- a/Bloxstrap/UI/Elements/Settings/MainWindow.xaml.cs +++ b/Bloxstrap/UI/Elements/Settings/MainWindow.xaml.cs @@ -17,7 +17,9 @@ public partial class MainWindow : INavigationWindow public MainWindow(bool showAlreadyRunningWarning) { var viewModel = new MainWindowViewModel(); + viewModel.RequestSaveNoticeEvent += (_, _) => SettingsSavedSnackbar.Show(); + viewModel.RequestCloseWindowEvent += (_, _) => Close(); DataContext = viewModel; @@ -64,6 +66,9 @@ private void WpfUiWindow_Closing(object sender, CancelEventArgs e) if (result != MessageBoxResult.Yes) e.Cancel = true; } + + if (!e.Cancel) + App.Terminate(); } } } diff --git a/Bloxstrap/UI/NotifyIconWrapper.cs b/Bloxstrap/UI/NotifyIconWrapper.cs index 6b32da39..ae0b0427 100644 --- a/Bloxstrap/UI/NotifyIconWrapper.cs +++ b/Bloxstrap/UI/NotifyIconWrapper.cs @@ -1,4 +1,5 @@ using Bloxstrap.Integrations; +using Bloxstrap.UI.Elements.About; using Bloxstrap.UI.Elements.ContextMenu; namespace Bloxstrap.UI @@ -10,18 +11,21 @@ public class NotifyIconWrapper : IDisposable 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 @@ public NotifyIconWrapper() }; _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 = activityWatcher; - - if (App.Settings.Prop.ShowServerDetails) - _activityWatcher.OnGameJoin += (_, _) => Task.Run(OnGameJoin); - } - - public void SetProcessId(int processId) - { - if (_processId is not null) - return; + _activityWatcher.OnGameJoin += OnGameJoin; - _processId = processId; + _menuContainer = new(_watcher); + _menuContainer.Show(); } - #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 @@ public void MouseClickEventHandler(object? sender, System.Windows.Forms.MouseEve #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 @@ public async void OnGameJoin() title, String.Format(Strings.ContextMenu_ServerInformation_Notification_Text, serverLocation), 10, - (_, _) => _menuContainer?.ShowServerInformationWindow() + (_, _) => _menuContainer.ShowServerInformationWindow() ); } #endregion @@ -151,9 +124,8 @@ public void Dispose() App.Logger.WriteLine("NotifyIconWrapper::Dispose", "Disposing NotifyIcon"); - _menuContainer?.Dispatcher.Invoke(_menuContainer.Close); - _notifyIcon?.Dispose(); - + _menuContainer.Dispatcher.Invoke(_menuContainer.Close); + _notifyIcon.Dispose(); GC.SuppressFinalize(this); } diff --git a/Bloxstrap/UI/ViewModels/ContextMenu/ServerInformationViewModel.cs b/Bloxstrap/UI/ViewModels/ContextMenu/ServerInformationViewModel.cs index 71d50c33..9d1b8753 100644 --- a/Bloxstrap/UI/ViewModels/ContextMenu/ServerInformationViewModel.cs +++ b/Bloxstrap/UI/ViewModels/ContextMenu/ServerInformationViewModel.cs @@ -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 @@ public ServerInformationViewModel(Window window, ActivityWatcher activityWatcher } private void CopyInstanceId() => Clipboard.SetDataObject(InstanceId); + + private void RequestClose() => RequestCloseEvent?.Invoke(this, EventArgs.Empty); } } diff --git a/Bloxstrap/UI/ViewModels/Dialogs/LaunchMenuViewModel.cs b/Bloxstrap/UI/ViewModels/Dialogs/LaunchMenuViewModel.cs index 991ae8d4..06809989 100644 --- a/Bloxstrap/UI/ViewModels/Dialogs/LaunchMenuViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Dialogs/LaunchMenuViewModel.cs @@ -22,6 +22,6 @@ public class LaunchMenuViewModel private void LaunchRoblox() => CloseWindowRequest?.Invoke(this, NextAction.LaunchRoblox); - private void LaunchAbout() => new MainWindow().Show(); + private void LaunchAbout() => new MainWindow().ShowDialog(); } } diff --git a/Bloxstrap/UI/ViewModels/Settings/MainWindowViewModel.cs b/Bloxstrap/UI/ViewModels/Settings/MainWindowViewModel.cs index 767bb0b1..5c04e505 100644 --- a/Bloxstrap/UI/ViewModels/Settings/MainWindowViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Settings/MainWindowViewModel.cs @@ -9,11 +9,17 @@ public class MainWindowViewModel : NotifyPropertyChangedViewModel 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 @@ private void SaveSettings() App.PendingSettingTasks.Clear(); - RequestSaveNoticeEvent?.Invoke(this, new EventArgs()); + RequestSaveNoticeEvent?.Invoke(this, EventArgs.Empty); } } } diff --git a/Bloxstrap/Watcher.cs b/Bloxstrap/Watcher.cs new file mode 100644 index 00000000..652675ed --- /dev/null +++ b/Bloxstrap/Watcher.cs @@ -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 _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); + } + } +}