diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index 8421d8d..f6e7072 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -27,12 +27,7 @@ namespace Bloxstrap public static bool IsSetupComplete { get; set; } = true; public static bool IsFirstRun { get; set; } = true; - public static bool IsQuiet { get; private set; } = false; - public static bool IsUninstall { get; private set; } = false; - public static bool IsNoLaunch { get; private set; } = false; - public static bool IsUpgrade { get; private set; } = false; - public static bool IsMenuLaunch { get; private set; } = false; - public static string[] LaunchArgs { get; private set; } = null!; + public static LaunchSettings LaunchSettings { get; private set; } = null!; public static BuildMetadataAttribute BuildMetadata = Assembly.GetExecutingAssembly().GetCustomAttribute()!; public static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2]; @@ -96,13 +91,22 @@ namespace Bloxstrap _showingExceptionDialog = true; - if (!IsQuiet) + if (!LaunchSettings.IsQuiet) Frontend.ShowExceptionDialog(exception); Terminate(ErrorCode.ERROR_INSTALL_FAILURE); #endif } + private void StartupFinished() + { + const string LOG_IDENT = "App::StartupFinished"; + + Logger.WriteLine(LOG_IDENT, "Successfully reached end of main thread. Terminating..."); + + Terminate(); + } + protected override void OnStartup(StartupEventArgs e) { const string LOG_IDENT = "App::OnStartup"; @@ -122,47 +126,10 @@ namespace Bloxstrap // see https://aka.ms/applicationconfiguration. ApplicationConfiguration.Initialize(); - LaunchArgs = e.Args; - -#if DEBUG - Logger.WriteLine(LOG_IDENT, $"Arguments: {string.Join(' ', LaunchArgs)}"); -#endif + LaunchSettings = new LaunchSettings(e.Args); HttpClient.Timeout = TimeSpan.FromSeconds(30); HttpClient.DefaultRequestHeaders.Add("User-Agent", ProjectRepository); - - if (LaunchArgs.Length > 0) - { - if (Array.IndexOf(LaunchArgs, "-preferences") != -1 || Array.IndexOf(LaunchArgs, "-menu") != -1) - { - Logger.WriteLine(LOG_IDENT, "Started with IsMenuLaunch flag"); - IsMenuLaunch = true; - } - - if (Array.IndexOf(LaunchArgs, "-quiet") != -1) - { - Logger.WriteLine(LOG_IDENT, "Started with IsQuiet flag"); - IsQuiet = true; - } - - if (Array.IndexOf(LaunchArgs, "-uninstall") != -1) - { - Logger.WriteLine(LOG_IDENT, "Started with IsUninstall flag"); - IsUninstall = true; - } - - if (Array.IndexOf(LaunchArgs, "-nolaunch") != -1) - { - Logger.WriteLine(LOG_IDENT, "Started with IsNoLaunch flag"); - IsNoLaunch = true; - } - - if (Array.IndexOf(LaunchArgs, "-upgrade") != -1) - { - Logger.WriteLine(LOG_IDENT, "Bloxstrap started with IsUpgrade flag"); - IsUpgrade = true; - } - } using (var checker = new InstallChecker()) { @@ -175,7 +142,7 @@ namespace Bloxstrap // just in case the user decides to cancel the install if (!IsFirstRun) { - Logger.Initialize(IsUninstall); + Logger.Initialize(LaunchSettings.IsUninstall); if (!Logger.Initialized) { @@ -188,18 +155,15 @@ namespace Bloxstrap FastFlags.Load(); } - if (!IsUninstall && !IsMenuLaunch) + if (!LaunchSettings.IsUninstall && !LaunchSettings.IsMenuLaunch) NotifyIcon = new(); #if !DEBUG - if (!IsUninstall && !IsFirstRun) + if (!LaunchSettings.IsUninstall && !IsFirstRun) InstallChecker.CheckUpgrade(); #endif - string commandLine = ""; - LaunchMode? launchMode = null; - - if (IsMenuLaunch) + if (LaunchSettings.IsMenuLaunch) { Process? menuProcess = Process.GetProcesses().Where(x => x.MainWindowTitle == $"{ProjectName} Menu").FirstOrDefault(); @@ -211,7 +175,7 @@ namespace Bloxstrap } else { - if (Process.GetProcessesByName(ProjectName).Length > 1 && !IsQuiet) + if (Process.GetProcessesByName(ProjectName).Length > 1 && !LaunchSettings.IsQuiet) Frontend.ShowMessageBox( Bloxstrap.Resources.Strings.Menu_AlreadyRunning, MessageBoxImage.Information @@ -219,152 +183,95 @@ namespace Bloxstrap Frontend.ShowMenu(); } - } - else if (LaunchArgs.Length > 0) - { - if (LaunchArgs[0].StartsWith("roblox-player:")) - { - commandLine = ProtocolHandler.ParseUri(LaunchArgs[0]); - launchMode = LaunchMode.Player; - } - else if (LaunchArgs[0].StartsWith("roblox:")) - { - if (Settings.Prop.UseDisableAppPatch) - Frontend.ShowMessageBox( - Bloxstrap.Resources.Strings.Bootstrapper_DeeplinkTempEnabled, - MessageBoxImage.Information - ); - - commandLine = $"--app --deeplink {LaunchArgs[0]}"; - - launchMode = LaunchMode.Player; - } - else if (LaunchArgs[0].StartsWith("roblox-studio:")) - { - commandLine = ProtocolHandler.ParseUri(LaunchArgs[0]); - - if (!commandLine.Contains("-startEvent")) - commandLine += " -startEvent www.roblox.com/robloxQTStudioStartedEvent"; - - launchMode = LaunchMode.Studio; - } - else if (LaunchArgs[0].StartsWith("roblox-studio-auth:")) - { - commandLine = HttpUtility.UrlDecode(LaunchArgs[0]); - - launchMode = LaunchMode.StudioAuth; - } - else if (LaunchArgs[0] == "-ide") - { - launchMode = LaunchMode.Studio; - - if (LaunchArgs.Length >= 2) - commandLine = $"-task EditFile -localPlaceFile \"{LaunchArgs[1]}\""; - } - else - { - commandLine = "--app"; - - launchMode = LaunchMode.Player; - } - } - else - { - commandLine = "--app"; - - launchMode = LaunchMode.Player; + StartupFinished(); + return; } - if (launchMode != null) + if (!IsFirstRun) + ShouldSaveConfigs = true; + + // start bootstrapper and show the bootstrapper modal if we're not running silently + Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper"); + Bootstrapper bootstrapper = new(LaunchSettings.RobloxLaunchArgs, LaunchSettings.RobloxLaunchMode); + IBootstrapperDialog? dialog = null; + + if (!LaunchSettings.IsQuiet) { - if (!IsFirstRun) - ShouldSaveConfigs = true; - - // start bootstrapper and show the bootstrapper modal if we're not running silently - Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper"); - Bootstrapper bootstrapper = new(commandLine, (LaunchMode)launchMode); - IBootstrapperDialog? dialog = null; + Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper dialog"); + dialog = Settings.Prop.BootstrapperStyle.GetNew(); + bootstrapper.Dialog = dialog; + dialog.Bootstrapper = bootstrapper; + } - if (!IsQuiet) + // handle roblox singleton mutex for multi-instance launching + // note we're handling it here in the main thread and NOT in the + // bootstrapper as handling mutexes in async contexts suuuuuucks + + Mutex? singletonMutex = null; + + if (Settings.Prop.MultiInstanceLaunching && LaunchSettings.RobloxLaunchMode == LaunchMode.Player) + { + Logger.WriteLine(LOG_IDENT, "Creating singleton mutex"); + + try { - Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper dialog"); - dialog = Settings.Prop.BootstrapperStyle.GetNew(); - bootstrapper.Dialog = dialog; - dialog.Bootstrapper = bootstrapper; + Mutex.OpenExisting("ROBLOX_singletonMutex"); + Logger.WriteLine(LOG_IDENT, "Warning - singleton mutex already exists!"); } - - // handle roblox singleton mutex for multi-instance launching - // note we're handling it here in the main thread and NOT in the - // bootstrapper as handling mutexes in async contexts suuuuuucks - - Mutex? singletonMutex = null; - - if (Settings.Prop.MultiInstanceLaunching && launchMode == LaunchMode.Player) + catch { - Logger.WriteLine(LOG_IDENT, "Creating singleton mutex"); - - try - { - Mutex.OpenExisting("ROBLOX_singletonMutex"); - Logger.WriteLine(LOG_IDENT, "Warning - singleton mutex already exists!"); - } - catch - { - // create the singleton mutex before the game client does - singletonMutex = new Mutex(true, "ROBLOX_singletonMutex"); - } + // create the singleton mutex before the game client does + singletonMutex = new Mutex(true, "ROBLOX_singletonMutex"); } + } - Task bootstrapperTask = Task.Run(async () => await bootstrapper.Run()).ContinueWith(t => - { - Logger.WriteLine(LOG_IDENT, "Bootstrapper task has finished"); + Task bootstrapperTask = Task.Run(async () => await bootstrapper.Run()).ContinueWith(t => + { + Logger.WriteLine(LOG_IDENT, "Bootstrapper task has finished"); - // notifyicon is blocking main thread, must be disposed here - NotifyIcon?.Dispose(); + // notifyicon is blocking main thread, must be disposed here + NotifyIcon?.Dispose(); - if (t.IsFaulted) - Logger.WriteLine(LOG_IDENT, "An exception occurred when running the bootstrapper"); + if (t.IsFaulted) + Logger.WriteLine(LOG_IDENT, "An exception occurred when running the bootstrapper"); - if (t.Exception is null) - return; + if (t.Exception is null) + return; - Logger.WriteException(LOG_IDENT, t.Exception); + Logger.WriteException(LOG_IDENT, t.Exception); - Exception exception = t.Exception; + Exception exception = t.Exception; #if !DEBUG - if (t.Exception.GetType().ToString() == "System.AggregateException") + if (t.Exception.GetType().ToString() == "System.AggregateException") exception = t.Exception.InnerException!; #endif - FinalizeExceptionHandling(exception, false); - }); + FinalizeExceptionHandling(exception, false); + }); - // 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(); + // 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 (!IsNoLaunch && Settings.Prop.EnableActivityTracking) - NotifyIcon?.InitializeContextMenu(); + if (!LaunchSettings.IsNoLaunch && Settings.Prop.EnableActivityTracking) + NotifyIcon?.InitializeContextMenu(); - Logger.WriteLine(LOG_IDENT, "Waiting for bootstrapper task to finish"); + Logger.WriteLine(LOG_IDENT, "Waiting for bootstrapper task to finish"); - bootstrapperTask.Wait(); + bootstrapperTask.Wait(); - if (singletonMutex is not null) - { - Logger.WriteLine(LOG_IDENT, "We have singleton mutex ownership! Running in background until all Roblox processes are closed"); + if (singletonMutex is not null) + { + Logger.WriteLine(LOG_IDENT, "We have singleton mutex ownership! Running in background until all Roblox processes are closed"); - // we've got ownership of the roblox singleton mutex! - // if we stop running, everything will screw up once any more roblox instances launched - while (Process.GetProcessesByName("RobloxPlayerBeta").Any()) - Thread.Sleep(5000); - } + // we've got ownership of the roblox singleton mutex! + // if we stop running, everything will screw up once any more roblox instances launched + while (Process.GetProcessesByName("RobloxPlayerBeta").Any()) + Thread.Sleep(5000); } - Logger.WriteLine(LOG_IDENT, "Successfully reached end of main thread. Terminating..."); - - Terminate(); + StartupFinished(); } } } diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index bcfbf8b..6214e2e 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -123,7 +123,7 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, "Running bootstrapper"); - if (App.IsUninstall) + if (App.LaunchSettings.IsUninstall) { Uninstall(); return; @@ -226,9 +226,9 @@ namespace Bloxstrap await mutex.ReleaseAsync(); - if (App.IsFirstRun && App.IsNoLaunch) + if (App.IsFirstRun && App.LaunchSettings.IsNoLaunch) Dialog?.ShowSuccess(Resources.Strings.Bootstrapper_SuccessfullyInstalled); - else if (!App.IsNoLaunch && !_cancelFired) + else if (!App.LaunchSettings.IsNoLaunch && !_cancelFired) await StartRoblox(); } @@ -302,7 +302,7 @@ namespace Bloxstrap MessageBoxImage.Error ); - if (!App.IsQuiet) + if (!App.LaunchSettings.IsQuiet) Utilities.ShellExecute("https://support.microsoft.com/en-us/topic/media-feature-pack-list-for-windows-n-editions-c1c6fffa-d052-8338-7a79-a4bb980a700a"); Dialog?.CloseBootstrapper(); @@ -655,7 +655,7 @@ namespace Bloxstrap FileName = downloadLocation, }; - foreach (string arg in App.LaunchArgs) + foreach (string arg in App.LaunchSettings.Args) startInfo.ArgumentList.Add(arg); App.Settings.Save(); diff --git a/Bloxstrap/InstallChecker.cs b/Bloxstrap/InstallChecker.cs index 2524928..d3237d2 100644 --- a/Bloxstrap/InstallChecker.cs +++ b/Bloxstrap/InstallChecker.cs @@ -124,7 +124,7 @@ namespace Bloxstrap App.BaseDirectory = Path.Combine(Paths.LocalAppData, App.ProjectName); App.Logger.Initialize(true); - if (App.IsQuiet) + if (App.LaunchSettings.IsQuiet) return; App.IsSetupComplete = false; @@ -159,7 +159,7 @@ namespace Bloxstrap MessageBoxResult result; // silently upgrade version if the command line flag is set or if we're launching from an auto update - if (App.IsUpgrade || isAutoUpgrade) + if (App.LaunchSettings.IsUpgrade || isAutoUpgrade) { result = MessageBoxResult.Yes; } @@ -238,7 +238,7 @@ namespace Bloxstrap (_, _) => Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/releases/tag/v{currentVersionInfo.ProductVersion}") ); } - else if (!App.IsQuiet) + else if (!App.LaunchSettings.IsQuiet) { Frontend.ShowMessageBox( string.Format(Resources.Strings.InstallChecker_Updated, currentVersionInfo.ProductVersion), diff --git a/Bloxstrap/InterProcessLock.cs b/Bloxstrap/InterProcessLock.cs new file mode 100644 index 0000000..e1938e8 --- /dev/null +++ b/Bloxstrap/InterProcessLock.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bloxstrap +{ + public class InterProcessLock : IDisposable + { + public Mutex Mutex { get; private set; } + + public bool IsAcquired { get; private set; } + + public InterProcessLock(string name, TimeSpan timeout) + { + Mutex = new Mutex(false, "Bloxstrap-" + name); + IsAcquired = Mutex.WaitOne(timeout); + } + + public void Dispose() + { + if (IsAcquired) + { + Mutex.ReleaseMutex(); + IsAcquired = false; + } + } + } +} diff --git a/Bloxstrap/LaunchSettings.cs b/Bloxstrap/LaunchSettings.cs new file mode 100644 index 0000000..16765fa --- /dev/null +++ b/Bloxstrap/LaunchSettings.cs @@ -0,0 +1,181 @@ +using Bloxstrap.Enums; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using System.Windows; + +namespace Bloxstrap +{ + public class LaunchSettings + { + [LaunchFlag(new[] { "-preferences", "-menu" })] + public bool IsMenuLaunch { get; private set; } = false; + + [LaunchFlag("-quiet")] + public bool IsQuiet { get; private set; } = false; + + [LaunchFlag("-uninstall")] + public bool IsUninstall { get; private set; } = false; + + [LaunchFlag("-nolaunch")] + public bool IsNoLaunch { get; private set; } = false; + + [LaunchFlag("-upgrade")] + public bool IsUpgrade { get; private set; } = false; + + public LaunchMode RobloxLaunchMode { get; private set; } = LaunchMode.Player; + + public string RobloxLaunchArgs { get; private set; } = "--app"; + + /// + /// Original launch arguments + /// + public string[] Args { get; private set; } + + private Dictionary? _flagMap; + + // pizzaboxer wanted this + private void ParseLaunchFlagProps() + { + _flagMap = new Dictionary(); + + foreach (var prop in typeof(LaunchSettings).GetProperties()) + { + var attr = prop.GetCustomAttribute(); + + if (attr == null) + continue; + + if (!string.IsNullOrEmpty(attr.Name)) + { + _flagMap[attr.Name] = prop; + } + else + { + foreach (var name in attr.Names!) + _flagMap[name] = prop; + } + } + } + + private void ParseFlag(string arg) + { + const string LOG_IDENT = "LaunchSettings::ParseFlag"; + + arg = arg.ToLowerInvariant(); + + if (_flagMap!.ContainsKey(arg)) + { + var prop = _flagMap[arg]; + prop.SetValue(this, true); + App.Logger.WriteLine(LOG_IDENT, $"Started with {prop.Name} flag"); + } + } + + private void ParseRoblox(string arg, ref int i) + { + if (arg.StartsWith("roblox-player:")) + { + RobloxLaunchArgs = ProtocolHandler.ParseUri(arg); + + RobloxLaunchMode = LaunchMode.Player; + } + else if (arg.StartsWith("roblox:")) + { + if (App.Settings.Prop.UseDisableAppPatch) + Frontend.ShowMessageBox( + Resources.Strings.Bootstrapper_DeeplinkTempEnabled, + MessageBoxImage.Information + ); + + RobloxLaunchArgs = $"--app --deeplink {arg}"; + + RobloxLaunchMode = LaunchMode.Player; + } + else if (arg.StartsWith("roblox-studio:")) + { + RobloxLaunchArgs = ProtocolHandler.ParseUri(arg); + + if (!RobloxLaunchArgs.Contains("-startEvent")) + RobloxLaunchArgs += " -startEvent www.roblox.com/robloxQTStudioStartedEvent"; + + RobloxLaunchMode = LaunchMode.Studio; + } + else if (arg.StartsWith("roblox-studio-auth:")) + { + RobloxLaunchArgs = HttpUtility.UrlDecode(arg); + + RobloxLaunchMode = LaunchMode.StudioAuth; + } + else if (arg == "-ide") + { + RobloxLaunchMode = LaunchMode.Studio; + + if (Args.Length >= 2) + { + string pathArg = Args[i + 1]; + + if (pathArg.StartsWith('-')) + return; // likely a launch flag, ignore it. + + i++; // path arg + RobloxLaunchArgs = $"-task EditFile -localPlaceFile \"{pathArg}\""; + } + } + } + + private void Parse() + { + const string LOG_IDENT = "LaunchSettings::Parse"; + + App.Logger.WriteLine(LOG_IDENT, "Parsing launch arguments"); + +#if DEBUG + App.Logger.WriteLine(LOG_IDENT, $"Launch arguments: {string.Join(' ', Args)}"); +#endif + + if (Args.Length == 0) + { + App.Logger.WriteLine(LOG_IDENT, "No launch arguments to parse"); + return; + } + + int idx = 0; + string firstArg = Args[0]; + + // check & handle roblox arg + if (!firstArg.StartsWith('-') || firstArg == "-ide") + { + ParseRoblox(firstArg, ref idx); + idx++; // roblox arg + } + + // check if there are any launch flags + if (idx > Args.Length - 1) + return; + + App.Logger.WriteLine(LOG_IDENT, "Parsing launch flags"); + + // map out launch flags + ParseLaunchFlagProps(); + + // parse any launch flags + for (int i = idx; i < Args.Length; i++) + ParseFlag(Args[i]); + + // cleanup flag map + _flagMap!.Clear(); + _flagMap = null; + } + + public LaunchSettings(string[] args) + { + Args = args; + Parse(); + } + } +} diff --git a/Bloxstrap/Models/Attributes/LaunchFlagAttribute.cs b/Bloxstrap/Models/Attributes/LaunchFlagAttribute.cs new file mode 100644 index 0000000..60c3886 --- /dev/null +++ b/Bloxstrap/Models/Attributes/LaunchFlagAttribute.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bloxstrap.Models.Attributes +{ + public class LaunchFlagAttribute : Attribute + { + public string? Name { get; private set; } + public string[]? Names { get; private set; } + + public LaunchFlagAttribute(string name) + { + Name = name; + } + + public LaunchFlagAttribute(string[] names) + { + Names = names; + } + } +} diff --git a/Bloxstrap/UI/Frontend.cs b/Bloxstrap/UI/Frontend.cs index bce2d8a..e66f8a2 100644 --- a/Bloxstrap/UI/Frontend.cs +++ b/Bloxstrap/UI/Frontend.cs @@ -12,7 +12,7 @@ namespace Bloxstrap.UI public static MessageBoxResult ShowMessageBox(string message, MessageBoxImage icon = MessageBoxImage.None, MessageBoxButton buttons = MessageBoxButton.OK, MessageBoxResult defaultResult = MessageBoxResult.None) { - if (App.IsQuiet) + if (App.LaunchSettings.IsQuiet) return defaultResult; switch (App.Settings.Prop.BootstrapperStyle)