Improve watcher/failed launch handling

Bloxstrap now attempts to identify the log file *when* Roblox launches, then passes it to the watcher
It does this so that it can determine if Roblox fails to launch
This commit is contained in:
pizzaboxer 2024-10-12 17:37:25 +01:00
parent b918d27452
commit aac6ec3d4c
No known key found for this signature in database
GPG Key ID: 59D4A1DBAD0F2BA8
6 changed files with 135 additions and 94 deletions

View File

@ -5,8 +5,6 @@ using System.Windows.Threading;
using Microsoft.Win32; using Microsoft.Win32;
using Bloxstrap.Models.SettingTasks.Base;
namespace Bloxstrap namespace Bloxstrap
{ {
/// <summary> /// <summary>

View File

@ -59,6 +59,8 @@ namespace Bloxstrap
private bool _mustUpgrade => String.IsNullOrEmpty(AppData.State.VersionGuid) || File.Exists(AppData.LockFilePath) || !File.Exists(AppData.ExecutablePath); private bool _mustUpgrade => String.IsNullOrEmpty(AppData.State.VersionGuid) || File.Exists(AppData.LockFilePath) || !File.Exists(AppData.ExecutablePath);
private bool _noConnection = false; private bool _noConnection = false;
private AsyncMutex? _mutex;
private int _appPid = 0; private int _appPid = 0;
public IBootstrapperDialog? Dialog = null; public IBootstrapperDialog? Dialog = null;
@ -191,6 +193,8 @@ namespace Bloxstrap
await using var mutex = new AsyncMutex(false, "Bloxstrap-Bootstrapper"); await using var mutex = new AsyncMutex(false, "Bloxstrap-Bootstrapper");
await mutex.AcquireAsync(_cancelTokenSource.Token); await mutex.AcquireAsync(_cancelTokenSource.Token);
_mutex = mutex;
// reload our configs since they've likely changed by now // reload our configs since they've likely changed by now
if (mutexExists) if (mutexExists)
{ {
@ -230,11 +234,14 @@ namespace Bloxstrap
else else
WindowsRegistry.RegisterPlayer(); WindowsRegistry.RegisterPlayer();
if (_launchMode != LaunchMode.Player)
await mutex.ReleaseAsync(); await mutex.ReleaseAsync();
if (!App.LaunchSettings.NoLaunchFlag.Active && !_cancelTokenSource.IsCancellationRequested) if (!App.LaunchSettings.NoLaunchFlag.Active && !_cancelTokenSource.IsCancellationRequested)
StartRoblox(); StartRoblox();
await mutex.ReleaseAsync();
Dialog?.CloseBootstrapper(); Dialog?.CloseBootstrapper();
} }
@ -336,13 +343,28 @@ namespace Bloxstrap
return; return;
} }
bool startEventSignalled; string? logFileName = null;
// TODO: figure out why this is causing roblox to block for some users
using (var startEvent = new EventWaitHandle(false, EventResetMode.ManualReset, AppData.StartEvent)) using (var startEvent = new EventWaitHandle(false, EventResetMode.ManualReset, AppData.StartEvent))
{ {
startEvent.Reset(); startEvent.Reset();
var logWatcher = new FileSystemWatcher()
{
Path = Path.Combine(Paths.LocalAppData, "Roblox\\logs"),
Filter = "*.log",
EnableRaisingEvents = true
};
var logCreatedEvent = new AutoResetEvent(false);
logWatcher.Created += (_, e) =>
{
logWatcher.EnableRaisingEvents = false;
logFileName = e.FullPath;
logCreatedEvent.Set();
};
// v2.2.0 - byfron will trip if we keep a process handle open for over a minute, so we're doing this now // v2.2.0 - byfron will trip if we keep a process handle open for over a minute, so we're doing this now
try try
{ {
@ -358,11 +380,26 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, $"Started Roblox (PID {_appPid}), waiting for start event"); App.Logger.WriteLine(LOG_IDENT, $"Started Roblox (PID {_appPid}), waiting for start event");
startEventSignalled = startEvent.WaitOne(TimeSpan.FromSeconds(5)); if (startEvent.WaitOne(TimeSpan.FromSeconds(5)))
App.Logger.WriteLine(LOG_IDENT, "Start event signalled");
else
App.Logger.WriteLine(LOG_IDENT, "Start event not signalled, implying successful launch");
logCreatedEvent.WaitOne(TimeSpan.FromSeconds(5));
if (String.IsNullOrEmpty(logFileName))
{
App.Logger.WriteLine(LOG_IDENT, "Unable to identify log file");
Frontend.ShowPlayerErrorDialog();
return;
}
else
{
App.Logger.WriteLine(LOG_IDENT, $"Got log file as {logFileName}");
} }
if (startEventSignalled) _mutex?.ReleaseAsync();
App.Logger.WriteLine(LOG_IDENT, "Start event signalled"); }
if (IsStudioLaunch) if (IsStudioLaunch)
return; return;
@ -391,23 +428,27 @@ namespace Bloxstrap
catch (Exception ex) catch (Exception ex)
{ {
App.Logger.WriteLine(LOG_IDENT, $"Failed to launch integration '{integration.Name}'!"); App.Logger.WriteLine(LOG_IDENT, $"Failed to launch integration '{integration.Name}'!");
App.Logger.WriteLine(LOG_IDENT, $"{ex.Message}"); App.Logger.WriteLine(LOG_IDENT, ex.Message);
} }
if (integration.AutoClose && pid != 0) if (integration.AutoClose && pid != 0)
autoclosePids.Add(pid); autoclosePids.Add(pid);
} }
string argPids = _appPid.ToString();
if (autoclosePids.Any())
argPids += $";{String.Join(',', autoclosePids)}";
if (App.Settings.Prop.EnableActivityTracking || App.LaunchSettings.TestModeFlag.Active || autoclosePids.Any()) if (App.Settings.Prop.EnableActivityTracking || App.LaunchSettings.TestModeFlag.Active || autoclosePids.Any())
{ {
using var ipl = new InterProcessLock("Watcher", TimeSpan.FromSeconds(5)); using var ipl = new InterProcessLock("Watcher", TimeSpan.FromSeconds(5));
string args = $"-watcher \"{argPids}\""; var watcherData = new WatcherData
{
ProcessId = _appPid,
LogFile = logFileName,
AutoclosePids = autoclosePids
};
string watcherDataArg = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(watcherData)));
string args = $"-watcher \"{watcherDataArg}\"";
if (App.LaunchSettings.TestModeFlag.Active) if (App.LaunchSettings.TestModeFlag.Active)
args += " -testmode"; args += " -testmode";

View File

@ -51,6 +51,12 @@
public bool IsDisposed = false; public bool IsDisposed = false;
public ActivityWatcher(string? logFile = null)
{
if (!String.IsNullOrEmpty(logFile))
LogLocation = logFile;
}
public async void Start() public async void Start()
{ {
const string LOG_IDENT = "ActivityWatcher::Start"; const string LOG_IDENT = "ActivityWatcher::Start";
@ -66,13 +72,15 @@
// //
// we'll tail the log file continuously, monitoring for any log entries that we need to determine the current game activity // we'll tail the log file continuously, monitoring for any log entries that we need to determine the current game activity
FileInfo logFileInfo;
if (String.IsNullOrEmpty(LogLocation))
{
string logDirectory = Path.Combine(Paths.LocalAppData, "Roblox\\logs"); string logDirectory = Path.Combine(Paths.LocalAppData, "Roblox\\logs");
if (!Directory.Exists(logDirectory)) if (!Directory.Exists(logDirectory))
return; return;
FileInfo logFileInfo;
// we need to make sure we're fetching the absolute latest log file // we need to make sure we're fetching the absolute latest log file
// if roblox doesn't start quickly enough, we can wind up fetching the previous log file // if roblox doesn't start quickly enough, we can wind up fetching the previous log file
// good rule of thumb is to find a log file that was created in the last 15 seconds or so // good rule of thumb is to find a log file that was created in the last 15 seconds or so
@ -97,26 +105,24 @@
OnLogOpen?.Invoke(this, EventArgs.Empty); OnLogOpen?.Invoke(this, EventArgs.Empty);
LogLocation = logFileInfo.FullName; LogLocation = logFileInfo.FullName;
FileStream logFileStream = logFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite); }
else
{
logFileInfo = new FileInfo(LogLocation);
}
var logFileStream = logFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
App.Logger.WriteLine(LOG_IDENT, $"Opened {LogLocation}"); App.Logger.WriteLine(LOG_IDENT, $"Opened {LogLocation}");
var logUpdatedEvent = new AutoResetEvent(false); using var streamReader = new StreamReader(logFileStream);
var logWatcher = new FileSystemWatcher()
{
Path = logDirectory,
Filter = Path.GetFileName(logFileInfo.FullName),
EnableRaisingEvents = true
};
logWatcher.Changed += (s, e) => logUpdatedEvent.Set();
using var sr = new StreamReader(logFileStream);
while (!IsDisposed) while (!IsDisposed)
{ {
string? log = await sr.ReadLineAsync(); string? log = await streamReader.ReadLineAsync();
if (log is null) if (log is null)
logUpdatedEvent.WaitOne(250); await Task.Delay(1000);
else else
ReadLogEntry(log); ReadLogEntry(log);
} }
@ -265,7 +271,7 @@
InGame = true; InGame = true;
Data.TimeJoined = DateTime.Now; Data.TimeJoined = DateTime.Now;
OnGameJoin?.Invoke(this, new EventArgs()); OnGameJoin?.Invoke(this, EventArgs.Empty);
} }
} }
else if (InGame && Data.PlaceId != 0) else if (InGame && Data.PlaceId != 0)
@ -282,7 +288,7 @@
InGame = false; InGame = false;
Data = new(); Data = new();
OnGameLeave?.Invoke(this, new EventArgs()); OnGameLeave?.Invoke(this, EventArgs.Empty);
} }
else if (entry.Contains(GameTeleportingEntry)) else if (entry.Contains(GameTeleportingEntry))
{ {

View File

@ -0,0 +1,11 @@
namespace Bloxstrap.Models
{
internal class WatcherData
{
public int ProcessId { get; set; }
public string? LogFile { get; set; }
public List<int>? AutoclosePids { get; set; }
}
}

View File

@ -2,9 +2,6 @@
using Bloxstrap.UI.Elements.Bootstrapper; using Bloxstrap.UI.Elements.Bootstrapper;
using Bloxstrap.UI.Elements.Dialogs; using Bloxstrap.UI.Elements.Dialogs;
using Bloxstrap.UI.Elements.Settings;
using Bloxstrap.UI.Elements.Installer;
using System.Drawing;
namespace Bloxstrap.UI namespace Bloxstrap.UI
{ {
@ -31,7 +28,8 @@ namespace Bloxstrap.UI
topLine = Strings.Dialog_PlayerError_Crash; topLine = Strings.Dialog_PlayerError_Crash;
ShowMessageBox($"{topLine}\n\n{Strings.Dialog_PlayerError_HelpInformation}", MessageBoxImage.Error); ShowMessageBox($"{topLine}\n\n{Strings.Dialog_PlayerError_HelpInformation}", MessageBoxImage.Error);
Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/wiki/Roblox-crashes-or-does-not-launch");
// Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/wiki/Roblox-crashes-or-does-not-launch");
} }
public static void ShowExceptionDialog(Exception exception) public static void ShowExceptionDialog(Exception exception)

View File

@ -1,14 +1,13 @@
using Bloxstrap.Integrations; using Bloxstrap.Integrations;
using Bloxstrap.Models;
namespace Bloxstrap namespace Bloxstrap
{ {
public class Watcher : IDisposable public class Watcher : IDisposable
{ {
private int _gameClientPid = 0;
private readonly InterProcessLock _lock = new("Watcher"); private readonly InterProcessLock _lock = new("Watcher");
private readonly List<int> _autoclosePids = new(); private readonly WatcherData? _watcherData;
private readonly NotifyIconWrapper? _notifyIcon; private readonly NotifyIconWrapper? _notifyIcon;
@ -26,53 +25,37 @@ namespace Bloxstrap
return; return;
} }
string? watcherData = App.LaunchSettings.WatcherFlag.Data; string? watcherDataArg = App.LaunchSettings.WatcherFlag.Data;
#if DEBUG #if DEBUG
if (String.IsNullOrEmpty(watcherData)) if (String.IsNullOrEmpty(watcherDataArg))
{ {
string path = Path.Combine(Paths.Roblox, "Player", "RobloxPlayerBeta.exe"); string path = Path.Combine(Paths.Roblox, "Player", "RobloxPlayerBeta.exe");
using var gameClientProcess = Process.Start(path); using var gameClientProcess = Process.Start(path);
_gameClientPid = gameClientProcess.Id;
_watcherData = new() { ProcessId = gameClientProcess.Id };
} }
#else #else
if (String.IsNullOrEmpty(watcherData)) if (String.IsNullOrEmpty(watcherDataArg))
throw new Exception("Watcher data not specified"); throw new Exception("Watcher data not specified");
#endif #endif
if (!String.IsNullOrEmpty(watcherData) && _gameClientPid == 0) if (!String.IsNullOrEmpty(watcherDataArg))
{ _watcherData = JsonSerializer.Deserialize<WatcherData>(Encoding.UTF8.GetString(Convert.FromBase64String(watcherDataArg)));
var split = watcherData.Split(';');
if (split.Length == 0) if (_watcherData is null)
_ = int.TryParse(watcherData, out _gameClientPid);
if (split.Length >= 1)
_ = int.TryParse(split[0], out _gameClientPid);
if (split.Length >= 2)
{
foreach (string strPid in split[1].Split(','))
{
if (int.TryParse(strPid, out int pid) && pid != 0)
_autoclosePids.Add(pid);
}
}
}
if (_gameClientPid == 0)
throw new Exception("Watcher data is invalid"); throw new Exception("Watcher data is invalid");
if (App.Settings.Prop.EnableActivityTracking) if (App.Settings.Prop.EnableActivityTracking)
{ {
ActivityWatcher = new(); ActivityWatcher = new(_watcherData.LogFile);
if (App.Settings.Prop.UseDisableAppPatch) if (App.Settings.Prop.UseDisableAppPatch)
{ {
ActivityWatcher.OnAppClose += delegate ActivityWatcher.OnAppClose += delegate
{ {
App.Logger.WriteLine(LOG_IDENT, "Received desktop app exit, closing Roblox"); App.Logger.WriteLine(LOG_IDENT, "Received desktop app exit, closing Roblox");
using var process = Process.GetProcessById(_gameClientPid); using var process = Process.GetProcessById(_watcherData.ProcessId);
process.CloseMainWindow(); process.CloseMainWindow();
}; };
} }
@ -84,11 +67,12 @@ namespace Bloxstrap
_notifyIcon = new(this); _notifyIcon = new(this);
} }
public void KillRobloxProcess() => CloseProcess(_gameClientPid, true); public void KillRobloxProcess() => CloseProcess(_watcherData!.ProcessId, true);
public void CloseProcess(int pid, bool force = false) public void CloseProcess(int pid, bool force = false)
{ {
const string LOG_IDENT = "Watcher::CloseProcess"; const string LOG_IDENT = "Watcher::CloseProcess";
try try
{ {
using var process = Process.GetProcessById(pid); using var process = Process.GetProcessById(pid);
@ -115,16 +99,19 @@ namespace Bloxstrap
public async Task Run() public async Task Run()
{ {
if (!_lock.IsAcquired) if (!_lock.IsAcquired || _watcherData is null)
return; return;
ActivityWatcher?.Start(); ActivityWatcher?.Start();
while (Utilities.GetProcessesSafe().Any(x => x.Id == _gameClientPid)) while (Utilities.GetProcessesSafe().Any(x => x.Id == _watcherData.ProcessId))
await Task.Delay(1000); await Task.Delay(1000);
foreach (int pid in _autoclosePids) if (_watcherData.AutoclosePids is not null)
{
foreach (int pid in _watcherData.AutoclosePids)
CloseProcess(pid); CloseProcess(pid);
}
if (App.LaunchSettings.TestModeFlag.Active) if (App.LaunchSettings.TestModeFlag.Active)
Process.Start(Paths.Process, "-settings -testmode"); Process.Start(Paths.Process, "-settings -testmode");