From 178ca64cfab9270dfa35b31e5675279044329650 Mon Sep 17 00:00:00 2001 From: JustArchi Date: Thu, 25 Jul 2019 17:09:20 +0200 Subject: [PATCH] Implement non-blocking IO saving for databases --- ArchiSteamFarm.sln.DotSettings | 2 +- ArchiSteamFarm/ArchiCryptoHelper.cs | 2 +- ArchiSteamFarm/ArchiWebHandler.cs | 2 +- ArchiSteamFarm/Bot.cs | 27 ++-- ArchiSteamFarm/BotConfig.cs | 19 +-- ArchiSteamFarm/BotDatabase.cs | 146 ++++++------------ ArchiSteamFarm/Commands.cs | 48 +++--- ArchiSteamFarm/GlobalConfig.cs | 18 +-- ArchiSteamFarm/GlobalDatabase.cs | 67 +++----- ArchiSteamFarm/Helpers/SerializableFile.cs | 106 +++++++++++++ .../IPC/Controllers/Api/BotController.cs | 2 +- 11 files changed, 236 insertions(+), 203 deletions(-) create mode 100644 ArchiSteamFarm/Helpers/SerializableFile.cs diff --git a/ArchiSteamFarm.sln.DotSettings b/ArchiSteamFarm.sln.DotSettings index 674c8d0ba..c2a288a8e 100644 --- a/ArchiSteamFarm.sln.DotSettings +++ b/ArchiSteamFarm.sln.DotSettings @@ -183,7 +183,7 @@ ExpressionBody ExpressionBody public private protected internal static extern new virtual abstract sealed override readonly unsafe volatile async - Shift, Bitwise, Conditional + Arithmetic, Shift, Bitwise, Conditional END_OF_LINE END_OF_LINE USE_TABS_ONLY diff --git a/ArchiSteamFarm/ArchiCryptoHelper.cs b/ArchiSteamFarm/ArchiCryptoHelper.cs index 5cec3c6ee..afd05dd06 100644 --- a/ArchiSteamFarm/ArchiCryptoHelper.cs +++ b/ArchiSteamFarm/ArchiCryptoHelper.cs @@ -91,7 +91,7 @@ namespace ArchiSteamFarm { } } - internal static byte[] GenerateSteamParentalHash(byte[] password, byte[] salt, byte hashLength, ESteamParentalAlgorithm steamParentalAlgorithm) { + internal static IEnumerable GenerateSteamParentalHash(byte[] password, byte[] salt, byte hashLength, ESteamParentalAlgorithm steamParentalAlgorithm) { if ((password == null) || (salt == null) || (hashLength == 0) || !Enum.IsDefined(typeof(ESteamParentalAlgorithm), steamParentalAlgorithm)) { ASF.ArchiLogger.LogNullError(nameof(password) + " || " + nameof(salt) + " || " + nameof(hashLength) + " || " + nameof(steamParentalAlgorithm)); diff --git a/ArchiSteamFarm/ArchiWebHandler.cs b/ArchiSteamFarm/ArchiWebHandler.cs index d60cd542d..a8ec0b47a 100644 --- a/ArchiSteamFarm/ArchiWebHandler.cs +++ b/ArchiSteamFarm/ArchiWebHandler.cs @@ -1976,7 +1976,7 @@ namespace ArchiSteamFarm { const string request = "/mobileconf/multiajaxop"; // Extra entry for sessionID - List> data = new List>(8 + (confirmations.Count * 2)) { + List> data = new List>(8 + confirmations.Count * 2) { new KeyValuePair("a", Bot.SteamID.ToString()), new KeyValuePair("k", confirmationHash), new KeyValuePair("m", "android"), diff --git a/ArchiSteamFarm/Bot.cs b/ArchiSteamFarm/Bot.cs index a30ac1035..05dcd35f4 100755 --- a/ArchiSteamFarm/Bot.cs +++ b/ArchiSteamFarm/Bot.cs @@ -466,14 +466,14 @@ namespace ArchiSteamFarm { } } - internal async Task AddGamesToRedeemInBackground(IOrderedDictionary gamesToRedeemInBackground) { + internal void AddGamesToRedeemInBackground(IOrderedDictionary gamesToRedeemInBackground) { if ((gamesToRedeemInBackground == null) || (gamesToRedeemInBackground.Count == 0)) { ArchiLogger.LogNullError(nameof(gamesToRedeemInBackground)); return; } - await BotDatabase.AddGamesToRedeemInBackground(gamesToRedeemInBackground).ConfigureAwait(false); + BotDatabase.AddGamesToRedeemInBackground(gamesToRedeemInBackground); if ((GamesRedeemerInBackgroundTimer == null) && BotDatabase.HasGamesToRedeemInBackground && IsConnectedAndLoggedOn) { Utilities.InBackground(RedeemGamesInBackground); @@ -880,7 +880,7 @@ namespace ArchiSteamFarm { IOrderedDictionary validGamesToRedeemInBackground = ValidateGamesToRedeemInBackground(gamesToRedeemInBackground); if ((validGamesToRedeemInBackground != null) && (validGamesToRedeemInBackground.Count > 0)) { - await AddGamesToRedeemInBackground(validGamesToRedeemInBackground).ConfigureAwait(false); + AddGamesToRedeemInBackground(validGamesToRedeemInBackground); } } @@ -1180,6 +1180,7 @@ namespace ArchiSteamFarm { int partLength; bool copyNewline = false; + // ReSharper disable ArrangeMissingParentheses - conflict with Roslyn if (message.Length - i > maxMessageLength) { int lastNewLine = message.LastIndexOf(Environment.NewLine, i + maxMessageLength - Environment.NewLine.Length, maxMessageLength - Environment.NewLine.Length, StringComparison.Ordinal); @@ -1199,6 +1200,7 @@ namespace ArchiSteamFarm { partLength--; } + // ReSharper restore ArrangeMissingParentheses string messagePart = message.Substring(i, partLength); messagePart = ASF.GlobalConfig.SteamMessagePrefix + (i > 0 ? "…" : "") + messagePart + (maxMessageLength < message.Length - i ? "…" : ""); @@ -1264,6 +1266,7 @@ namespace ArchiSteamFarm { int i = 0; + // ReSharper disable ArrangeMissingParentheses - conflict with Roslyn while (i < message.Length) { int partLength; bool copyNewline = false; @@ -1287,6 +1290,7 @@ namespace ArchiSteamFarm { partLength--; } + // ReSharper restore ArrangeMissingParentheses string messagePart = message.Substring(i, partLength); messagePart = ASF.GlobalConfig.SteamMessagePrefix + (i > 0 ? "…" : "") + messagePart + (maxMessageLength < message.Length - i ? "…" : ""); @@ -1716,7 +1720,8 @@ namespace ArchiSteamFarm { } authenticator.Init(this); - await BotDatabase.SetMobileAuthenticator(authenticator).ConfigureAwait(false); + BotDatabase.MobileAuthenticator = authenticator; + File.Delete(maFilePath); } catch (Exception e) { ArchiLogger.LogGenericException(e); @@ -1979,7 +1984,7 @@ namespace ArchiSteamFarm { } } else { // If we're not using login keys, ensure we don't have any saved - await BotDatabase.SetLoginKey().ConfigureAwait(false); + BotDatabase.LoginKey = null; } if (!await InitLoginAndPassword(string.IsNullOrEmpty(loginKey)).ConfigureAwait(false)) { @@ -2070,7 +2075,7 @@ namespace ArchiSteamFarm { break; case EResult.InvalidPassword: - await BotDatabase.SetLoginKey().ConfigureAwait(false); + BotDatabase.LoginKey = null; ArchiLogger.LogGenericInfo(Strings.BotRemovedExpiredLoginKey); break; @@ -2405,7 +2410,7 @@ namespace ArchiSteamFarm { } if ((callback.CellID != 0) && (callback.CellID != ASF.GlobalDatabase.CellID)) { - await ASF.GlobalDatabase.SetCellID(callback.CellID).ConfigureAwait(false); + ASF.GlobalDatabase.CellID = callback.CellID; } // Handle steamID-based maFile @@ -2530,7 +2535,7 @@ namespace ArchiSteamFarm { } } - private async void OnLoginKey(SteamUser.LoginKeyCallback callback) { + private void OnLoginKey(SteamUser.LoginKeyCallback callback) { if (string.IsNullOrEmpty(callback?.LoginKey)) { ArchiLogger.LogNullError(nameof(callback) + " || " + nameof(callback.LoginKey)); @@ -2547,7 +2552,7 @@ namespace ArchiSteamFarm { loginKey = ArchiCryptoHelper.Encrypt(BotConfig.PasswordFormat, loginKey); } - await BotDatabase.SetLoginKey(loginKey).ConfigureAwait(false); + BotDatabase.LoginKey = loginKey; SteamUser.AcceptNewLoginKey(callback); } @@ -2845,7 +2850,7 @@ namespace ArchiSteamFarm { break; } - await BotDatabase.RemoveGameToRedeemInBackground(key).ConfigureAwait(false); + BotDatabase.RemoveGameToRedeemInBackground(key); // If user omitted the name or intentionally provided the same name as key, replace it with the Steam result if (name.Equals(key) && (result.Items != null) && (result.Items.Count > 0)) { @@ -2984,7 +2989,7 @@ namespace ArchiSteamFarm { } if (i >= steamParentalCode.Length) { - byte[] passwordHash = ArchiCryptoHelper.GenerateSteamParentalHash(password, settings.salt, (byte) settings.passwordhash.Length, steamParentalAlgorithm); + IEnumerable passwordHash = ArchiCryptoHelper.GenerateSteamParentalHash(password, settings.salt, (byte) settings.passwordhash.Length, steamParentalAlgorithm); if (passwordHash.SequenceEqual(settings.passwordhash)) { return (true, steamParentalCode); diff --git a/ArchiSteamFarm/BotConfig.cs b/ArchiSteamFarm/BotConfig.cs index 151819e91..ef23669bd 100644 --- a/ArchiSteamFarm/BotConfig.cs +++ b/ArchiSteamFarm/BotConfig.cs @@ -173,6 +173,7 @@ namespace ArchiSteamFarm { return result; } + set { if (!string.IsNullOrEmpty(value) && (PasswordFormat != ArchiCryptoHelper.ECryptoMethod.PlainText)) { value = ArchiCryptoHelper.Encrypt(PasswordFormat, value); @@ -190,37 +191,37 @@ namespace ArchiSteamFarm { [JsonProperty] internal string SteamLogin { - get => _SteamLogin; + get => BackingSteamLogin; set { IsSteamLoginSet = true; - _SteamLogin = value; + BackingSteamLogin = value; } } [JsonProperty] internal string SteamParentalCode { - get => _SteamParentalCode; + get => BackingSteamParentalCode; set { IsSteamParentalCodeSet = true; - _SteamParentalCode = value; + BackingSteamParentalCode = value; } } [JsonProperty] internal string SteamPassword { - get => _SteamPassword; + get => BackingSteamPassword; set { IsSteamPasswordSet = true; - _SteamPassword = value; + BackingSteamPassword = value; } } - private string _SteamLogin = DefaultSteamLogin; - private string _SteamParentalCode = DefaultSteamParentalCode; - private string _SteamPassword = DefaultSteamPassword; + private string BackingSteamLogin = DefaultSteamLogin; + private string BackingSteamParentalCode = DefaultSteamParentalCode; + private string BackingSteamPassword = DefaultSteamPassword; private bool ShouldSerializeSensitiveDetails = true; [JsonProperty(PropertyName = SharedInfo.UlongCompatibilityStringPrefix + nameof(SteamMasterClanID), Required = Required.DisallowNull)] diff --git a/ArchiSteamFarm/BotDatabase.cs b/ArchiSteamFarm/BotDatabase.cs index 773568320..c5b048ae7 100644 --- a/ArchiSteamFarm/BotDatabase.cs +++ b/ArchiSteamFarm/BotDatabase.cs @@ -24,16 +24,16 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.IO; -using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Collections; +using ArchiSteamFarm.Helpers; using ArchiSteamFarm.Localization; using JetBrains.Annotations; using Newtonsoft.Json; using SteamKit2; namespace ArchiSteamFarm { - internal sealed class BotDatabase : IDisposable { + internal sealed class BotDatabase : SerializableFile { internal uint GamesToRedeemInBackgroundCount { get { lock (GamesToRedeemInBackground) { @@ -48,8 +48,6 @@ namespace ArchiSteamFarm { [JsonProperty(Required = Required.DisallowNull)] private readonly ConcurrentHashSet BlacklistedFromTradesSteamIDs = new ConcurrentHashSet(); - private readonly SemaphoreSlim FileSemaphore = new SemaphoreSlim(1, 1); - [JsonProperty(Required = Required.DisallowNull)] private readonly OrderedDictionary GamesToRedeemInBackground = new OrderedDictionary(); @@ -59,14 +57,37 @@ namespace ArchiSteamFarm { [JsonProperty(Required = Required.DisallowNull)] private readonly ConcurrentHashSet IdlingPriorityAppIDs = new ConcurrentHashSet(); + internal string LoginKey { + get => BackingLoginKey; + + set { + if (BackingLoginKey == value) { + return; + } + + BackingLoginKey = value; + Utilities.InBackground(Save); + } + } + + internal MobileAuthenticator MobileAuthenticator { + get => BackingMobileAuthenticator; + + set { + if (BackingMobileAuthenticator == value) { + return; + } + + BackingMobileAuthenticator = value; + Utilities.InBackground(Save); + } + } + [JsonProperty(PropertyName = "_LoginKey")] - internal string LoginKey { get; private set; } + private string BackingLoginKey; [JsonProperty(PropertyName = "_MobileAuthenticator")] - internal MobileAuthenticator MobileAuthenticator { get; private set; } - - private string FilePath; - private bool ReadOnly; + private MobileAuthenticator BackingMobileAuthenticator; private BotDatabase([NotNull] string filePath) { if (string.IsNullOrEmpty(filePath)) { @@ -79,9 +100,7 @@ namespace ArchiSteamFarm { [JsonConstructor] private BotDatabase() { } - public void Dispose() => FileSemaphore.Dispose(); - - internal async Task AddBlacklistedFromTradesSteamIDs(IReadOnlyCollection steamIDs) { + internal void AddBlacklistedFromTradesSteamIDs(IReadOnlyCollection steamIDs) { if ((steamIDs == null) || (steamIDs.Count == 0)) { ASF.ArchiLogger.LogNullError(nameof(steamIDs)); @@ -89,11 +108,11 @@ namespace ArchiSteamFarm { } if (BlacklistedFromTradesSteamIDs.AddRange(steamIDs)) { - await Save().ConfigureAwait(false); + Utilities.InBackground(Save); } } - internal async Task AddGamesToRedeemInBackground(IOrderedDictionary games) { + internal void AddGamesToRedeemInBackground(IOrderedDictionary games) { if ((games == null) || (games.Count == 0)) { ASF.ArchiLogger.LogNullError(nameof(games)); @@ -114,11 +133,11 @@ namespace ArchiSteamFarm { } if (save) { - await Save().ConfigureAwait(false); + Utilities.InBackground(Save); } } - internal async Task AddIdlingBlacklistedAppIDs(IReadOnlyCollection appIDs) { + internal void AddIdlingBlacklistedAppIDs(IReadOnlyCollection appIDs) { if ((appIDs == null) || (appIDs.Count == 0)) { ASF.ArchiLogger.LogNullError(nameof(appIDs)); @@ -126,11 +145,11 @@ namespace ArchiSteamFarm { } if (IdlingBlacklistedAppIDs.AddRange(appIDs)) { - await Save().ConfigureAwait(false); + Utilities.InBackground(Save); } } - internal async Task AddIdlingPriorityAppIDs(IReadOnlyCollection appIDs) { + internal void AddIdlingPriorityAppIDs(IReadOnlyCollection appIDs) { if ((appIDs == null) || (appIDs.Count == 0)) { ASF.ArchiLogger.LogNullError(nameof(appIDs)); @@ -138,7 +157,7 @@ namespace ArchiSteamFarm { } if (IdlingPriorityAppIDs.AddRange(appIDs)) { - await Save().ConfigureAwait(false); + Utilities.InBackground(Save); } } @@ -228,25 +247,7 @@ namespace ArchiSteamFarm { return IdlingPriorityAppIDs.Contains(appID); } - internal async Task MakeReadOnly() { - if (ReadOnly) { - return; - } - - await FileSemaphore.WaitAsync().ConfigureAwait(false); - - try { - if (ReadOnly) { - return; - } - - ReadOnly = true; - } finally { - FileSemaphore.Release(); - } - } - - internal async Task RemoveBlacklistedFromTradesSteamIDs(IReadOnlyCollection steamIDs) { + internal void RemoveBlacklistedFromTradesSteamIDs(IReadOnlyCollection steamIDs) { if ((steamIDs == null) || (steamIDs.Count == 0)) { ASF.ArchiLogger.LogNullError(nameof(steamIDs)); @@ -254,11 +255,11 @@ namespace ArchiSteamFarm { } if (BlacklistedFromTradesSteamIDs.RemoveRange(steamIDs)) { - await Save().ConfigureAwait(false); + Utilities.InBackground(Save); } } - internal async Task RemoveGameToRedeemInBackground(string key) { + internal void RemoveGameToRedeemInBackground(string key) { if (string.IsNullOrEmpty(key)) { ASF.ArchiLogger.LogNullError(nameof(key)); @@ -273,10 +274,10 @@ namespace ArchiSteamFarm { GamesToRedeemInBackground.Remove(key); } - await Save().ConfigureAwait(false); + Utilities.InBackground(Save); } - internal async Task RemoveIdlingBlacklistedAppIDs(IReadOnlyCollection appIDs) { + internal void RemoveIdlingBlacklistedAppIDs(IReadOnlyCollection appIDs) { if ((appIDs == null) || (appIDs.Count == 0)) { ASF.ArchiLogger.LogNullError(nameof(appIDs)); @@ -284,11 +285,11 @@ namespace ArchiSteamFarm { } if (IdlingBlacklistedAppIDs.RemoveRange(appIDs)) { - await Save().ConfigureAwait(false); + Utilities.InBackground(Save); } } - internal async Task RemoveIdlingPriorityAppIDs(IReadOnlyCollection appIDs) { + internal void RemoveIdlingPriorityAppIDs(IReadOnlyCollection appIDs) { if ((appIDs == null) || (appIDs.Count == 0)) { ASF.ArchiLogger.LogNullError(nameof(appIDs)); @@ -296,62 +297,7 @@ namespace ArchiSteamFarm { } if (IdlingPriorityAppIDs.RemoveRange(appIDs)) { - await Save().ConfigureAwait(false); - } - } - - internal async Task SetLoginKey(string value = null) { - if (value == LoginKey) { - return; - } - - LoginKey = value; - await Save().ConfigureAwait(false); - } - - internal async Task SetMobileAuthenticator(MobileAuthenticator value = null) { - if (value == MobileAuthenticator) { - return; - } - - MobileAuthenticator = value; - await Save().ConfigureAwait(false); - } - - private async Task Save() { - if (ReadOnly) { - return; - } - - string json = JsonConvert.SerializeObject(this); - - if (string.IsNullOrEmpty(json)) { - ASF.ArchiLogger.LogNullError(nameof(json)); - - return; - } - - string newFilePath = FilePath + ".new"; - - await FileSemaphore.WaitAsync().ConfigureAwait(false); - - try { - if (ReadOnly) { - return; - } - - // We always want to write entire content to temporary file first, in order to never load corrupted data, also when target file doesn't exist - await RuntimeCompatibility.File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false); - - if (File.Exists(FilePath)) { - File.Replace(newFilePath, FilePath, null); - } else { - File.Move(newFilePath, FilePath); - } - } catch (Exception e) { - ASF.ArchiLogger.LogGenericException(e); - } finally { - FileSemaphore.Release(); + Utilities.InBackground(Save); } } diff --git a/ArchiSteamFarm/Commands.cs b/ArchiSteamFarm/Commands.cs index 10679223b..bd9363c02 100644 --- a/ArchiSteamFarm/Commands.cs +++ b/ArchiSteamFarm/Commands.cs @@ -171,11 +171,11 @@ namespace ArchiSteamFarm { case "BLADD" when args.Length > 2: return await ResponseBlacklistAdd(steamID, args[1], Utilities.GetArgsAsText(args, 2, ",")).ConfigureAwait(false); case "BLADD": - return await ResponseBlacklistAdd(steamID, args[1]).ConfigureAwait(false); + return ResponseBlacklistAdd(steamID, args[1]); case "BLRM" when args.Length > 2: return await ResponseBlacklistRemove(steamID, args[1], Utilities.GetArgsAsText(args, 2, ",")).ConfigureAwait(false); case "BLRM": - return await ResponseBlacklistRemove(steamID, args[1]).ConfigureAwait(false); + return ResponseBlacklistRemove(steamID, args[1]); case "FARM": return await ResponseFarm(steamID, Utilities.GetArgsAsText(args, 1, ",")).ConfigureAwait(false); case "INPUT" when args.Length > 3: @@ -187,21 +187,21 @@ namespace ArchiSteamFarm { case "IBADD" when args.Length > 2: return await ResponseIdleBlacklistAdd(steamID, args[1], Utilities.GetArgsAsText(args, 2, ",")).ConfigureAwait(false); case "IBADD": - return await ResponseIdleBlacklistAdd(steamID, args[1]).ConfigureAwait(false); + return ResponseIdleBlacklistAdd(steamID, args[1]); case "IBRM" when args.Length > 2: return await ResponseIdleBlacklistRemove(steamID, args[1], Utilities.GetArgsAsText(args, 2, ",")).ConfigureAwait(false); case "IBRM": - return await ResponseIdleBlacklistRemove(steamID, args[1]).ConfigureAwait(false); + return ResponseIdleBlacklistRemove(steamID, args[1]); case "IQ": return await ResponseIdleQueue(steamID, Utilities.GetArgsAsText(args, 1, ",")).ConfigureAwait(false); case "IQADD" when args.Length > 2: return await ResponseIdleQueueAdd(steamID, args[1], Utilities.GetArgsAsText(args, 2, ",")).ConfigureAwait(false); case "IQADD": - return await ResponseIdleQueueAdd(steamID, args[1]).ConfigureAwait(false); + return ResponseIdleQueueAdd(steamID, args[1]); case "IQRM" when args.Length > 2: return await ResponseIdleQueueRemove(steamID, args[1], Utilities.GetArgsAsText(args, 2, ",")).ConfigureAwait(false); case "IQRM": - return await ResponseIdleQueueRemove(steamID, args[1]).ConfigureAwait(false); + return ResponseIdleQueueRemove(steamID, args[1]); case "LEVEL": return await ResponseLevel(steamID, Utilities.GetArgsAsText(args, 1, ",")).ConfigureAwait(false); case "LOOT": @@ -917,7 +917,7 @@ namespace ArchiSteamFarm { return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null; } - private async Task ResponseBlacklistAdd(ulong steamID, string targetSteamIDs) { + private string ResponseBlacklistAdd(ulong steamID, string targetSteamIDs) { if ((steamID == 0) || string.IsNullOrEmpty(targetSteamIDs)) { Bot.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(targetSteamIDs)); @@ -944,7 +944,7 @@ namespace ArchiSteamFarm { targetIDs.Add(targetID); } - await Bot.BotDatabase.AddBlacklistedFromTradesSteamIDs(targetIDs).ConfigureAwait(false); + Bot.BotDatabase.AddBlacklistedFromTradesSteamIDs(targetIDs); return FormatBotResponse(Strings.Done); } @@ -963,14 +963,14 @@ namespace ArchiSteamFarm { return ASF.IsOwner(steamID) ? FormatStaticResponse(string.Format(Strings.BotNotFound, botNames)) : null; } - IList results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseBlacklistAdd(steamID, targetSteamIDs))).ConfigureAwait(false); + IList results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseBlacklistAdd(steamID, targetSteamIDs)))).ConfigureAwait(false); List responses = new List(results.Where(result => !string.IsNullOrEmpty(result))); return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null; } - private async Task ResponseBlacklistRemove(ulong steamID, string targetSteamIDs) { + private string ResponseBlacklistRemove(ulong steamID, string targetSteamIDs) { if ((steamID == 0) || string.IsNullOrEmpty(targetSteamIDs)) { Bot.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(targetSteamIDs)); @@ -997,7 +997,7 @@ namespace ArchiSteamFarm { targetIDs.Add(targetID); } - await Bot.BotDatabase.RemoveBlacklistedFromTradesSteamIDs(targetIDs).ConfigureAwait(false); + Bot.BotDatabase.RemoveBlacklistedFromTradesSteamIDs(targetIDs); return FormatBotResponse(Strings.Done); } @@ -1016,7 +1016,7 @@ namespace ArchiSteamFarm { return ASF.IsOwner(steamID) ? FormatStaticResponse(string.Format(Strings.BotNotFound, botNames)) : null; } - IList results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseBlacklistRemove(steamID, targetSteamIDs))).ConfigureAwait(false); + IList results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseBlacklistRemove(steamID, targetSteamIDs)))).ConfigureAwait(false); List responses = new List(results.Where(result => !string.IsNullOrEmpty(result))); @@ -1131,7 +1131,7 @@ namespace ArchiSteamFarm { return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null; } - private async Task ResponseIdleBlacklistAdd(ulong steamID, string targetAppIDs) { + private string ResponseIdleBlacklistAdd(ulong steamID, string targetAppIDs) { if ((steamID == 0) || string.IsNullOrEmpty(targetAppIDs)) { Bot.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(targetAppIDs)); @@ -1158,7 +1158,7 @@ namespace ArchiSteamFarm { appIDs.Add(appID); } - await Bot.BotDatabase.AddIdlingBlacklistedAppIDs(appIDs).ConfigureAwait(false); + Bot.BotDatabase.AddIdlingBlacklistedAppIDs(appIDs); return FormatBotResponse(Strings.Done); } @@ -1177,14 +1177,14 @@ namespace ArchiSteamFarm { return ASF.IsOwner(steamID) ? FormatStaticResponse(string.Format(Strings.BotNotFound, botNames)) : null; } - IList results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseIdleBlacklistAdd(steamID, targetAppIDs))).ConfigureAwait(false); + IList results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseIdleBlacklistAdd(steamID, targetAppIDs)))).ConfigureAwait(false); List responses = new List(results.Where(result => !string.IsNullOrEmpty(result))); return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null; } - private async Task ResponseIdleBlacklistRemove(ulong steamID, string targetAppIDs) { + private string ResponseIdleBlacklistRemove(ulong steamID, string targetAppIDs) { if ((steamID == 0) || string.IsNullOrEmpty(targetAppIDs)) { Bot.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(targetAppIDs)); @@ -1211,7 +1211,7 @@ namespace ArchiSteamFarm { appIDs.Add(appID); } - await Bot.BotDatabase.RemoveIdlingBlacklistedAppIDs(appIDs).ConfigureAwait(false); + Bot.BotDatabase.RemoveIdlingBlacklistedAppIDs(appIDs); return FormatBotResponse(Strings.Done); } @@ -1230,7 +1230,7 @@ namespace ArchiSteamFarm { return ASF.IsOwner(steamID) ? FormatStaticResponse(string.Format(Strings.BotNotFound, botNames)) : null; } - IList results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseIdleBlacklistRemove(steamID, targetAppIDs))).ConfigureAwait(false); + IList results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseIdleBlacklistRemove(steamID, targetAppIDs)))).ConfigureAwait(false); List responses = new List(results.Where(result => !string.IsNullOrEmpty(result))); @@ -1274,7 +1274,7 @@ namespace ArchiSteamFarm { return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null; } - private async Task ResponseIdleQueueAdd(ulong steamID, string targetAppIDs) { + private string ResponseIdleQueueAdd(ulong steamID, string targetAppIDs) { if ((steamID == 0) || string.IsNullOrEmpty(targetAppIDs)) { Bot.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(targetAppIDs)); @@ -1301,7 +1301,7 @@ namespace ArchiSteamFarm { appIDs.Add(appID); } - await Bot.BotDatabase.AddIdlingPriorityAppIDs(appIDs).ConfigureAwait(false); + Bot.BotDatabase.AddIdlingPriorityAppIDs(appIDs); return FormatBotResponse(Strings.Done); } @@ -1320,14 +1320,14 @@ namespace ArchiSteamFarm { return ASF.IsOwner(steamID) ? FormatStaticResponse(string.Format(Strings.BotNotFound, botNames)) : null; } - IList results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseIdleQueueAdd(steamID, targetAppIDs))).ConfigureAwait(false); + IList results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseIdleQueueAdd(steamID, targetAppIDs)))).ConfigureAwait(false); List responses = new List(results.Where(result => !string.IsNullOrEmpty(result))); return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null; } - private async Task ResponseIdleQueueRemove(ulong steamID, string targetAppIDs) { + private string ResponseIdleQueueRemove(ulong steamID, string targetAppIDs) { if ((steamID == 0) || string.IsNullOrEmpty(targetAppIDs)) { Bot.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(targetAppIDs)); @@ -1354,7 +1354,7 @@ namespace ArchiSteamFarm { appIDs.Add(appID); } - await Bot.BotDatabase.RemoveIdlingPriorityAppIDs(appIDs).ConfigureAwait(false); + Bot.BotDatabase.RemoveIdlingPriorityAppIDs(appIDs); return FormatBotResponse(Strings.Done); } @@ -1373,7 +1373,7 @@ namespace ArchiSteamFarm { return ASF.IsOwner(steamID) ? FormatStaticResponse(string.Format(Strings.BotNotFound, botNames)) : null; } - IList results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseIdleQueueRemove(steamID, targetAppIDs))).ConfigureAwait(false); + IList results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseIdleQueueRemove(steamID, targetAppIDs)))).ConfigureAwait(false); List responses = new List(results.Where(result => !string.IsNullOrEmpty(result))); diff --git a/ArchiSteamFarm/GlobalConfig.cs b/ArchiSteamFarm/GlobalConfig.cs index bc8ab4152..cb47ca3cc 100644 --- a/ArchiSteamFarm/GlobalConfig.cs +++ b/ArchiSteamFarm/GlobalConfig.cs @@ -145,8 +145,8 @@ namespace ArchiSteamFarm { internal WebProxy WebProxy { get { - if (_WebProxy != null) { - return _WebProxy; + if (BackingWebProxy != null) { + return BackingWebProxy; } if (string.IsNullOrEmpty(WebProxyText)) { @@ -163,7 +163,7 @@ namespace ArchiSteamFarm { return null; } - _WebProxy = new WebProxy { + BackingWebProxy = new WebProxy { Address = uri, BypassProxyOnLocal = true }; @@ -179,10 +179,10 @@ namespace ArchiSteamFarm { credentials.Password = WebProxyPassword; } - _WebProxy.Credentials = credentials; + BackingWebProxy.Credentials = credentials; } - return _WebProxy; + return BackingWebProxy; } } @@ -201,16 +201,16 @@ namespace ArchiSteamFarm { [JsonProperty] internal string WebProxyPassword { - get => _WebProxyPassword; + get => BackingWebProxyPassword; set { IsWebProxyPasswordSet = true; - _WebProxyPassword = value; + BackingWebProxyPassword = value; } } - private WebProxy _WebProxy; - private string _WebProxyPassword = DefaultWebProxyPassword; + private WebProxy BackingWebProxy; + private string BackingWebProxyPassword = DefaultWebProxyPassword; private bool ShouldSerializeSensitiveDetails = true; [JsonProperty(PropertyName = SharedInfo.UlongCompatibilityStringPrefix + nameof(SteamOwnerID), Required = Required.DisallowNull)] diff --git a/ArchiSteamFarm/GlobalDatabase.cs b/ArchiSteamFarm/GlobalDatabase.cs index dcce4ec25..33018a1d0 100644 --- a/ArchiSteamFarm/GlobalDatabase.cs +++ b/ArchiSteamFarm/GlobalDatabase.cs @@ -26,13 +26,14 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using ArchiSteamFarm.Helpers; using ArchiSteamFarm.Localization; using ArchiSteamFarm.SteamKit2; using JetBrains.Annotations; using Newtonsoft.Json; namespace ArchiSteamFarm { - public sealed class GlobalDatabase : IDisposable { + public sealed class GlobalDatabase : SerializableFile { [JsonProperty(Required = Required.DisallowNull)] public readonly Guid Guid = Guid.NewGuid(); @@ -42,13 +43,23 @@ namespace ArchiSteamFarm { [JsonProperty(Required = Required.DisallowNull)] internal readonly InMemoryServerListProvider ServerListProvider = new InMemoryServerListProvider(); - private readonly SemaphoreSlim FileSemaphore = new SemaphoreSlim(1, 1); private readonly SemaphoreSlim PackagesRefreshSemaphore = new SemaphoreSlim(1, 1); - [JsonProperty(PropertyName = "_CellID", Required = Required.DisallowNull)] - internal uint CellID { get; private set; } + internal uint CellID { + get => BackingCellID; - private string FilePath; + set { + if (BackingCellID == value) { + return; + } + + BackingCellID = value; + Utilities.InBackground(Save); + } + } + + [JsonProperty(PropertyName = "_CellID", Required = Required.DisallowNull)] + private uint BackingCellID; private GlobalDatabase([NotNull] string filePath) : this() { if (string.IsNullOrEmpty(filePath)) { @@ -61,13 +72,15 @@ namespace ArchiSteamFarm { [JsonConstructor] private GlobalDatabase() => ServerListProvider.ServerListUpdated += OnServerListUpdated; - public void Dispose() { + public override void Dispose() { // Events we registered ServerListProvider.ServerListUpdated -= OnServerListUpdated; // Those are objects that are always being created if constructor doesn't throw exception - FileSemaphore.Dispose(); PackagesRefreshSemaphore.Dispose(); + + // Base dispose + base.Dispose(); } [ItemCanBeNull] @@ -159,52 +172,14 @@ namespace ArchiSteamFarm { PackagesData[packageID] = package; } - await Save().ConfigureAwait(false); + Utilities.InBackground(Save); } finally { PackagesRefreshSemaphore.Release(); } } - internal async Task SetCellID(uint value = 0) { - if (value == CellID) { - return; - } - - CellID = value; - await Save().ConfigureAwait(false); - } - private async void OnServerListUpdated(object sender, EventArgs e) => await Save().ConfigureAwait(false); - private async Task Save() { - string json = JsonConvert.SerializeObject(this); - - if (string.IsNullOrEmpty(json)) { - ASF.ArchiLogger.LogNullError(nameof(json)); - - return; - } - - string newFilePath = FilePath + ".new"; - - await FileSemaphore.WaitAsync().ConfigureAwait(false); - - try { - // We always want to write entire content to temporary file first, in order to never load corrupted data, also when target file doesn't exist - await RuntimeCompatibility.File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false); - - if (File.Exists(FilePath)) { - File.Replace(newFilePath, FilePath, null); - } else { - File.Move(newFilePath, FilePath); - } - } catch (Exception e) { - ASF.ArchiLogger.LogGenericException(e); - } finally { - FileSemaphore.Release(); - } - } - // ReSharper disable UnusedMember.Global public bool ShouldSerializeCellID() => CellID != 0; public bool ShouldSerializePackagesData() => PackagesData.Count > 0; diff --git a/ArchiSteamFarm/Helpers/SerializableFile.cs b/ArchiSteamFarm/Helpers/SerializableFile.cs new file mode 100644 index 000000000..546eeccc7 --- /dev/null +++ b/ArchiSteamFarm/Helpers/SerializableFile.cs @@ -0,0 +1,106 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2019 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// | +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// | +// http://www.apache.org/licenses/LICENSE-2.0 +// | +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace ArchiSteamFarm.Helpers { + public abstract class SerializableFile : IDisposable { + private readonly SemaphoreSlim FileSemaphore = new SemaphoreSlim(1, 1); + + protected string FilePath { private get; set; } + + private bool ReadOnly; + private bool SavingScheduled; + + public virtual void Dispose() => FileSemaphore.Dispose(); + + protected async Task Save() { + if (ReadOnly || string.IsNullOrEmpty(FilePath)) { + return; + } + + lock (FileSemaphore) { + if (SavingScheduled) { + return; + } + + SavingScheduled = true; + } + + await FileSemaphore.WaitAsync().ConfigureAwait(false); + + try { + lock (FileSemaphore) { + SavingScheduled = false; + } + + if (ReadOnly) { + return; + } + + string json = JsonConvert.SerializeObject(this, Debugging.IsUserDebugging ? Formatting.Indented : Formatting.None); + + if (string.IsNullOrEmpty(json)) { + ASF.ArchiLogger.LogNullError(nameof(json)); + + return; + } + + string newFilePath = FilePath + ".new"; + + // We always want to write entire content to temporary file first, in order to never load corrupted data, also when target file doesn't exist + await RuntimeCompatibility.File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false); + + if (File.Exists(FilePath)) { + File.Replace(newFilePath, FilePath, null); + } else { + File.Move(newFilePath, FilePath); + } + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); + } finally { + FileSemaphore.Release(); + } + } + + internal async Task MakeReadOnly() { + if (ReadOnly) { + return; + } + + await FileSemaphore.WaitAsync().ConfigureAwait(false); + + try { + if (ReadOnly) { + return; + } + + ReadOnly = true; + } finally { + FileSemaphore.Release(); + } + } + } +} diff --git a/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs b/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs index 994d11be2..79372e1f9 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs @@ -233,7 +233,7 @@ namespace ArchiSteamFarm.IPC.Controllers.Api { return BadRequest(new GenericResponse(false, string.Format(Strings.ErrorIsEmpty, nameof(validGamesToRedeemInBackground)))); } - await Utilities.InParallel(bots.Select(bot => bot.AddGamesToRedeemInBackground(validGamesToRedeemInBackground))).ConfigureAwait(false); + await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.AddGamesToRedeemInBackground(validGamesToRedeemInBackground)))).ConfigureAwait(false); Dictionary result = new Dictionary(bots.Count, Bot.BotsComparer);