Skip to content

Commit

Permalink
Move activity watcher to separate process (#810)
Browse files Browse the repository at this point in the history
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!
  • Loading branch information
pizzaboxer committed Aug 28, 2024
1 parent cf45d9c commit fd290f9
Show file tree
Hide file tree
Showing 17 changed files with 328 additions and 226 deletions.
39 changes: 21 additions & 18 deletions Bloxstrap/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Bloxstrap.Models.SettingTasks.Base;
using Bloxstrap.UI.Elements.About.Pages;
using Bloxstrap.UI.Elements.About;
using System;

namespace Bloxstrap
{
Expand Down Expand Up @@ -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<string, BaseTask> PendingSettingTasks = new();
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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";
Expand All @@ -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
}
}
}
90 changes: 21 additions & 69 deletions Bloxstrap/Bootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

using Microsoft.Win32;

using Bloxstrap.Integrations;
using Bloxstrap.Resources;
using Bloxstrap.AppData;

namespace Bloxstrap
Expand Down Expand Up @@ -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,
Expand All @@ -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<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();
Expand All @@ -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<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
Expand All @@ -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()
Expand Down
34 changes: 16 additions & 18 deletions Bloxstrap/Integrations/ActivityWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ 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;

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;

Expand All @@ -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:
//
Expand Down Expand Up @@ -84,31 +79,34 @@ 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),
EnableRaisingEvents = true
};
logWatcher.Changed += (s, e) => logUpdatedEvent.Set();

using StreamReader sr = new(logFileStream);
using var sr = new StreamReader(logFileStream);

while (!IsDisposed)
{
Expand All @@ -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);

Expand Down Expand Up @@ -302,7 +300,7 @@ public async Task<string> GetServerLocation()
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 = "?";
Expand All @@ -312,7 +310,7 @@ public async Task<string> GetServerLocation()
location = $"{ipInfo.City}, {ipInfo.Region}, {ipInfo.Country}";

if (!ActivityInGame)
return $"? ({Resources.Strings.ActivityTracker_LeftGame})";
return $"? ({Strings.ActivityTracker_LeftGame})";

GeolocationCache[ActivityMachineAddress] = location;

Expand All @@ -323,7 +321,7 @@ public async Task<string> 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})";
}
}

Expand Down
3 changes: 3 additions & 0 deletions Bloxstrap/Integrations/DiscordRichPresence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ public async Task<bool> 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<UniverseIdResponse>($"https://apis.roblox.com/universes/v1/places/{placeId}/universe");
if (universeIdResponse is null)
{
Expand Down Expand Up @@ -282,6 +284,7 @@ public async Task<bool> 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");
Expand Down
Loading

0 comments on commit fd290f9

Please sign in to comment.