Skip to content

Commit

Permalink
Draft: new installer system
Browse files Browse the repository at this point in the history
the beginning of a long arduous cleanup of two years of debt
  • Loading branch information
pizzaboxer committed Aug 10, 2024
1 parent d1343d3 commit 776dbc4
Show file tree
Hide file tree
Showing 75 changed files with 2,312 additions and 1,455 deletions.
223 changes: 58 additions & 165 deletions Bloxstrap/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using System.Reflection;
using System.Web;
using System.Windows;
using System.Windows.Threading;

using Windows.Win32;
using Windows.Win32.Foundation;
using Microsoft.Win32;

using Bloxstrap.Resources;

namespace Bloxstrap
{
Expand All @@ -15,29 +15,27 @@ public partial class App : Application
{
public const string ProjectName = "Bloxstrap";
public const string ProjectRepository = "pizzaboxer/bloxstrap";

public const string RobloxPlayerAppName = "RobloxPlayerBeta";
public const string RobloxStudioAppName = "RobloxStudioBeta";

// used only for communicating between app and menu - use Directories.Base for anything else
public static string BaseDirectory = null!;
public static string? CustomFontLocation;

public static bool ShouldSaveConfigs { get; set; } = false;

public static bool IsSetupComplete { get; set; } = true;
public static bool IsFirstRun { get; set; } = true;
// simple shorthand for extremely frequently used and long string - this goes under HKCU
public const string UninstallKey = $@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{ProjectName}";

public static LaunchSettings LaunchSettings { get; private set; } = null!;

public static BuildMetadataAttribute BuildMetadata = Assembly.GetExecutingAssembly().GetCustomAttribute<BuildMetadataAttribute>()!;

public static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2];

public static NotifyIconWrapper? NotifyIcon { get; private set; }
public static NotifyIconWrapper? NotifyIcon { get; set; }

public static readonly Logger Logger = new();

public static readonly JsonManager<Settings> Settings = new();

public static readonly JsonManager<State> State = new();

public static readonly FastFlagManager FastFlags = new();

public static readonly HttpClient HttpClient = new(
Expand All @@ -52,18 +50,10 @@ public partial class App : Application

public static void Terminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS)
{
if (IsFirstRun)
{
if (exitCode == ErrorCode.ERROR_CANCELLED)
exitCode = ErrorCode.ERROR_INSTALL_USEREXIT;
}

int exitCodeNum = (int)exitCode;

Logger.WriteLine("App::Terminate", $"Terminating with exit code {exitCodeNum} ({exitCode})");

Settings.Save();
State.Save();
NotifyIcon?.Dispose();

Environment.Exit(exitCodeNum);
Expand Down Expand Up @@ -98,16 +88,7 @@ public static void FinalizeExceptionHandling(Exception exception, bool log = tru
#endif
}

private void StartupFinished()
{
const string LOG_IDENT = "App::StartupFinished";

Logger.WriteLine(LOG_IDENT, "Successfully reached end of main thread. Terminating...");

Terminate();
}

protected override async void OnStartup(StartupEventArgs e)
protected override void OnStartup(StartupEventArgs e)
{
const string LOG_IDENT = "App::OnStartup";

Expand All @@ -128,169 +109,81 @@ protected override async void OnStartup(StartupEventArgs e)
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();

LaunchSettings = new LaunchSettings(e.Args);
HttpClient.Timeout = TimeSpan.FromSeconds(30);
HttpClient.DefaultRequestHeaders.Add("User-Agent", ProjectRepository);

using (var checker = new InstallChecker())
{
checker.Check();
}
LaunchSettings = new LaunchSettings(e.Args);

Paths.Initialize(BaseDirectory);
// installation check begins here
using var uninstallKey = Registry.CurrentUser.OpenSubKey(UninstallKey);
string? installLocation = null;

if (uninstallKey?.GetValue("InstallLocation") is string value && Directory.Exists(value))
installLocation = value;

// we shouldn't save settings on the first run until the first installation is finished,
// just in case the user decides to cancel the install
if (!IsFirstRun)
// silently change install location if we detect a portable run
// this should also handle renaming of the user profile folder
if (installLocation is null && Directory.GetParent(Paths.Process)?.FullName is string processDir)
{
Settings.Load();
State.Load();
FastFlags.Load();

if (!Locale.SupportedLocales.ContainsKey(Settings.Prop.Locale))
var files = Directory.GetFiles(processDir).Select(x => Path.GetFileName(x)).ToArray();
var installer = new Installer
{
Settings.Prop.Locale = "nil";
Settings.Save();
InstallLocation = processDir,
IsImplicitInstall = true
};

// check if settings.json and state.json are the only files in the folder, and if we can write to it
if (files.Length <= 3
&& files.Contains("Settings.json")
&& files.Contains("State.json")
&& installer.CheckInstallLocation())
{
Logger.WriteLine(LOG_IDENT, $"Changing install location to '{processDir}'");
installer.DoInstall();
installLocation = processDir;
}

Locale.Set(Settings.Prop.Locale);
}

LaunchSettings.ParseRoblox();

HttpClient.Timeout = TimeSpan.FromSeconds(30);
HttpClient.DefaultRequestHeaders.Add("User-Agent", ProjectRepository);

// TEMPORARY FILL-IN FOR NEW FUNCTIONALITY
// REMOVE WHEN LARGER REFACTORING IS DONE
var connectionResult = await RobloxDeployment.InitializeConnectivity();

if (connectionResult is not null)
if (installLocation is null)
{
Logger.WriteException(LOG_IDENT, connectionResult);

Frontend.ShowConnectivityDialog(
Bloxstrap.Resources.Strings.Dialog_Connectivity_UnableToConnect,
Bloxstrap.Resources.Strings.Bootstrapper_Connectivity_Preventing,
connectionResult
);

return;
Logger.Initialize(true);
LaunchHandler.LaunchInstaller();
}

if (LaunchSettings.IsUninstall && IsFirstRun)
else
{
Frontend.ShowMessageBox(Bloxstrap.Resources.Strings.Bootstrapper_FirstRunUninstall, MessageBoxImage.Error);
Terminate(ErrorCode.ERROR_INVALID_FUNCTION);
return;
}
Paths.Initialize(installLocation);

// ensure executable is in the install directory
if (Paths.Process != Paths.Application && !File.Exists(Paths.Application))
File.Copy(Paths.Process, Paths.Application);

// we shouldn't save settings on the first run until the first installation is finished,
// just in case the user decides to cancel the install
if (!IsFirstRun)
{
Logger.Initialize(LaunchSettings.IsUninstall);

if (!Logger.Initialized && !Logger.NoWriteMode)
{
Logger.WriteLine(LOG_IDENT, "Possible duplicate launch detected, terminating.");
Terminate();
}
}

if (!LaunchSettings.IsUninstall && !LaunchSettings.IsMenuLaunch)
NotifyIcon = new();

#if !DEBUG
if (!LaunchSettings.IsUninstall && !IsFirstRun)
InstallChecker.CheckUpgrade();
#endif

if (LaunchSettings.IsMenuLaunch)
{
Process? menuProcess = Utilities.GetProcessesSafe().Where(x => x.MainWindowTitle == $"{ProjectName} Menu").FirstOrDefault();
Settings.Load();
State.Load();
FastFlags.Load();

if (menuProcess is not null)
{
var handle = menuProcess.MainWindowHandle;
Logger.WriteLine(LOG_IDENT, $"Found an already existing menu window with handle {handle}");
PInvoke.SetForegroundWindow((HWND)handle);
}
else
if (!Locale.SupportedLocales.ContainsKey(Settings.Prop.Locale))
{
bool showAlreadyRunningWarning = Process.GetProcessesByName(ProjectName).Length > 1 && !LaunchSettings.IsQuiet;
Frontend.ShowMenu(showAlreadyRunningWarning);
Settings.Prop.Locale = "nil";
Settings.Save();
}

StartupFinished();
return;
}

if (!IsFirstRun)
ShouldSaveConfigs = true;

if (Settings.Prop.ConfirmLaunches && Mutex.TryOpenExisting("ROBLOX_singletonMutex", out var _))
{
// this currently doesn't work very well since it relies on checking the existence of the singleton mutex
// which often hangs around for a few seconds after the window closes
// it would be better to have this rely on the activity tracker when we implement IPC in the planned refactoring

var result = Frontend.ShowMessageBox(Bloxstrap.Resources.Strings.Bootstrapper_ConfirmLaunch, MessageBoxImage.Warning, MessageBoxButton.YesNo);

if (result != MessageBoxResult.Yes)
{
StartupFinished();
return;
}
}
Locale.Set(Settings.Prop.Locale);

// 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.IsUninstall)
Installer.HandleUpgrade();

if (!LaunchSettings.IsQuiet)
{
Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper dialog");
dialog = Settings.Prop.BootstrapperStyle.GetNew();
bootstrapper.Dialog = dialog;
dialog.Bootstrapper = bootstrapper;
LaunchHandler.ProcessLaunchArgs();
}

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();
if (t.IsFaulted)
Logger.WriteLine(LOG_IDENT, "An exception occurred when running the bootstrapper");
if (t.Exception is null)
return;
Logger.WriteException(LOG_IDENT, t.Exception);
Exception exception = t.Exception;
#if !DEBUG
if (t.Exception.GetType().ToString() == "System.AggregateException")
exception = t.Exception.InnerException!;
#endif
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();

if (!LaunchSettings.IsNoLaunch && Settings.Prop.EnableActivityTracking)
NotifyIcon?.InitializeContextMenu();

Logger.WriteLine(LOG_IDENT, "Waiting for bootstrapper task to finish");

bootstrapperTask.Wait();

StartupFinished();
Terminate();
}
}
}
5 changes: 2 additions & 3 deletions Bloxstrap/Bloxstrap.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
<UseWPF>true</UseWPF>
<UseWindowsForms>True</UseWindowsForms>
<ApplicationIcon>Bloxstrap.ico</ApplicationIcon>
<Version>2.7.0</Version>
<FileVersion>2.7.0</FileVersion>
<Version>2.8.0</Version>
<FileVersion>2.8.0</FileVersion>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
Expand All @@ -20,7 +20,6 @@
<Resource Include="Resources\Fonts\Rubik-VariableFont_wght.ttf" />
<Resource Include="Resources\BootstrapperStyles\ByfronDialog\ByfronLogoDark.jpg" />
<Resource Include="Resources\BootstrapperStyles\ByfronDialog\ByfronLogoLight.jpg" />
<Resource Include="Resources\Menu\StartMenu.png" />
<Resource Include="Resources\MessageBox\Error.png" />
<Resource Include="Resources\MessageBox\Information.png" />
<Resource Include="Resources\MessageBox\Question.png" />
Expand Down
Loading

0 comments on commit 776dbc4

Please sign in to comment.