mirror of
https://github.com/bloxstraplabs/bloxstrap.git
synced 2025-04-19 00:51:30 -07:00
Background updates (#4861)
* add background updating * add RobloxState * fix potential race condition with RobloxState * update ForceRobloxReinstallation in menu * disable AssertReadOnlyDirectory * add storage space check * add logging to IsEligibleForBackgroundUpdate * add a setting to toggle background updates * fix mutex names being mixed up * update string * update strings * update strings
This commit is contained in:
parent
893aecbdd1
commit
ca36306254
@ -42,7 +42,7 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
public static bool IsProductionBuild => IsActionBuild && BuildMetadata.CommitRef.StartsWith("tag", StringComparison.Ordinal);
|
public static bool IsProductionBuild => IsActionBuild && BuildMetadata.CommitRef.StartsWith("tag", StringComparison.Ordinal);
|
||||||
|
|
||||||
public static bool IsStudioVisible => !String.IsNullOrEmpty(App.State.Prop.Studio.VersionGuid);
|
public static bool IsStudioVisible => !String.IsNullOrEmpty(App.RobloxState.Prop.Studio.VersionGuid);
|
||||||
|
|
||||||
public static readonly MD5 MD5Provider = MD5.Create();
|
public static readonly MD5 MD5Provider = MD5.Create();
|
||||||
|
|
||||||
@ -54,6 +54,8 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
public static readonly JsonManager<State> State = new();
|
public static readonly JsonManager<State> State = new();
|
||||||
|
|
||||||
|
public static readonly JsonManager<RobloxState> RobloxState = new();
|
||||||
|
|
||||||
public static readonly FastFlagManager FastFlags = new();
|
public static readonly FastFlagManager FastFlags = new();
|
||||||
|
|
||||||
public static readonly HttpClient HttpClient = new(
|
public static readonly HttpClient HttpClient = new(
|
||||||
@ -336,6 +338,7 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
Settings.Load();
|
Settings.Load();
|
||||||
State.Load();
|
State.Load();
|
||||||
|
RobloxState.Load();
|
||||||
FastFlags.Load();
|
FastFlags.Load();
|
||||||
|
|
||||||
if (!Locale.SupportedLocales.ContainsKey(Settings.Prop.Locale))
|
if (!Locale.SupportedLocales.ContainsKey(Settings.Prop.Locale))
|
||||||
|
@ -16,7 +16,7 @@ namespace Bloxstrap.AppData
|
|||||||
|
|
||||||
public override string ExecutableName => "RobloxPlayerBeta.exe";
|
public override string ExecutableName => "RobloxPlayerBeta.exe";
|
||||||
|
|
||||||
public override AppState State => App.State.Prop.Player;
|
public override AppState State => App.RobloxState.Prop.Player;
|
||||||
|
|
||||||
public override IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } = new Dictionary<string, string>()
|
public override IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } = new Dictionary<string, string>()
|
||||||
{
|
{
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
public override string ExecutableName => "RobloxStudioBeta.exe";
|
public override string ExecutableName => "RobloxStudioBeta.exe";
|
||||||
|
|
||||||
public override AppState State => App.State.Prop.Studio;
|
public override AppState State => App.RobloxState.Prop.Studio;
|
||||||
|
|
||||||
public override IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } = new Dictionary<string, string>()
|
public override IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } = new Dictionary<string, string>()
|
||||||
{
|
{
|
||||||
|
@ -49,6 +49,7 @@ namespace Bloxstrap
|
|||||||
private LaunchMode _launchMode;
|
private LaunchMode _launchMode;
|
||||||
|
|
||||||
private string _launchCommandLine = App.LaunchSettings.RobloxLaunchArgs;
|
private string _launchCommandLine = App.LaunchSettings.RobloxLaunchArgs;
|
||||||
|
private Version? _latestVersion = null;
|
||||||
private string _latestVersionGuid = null!;
|
private string _latestVersionGuid = null!;
|
||||||
private string _latestVersionDirectory = null!;
|
private string _latestVersionDirectory = null!;
|
||||||
private PackageManifest _versionPackageManifest = null!;
|
private PackageManifest _versionPackageManifest = null!;
|
||||||
@ -60,7 +61,7 @@ namespace Bloxstrap
|
|||||||
private long _totalDownloadedBytes = 0;
|
private long _totalDownloadedBytes = 0;
|
||||||
private bool _packageExtractionSuccess = true;
|
private bool _packageExtractionSuccess = true;
|
||||||
|
|
||||||
private bool _mustUpgrade => App.LaunchSettings.ForceFlag.Active || String.IsNullOrEmpty(AppData.State.VersionGuid) || !File.Exists(AppData.ExecutablePath);
|
private bool _mustUpgrade => App.LaunchSettings.ForceFlag.Active || App.State.Prop.ForceReinstall || String.IsNullOrEmpty(AppData.State.VersionGuid) || !File.Exists(AppData.ExecutablePath);
|
||||||
private bool _noConnection = false;
|
private bool _noConnection = false;
|
||||||
|
|
||||||
private AsyncMutex? _mutex;
|
private AsyncMutex? _mutex;
|
||||||
@ -70,6 +71,9 @@ namespace Bloxstrap
|
|||||||
public IBootstrapperDialog? Dialog = null;
|
public IBootstrapperDialog? Dialog = null;
|
||||||
|
|
||||||
public bool IsStudioLaunch => _launchMode != LaunchMode.Player;
|
public bool IsStudioLaunch => _launchMode != LaunchMode.Player;
|
||||||
|
|
||||||
|
public string MutexName { get; set; } = "Bloxstrap-Bootstrapper";
|
||||||
|
public bool QuitIfMutexExists { get; set; } = false;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Core
|
#region Core
|
||||||
@ -198,22 +202,24 @@ namespace Bloxstrap
|
|||||||
// ensure only one instance of the bootstrapper is running at the time
|
// ensure only one instance of the bootstrapper is running at the time
|
||||||
// so that we don't have stuff like two updates happening simultaneously
|
// so that we don't have stuff like two updates happening simultaneously
|
||||||
|
|
||||||
bool mutexExists = false;
|
bool mutexExists = Utilities.DoesMutexExist(MutexName);
|
||||||
|
|
||||||
try
|
if (mutexExists)
|
||||||
{
|
{
|
||||||
Mutex.OpenExisting("Bloxstrap-Bootstrapper").Close();
|
if (!QuitIfMutexExists)
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Bloxstrap-Bootstrapper mutex exists, waiting...");
|
{
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, $"{MutexName} mutex exists, waiting...");
|
||||||
SetStatus(Strings.Bootstrapper_Status_WaitingOtherInstances);
|
SetStatus(Strings.Bootstrapper_Status_WaitingOtherInstances);
|
||||||
mutexExists = true;
|
|
||||||
}
|
}
|
||||||
catch (Exception)
|
else
|
||||||
{
|
{
|
||||||
// no mutex exists
|
App.Logger.WriteLine(LOG_IDENT, $"{MutexName} mutex exists, exiting!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait for mutex to be released if it's not yet
|
// wait for mutex to be released if it's not yet
|
||||||
await using var mutex = new AsyncMutex(false, "Bloxstrap-Bootstrapper");
|
await using var mutex = new AsyncMutex(false, MutexName);
|
||||||
await mutex.AcquireAsync(_cancelTokenSource.Token);
|
await mutex.AcquireAsync(_cancelTokenSource.Token);
|
||||||
|
|
||||||
_mutex = mutex;
|
_mutex = mutex;
|
||||||
@ -223,6 +229,7 @@ namespace Bloxstrap
|
|||||||
{
|
{
|
||||||
App.Settings.Load();
|
App.Settings.Load();
|
||||||
App.State.Load();
|
App.State.Load();
|
||||||
|
App.RobloxState.Load();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_noConnection)
|
if (!_noConnection)
|
||||||
@ -237,12 +244,35 @@ namespace Bloxstrap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CleanupVersionsFolder(); // cleanup after background updater
|
||||||
|
|
||||||
bool allModificationsApplied = true;
|
bool allModificationsApplied = true;
|
||||||
|
|
||||||
if (!_noConnection)
|
if (!_noConnection)
|
||||||
{
|
{
|
||||||
if (AppData.State.VersionGuid != _latestVersionGuid || _mustUpgrade)
|
if (AppData.State.VersionGuid != _latestVersionGuid || _mustUpgrade)
|
||||||
|
{
|
||||||
|
bool backgroundUpdaterMutexOpen = Utilities.DoesMutexExist("Bloxstrap-BackgroundUpdater");
|
||||||
|
if (App.LaunchSettings.BackgroundUpdaterFlag.Active)
|
||||||
|
backgroundUpdaterMutexOpen = false; // we want to actually update lol
|
||||||
|
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, $"Background updater running: {backgroundUpdaterMutexOpen}");
|
||||||
|
|
||||||
|
if (backgroundUpdaterMutexOpen && _mustUpgrade)
|
||||||
|
{
|
||||||
|
// I am Forced Upgrade, killer of Background Updates
|
||||||
|
Utilities.KillBackgroundUpdater();
|
||||||
|
backgroundUpdaterMutexOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!backgroundUpdaterMutexOpen)
|
||||||
|
{
|
||||||
|
if (IsEligibleForBackgroundUpdate())
|
||||||
|
StartBackgroundUpdater();
|
||||||
|
else
|
||||||
await UpgradeRoblox();
|
await UpgradeRoblox();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_cancelTokenSource.IsCancellationRequested)
|
if (_cancelTokenSource.IsCancellationRequested)
|
||||||
return;
|
return;
|
||||||
@ -339,11 +369,13 @@ namespace Bloxstrap
|
|||||||
key.SetValueSafe("www.roblox.com", Deployment.IsDefaultChannel ? "" : Deployment.Channel);
|
key.SetValueSafe("www.roblox.com", Deployment.IsDefaultChannel ? "" : Deployment.Channel);
|
||||||
|
|
||||||
_latestVersionGuid = clientVersion.VersionGuid;
|
_latestVersionGuid = clientVersion.VersionGuid;
|
||||||
|
_latestVersion = Utilities.ParseVersionSafe(clientVersion.Version);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Version set to {App.LaunchSettings.VersionFlag.Data} from arguments");
|
App.Logger.WriteLine(LOG_IDENT, $"Version set to {App.LaunchSettings.VersionFlag.Data} from arguments");
|
||||||
_latestVersionGuid = App.LaunchSettings.VersionFlag.Data;
|
_latestVersionGuid = App.LaunchSettings.VersionFlag.Data;
|
||||||
|
// we can't determine the version
|
||||||
}
|
}
|
||||||
|
|
||||||
_latestVersionDirectory = Path.Combine(Paths.Versions, _latestVersionGuid);
|
_latestVersionDirectory = Path.Combine(Paths.Versions, _latestVersionGuid);
|
||||||
@ -366,6 +398,79 @@ namespace Bloxstrap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsEligibleForBackgroundUpdate()
|
||||||
|
{
|
||||||
|
const string LOG_IDENT = "Bootstrapper::IsEligibleForBackgroundUpdate";
|
||||||
|
|
||||||
|
if (App.LaunchSettings.BackgroundUpdaterFlag.Active)
|
||||||
|
{
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, "Not eligible: Is the background updater process");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!App.Settings.Prop.BackgroundUpdatesEnabled)
|
||||||
|
{
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, "Not eligible: Background updates disabled");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsStudioLaunch)
|
||||||
|
{
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, "Not eligible: Studio launch");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_mustUpgrade)
|
||||||
|
{
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, "Not eligible: Must upgrade is true");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// at least 3GB of free space
|
||||||
|
const long minimumFreeSpace = 3_000_000_000;
|
||||||
|
long space = Filesystem.GetFreeDiskSpace(Paths.Base);
|
||||||
|
if (space < minimumFreeSpace)
|
||||||
|
{
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, $"Not eligible: User has {space} free space, at least {minimumFreeSpace} is required");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_latestVersion == default)
|
||||||
|
{
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, "Not eligible: Latest version is undefined");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Version? currentVersion = Utilities.GetRobloxVersion(AppData);
|
||||||
|
if (currentVersion == default)
|
||||||
|
{
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, "Not eligible: Current version is undefined");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// always normally upgrade for downgrades
|
||||||
|
if (currentVersion.Minor > _latestVersion.Minor)
|
||||||
|
{
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, "Not eligible: Downgrade");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// only background update if we're:
|
||||||
|
// - one major update behind
|
||||||
|
// - the same major update
|
||||||
|
int diff = _latestVersion.Minor - currentVersion.Minor;
|
||||||
|
if (diff == 0 || diff == 1)
|
||||||
|
{
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, "Eligible");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, $"Not eligible: Major version diff is {diff}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void StartRoblox()
|
private void StartRoblox()
|
||||||
{
|
{
|
||||||
const string LOG_IDENT = "Bootstrapper::StartRoblox";
|
const string LOG_IDENT = "Bootstrapper::StartRoblox";
|
||||||
@ -718,13 +823,20 @@ namespace Bloxstrap
|
|||||||
{
|
{
|
||||||
const string LOG_IDENT = "Bootstrapper::CleanupVersionsFolder";
|
const string LOG_IDENT = "Bootstrapper::CleanupVersionsFolder";
|
||||||
|
|
||||||
|
if (App.LaunchSettings.BackgroundUpdaterFlag.Active)
|
||||||
|
{
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, "Background updater tried to cleanup, stopping!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (string dir in Directory.GetDirectories(Paths.Versions))
|
foreach (string dir in Directory.GetDirectories(Paths.Versions))
|
||||||
{
|
{
|
||||||
string dirName = Path.GetFileName(dir);
|
string dirName = Path.GetFileName(dir);
|
||||||
|
|
||||||
if (dirName != App.State.Prop.Player.VersionGuid && dirName != App.State.Prop.Studio.VersionGuid)
|
if (dirName != App.RobloxState.Prop.Player.VersionGuid && dirName != App.RobloxState.Prop.Studio.VersionGuid)
|
||||||
{
|
{
|
||||||
Filesystem.AssertReadOnlyDirectory(dir);
|
// TODO: this is too expensive
|
||||||
|
//Filesystem.AssertReadOnlyDirectory(dir);
|
||||||
|
|
||||||
// check if it's still being used first
|
// check if it's still being used first
|
||||||
// we dont want to accidentally delete the files of a running roblox instance
|
// we dont want to accidentally delete the files of a running roblox instance
|
||||||
@ -801,11 +913,11 @@ namespace Bloxstrap
|
|||||||
_isInstalling = true;
|
_isInstalling = true;
|
||||||
|
|
||||||
// make sure nothing is running before continuing upgrade
|
// make sure nothing is running before continuing upgrade
|
||||||
if (!IsStudioLaunch) // TODO: wait for studio processes to close before updating to prevent data loss
|
if (!App.LaunchSettings.BackgroundUpdaterFlag.Active && !IsStudioLaunch) // TODO: wait for studio processes to close before updating to prevent data loss
|
||||||
KillRobloxPlayers();
|
KillRobloxPlayers();
|
||||||
|
|
||||||
// get a fully clean install
|
// get a fully clean install
|
||||||
if (Directory.Exists(_latestVersionDirectory))
|
if (!App.LaunchSettings.BackgroundUpdaterFlag.Active && Directory.Exists(_latestVersionDirectory))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -958,8 +1070,8 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
var allPackageHashes = new List<string>();
|
var allPackageHashes = new List<string>();
|
||||||
|
|
||||||
allPackageHashes.AddRange(App.State.Prop.Player.PackageHashes.Values);
|
allPackageHashes.AddRange(App.RobloxState.Prop.Player.PackageHashes.Values);
|
||||||
allPackageHashes.AddRange(App.State.Prop.Studio.PackageHashes.Values);
|
allPackageHashes.AddRange(App.RobloxState.Prop.Studio.PackageHashes.Values);
|
||||||
|
|
||||||
if (!App.Settings.Prop.DebugDisableVersionPackageCleanup)
|
if (!App.Settings.Prop.DebugDisableVersionPackageCleanup)
|
||||||
{
|
{
|
||||||
@ -988,7 +1100,7 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
AppData.State.Size = distributionSize;
|
AppData.State.Size = distributionSize;
|
||||||
|
|
||||||
int totalSize = App.State.Prop.Player.Size + App.State.Prop.Studio.Size;
|
int totalSize = App.RobloxState.Prop.Player.Size + App.RobloxState.Prop.Studio.Size;
|
||||||
|
|
||||||
using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey))
|
using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey))
|
||||||
{
|
{
|
||||||
@ -998,10 +1110,26 @@ namespace Bloxstrap
|
|||||||
App.Logger.WriteLine(LOG_IDENT, $"Registered as {totalSize} KB");
|
App.Logger.WriteLine(LOG_IDENT, $"Registered as {totalSize} KB");
|
||||||
|
|
||||||
App.State.Save();
|
App.State.Save();
|
||||||
|
App.RobloxState.Save();
|
||||||
|
|
||||||
_isInstalling = false;
|
_isInstalling = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void StartBackgroundUpdater()
|
||||||
|
{
|
||||||
|
const string LOG_IDENT = "Bootstrapper::StartBackgroundUpdater";
|
||||||
|
|
||||||
|
if (Utilities.DoesMutexExist("Bloxstrap-BackgroundUpdater"))
|
||||||
|
{
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, "Background updater already running");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, "Starting background updater");
|
||||||
|
|
||||||
|
Process.Start(Paths.Process, "-backgroundupdater");
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<bool> ApplyModifications()
|
private async Task<bool> ApplyModifications()
|
||||||
{
|
{
|
||||||
const string LOG_IDENT = "Bootstrapper::ApplyModifications";
|
const string LOG_IDENT = "Bootstrapper::ApplyModifications";
|
||||||
@ -1135,7 +1263,7 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
var fileRestoreMap = new Dictionary<string, List<string>>();
|
var fileRestoreMap = new Dictionary<string, List<string>>();
|
||||||
|
|
||||||
foreach (string fileLocation in App.State.Prop.ModManifest)
|
foreach (string fileLocation in App.RobloxState.Prop.ModManifest)
|
||||||
{
|
{
|
||||||
if (modFolderFiles.Contains(fileLocation))
|
if (modFolderFiles.Contains(fileLocation))
|
||||||
continue;
|
continue;
|
||||||
@ -1180,8 +1308,17 @@ namespace Bloxstrap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
App.State.Prop.ModManifest = modFolderFiles;
|
// make sure we're not overwriting a new update
|
||||||
App.State.Save();
|
// if we're the background update process, always overwrite
|
||||||
|
if (App.LaunchSettings.BackgroundUpdaterFlag.Active || !App.RobloxState.HasFileOnDiskChanged())
|
||||||
|
{
|
||||||
|
App.RobloxState.Prop.ModManifest = modFolderFiles;
|
||||||
|
App.RobloxState.Save();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, "RobloxState disk mismatch, not saving ModManifest");
|
||||||
|
}
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Finished checking file mods");
|
App.Logger.WriteLine(LOG_IDENT, $"Finished checking file mods");
|
||||||
|
|
||||||
|
@ -197,7 +197,7 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
var processes = new List<Process>();
|
var processes = new List<Process>();
|
||||||
|
|
||||||
if (!String.IsNullOrEmpty(App.State.Prop.Player.VersionGuid))
|
if (!String.IsNullOrEmpty(App.RobloxState.Prop.Player.VersionGuid))
|
||||||
processes.AddRange(Process.GetProcessesByName(App.RobloxPlayerAppName));
|
processes.AddRange(Process.GetProcessesByName(App.RobloxPlayerAppName));
|
||||||
|
|
||||||
if (App.IsStudioVisible)
|
if (App.IsStudioVisible)
|
||||||
@ -587,16 +587,23 @@ namespace Bloxstrap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Utilities.CompareVersions(existingVer, "2.8.3") == VersionComparison.LessThan)
|
if (Utilities.CompareVersions(existingVer, "2.9.0") == VersionComparison.LessThan)
|
||||||
{
|
{
|
||||||
// force reinstallation
|
// move from App.State to App.RobloxState
|
||||||
App.State.Prop.Player.VersionGuid = "";
|
if (App.State.Prop.GetDeprecatedPlayer() != null)
|
||||||
App.State.Prop.Studio.VersionGuid = "";
|
App.RobloxState.Prop.Player = App.State.Prop.GetDeprecatedPlayer()!;
|
||||||
|
|
||||||
|
if (App.State.Prop.GetDeprecatedStudio() != null)
|
||||||
|
App.RobloxState.Prop.Studio = App.State.Prop.GetDeprecatedStudio()!;
|
||||||
|
|
||||||
|
if (App.State.Prop.GetDeprecatedModManifest() != null)
|
||||||
|
App.RobloxState.Prop.ModManifest = App.State.Prop.GetDeprecatedModManifest()!;
|
||||||
}
|
}
|
||||||
|
|
||||||
App.Settings.Save();
|
App.Settings.Save();
|
||||||
App.FastFlags.Save();
|
App.FastFlags.Save();
|
||||||
App.State.Save();
|
App.State.Save();
|
||||||
|
App.RobloxState.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentVer is null)
|
if (currentVer is null)
|
||||||
|
@ -8,6 +8,11 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
public T Prop { get; set; } = new();
|
public T Prop { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The file hash when last retrieved from disk
|
||||||
|
/// </summary>
|
||||||
|
public string? LastFileHash { get; private set; }
|
||||||
|
|
||||||
public virtual string ClassName => typeof(T).Name;
|
public virtual string ClassName => typeof(T).Name;
|
||||||
|
|
||||||
public virtual string FileLocation => Path.Combine(Paths.Base, $"{ClassName}.json");
|
public virtual string FileLocation => Path.Combine(Paths.Base, $"{ClassName}.json");
|
||||||
@ -22,12 +27,15 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
T? settings = JsonSerializer.Deserialize<T>(File.ReadAllText(FileLocation));
|
string contents = File.ReadAllText(FileLocation);
|
||||||
|
|
||||||
|
T? settings = JsonSerializer.Deserialize<T>(contents);
|
||||||
|
|
||||||
if (settings is null)
|
if (settings is null)
|
||||||
throw new ArgumentNullException("Deserialization returned null");
|
throw new ArgumentNullException("Deserialization returned null");
|
||||||
|
|
||||||
Prop = settings;
|
Prop = settings;
|
||||||
|
LastFileHash = MD5Hash.FromString(contents);
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Loaded successfully!");
|
App.Logger.WriteLine(LOG_IDENT, "Loaded successfully!");
|
||||||
}
|
}
|
||||||
@ -74,7 +82,11 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.WriteAllText(FileLocation, JsonSerializer.Serialize(Prop, new JsonSerializerOptions { WriteIndented = true }));
|
string contents = JsonSerializer.Serialize(Prop, new JsonSerializerOptions { WriteIndented = true });
|
||||||
|
|
||||||
|
File.WriteAllText(FileLocation, contents);
|
||||||
|
|
||||||
|
LastFileHash = MD5Hash.FromString(contents);
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
@ -89,5 +101,13 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Save complete!");
|
App.Logger.WriteLine(LOG_IDENT, "Save complete!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is the file on disk different to the one deserialised during this session?
|
||||||
|
/// </summary>
|
||||||
|
public bool HasFileOnDiskChanged()
|
||||||
|
{
|
||||||
|
return LastFileHash != MD5Hash.FromFile(FileLocation);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ using Windows.Win32;
|
|||||||
using Windows.Win32.Foundation;
|
using Windows.Win32.Foundation;
|
||||||
|
|
||||||
using Bloxstrap.UI.Elements.Dialogs;
|
using Bloxstrap.UI.Elements.Dialogs;
|
||||||
|
using Bloxstrap.Enums;
|
||||||
|
|
||||||
namespace Bloxstrap
|
namespace Bloxstrap
|
||||||
{
|
{
|
||||||
@ -58,6 +59,11 @@ namespace Bloxstrap
|
|||||||
App.Logger.WriteLine(LOG_IDENT, "Opening watcher");
|
App.Logger.WriteLine(LOG_IDENT, "Opening watcher");
|
||||||
LaunchWatcher();
|
LaunchWatcher();
|
||||||
}
|
}
|
||||||
|
else if (App.LaunchSettings.BackgroundUpdaterFlag.Active)
|
||||||
|
{
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, "Opening background updater");
|
||||||
|
LaunchBackgroundUpdater();
|
||||||
|
}
|
||||||
else if (App.LaunchSettings.RobloxLaunchMode != LaunchMode.None)
|
else if (App.LaunchSettings.RobloxLaunchMode != LaunchMode.None)
|
||||||
{
|
{
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Opening bootstrapper ({App.LaunchSettings.RobloxLaunchMode})");
|
App.Logger.WriteLine(LOG_IDENT, $"Opening bootstrapper ({App.LaunchSettings.RobloxLaunchMode})");
|
||||||
@ -295,5 +301,51 @@ namespace Bloxstrap
|
|||||||
App.Terminate();
|
App.Terminate();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void LaunchBackgroundUpdater()
|
||||||
|
{
|
||||||
|
const string LOG_IDENT = "LaunchHandler::LaunchBackgroundUpdater";
|
||||||
|
|
||||||
|
// Activate some LaunchFlags we need
|
||||||
|
App.LaunchSettings.QuietFlag.Active = true;
|
||||||
|
App.LaunchSettings.NoLaunchFlag.Active = true;
|
||||||
|
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper");
|
||||||
|
App.Bootstrapper = new Bootstrapper(LaunchMode.Player)
|
||||||
|
{
|
||||||
|
MutexName = "Bloxstrap-BackgroundUpdater",
|
||||||
|
QuitIfMutexExists = true
|
||||||
|
};
|
||||||
|
|
||||||
|
CancellationTokenSource cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, "Started event waiter");
|
||||||
|
using (EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.AutoReset, "Bloxstrap-BackgroundUpdaterKillEvent"))
|
||||||
|
handle.WaitOne();
|
||||||
|
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, "Received close event, killing it all!");
|
||||||
|
App.Bootstrapper.Cancel();
|
||||||
|
}, cts.Token);
|
||||||
|
|
||||||
|
Task.Run(App.Bootstrapper.Run).ContinueWith(t =>
|
||||||
|
{
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, "Bootstrapper task has finished");
|
||||||
|
cts.Cancel(); // stop event waiter
|
||||||
|
|
||||||
|
if (t.IsFaulted)
|
||||||
|
{
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the bootstrapper");
|
||||||
|
|
||||||
|
if (t.Exception is not null)
|
||||||
|
App.FinalizeExceptionHandling(t.Exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
App.Terminate();
|
||||||
|
});
|
||||||
|
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, "Exiting");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
public LaunchFlag WatcherFlag { get; } = new("watcher");
|
public LaunchFlag WatcherFlag { get; } = new("watcher");
|
||||||
|
|
||||||
|
public LaunchFlag BackgroundUpdaterFlag { get; } = new("backgroundupdater");
|
||||||
|
|
||||||
public LaunchFlag QuietFlag { get; } = new("quiet");
|
public LaunchFlag QuietFlag { get; } = new("quiet");
|
||||||
|
|
||||||
public LaunchFlag UninstallFlag { get; } = new("uninstall");
|
public LaunchFlag UninstallFlag { get; } = new("uninstall");
|
||||||
|
11
Bloxstrap/Models/Persistable/RobloxState.cs
Normal file
11
Bloxstrap/Models/Persistable/RobloxState.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace Bloxstrap.Models.Persistable
|
||||||
|
{
|
||||||
|
public class RobloxState
|
||||||
|
{
|
||||||
|
public AppState Player { get; set; } = new();
|
||||||
|
|
||||||
|
public AppState Studio { get; set; } = new();
|
||||||
|
|
||||||
|
public List<string> ModManifest { get; set; } = new();
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@ namespace Bloxstrap.Models.Persistable
|
|||||||
public bool UseFastFlagManager { get; set; } = true;
|
public bool UseFastFlagManager { get; set; } = true;
|
||||||
public bool WPFSoftwareRender { get; set; } = false;
|
public bool WPFSoftwareRender { get; set; } = false;
|
||||||
public bool EnableAnalytics { get; set; } = true;
|
public bool EnableAnalytics { get; set; } = true;
|
||||||
|
public bool BackgroundUpdatesEnabled { get; set; } = true;
|
||||||
public bool DebugDisableVersionPackageCleanup { get; set; } = false;
|
public bool DebugDisableVersionPackageCleanup { get; set; } = false;
|
||||||
public string? SelectedCustomTheme { get; set; } = null;
|
public string? SelectedCustomTheme { get; set; } = null;
|
||||||
|
|
||||||
|
@ -6,12 +6,28 @@
|
|||||||
|
|
||||||
public bool PromptWebView2Install { get; set; } = true;
|
public bool PromptWebView2Install { get; set; } = true;
|
||||||
|
|
||||||
public AppState Player { get; set; } = new();
|
public bool ForceReinstall { get; set; } = false;
|
||||||
|
|
||||||
public AppState Studio { get; set; } = new();
|
|
||||||
|
|
||||||
public WindowState SettingsWindow { get; set; } = new();
|
public WindowState SettingsWindow { get; set; } = new();
|
||||||
|
|
||||||
public List<string> ModManifest { get; set; } = new();
|
#region Deprecated properties
|
||||||
|
/// <summary>
|
||||||
|
/// Deprecated, use App.RobloxState.Player
|
||||||
|
/// </summary>
|
||||||
|
public AppState? Player { private get; set; }
|
||||||
|
public AppState? GetDeprecatedPlayer() => Player;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deprecated, use App.RobloxState.Studio
|
||||||
|
/// </summary>
|
||||||
|
public AppState? Studio { private get; set; }
|
||||||
|
public AppState? GetDeprecatedStudio() => Studio;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deprecated, use App.RobloxState.ModManifest
|
||||||
|
/// </summary>
|
||||||
|
public List<string>? ModManifest { private get; set; }
|
||||||
|
public List<string>? GetDeprecatedModManifest() => ModManifest;
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
18
Bloxstrap/Resources/Strings.Designer.cs
generated
18
Bloxstrap/Resources/Strings.Designer.cs
generated
@ -2545,6 +2545,24 @@ namespace Bloxstrap.Resources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Update Roblox in the background instead of waiting. Not recommended for slow networks. At least 3GB of free storage space is required for this feature to work..
|
||||||
|
/// </summary>
|
||||||
|
public static string Menu_Behaviour_BackgroundUpdates_Description {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Menu.Behaviour.BackgroundUpdates.Description", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Background updates.
|
||||||
|
/// </summary>
|
||||||
|
public static string Menu_Behaviour_BackgroundUpdates_Title {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Menu.Behaviour.BackgroundUpdates.Title", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Prevent against closures of your existing game from accidentally launching another one..
|
/// Looks up a localized string similar to Prevent against closures of your existing game from accidentally launching another one..
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1445,4 +1445,10 @@ Defaulting to Fluent.</value>
|
|||||||
<data name="Menu.Appearance.CustomThemes.RenameFailed" xml:space="preserve">
|
<data name="Menu.Appearance.CustomThemes.RenameFailed" xml:space="preserve">
|
||||||
<value>Failed to rename custom theme {0}: {1}</value>
|
<value>Failed to rename custom theme {0}: {1}</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Menu.Behaviour.BackgroundUpdates.Title" xml:space="preserve">
|
||||||
|
<value>Background updates</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu.Behaviour.BackgroundUpdates.Description" xml:space="preserve">
|
||||||
|
<value>Update Roblox in the background instead of waiting. Not recommended for slow networks. At least 3GB of free storage space is required for this feature to work.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
@ -104,7 +104,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
|
|
||||||
public ByfronDialog()
|
public ByfronDialog()
|
||||||
{
|
{
|
||||||
string version = Utilities.GetRobloxVersion(Bootstrapper?.IsStudioLaunch ?? false);
|
string version = Utilities.GetRobloxVersionStr(Bootstrapper?.IsStudioLaunch ?? false);
|
||||||
_viewModel = new ByfronDialogViewModel(this, version);
|
_viewModel = new ByfronDialogViewModel(this, version);
|
||||||
DataContext = _viewModel;
|
DataContext = _viewModel;
|
||||||
Title = App.Settings.Prop.BootstrapperTitle;
|
Title = App.Settings.Prop.BootstrapperTitle;
|
||||||
|
@ -30,13 +30,19 @@
|
|||||||
<ui:ToggleSwitch IsChecked="{Binding ForceRobloxLanguage, Mode=TwoWay}" />
|
<ui:ToggleSwitch IsChecked="{Binding ForceRobloxLanguage, Mode=TwoWay}" />
|
||||||
</controls:OptionControl>
|
</controls:OptionControl>
|
||||||
|
|
||||||
|
<controls:OptionControl
|
||||||
|
Header="{x:Static resources:Strings.Menu_Behaviour_BackgroundUpdates_Title}"
|
||||||
|
Description="{x:Static resources:Strings.Menu_Behaviour_BackgroundUpdates_Description}">
|
||||||
|
<ui:ToggleSwitch IsChecked="{Binding BackgroundUpdates, Mode=TwoWay}" />
|
||||||
|
</controls:OptionControl>
|
||||||
|
|
||||||
<controls:OptionControl
|
<controls:OptionControl
|
||||||
Header="{x:Static resources:Strings.Menu_Behaviour_ForceRobloxReinstall_Title}"
|
Header="{x:Static resources:Strings.Menu_Behaviour_ForceRobloxReinstall_Title}"
|
||||||
Description="{x:Static resources:Strings.Menu_Behaviour_ForceRobloxReinstall_Description}">
|
Description="{x:Static resources:Strings.Menu_Behaviour_ForceRobloxReinstall_Description}">
|
||||||
<controls:OptionControl.Style>
|
<controls:OptionControl.Style>
|
||||||
<Style TargetType="controls:OptionControl">
|
<Style TargetType="controls:OptionControl">
|
||||||
<Style.Triggers>
|
<Style.Triggers>
|
||||||
<DataTrigger Binding="{Binding ForceRobloxReinstallation, Mode=OneTime}" Value="True">
|
<DataTrigger Binding="{Binding IsRobloxInstallationMissing, Mode=OneTime}" Value="True">
|
||||||
<Setter Property="IsEnabled" Value="False" />
|
<Setter Property="IsEnabled" Value="False" />
|
||||||
</DataTrigger>
|
</DataTrigger>
|
||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
{
|
{
|
||||||
public class BehaviourViewModel : NotifyPropertyChangedViewModel
|
public class BehaviourViewModel : NotifyPropertyChangedViewModel
|
||||||
{
|
{
|
||||||
private string _oldPlayerVersionGuid = "";
|
|
||||||
private string _oldStudioVersionGuid = "";
|
|
||||||
|
|
||||||
public bool ConfirmLaunches
|
public bool ConfirmLaunches
|
||||||
{
|
{
|
||||||
get => App.Settings.Prop.ConfirmLaunches;
|
get => App.Settings.Prop.ConfirmLaunches;
|
||||||
@ -17,26 +14,18 @@
|
|||||||
set => App.Settings.Prop.ForceRobloxLanguage = value;
|
set => App.Settings.Prop.ForceRobloxLanguage = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool BackgroundUpdates
|
||||||
|
{
|
||||||
|
get => App.Settings.Prop.BackgroundUpdatesEnabled;
|
||||||
|
set => App.Settings.Prop.BackgroundUpdatesEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsRobloxInstallationMissing => String.IsNullOrEmpty(App.RobloxState.Prop.Player.VersionGuid) && String.IsNullOrEmpty(App.RobloxState.Prop.Studio.VersionGuid);
|
||||||
|
|
||||||
public bool ForceRobloxReinstallation
|
public bool ForceRobloxReinstallation
|
||||||
{
|
{
|
||||||
// wouldnt it be better to check old version guids?
|
get => App.State.Prop.ForceReinstall || IsRobloxInstallationMissing;
|
||||||
// what about fresh installs?
|
set => App.State.Prop.ForceReinstall = value;
|
||||||
get => String.IsNullOrEmpty(App.State.Prop.Player.VersionGuid) && String.IsNullOrEmpty(App.State.Prop.Studio.VersionGuid);
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value)
|
|
||||||
{
|
|
||||||
_oldPlayerVersionGuid = App.State.Prop.Player.VersionGuid;
|
|
||||||
_oldStudioVersionGuid = App.State.Prop.Studio.VersionGuid;
|
|
||||||
App.State.Prop.Player.VersionGuid = "";
|
|
||||||
App.State.Prop.Studio.VersionGuid = "";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
App.State.Prop.Player.VersionGuid = _oldPlayerVersionGuid;
|
|
||||||
App.State.Prop.Studio.VersionGuid = _oldStudioVersionGuid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,10 +75,24 @@ namespace Bloxstrap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetRobloxVersion(bool studio)
|
/// <summary>
|
||||||
|
/// Parses the input version string and prints if fails
|
||||||
|
/// </summary>
|
||||||
|
public static Version? ParseVersionSafe(string versionStr)
|
||||||
{
|
{
|
||||||
IAppData data = studio ? new RobloxStudioData() : new RobloxPlayerData();
|
const string LOG_IDENT = "Utilities::ParseVersionSafe";
|
||||||
|
|
||||||
|
if (!Version.TryParse(versionStr, out Version? version))
|
||||||
|
{
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, $"Failed to convert {versionStr} to a valid Version type.");
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetRobloxVersionStr(IAppData data)
|
||||||
|
{
|
||||||
string playerLocation = data.ExecutablePath;
|
string playerLocation = data.ExecutablePath;
|
||||||
|
|
||||||
if (!File.Exists(playerLocation))
|
if (!File.Exists(playerLocation))
|
||||||
@ -92,6 +106,19 @@ namespace Bloxstrap
|
|||||||
return versionInfo.ProductVersion.Replace(", ", ".");
|
return versionInfo.ProductVersion.Replace(", ", ".");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string GetRobloxVersionStr(bool studio)
|
||||||
|
{
|
||||||
|
IAppData data = studio ? new RobloxStudioData() : new RobloxPlayerData();
|
||||||
|
|
||||||
|
return GetRobloxVersionStr(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Version? GetRobloxVersion(IAppData data)
|
||||||
|
{
|
||||||
|
string str = GetRobloxVersionStr(data);
|
||||||
|
return ParseVersionSafe(str);
|
||||||
|
}
|
||||||
|
|
||||||
public static Process[] GetProcessesSafe()
|
public static Process[] GetProcessesSafe()
|
||||||
{
|
{
|
||||||
const string LOG_IDENT = "Utilities::GetProcessesSafe";
|
const string LOG_IDENT = "Utilities::GetProcessesSafe";
|
||||||
@ -107,5 +134,24 @@ namespace Bloxstrap
|
|||||||
return Array.Empty<Process>(); // can we retry?
|
return Array.Empty<Process>(); // can we retry?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool DoesMutexExist(string name)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Mutex.OpenExisting(name).Close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void KillBackgroundUpdater()
|
||||||
|
{
|
||||||
|
using EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.AutoReset, "Bloxstrap-BackgroundUpdaterKillEvent");
|
||||||
|
handle.Set();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,11 @@ namespace Bloxstrap.Utility
|
|||||||
return FromStream(stream);
|
return FromStream(stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string FromString(string str)
|
||||||
|
{
|
||||||
|
return FromBytes(Encoding.UTF8.GetBytes(str));
|
||||||
|
}
|
||||||
|
|
||||||
public static string Stringify(byte[] hash)
|
public static string Stringify(byte[] hash)
|
||||||
{
|
{
|
||||||
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
|
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
|
||||||
|
Loading…
Reference in New Issue
Block a user