mirror of
https://github.com/bloxstraplabs/bloxstrap.git
synced 2025-04-15 01:31:30 -07:00
586 lines
23 KiB
C#
586 lines
23 KiB
C#
using System.Windows;
|
|
using Microsoft.Win32;
|
|
|
|
namespace Bloxstrap
|
|
{
|
|
internal class Installer
|
|
{
|
|
private static string DesktopShortcut => Path.Combine(Paths.Desktop, "Bloxstrap.lnk");
|
|
|
|
private static string StartMenuShortcut => Path.Combine(Paths.WindowsStartMenu, "Bloxstrap.lnk");
|
|
|
|
public string InstallLocation = Path.Combine(Paths.LocalAppData, "Bloxstrap");
|
|
|
|
public bool ExistingDataPresent => File.Exists(Path.Combine(InstallLocation, "Settings.json"));
|
|
|
|
public bool CreateDesktopShortcuts = true;
|
|
|
|
public bool CreateStartMenuShortcuts = true;
|
|
|
|
public bool IsImplicitInstall = false;
|
|
|
|
public string InstallLocationError { get; set; } = "";
|
|
|
|
public void DoInstall()
|
|
{
|
|
const string LOG_IDENT = "Installer::DoInstall";
|
|
|
|
App.Logger.WriteLine(LOG_IDENT, "Beginning installation");
|
|
|
|
// should've been created earlier from the write test anyway
|
|
Directory.CreateDirectory(InstallLocation);
|
|
|
|
Paths.Initialize(InstallLocation);
|
|
|
|
if (!IsImplicitInstall)
|
|
{
|
|
Filesystem.AssertReadOnly(Paths.Application);
|
|
|
|
try
|
|
{
|
|
File.Copy(Paths.Process, Paths.Application, true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
App.Logger.WriteLine(LOG_IDENT, "Could not overwrite executable");
|
|
App.Logger.WriteException(LOG_IDENT, ex);
|
|
|
|
Frontend.ShowMessageBox(Strings.Installer_Install_CannotOverwrite, MessageBoxImage.Error);
|
|
App.Terminate(ErrorCode.ERROR_INSTALL_FAILURE);
|
|
}
|
|
}
|
|
|
|
// TODO: registry access checks, i'll need to look back on issues to see what the error looks like
|
|
using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey))
|
|
{
|
|
uninstallKey.SetValue("DisplayIcon", $"{Paths.Application},0");
|
|
uninstallKey.SetValue("DisplayName", App.ProjectName);
|
|
|
|
uninstallKey.SetValue("DisplayVersion", App.Version);
|
|
|
|
if (uninstallKey.GetValue("InstallDate") is null)
|
|
uninstallKey.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd"));
|
|
|
|
uninstallKey.SetValue("InstallLocation", Paths.Base);
|
|
uninstallKey.SetValue("NoRepair", 1);
|
|
uninstallKey.SetValue("Publisher", App.ProjectOwner);
|
|
uninstallKey.SetValue("ModifyPath", $"\"{Paths.Application}\" -settings");
|
|
uninstallKey.SetValue("QuietUninstallString", $"\"{Paths.Application}\" -uninstall -quiet");
|
|
uninstallKey.SetValue("UninstallString", $"\"{Paths.Application}\" -uninstall");
|
|
uninstallKey.SetValue("HelpLink", App.ProjectHelpLink);
|
|
uninstallKey.SetValue("URLInfoAbout", App.ProjectSupportLink);
|
|
uninstallKey.SetValue("URLUpdateInfo", App.ProjectDownloadLink);
|
|
}
|
|
|
|
// only register player, for the scenario where the user installs bloxstrap, closes it,
|
|
// and then launches from the website expecting it to work
|
|
// studio can be implicitly registered when it's first launched manually
|
|
WindowsRegistry.RegisterPlayer();
|
|
|
|
if (CreateDesktopShortcuts)
|
|
Shortcut.Create(Paths.Application, "", DesktopShortcut);
|
|
|
|
if (CreateStartMenuShortcuts)
|
|
Shortcut.Create(Paths.Application, "", StartMenuShortcut);
|
|
|
|
// existing configuration persisting from an earlier install
|
|
App.Settings.Load(false);
|
|
App.State.Load(false);
|
|
App.FastFlags.Load(false);
|
|
|
|
if (!String.IsNullOrEmpty(App.State.Prop.Studio.VersionGuid))
|
|
WindowsRegistry.RegisterStudio();
|
|
|
|
App.Logger.WriteLine(LOG_IDENT, "Installation finished");
|
|
}
|
|
|
|
private bool ValidateLocation()
|
|
{
|
|
// prevent from installing to the root of a drive
|
|
if (InstallLocation.Length <= 3)
|
|
return false;
|
|
|
|
// unc path, just to be safe
|
|
if (InstallLocation.StartsWith("\\\\"))
|
|
return false;
|
|
|
|
if (InstallLocation.StartsWith(Path.GetTempPath(), StringComparison.InvariantCultureIgnoreCase)
|
|
|| InstallLocation.Contains("\\Temp\\", StringComparison.InvariantCultureIgnoreCase))
|
|
return false;
|
|
|
|
// prevent from installing to a onedrive folder
|
|
if (InstallLocation.Contains("OneDrive", StringComparison.InvariantCultureIgnoreCase))
|
|
return false;
|
|
|
|
// prevent from installing to an essential user profile folder (e.g. Documents, Downloads, Contacts idk)
|
|
if (String.Compare(Directory.GetParent(InstallLocation)?.FullName, Paths.UserProfile, StringComparison.InvariantCultureIgnoreCase) == 0)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool CheckInstallLocation()
|
|
{
|
|
if (string.IsNullOrEmpty(InstallLocation))
|
|
{
|
|
InstallLocationError = Strings.Menu_InstallLocation_NotSet;
|
|
}
|
|
else if (!ValidateLocation())
|
|
{
|
|
InstallLocationError = Strings.Menu_InstallLocation_CantInstall;
|
|
}
|
|
else
|
|
{
|
|
if (!IsImplicitInstall
|
|
&& !InstallLocation.EndsWith(App.ProjectName, StringComparison.InvariantCultureIgnoreCase)
|
|
&& Directory.Exists(InstallLocation)
|
|
&& Directory.EnumerateFileSystemEntries(InstallLocation).Any())
|
|
{
|
|
string suggestedChange = Path.Combine(InstallLocation, App.ProjectName);
|
|
|
|
MessageBoxResult result = Frontend.ShowMessageBox(
|
|
String.Format(Strings.Menu_InstallLocation_NotEmpty, suggestedChange),
|
|
MessageBoxImage.Warning,
|
|
MessageBoxButton.YesNoCancel,
|
|
MessageBoxResult.Yes
|
|
);
|
|
|
|
if (result == MessageBoxResult.Yes)
|
|
InstallLocation = suggestedChange;
|
|
else if (result == MessageBoxResult.Cancel || result == MessageBoxResult.None)
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
// check if we can write to the directory (a bit hacky but eh)
|
|
string testFile = Path.Combine(InstallLocation, $"{App.ProjectName}WriteTest.txt");
|
|
|
|
Directory.CreateDirectory(InstallLocation);
|
|
File.WriteAllText(testFile, "");
|
|
File.Delete(testFile);
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
InstallLocationError = Strings.Menu_InstallLocation_NoWritePerms;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
InstallLocationError = ex.Message;
|
|
}
|
|
}
|
|
|
|
return String.IsNullOrEmpty(InstallLocationError);
|
|
}
|
|
|
|
public static void DoUninstall(bool keepData)
|
|
{
|
|
const string LOG_IDENT = "Installer::DoUninstall";
|
|
|
|
var processes = new List<Process>();
|
|
|
|
if (!String.IsNullOrEmpty(App.State.Prop.Player.VersionGuid))
|
|
processes.AddRange(Process.GetProcessesByName(App.RobloxPlayerAppName));
|
|
|
|
if (!String.IsNullOrEmpty(App.State.Prop.Studio.VersionGuid))
|
|
processes.AddRange(Process.GetProcessesByName(App.RobloxStudioAppName));
|
|
|
|
// prompt to shutdown roblox if its currently running
|
|
if (processes.Any())
|
|
{
|
|
var result = Frontend.ShowMessageBox(
|
|
Strings.Bootstrapper_Uninstall_RobloxRunning,
|
|
MessageBoxImage.Information,
|
|
MessageBoxButton.OKCancel,
|
|
MessageBoxResult.OK
|
|
);
|
|
|
|
if (result != MessageBoxResult.OK)
|
|
{
|
|
App.Terminate(ErrorCode.ERROR_CANCELLED);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
foreach (var process in processes)
|
|
{
|
|
process.Kill();
|
|
process.Close();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
App.Logger.WriteLine(LOG_IDENT, $"Failed to close process! {ex}");
|
|
}
|
|
}
|
|
|
|
string robloxFolder = Path.Combine(Paths.LocalAppData, "Roblox");
|
|
bool playerStillInstalled = true;
|
|
bool studioStillInstalled = true;
|
|
|
|
// check if stock bootstrapper is still installed
|
|
using var playerKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player");
|
|
var playerFolder = playerKey?.GetValue("InstallLocation");
|
|
|
|
if (playerKey is null || playerFolder is not string)
|
|
{
|
|
playerStillInstalled = false;
|
|
|
|
WindowsRegistry.Unregister("roblox");
|
|
WindowsRegistry.Unregister("roblox-player");
|
|
}
|
|
else
|
|
{
|
|
string playerPath = Path.Combine((string)playerFolder, "RobloxPlayerBeta.exe");
|
|
|
|
WindowsRegistry.RegisterPlayer(playerPath, "%1");
|
|
}
|
|
|
|
using var studioKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-studio");
|
|
var studioFolder = studioKey?.GetValue("InstallLocation");
|
|
|
|
if (studioKey is null || studioFolder is not string)
|
|
{
|
|
studioStillInstalled = false;
|
|
|
|
WindowsRegistry.Unregister("roblox-studio");
|
|
WindowsRegistry.Unregister("roblox-studio-auth");
|
|
|
|
WindowsRegistry.Unregister("Roblox.Place");
|
|
WindowsRegistry.Unregister(".rbxl");
|
|
WindowsRegistry.Unregister(".rbxlx");
|
|
}
|
|
else
|
|
{
|
|
string studioPath = Path.Combine((string)studioFolder, "RobloxStudioBeta.exe");
|
|
string studioLauncherPath = Path.Combine((string)studioFolder, "RobloxStudioLauncherBeta.exe");
|
|
|
|
WindowsRegistry.RegisterStudioProtocol(studioPath, "%1");
|
|
WindowsRegistry.RegisterStudioFileClass(studioPath, "-ide \"%1\"");
|
|
}
|
|
|
|
var cleanupSequence = new List<Action>
|
|
{
|
|
() =>
|
|
{
|
|
foreach (var file in Directory.GetFiles(Paths.Desktop).Where(x => x.EndsWith("lnk")))
|
|
{
|
|
var shortcut = ShellLink.Shortcut.ReadFromFile(file);
|
|
|
|
if (shortcut.ExtraData.EnvironmentVariableDataBlock?.TargetUnicode == Paths.Application)
|
|
File.Delete(file);
|
|
}
|
|
},
|
|
|
|
() => File.Delete(StartMenuShortcut),
|
|
|
|
() => Directory.Delete(Paths.Downloads, true),
|
|
() => Directory.Delete(Paths.Roblox, true),
|
|
|
|
() => File.Delete(App.State.FileLocation)
|
|
};
|
|
|
|
if (!keepData)
|
|
{
|
|
cleanupSequence.AddRange(new List<Action>
|
|
{
|
|
() => Directory.Delete(Paths.Modifications, true),
|
|
() => Directory.Delete(Paths.Logs, true),
|
|
|
|
() => File.Delete(App.Settings.FileLocation)
|
|
});
|
|
}
|
|
|
|
bool deleteFolder = false;
|
|
|
|
if (Directory.Exists(Paths.Base))
|
|
{
|
|
var folderFiles = Directory.GetFiles(Paths.Base);
|
|
deleteFolder = folderFiles.Length == 1 && folderFiles.First().EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase);
|
|
}
|
|
|
|
if (deleteFolder)
|
|
cleanupSequence.Add(() => Directory.Delete(Paths.Base, true));
|
|
|
|
if (!playerStillInstalled && !studioStillInstalled && Directory.Exists(robloxFolder))
|
|
cleanupSequence.Add(() => Directory.Delete(robloxFolder, true));
|
|
|
|
cleanupSequence.Add(() => Registry.CurrentUser.DeleteSubKey(App.UninstallKey));
|
|
|
|
foreach (var process in cleanupSequence)
|
|
{
|
|
try
|
|
{
|
|
process();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
App.Logger.WriteLine(LOG_IDENT, $"Encountered exception when running cleanup sequence (#{cleanupSequence.IndexOf(process)})");
|
|
App.Logger.WriteException(LOG_IDENT, ex);
|
|
}
|
|
}
|
|
|
|
if (Directory.Exists(Paths.Base))
|
|
{
|
|
// this is definitely one of the workaround hacks of all time
|
|
|
|
string deleteCommand;
|
|
|
|
if (deleteFolder)
|
|
deleteCommand = $"del /Q \"{Paths.Base}\\*\" && rmdir \"{Paths.Base}\"";
|
|
else
|
|
deleteCommand = $"del /Q \"{Paths.Application}\"";
|
|
|
|
Process.Start(new ProcessStartInfo()
|
|
{
|
|
FileName = "cmd.exe",
|
|
Arguments = $"/c timeout 5 && {deleteCommand}",
|
|
UseShellExecute = true,
|
|
WindowStyle = ProcessWindowStyle.Hidden
|
|
});
|
|
}
|
|
}
|
|
|
|
public static void HandleUpgrade()
|
|
{
|
|
const string LOG_IDENT = "Installer::HandleUpgrade";
|
|
|
|
if (!File.Exists(Paths.Application) || Paths.Process == Paths.Application)
|
|
return;
|
|
|
|
// 2.0.0 downloads updates to <BaseFolder>/Updates so lol
|
|
bool isAutoUpgrade = App.LaunchSettings.UpgradeFlag.Active
|
|
|| Paths.Process.StartsWith(Path.Combine(Paths.Base, "Updates"))
|
|
|| Paths.Process.StartsWith(Paths.Temp);
|
|
|
|
var existingVer = FileVersionInfo.GetVersionInfo(Paths.Application).ProductVersion;
|
|
var currentVer = FileVersionInfo.GetVersionInfo(Paths.Process).ProductVersion;
|
|
|
|
if (MD5Hash.FromFile(Paths.Process) == MD5Hash.FromFile(Paths.Application))
|
|
return;
|
|
|
|
if (currentVer is not null && existingVer is not null && Utilities.CompareVersions(currentVer, existingVer) == VersionComparison.LessThan)
|
|
{
|
|
var result = Frontend.ShowMessageBox(
|
|
Strings.InstallChecker_VersionLessThanInstalled,
|
|
MessageBoxImage.Question,
|
|
MessageBoxButton.YesNo
|
|
);
|
|
|
|
if (result != MessageBoxResult.Yes)
|
|
return;
|
|
}
|
|
|
|
// silently upgrade version if the command line flag is set or if we're launching from an auto update
|
|
if (!isAutoUpgrade)
|
|
{
|
|
var result = Frontend.ShowMessageBox(
|
|
Strings.InstallChecker_VersionDifferentThanInstalled,
|
|
MessageBoxImage.Question,
|
|
MessageBoxButton.YesNo
|
|
);
|
|
|
|
if (result != MessageBoxResult.Yes)
|
|
return;
|
|
}
|
|
|
|
App.Logger.WriteLine(LOG_IDENT, "Doing upgrade");
|
|
|
|
Filesystem.AssertReadOnly(Paths.Application);
|
|
|
|
using (var ipl = new InterProcessLock("AutoUpdater", TimeSpan.FromSeconds(5)))
|
|
{
|
|
if (!ipl.IsAcquired)
|
|
{
|
|
App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not obtain singleton mutex)");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// prior to 2.8.0, auto-updating was handled with this... bruteforce method
|
|
// now it's handled with the system mutex you see above, but we need to keep this logic for <2.8.0 versions
|
|
for (int i = 1; i <= 10; i++)
|
|
{
|
|
try
|
|
{
|
|
File.Copy(Paths.Process, Paths.Application, true);
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (i == 1)
|
|
{
|
|
App.Logger.WriteLine(LOG_IDENT, "Waiting for write permissions to update version");
|
|
}
|
|
else if (i == 10)
|
|
{
|
|
App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not get write permissions after 10 tries/5 seconds)");
|
|
App.Logger.WriteException(LOG_IDENT, ex);
|
|
return;
|
|
}
|
|
|
|
Thread.Sleep(500);
|
|
}
|
|
}
|
|
|
|
using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey))
|
|
{
|
|
uninstallKey.SetValue("DisplayVersion", App.Version);
|
|
|
|
uninstallKey.SetValue("Publisher", App.ProjectOwner);
|
|
uninstallKey.SetValue("HelpLink", App.ProjectHelpLink);
|
|
uninstallKey.SetValue("URLInfoAbout", App.ProjectSupportLink);
|
|
uninstallKey.SetValue("URLUpdateInfo", App.ProjectDownloadLink);
|
|
}
|
|
|
|
// update migrations
|
|
|
|
if (existingVer is not null)
|
|
{
|
|
if (Utilities.CompareVersions(existingVer, "2.2.0") == VersionComparison.LessThan)
|
|
{
|
|
string path = Path.Combine(Paths.Integrations, "rbxfpsunlocker");
|
|
|
|
try
|
|
{
|
|
if (Directory.Exists(path))
|
|
Directory.Delete(path, true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
App.Logger.WriteException(LOG_IDENT, ex);
|
|
}
|
|
}
|
|
|
|
if (Utilities.CompareVersions(existingVer, "2.3.0") == VersionComparison.LessThan)
|
|
{
|
|
string injectorLocation = Path.Combine(Paths.Modifications, "dxgi.dll");
|
|
string configLocation = Path.Combine(Paths.Modifications, "ReShade.ini");
|
|
|
|
if (File.Exists(injectorLocation))
|
|
{
|
|
Frontend.ShowMessageBox(
|
|
Strings.Bootstrapper_HyperionUpdateInfo,
|
|
MessageBoxImage.Warning
|
|
);
|
|
|
|
File.Delete(injectorLocation);
|
|
}
|
|
|
|
if (File.Exists(configLocation))
|
|
File.Delete(configLocation);
|
|
}
|
|
|
|
|
|
if (Utilities.CompareVersions(existingVer, "2.5.0") == VersionComparison.LessThan)
|
|
{
|
|
App.FastFlags.SetValue("DFFlagDisableDPIScale", null);
|
|
App.FastFlags.SetValue("DFFlagVariableDPIScale2", null);
|
|
}
|
|
|
|
if (Utilities.CompareVersions(existingVer, "2.5.1") == VersionComparison.LessThan)
|
|
{
|
|
App.FastFlags.SetValue("FIntDebugForceMSAASamples", null);
|
|
|
|
if (App.FastFlags.GetPreset("UI.Menu.Style.DisableV2") is not null)
|
|
App.FastFlags.SetPreset("UI.Menu.Style.ABTest", false);
|
|
}
|
|
|
|
if (Utilities.CompareVersions(existingVer, "2.5.3") == VersionComparison.LessThan)
|
|
{
|
|
string? val = App.FastFlags.GetPreset("UI.Menu.Style.EnableV4.1");
|
|
if (App.FastFlags.GetPreset("UI.Menu.Style.EnableV4.2") != val)
|
|
App.FastFlags.SetPreset("UI.Menu.Style.EnableV4.2", val);
|
|
}
|
|
|
|
if (Utilities.CompareVersions(existingVer, "2.6.0") == VersionComparison.LessThan)
|
|
{
|
|
if (App.Settings.Prop.UseDisableAppPatch)
|
|
{
|
|
try
|
|
{
|
|
File.Delete(Path.Combine(Paths.Modifications, "ExtraContent\\places\\Mobile.rbxl"));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
App.Logger.WriteException(LOG_IDENT, ex);
|
|
}
|
|
|
|
App.Settings.Prop.EnableActivityTracking = true;
|
|
}
|
|
|
|
if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ClassicFluentDialog)
|
|
App.Settings.Prop.BootstrapperStyle = BootstrapperStyle.FluentDialog;
|
|
|
|
_ = int.TryParse(App.FastFlags.GetPreset("Rendering.Framerate"), out int x);
|
|
if (x == 0)
|
|
App.FastFlags.SetPreset("Rendering.Framerate", null);
|
|
}
|
|
|
|
if (Utilities.CompareVersions(existingVer, "2.8.0") == VersionComparison.LessThan)
|
|
{
|
|
string oldDesktopPath = Path.Combine(Paths.Desktop, "Play Roblox.lnk");
|
|
string oldStartPath = Path.Combine(Paths.WindowsStartMenu, "Bloxstrap");
|
|
|
|
if (File.Exists(oldDesktopPath))
|
|
File.Move(oldDesktopPath, DesktopShortcut, true);
|
|
|
|
if (Directory.Exists(oldStartPath))
|
|
{
|
|
try
|
|
{
|
|
Directory.Delete(oldStartPath, true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
App.Logger.WriteException(LOG_IDENT, ex);
|
|
}
|
|
|
|
Shortcut.Create(Paths.Application, "", StartMenuShortcut);
|
|
}
|
|
|
|
Registry.CurrentUser.DeleteSubKeyTree("Software\\Bloxstrap", false);
|
|
|
|
WindowsRegistry.RegisterPlayer();
|
|
|
|
string? oldV2Val = App.FastFlags.GetValue("FFlagDisableNewIGMinDUA");
|
|
|
|
if (oldV2Val is not null)
|
|
{
|
|
if (oldV2Val == "True")
|
|
App.FastFlags.SetPreset("UI.Menu.Style.V2Rollout", "0");
|
|
else
|
|
App.FastFlags.SetPreset("UI.Menu.Style.V2Rollout", "100");
|
|
|
|
App.FastFlags.SetValue("FFlagDisableNewIGMinDUA", null);
|
|
}
|
|
|
|
App.FastFlags.SetValue("FFlagFixGraphicsQuality", null);
|
|
|
|
Directory.Delete(Path.Combine(Paths.Base, "Versions"));
|
|
}
|
|
|
|
App.Settings.Save();
|
|
App.FastFlags.Save();
|
|
}
|
|
|
|
if (currentVer is null)
|
|
return;
|
|
|
|
if (isAutoUpgrade)
|
|
{
|
|
Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/wiki/Release-notes-for-Bloxstrap-v{currentVer}");
|
|
}
|
|
else
|
|
{
|
|
Frontend.ShowMessageBox(
|
|
string.Format(Strings.InstallChecker_Updated, currentVer),
|
|
MessageBoxImage.Information,
|
|
MessageBoxButton.OK
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|