diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index d760390..12b62fa 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -277,7 +277,7 @@ namespace Bloxstrap } } - Task bootstrapperTask = Task.Run(() => bootstrapper.Run()).ContinueWith(t => + Task bootstrapperTask = Task.Run(async () => await bootstrapper.Run()).ContinueWith(t => { Logger.WriteLine(LOG_IDENT, "Bootstrapper task has finished"); diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index 48fc5dc..0b7d2f3 100644 --- a/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap.csproj @@ -7,8 +7,8 @@ true True Bloxstrap.ico - 2.5.1 - 2.5.1.0 + 2.5.2 + 2.5.2.0 app.manifest diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 0302340..d68c50c 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -52,7 +52,6 @@ namespace Bloxstrap private readonly CancellationTokenSource _cancelTokenSource = new(); private static bool FreshInstall => String.IsNullOrEmpty(App.State.Prop.VersionGuid); - private static string DesktopShortcutLocation => Path.Combine(Paths.Desktop, "Play Roblox.lnk"); private string _playerLocation => Path.Combine(_versionFolder, "RobloxPlayerBeta.exe"); @@ -407,6 +406,9 @@ namespace Bloxstrap return; } + if (_cancelFired) + return; + App.Logger.WriteLine(LOG_IDENT, "Cancelling install..."); _cancelTokenSource.Cancel(); @@ -508,45 +510,32 @@ namespace Bloxstrap if (!Directory.Exists(Paths.StartMenu)) { Directory.CreateDirectory(Paths.StartMenu); - - ShellLink.Shortcut.CreateShortcut(Paths.Application, "", Paths.Application, 0) - .WriteToFile(Path.Combine(Paths.StartMenu, "Play Roblox.lnk")); - - ShellLink.Shortcut.CreateShortcut(Paths.Application, "-menu", Paths.Application, 0) - .WriteToFile(Path.Combine(Paths.StartMenu, $"{App.ProjectName} Menu.lnk")); } else { // v2.0.0 - rebadge configuration menu as just "Bloxstrap Menu" string oldMenuShortcut = Path.Combine(Paths.StartMenu, $"Configure {App.ProjectName}.lnk"); - string newMenuShortcut = Path.Combine(Paths.StartMenu, $"{App.ProjectName} Menu.lnk"); if (File.Exists(oldMenuShortcut)) File.Delete(oldMenuShortcut); - - if (!File.Exists(newMenuShortcut)) - ShellLink.Shortcut.CreateShortcut(Paths.Application, "-menu", Paths.Application, 0) - .WriteToFile(newMenuShortcut); } + Utility.Shortcut.Create(Paths.Application, "", Path.Combine(Paths.StartMenu, "Play Roblox.lnk")); + Utility.Shortcut.Create(Paths.Application, "-menu", Path.Combine(Paths.StartMenu, $"{App.ProjectName} Menu.lnk")); + if (App.Settings.Prop.CreateDesktopIcon) { - if (!File.Exists(DesktopShortcutLocation)) + try { - try - { - ShellLink.Shortcut.CreateShortcut(Paths.Application, "", Paths.Application, 0) - .WriteToFile(DesktopShortcutLocation); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, "Could not create desktop shortcut, aborting"); - App.Logger.WriteException(LOG_IDENT, ex); - } - } + Utility.Shortcut.Create(Paths.Application, "", Path.Combine(Paths.Desktop, "Play Roblox.lnk")); - // one-time toggle, set it back to false - App.Settings.Prop.CreateDesktopIcon = false; + // one-time toggle, set it back to false + App.Settings.Prop.CreateDesktopIcon = false; + } + catch (Exception) + { + // suppress, we likely just don't have write perms for the desktop folder + } } } @@ -589,39 +578,51 @@ namespace Bloxstrap return; } - SetStatus($"Getting the latest {App.ProjectName}..."); - - // 64-bit is always the first option - GithubReleaseAsset asset = releaseInfo.Assets[0]; - string downloadLocation = Path.Combine(Paths.LocalAppData, "Temp", asset.Name); - - App.Logger.WriteLine(LOG_IDENT, $"Downloading {releaseInfo.TagName}..."); - - if (!File.Exists(downloadLocation)) + + try { - var response = await App.HttpClient.GetAsync(asset.BrowserDownloadUrl); + // 64-bit is always the first option + GithubReleaseAsset asset = releaseInfo.Assets[0]; + string downloadLocation = Path.Combine(Paths.LocalAppData, "Temp", asset.Name); - await using var fileStream = new FileStream(downloadLocation, FileMode.CreateNew); - await response.Content.CopyToAsync(fileStream); + App.Logger.WriteLine(LOG_IDENT, $"Downloading {releaseInfo.TagName}..."); + + if (!File.Exists(downloadLocation)) + { + var response = await App.HttpClient.GetAsync(asset.BrowserDownloadUrl); + + await using var fileStream = new FileStream(downloadLocation, FileMode.CreateNew); + await response.Content.CopyToAsync(fileStream); + } + + App.Logger.WriteLine(LOG_IDENT, $"Starting {releaseInfo.TagName}..."); + + ProcessStartInfo startInfo = new() + { + FileName = downloadLocation, + }; + + foreach (string arg in App.LaunchArgs) + startInfo.ArgumentList.Add(arg); + + App.Settings.Save(); + App.ShouldSaveConfigs = false; + + Process.Start(startInfo); + + App.Terminate(); } - - App.Logger.WriteLine(LOG_IDENT, $"Starting {releaseInfo.TagName}..."); - - ProcessStartInfo startInfo = new() + catch (Exception ex) { - FileName = downloadLocation, - }; + App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the auto-updater"); + App.Logger.WriteException(LOG_IDENT, ex); - foreach (string arg in App.LaunchArgs) - startInfo.ArgumentList.Add(arg); - - App.Settings.Save(); - App.ShouldSaveConfigs = false; - - Process.Start(startInfo); - - App.Terminate(); + Controls.ShowMessageBox( + $"Bloxstrap was unable to auto-update to {releaseInfo.TagName}. Please update it manually by downloading and running the latest release from the GitHub page.", + MessageBoxImage.Information + ); + } } private void Uninstall() @@ -847,7 +848,16 @@ namespace Bloxstrap if (!_versionPackageManifest.Exists(package => filename.Contains(package.Signature))) { App.Logger.WriteLine(LOG_IDENT, $"Deleting unused package {filename}"); - File.Delete(filename); + + try + { + File.Delete(filename); + } + catch (Exception ex) + { + App.Logger.WriteLine(LOG_IDENT, $"Failed to delete {filename}!"); + App.Logger.WriteException(LOG_IDENT, ex); + } } } @@ -980,7 +990,7 @@ namespace Bloxstrap { const string LOG_IDENT = "Bootstrapper::ApplyModifications"; - if (Process.GetProcessesByName("RobloxPlayerBeta").Where(x => x.MainModule!.FileName == _playerLocation).Any()) + if (Process.GetProcessesByName("RobloxPlayerBeta").Any()) { App.Logger.WriteLine(LOG_IDENT, "Roblox is running, aborting mod check"); return; @@ -1328,7 +1338,7 @@ namespace Bloxstrap { var response = await App.HttpClient.GetAsync(packageUrl, HttpCompletionOption.ResponseHeadersRead, _cancelTokenSource.Token); await using var stream = await response.Content.ReadAsStreamAsync(_cancelTokenSource.Token); - await using var fileStream = new FileStream(packageLocation, FileMode.CreateNew, FileAccess.Write, FileShare.Delete); + await using var fileStream = new FileStream(packageLocation, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Delete); while (true) { @@ -1351,6 +1361,9 @@ namespace Bloxstrap _totalDownloadedBytes += bytesRead; UpdateProgressBar(); } + + if (MD5Hash.FromStream(fileStream) != package.Signature) + throw new Exception("Signature does not match!"); App.Logger.WriteLine(LOG_IDENT, $"Finished downloading! ({totalBytesRead} bytes total)"); break; @@ -1368,6 +1381,15 @@ namespace Bloxstrap _totalDownloadedBytes -= totalBytesRead; UpdateProgressBar(); + + // attempt download over HTTP + // this isn't actually that unsafe - signatures were fetched earlier over HTTPS + // so we've already established that our signatures are legit, and that there's very likely no MITM anyway + if (ex.GetType() == typeof(IOException) && !packageUrl.StartsWith("http://")) + { + App.Logger.WriteLine(LOG_IDENT, "Retrying download over HTTP..."); + packageUrl = packageUrl.Replace("https://", "http://"); + } } } } @@ -1406,17 +1428,36 @@ namespace Bloxstrap if (directory is not null) Directory.CreateDirectory(directory); + var fileManifest = _versionFileManifest.FirstOrDefault(x => x.Name == Path.Combine(PackageDirectories[package.Name], entry.FullName)); + string? signature = fileManifest?.Signature; + if (File.Exists(extractPath)) { - var fileManifest = _versionFileManifest.FirstOrDefault(x => x.Name == Path.Combine(PackageDirectories[package.Name], entry.FullName)); - - if (fileManifest is not null && MD5Hash.FromFile(extractPath) == fileManifest.Signature) + if (signature is not null && MD5Hash.FromFile(extractPath) == signature) continue; File.Delete(extractPath); } - entry.ExtractToFile(extractPath, true); + bool retry = false; + + do + { + using var entryStream = entry.Open(); + using var fileStream = new FileStream(extractPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, bufferSize: 0x1000); + await entryStream.CopyToAsync(fileStream); + + if (signature is not null && MD5Hash.FromStream(fileStream) != signature) + { + if (retry) + throw new AssertionException($"Checksum of {entry.FullName} post-extraction did not match manifest"); + + retry = true; + } + } + while (retry); + + File.SetLastWriteTime(extractPath, entry.LastWriteTime.DateTime); } App.Logger.WriteLine(LOG_IDENT, $"Finished extracting {package.Name}"); diff --git a/Bloxstrap/Enums/AssemblyLoadStatus.cs b/Bloxstrap/Enums/AssemblyLoadStatus.cs new file mode 100644 index 0000000..08ef104 --- /dev/null +++ b/Bloxstrap/Enums/AssemblyLoadStatus.cs @@ -0,0 +1,9 @@ +namespace Bloxstrap.Enums +{ + enum AssemblyLoadStatus + { + NotAttempted, + Failed, + Successful + } +} diff --git a/Bloxstrap/Enums/ErrorCode.cs b/Bloxstrap/Enums/ErrorCode.cs index d9fbd42..ba6b65e 100644 --- a/Bloxstrap/Enums/ErrorCode.cs +++ b/Bloxstrap/Enums/ErrorCode.cs @@ -2,6 +2,7 @@ { // https://learn.microsoft.com/en-us/windows/win32/msi/error-codes // https://i-logic.com/serial/errorcodes.htm + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/705fb797-2175-4a90-b5a3-3918024b10b8 // just the ones that we're interested in public enum ErrorCode @@ -9,6 +10,8 @@ ERROR_SUCCESS = 0, ERROR_INSTALL_USEREXIT = 1602, ERROR_INSTALL_FAILURE = 1603, - ERROR_CANCELLED = 1223 + ERROR_CANCELLED = 1223, + + CO_E_APPNOTFOUND = -2147221003 } } diff --git a/Bloxstrap/Exceptions/AssertionException.cs b/Bloxstrap/Exceptions/AssertionException.cs new file mode 100644 index 0000000..5555baa --- /dev/null +++ b/Bloxstrap/Exceptions/AssertionException.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bloxstrap.Exceptions +{ + internal class AssertionException : Exception + { + public AssertionException(string message) + : base($"{message}\n\nThis is very likely just an off-chance error. Please report this first, and then start {App.ProjectName} again.") + { + } + } +} diff --git a/Bloxstrap/FastFlagManager.cs b/Bloxstrap/FastFlagManager.cs index 6f435b7..b3b94ea 100644 --- a/Bloxstrap/FastFlagManager.cs +++ b/Bloxstrap/FastFlagManager.cs @@ -140,9 +140,16 @@ namespace Bloxstrap else { if (Prop.ContainsKey(key)) + { + if (key == Prop[key].ToString()) + return; + App.Logger.WriteLine(LOG_IDENT, $"Changing of '{key}' from '{Prop[key]}' to '{value}' is pending"); + } else + { App.Logger.WriteLine(LOG_IDENT, $"Setting of '{key}' to '{value}' is pending"); + } Prop[key] = value.ToString()!; } diff --git a/Bloxstrap/UI/Elements/Base/WpfUiWindow.cs b/Bloxstrap/UI/Elements/Base/WpfUiWindow.cs new file mode 100644 index 0000000..d941ea8 --- /dev/null +++ b/Bloxstrap/UI/Elements/Base/WpfUiWindow.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Wpf.Ui.Appearance; +using Wpf.Ui.Controls; +using Wpf.Ui.Mvvm.Contracts; +using Wpf.Ui.Mvvm.Services; + +namespace Bloxstrap.UI.Elements.Base +{ + public class WpfUiWindow : UiWindow + { + private readonly IThemeService _themeService = new ThemeService(); + + public void ApplyTheme() + { + _themeService.SetTheme(App.Settings.Prop.Theme.GetFinal() == Enums.Theme.Dark ? ThemeType.Dark : ThemeType.Light); + _themeService.SetSystemAccent(); + } + } +} diff --git a/Bloxstrap/UI/Elements/Bootstrapper/Base/WinFormsDialogBase.cs b/Bloxstrap/UI/Elements/Bootstrapper/Base/WinFormsDialogBase.cs index ba14b07..69d4b36 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/Base/WinFormsDialogBase.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/Base/WinFormsDialogBase.cs @@ -8,6 +8,8 @@ namespace Bloxstrap.UI.Elements.Bootstrapper.Base { public Bloxstrap.Bootstrapper? Bootstrapper { get; set; } + private bool _isClosing; + #region UI Elements protected virtual string _message { get; set; } = "Please wait..."; protected virtual ProgressBarStyle _progressStyle { get; set; } @@ -81,11 +83,15 @@ namespace Bloxstrap.UI.Elements.Bootstrapper.Base Icon = App.Settings.Prop.BootstrapperIcon.GetIcon(); } - public void ButtonCancel_Click(object? sender, EventArgs e) + #region WinForms event handlers + public void ButtonCancel_Click(object? sender, EventArgs e) => Close(); + + public void Dialog_FormClosing(object sender, FormClosingEventArgs e) { - Bootstrapper?.CancelInstall(); - Close(); + if (!_isClosing) + Bootstrapper?.CancelInstall(); } + #endregion #region IBootstrapperDialog Methods public void ShowBootstrapper() => ShowDialog(); @@ -93,9 +99,14 @@ namespace Bloxstrap.UI.Elements.Bootstrapper.Base public virtual void CloseBootstrapper() { if (InvokeRequired) + { Invoke(CloseBootstrapper); + } else + { + _isClosing = true; Close(); + } } public virtual void ShowSuccess(string message, Action? callback) => BaseFunctions.ShowSuccess(message, callback); diff --git a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml index b2b9387..183be50 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml +++ b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml @@ -10,7 +10,8 @@ WindowStyle="None" WindowStartupLocation="CenterScreen" AllowsTransparency="True" - Background="Transparent"> + Background="Transparent" + Closing="Window_Closing"> diff --git a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs index 88020ad..b01d083 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs @@ -1,4 +1,5 @@ using System.Windows; +using System.ComponentModel; using System.Windows.Forms; using System.Windows.Media; using System.Windows.Media.Imaging; @@ -17,6 +18,8 @@ namespace Bloxstrap.UI.Elements.Bootstrapper public Bloxstrap.Bootstrapper? Bootstrapper { get; set; } + private bool _isClosing; + #region UI Elements public string Message { @@ -84,12 +87,21 @@ namespace Bloxstrap.UI.Elements.Bootstrapper InitializeComponent(); } + private void Window_Closing(object sender, CancelEventArgs e) + { + if (!_isClosing) + Bootstrapper?.CancelInstall(); + } #region IBootstrapperDialog Methods // Referencing FluentDialog public void ShowBootstrapper() => this.ShowDialog(); - public void CloseBootstrapper() => Dispatcher.BeginInvoke(this.Close); + public void CloseBootstrapper() + { + _isClosing = true; + Dispatcher.BeginInvoke(this.Close); + } public void ShowSuccess(string message, Action? callback) => BaseFunctions.ShowSuccess(message, callback); #endregion diff --git a/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml b/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml index 374a823..40cc7f9 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml +++ b/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml @@ -1,9 +1,10 @@ - + WindowStartupLocation="CenterScreen" + Closing="UiWindow_Closing"> @@ -45,4 +47,4 @@