From 6c2272483575036bae2477d5dd13bdcd7d65f4f6 Mon Sep 17 00:00:00 2001 From: JustArchi Date: Wed, 20 May 2020 22:01:29 +0200 Subject: [PATCH] Implement OS-wide, cross-process semaphores for ASF instances Distinction is preserved on webproxy basis (even for LoginLimiter which shouldn't work like that) Addresses crucial part of #1798 --- ArchiSteamFarm/ASF.cs | 24 +++ ArchiSteamFarm/Actions.cs | 11 +- ArchiSteamFarm/ArchiWebHandler.cs | 46 +++-- ArchiSteamFarm/Bot.cs | 25 ++- .../CrossProcessMutexBasedSemaphore.cs | 177 ++++++++++++++++++ .../Helpers/CrossProcessSemaphore.cs | 53 ++++++ .../Helpers/ICrossProcessSemaphore.cs | 31 +++ ArchiSteamFarm/MobileAuthenticator.cs | 11 +- ArchiSteamFarm/OS.cs | 32 +++- 9 files changed, 376 insertions(+), 34 deletions(-) create mode 100644 ArchiSteamFarm/Helpers/CrossProcessMutexBasedSemaphore.cs create mode 100644 ArchiSteamFarm/Helpers/CrossProcessSemaphore.cs create mode 100644 ArchiSteamFarm/Helpers/ICrossProcessSemaphore.cs diff --git a/ArchiSteamFarm/ASF.cs b/ArchiSteamFarm/ASF.cs index b9ded3019..a8bd7b4a5 100644 --- a/ArchiSteamFarm/ASF.cs +++ b/ArchiSteamFarm/ASF.cs @@ -22,11 +22,14 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.IO.Compression; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; +using ArchiSteamFarm.Helpers; using ArchiSteamFarm.IPC; using ArchiSteamFarm.Localization; using ArchiSteamFarm.NLog; @@ -55,6 +58,12 @@ namespace ArchiSteamFarm { [PublicAPI] public static WebBrowser WebBrowser { get; internal set; } + internal static ICrossProcessSemaphore ConfirmationsSemaphore { get; private set; } + internal static ICrossProcessSemaphore GiftsSemaphore { get; private set; } + internal static ICrossProcessSemaphore InventorySemaphore { get; private set; } + internal static ICrossProcessSemaphore LoginRateLimitingSemaphore { get; private set; } + internal static ImmutableDictionary WebLimitingSemaphores { get; private set; } + private static readonly SemaphoreSlim UpdateSemaphore = new SemaphoreSlim(1, 1); private static Timer AutoUpdatesTimer; @@ -131,6 +140,21 @@ namespace ArchiSteamFarm { } GlobalConfig = globalConfig; + + string webProxyText = !string.IsNullOrEmpty(globalConfig.WebProxyText) ? "-" + Convert.ToBase64String(Encoding.UTF8.GetBytes(globalConfig.WebProxyText)) : ""; + + ConfirmationsSemaphore ??= OS.CreateCrossProcessSemaphore(nameof(ConfirmationsSemaphore) + webProxyText); + GiftsSemaphore ??= OS.CreateCrossProcessSemaphore(nameof(GiftsSemaphore) + webProxyText); + InventorySemaphore ??= OS.CreateCrossProcessSemaphore(nameof(InventorySemaphore) + webProxyText); + LoginRateLimitingSemaphore ??= OS.CreateCrossProcessSemaphore(nameof(LoginRateLimitingSemaphore) + webProxyText); + + WebLimitingSemaphores ??= new Dictionary(4, StringComparer.OrdinalIgnoreCase) { + { nameof(ArchiWebHandler), (OS.CreateCrossProcessSemaphore(nameof(ArchiWebHandler) + webProxyText), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }, + { ArchiWebHandler.SteamCommunityURL, (OS.CreateCrossProcessSemaphore(nameof(ArchiWebHandler) + webProxyText + "-" + nameof(ArchiWebHandler.SteamCommunityURL)), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }, + { ArchiWebHandler.SteamHelpURL, (OS.CreateCrossProcessSemaphore(nameof(ArchiWebHandler) + webProxyText + "-" + nameof(ArchiWebHandler.SteamHelpURL)), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }, + { ArchiWebHandler.SteamStoreURL, (OS.CreateCrossProcessSemaphore(nameof(ArchiWebHandler) + webProxyText + "-" + nameof(ArchiWebHandler.SteamStoreURL)), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }, + { WebAPI.DefaultBaseAddress.Host, (OS.CreateCrossProcessSemaphore(nameof(ArchiWebHandler) + webProxyText + "-" + nameof(WebAPI)), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) } + }.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase); } internal static void InitGlobalDatabase(GlobalDatabase globalDatabase) { diff --git a/ArchiSteamFarm/Actions.cs b/ArchiSteamFarm/Actions.cs index 1f141b627..accc322bd 100644 --- a/ArchiSteamFarm/Actions.cs +++ b/ArchiSteamFarm/Actions.cs @@ -36,7 +36,6 @@ using SteamKit2; namespace ArchiSteamFarm { public sealed class Actions : IAsyncDisposable { private static readonly SemaphoreSlim GiftCardsSemaphore = new SemaphoreSlim(1, 1); - private static readonly SemaphoreSlim GiftsSemaphore = new SemaphoreSlim(1, 1); private readonly Bot Bot; private readonly ConcurrentHashSet HandledGifts = new ConcurrentHashSet(); @@ -440,16 +439,22 @@ namespace ArchiSteamFarm { private ulong GetFirstSteamMasterID() => Bot.BotConfig.SteamUserPermissions.Where(kv => (kv.Key != 0) && (kv.Value == BotConfig.EPermission.Master)).Select(kv => kv.Key).OrderByDescending(steamID => steamID != Bot.SteamID).ThenBy(steamID => steamID).FirstOrDefault(); private static async Task LimitGiftsRequestsAsync() { + if (ASF.GiftsSemaphore == null) { + ASF.ArchiLogger.LogNullError(nameof(ASF.GiftsSemaphore)); + + return; + } + if (ASF.GlobalConfig.GiftsLimiterDelay == 0) { return; } - await GiftsSemaphore.WaitAsync().ConfigureAwait(false); + await ASF.GiftsSemaphore.WaitAsync().ConfigureAwait(false); Utilities.InBackground( async () => { await Task.Delay(ASF.GlobalConfig.GiftsLimiterDelay * 1000).ConfigureAwait(false); - GiftsSemaphore.Release(); + ASF.GiftsSemaphore.Release(); } ); } diff --git a/ArchiSteamFarm/ArchiWebHandler.cs b/ArchiSteamFarm/ArchiWebHandler.cs index 3735d0311..04cccf34b 100644 --- a/ArchiSteamFarm/ArchiWebHandler.cs +++ b/ArchiSteamFarm/ArchiWebHandler.cs @@ -62,16 +62,6 @@ namespace ArchiSteamFarm { private const string SteamHelpHost = "help.steampowered.com"; private const string SteamStoreHost = "store.steampowered.com"; - private static readonly SemaphoreSlim InventorySemaphore = new SemaphoreSlim(1, 1); - - private static readonly ImmutableDictionary WebLimitingSemaphores = new Dictionary(4, StringComparer.Ordinal) { - { nameof(ArchiWebHandler), (new SemaphoreSlim(1, 1), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }, - { SteamCommunityURL, (new SemaphoreSlim(1, 1), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }, - { SteamHelpURL, (new SemaphoreSlim(1, 1), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }, - { SteamStoreURL, (new SemaphoreSlim(1, 1), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }, - { WebAPI.DefaultBaseAddress.Host, (new SemaphoreSlim(1, 1), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) } - }.ToImmutableDictionary(StringComparer.Ordinal); - [PublicAPI] public readonly ArchiCacheable CachedApiKey; @@ -129,6 +119,10 @@ namespace ArchiSteamFarm { throw new ArgumentException(string.Format(Strings.ErrorObjectIsNull, nameof(appID) + " || " + nameof(contextID))); } + if (ASF.InventorySemaphore == null) { + throw new ArgumentNullException(nameof(ASF.InventorySemaphore)); + } + if (steamID == 0) { if (!Initialized) { for (byte i = 0; (i < ASF.GlobalConfig.ConnectionTimeout) && !Initialized && Bot.IsConnectedAndLoggedOn; i++) { @@ -152,7 +146,7 @@ namespace ArchiSteamFarm { HashSet assetIDs = new HashSet(); while (true) { - await InventorySemaphore.WaitAsync().ConfigureAwait(false); + await ASF.InventorySemaphore.WaitAsync().ConfigureAwait(false); try { Steam.InventoryResponse response = await UrlGetToJsonObjectWithSession(SteamCommunityURL, request + (startAssetID > 0 ? "&start_assetid=" + startAssetID : "")).ConfigureAwait(false); @@ -222,12 +216,12 @@ namespace ArchiSteamFarm { startAssetID = response.LastAssetID; } finally { if (ASF.GlobalConfig.InventoryLimiterDelay == 0) { - InventorySemaphore.Release(); + ASF.InventorySemaphore.Release(); } else { Utilities.InBackground( async () => { await Task.Delay(ASF.GlobalConfig.InventoryLimiterDelay * 1000).ConfigureAwait(false); - InventorySemaphore.Release(); + ASF.InventorySemaphore.Release(); } ); } @@ -1183,14 +1177,20 @@ namespace ArchiSteamFarm { return default; } + if (ASF.WebLimitingSemaphores == null) { + ASF.ArchiLogger.LogNullError(nameof(ASF.WebLimitingSemaphores)); + + return default; + } + if (ASF.GlobalConfig.WebLimiterDelay == 0) { return await function().ConfigureAwait(false); } - if (!WebLimitingSemaphores.TryGetValue(service, out (SemaphoreSlim RateLimitingSemaphore, SemaphoreSlim OpenConnectionsSemaphore) limiters)) { + if (!ASF.WebLimitingSemaphores.TryGetValue(service, out (ICrossProcessSemaphore RateLimitingSemaphore, SemaphoreSlim OpenConnectionsSemaphore) limiters)) { ASF.ArchiLogger.LogGenericWarning(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(service), service)); - if (!WebLimitingSemaphores.TryGetValue(nameof(ArchiWebHandler), out limiters)) { + if (!ASF.WebLimitingSemaphores.TryGetValue(nameof(ArchiWebHandler), out limiters)) { ASF.ArchiLogger.LogNullError(nameof(limiters)); return await function().ConfigureAwait(false); @@ -2155,9 +2155,15 @@ namespace ArchiSteamFarm { } internal async Task MarkInventory() { + if (ASF.InventorySemaphore == null) { + Bot.ArchiLogger.LogNullError(nameof(ASF.InventorySemaphore)); + + return; + } + // We aim to have a maximum of 2 tasks, one already working, and one waiting in the queue // This way we can call this function as many times as needed e.g. because of Steam events - lock (InventorySemaphore) { + lock (ASF.InventorySemaphore) { if (MarkingInventoryScheduled) { return; } @@ -2165,10 +2171,10 @@ namespace ArchiSteamFarm { MarkingInventoryScheduled = true; } - await InventorySemaphore.WaitAsync().ConfigureAwait(false); + await ASF.InventorySemaphore.WaitAsync().ConfigureAwait(false); try { - lock (InventorySemaphore) { + lock (ASF.InventorySemaphore) { MarkingInventoryScheduled = false; } @@ -2176,12 +2182,12 @@ namespace ArchiSteamFarm { await UrlHeadWithSession(SteamCommunityURL, request, false).ConfigureAwait(false); } finally { if (ASF.GlobalConfig.InventoryLimiterDelay == 0) { - InventorySemaphore.Release(); + ASF.InventorySemaphore.Release(); } else { Utilities.InBackground( async () => { await Task.Delay(ASF.GlobalConfig.InventoryLimiterDelay * 1000).ConfigureAwait(false); - InventorySemaphore.Release(); + ASF.InventorySemaphore.Release(); } ); } diff --git a/ArchiSteamFarm/Bot.cs b/ArchiSteamFarm/Bot.cs index a6526e3bf..7edcdfe89 100755 --- a/ArchiSteamFarm/Bot.cs +++ b/ArchiSteamFarm/Bot.cs @@ -62,7 +62,6 @@ namespace ArchiSteamFarm { internal static EOSType OSType { get; private set; } = EOSType.Unknown; private static readonly SemaphoreSlim BotsSemaphore = new SemaphoreSlim(1, 1); - private static readonly SemaphoreSlim LoginRateLimitingSemaphore = new SemaphoreSlim(1, 1); private static readonly SemaphoreSlim LoginSemaphore = new SemaphoreSlim(1, 1); [JsonIgnore] @@ -1939,9 +1938,15 @@ namespace ArchiSteamFarm { } private static async Task LimitLoginRequestsAsync() { + if (ASF.LoginRateLimitingSemaphore == null) { + ASF.ArchiLogger.LogNullError(nameof(ASF.LoginRateLimitingSemaphore)); + + return; + } + if (ASF.GlobalConfig.LoginLimiterDelay == 0) { - await LoginRateLimitingSemaphore.WaitAsync().ConfigureAwait(false); - LoginRateLimitingSemaphore.Release(); + await ASF.LoginRateLimitingSemaphore.WaitAsync().ConfigureAwait(false); + ASF.LoginRateLimitingSemaphore.Release(); return; } @@ -1949,8 +1954,8 @@ namespace ArchiSteamFarm { await LoginSemaphore.WaitAsync().ConfigureAwait(false); try { - await LoginRateLimitingSemaphore.WaitAsync().ConfigureAwait(false); - LoginRateLimitingSemaphore.Release(); + await ASF.LoginRateLimitingSemaphore.WaitAsync().ConfigureAwait(false); + ASF.LoginRateLimitingSemaphore.Release(); } finally { Utilities.InBackground( async () => { @@ -2074,6 +2079,12 @@ namespace ArchiSteamFarm { return; } + if (ASF.LoginRateLimitingSemaphore == null) { + ASF.ArchiLogger.LogNullError(nameof(ASF.LoginRateLimitingSemaphore)); + + return; + } + EResult lastLogOnResult = LastLogOnResult; LastLogOnResult = EResult.Invalid; HeartBeatFailures = 0; @@ -2120,14 +2131,14 @@ namespace ArchiSteamFarm { case EResult.RateLimitExceeded: ArchiLogger.LogGenericInfo(string.Format(Strings.BotRateLimitExceeded, TimeSpan.FromMinutes(LoginCooldownInMinutes).ToHumanReadable())); - if (!await LoginRateLimitingSemaphore.WaitAsync(1000 * WebBrowser.MaxTries).ConfigureAwait(false)) { + if (!await ASF.LoginRateLimitingSemaphore.WaitAsync(1000 * WebBrowser.MaxTries).ConfigureAwait(false)) { break; } try { await Task.Delay(LoginCooldownInMinutes * 60 * 1000).ConfigureAwait(false); } finally { - LoginRateLimitingSemaphore.Release(); + ASF.LoginRateLimitingSemaphore.Release(); } break; diff --git a/ArchiSteamFarm/Helpers/CrossProcessMutexBasedSemaphore.cs b/ArchiSteamFarm/Helpers/CrossProcessMutexBasedSemaphore.cs new file mode 100644 index 000000000..8160c8cb0 --- /dev/null +++ b/ArchiSteamFarm/Helpers/CrossProcessMutexBasedSemaphore.cs @@ -0,0 +1,177 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2020 Ł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.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace ArchiSteamFarm.Helpers { + internal sealed class CrossProcessMutexBasedSemaphore : ICrossProcessSemaphore { + private const int SpinLockDelay = 1000; // In milliseconds + + private readonly SemaphoreSlim LocalSemaphore = new SemaphoreSlim(1, 1); + + private readonly string Name; + + private Mutex Mutex; + private TaskCompletionSource ReleasedTask = new TaskCompletionSource(); + + internal CrossProcessMutexBasedSemaphore([NotNull] string name) { + if (string.IsNullOrEmpty(name)) { + throw new ArgumentNullException(nameof(name)); + } + + Name = "Global\\" + name; + ReleasedTask.SetResult(true); + } + + public void Dispose() { + LocalSemaphore.Dispose(); + + Mutex?.Dispose(); + } + + void ICrossProcessSemaphore.Release() { + if (Mutex == null) { + throw new ArgumentNullException(nameof(Mutex) + " || " + nameof(ReleasedTask)); + } + + lock (LocalSemaphore) { + if (Mutex == null) { + throw new ArgumentNullException(nameof(Mutex) + " || " + nameof(ReleasedTask)); + } + + Mutex.Dispose(); + Mutex = null; + + ReleasedTask.SetResult(true); + } + } + + async Task ICrossProcessSemaphore.WaitAsync() { + await LocalSemaphore.WaitAsync().ConfigureAwait(false); + + try { + while (true) { + if (Mutex != null) { + await Task.Delay(SpinLockDelay).ConfigureAwait(false); + + continue; + } + + // ReSharper disable once InconsistentlySynchronizedField - we do not synchronize TaskCompletionSource alone, but the whole process + await ReleasedTask.Task.ConfigureAwait(false); + + lock (LocalSemaphore) { + Mutex mutex = new Mutex(false, Name); + + try { + mutex.WaitOne(); + } catch (AbandonedMutexException) { + // Ignored, this is fine, other ASF process has been closed + } + + ReleasedTask = new TaskCompletionSource(); + Mutex = mutex; + + return; + } + } + } finally { + LocalSemaphore.Release(); + } + } + + async Task ICrossProcessSemaphore.WaitAsync(int millisecondsTimeout) { + Stopwatch stopwatch = Stopwatch.StartNew(); + + if (!await LocalSemaphore.WaitAsync(millisecondsTimeout).ConfigureAwait(false)) { + stopwatch.Stop(); + + return false; + } + + try { + stopwatch.Stop(); + + millisecondsTimeout -= (int) stopwatch.ElapsedMilliseconds; + + if (millisecondsTimeout <= 0) { + return false; + } + + while (true) { + if (Mutex != null) { + if (millisecondsTimeout <= SpinLockDelay) { + return false; + } + + await Task.Delay(SpinLockDelay).ConfigureAwait(false); + millisecondsTimeout -= SpinLockDelay; + + continue; + } + + // ReSharper disable InconsistentlySynchronizedField - we do not synchronize TaskCompletionSource alone, but the whole process + + if (!ReleasedTask.Task.IsCompleted) { + stopwatch.Restart(); + + if (await Task.WhenAny(ReleasedTask.Task, Task.Delay(millisecondsTimeout)).ConfigureAwait(false) != ReleasedTask.Task) { + return false; + } + + stopwatch.Stop(); + + millisecondsTimeout -= (int) stopwatch.ElapsedMilliseconds; + + if (millisecondsTimeout <= 0) { + return false; + } + } + + // ReSharper restore InconsistentlySynchronizedField + + lock (LocalSemaphore) { + Mutex mutex = new Mutex(false, Name); + + try { + if (!mutex.WaitOne(millisecondsTimeout)) { + return false; + } + } catch (AbandonedMutexException) { + // Ignored, this is fine + } + + ReleasedTask = new TaskCompletionSource(); + Mutex = mutex; + + return true; + } + } + } finally { + LocalSemaphore.Release(); + } + } + } +} diff --git a/ArchiSteamFarm/Helpers/CrossProcessSemaphore.cs b/ArchiSteamFarm/Helpers/CrossProcessSemaphore.cs new file mode 100644 index 000000000..93a8bcc77 --- /dev/null +++ b/ArchiSteamFarm/Helpers/CrossProcessSemaphore.cs @@ -0,0 +1,53 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2020 Ł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.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace ArchiSteamFarm.Helpers { + internal sealed class CrossProcessSemaphore : ICrossProcessSemaphore { + private readonly Semaphore GlobalSemaphore; + + internal CrossProcessSemaphore([NotNull] string name) { + if (string.IsNullOrEmpty(name)) { + throw new ArgumentNullException(nameof(name)); + } + + GlobalSemaphore = new Semaphore(1, 1, name); + } + + public void Dispose() => GlobalSemaphore.Dispose(); + + void ICrossProcessSemaphore.Release() => GlobalSemaphore.Release(); + + [NotNull] + Task ICrossProcessSemaphore.WaitAsync() { + GlobalSemaphore.WaitOne(); + + return Task.CompletedTask; + } + + [NotNull] + Task ICrossProcessSemaphore.WaitAsync(int millisecondsTimeout) => Task.FromResult(GlobalSemaphore.WaitOne(millisecondsTimeout)); + } +} diff --git a/ArchiSteamFarm/Helpers/ICrossProcessSemaphore.cs b/ArchiSteamFarm/Helpers/ICrossProcessSemaphore.cs new file mode 100644 index 000000000..cdb5bc413 --- /dev/null +++ b/ArchiSteamFarm/Helpers/ICrossProcessSemaphore.cs @@ -0,0 +1,31 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2020 Ł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.Threading.Tasks; + +namespace ArchiSteamFarm.Helpers { + internal interface ICrossProcessSemaphore : IDisposable { + internal void Release(); + internal Task WaitAsync(); + internal Task WaitAsync(int millisecondsTimeout); + } +} diff --git a/ArchiSteamFarm/MobileAuthenticator.cs b/ArchiSteamFarm/MobileAuthenticator.cs index d54e7f034..93dc9758b 100644 --- a/ArchiSteamFarm/MobileAuthenticator.cs +++ b/ArchiSteamFarm/MobileAuthenticator.cs @@ -40,7 +40,6 @@ namespace ArchiSteamFarm { private const byte SteamTimeTTL = 24; // For how many hours we can assume that SteamTimeDifference is correct private static readonly char[] CodeCharacters = { '2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'T', 'V', 'W', 'X', 'Y' }; - private static readonly SemaphoreSlim ConfirmationsSemaphore = new SemaphoreSlim(1, 1); private static readonly SemaphoreSlim TimeSemaphore = new SemaphoreSlim(1, 1); private static DateTime LastSteamTimeCheck; @@ -439,16 +438,22 @@ namespace ArchiSteamFarm { } private static async Task LimitConfirmationsRequestsAsync() { + if (ASF.ConfirmationsSemaphore == null) { + ASF.ArchiLogger.LogNullError(nameof(ASF.ConfirmationsSemaphore)); + + return; + } + if (ASF.GlobalConfig.ConfirmationsLimiterDelay == 0) { return; } - await ConfirmationsSemaphore.WaitAsync().ConfigureAwait(false); + await ASF.ConfirmationsSemaphore.WaitAsync().ConfigureAwait(false); Utilities.InBackground( async () => { await Task.Delay(ASF.GlobalConfig.ConfirmationsLimiterDelay * 1000).ConfigureAwait(false); - ConfirmationsSemaphore.Release(); + ASF.ConfirmationsSemaphore.Release(); } ); } diff --git a/ArchiSteamFarm/OS.cs b/ArchiSteamFarm/OS.cs index 9eb06f665..61c320bbd 100644 --- a/ArchiSteamFarm/OS.cs +++ b/ArchiSteamFarm/OS.cs @@ -26,6 +26,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Threading; +using ArchiSteamFarm.Helpers; using ArchiSteamFarm.Localization; using JetBrains.Annotations; @@ -60,6 +61,25 @@ namespace ArchiSteamFarm { } } + internal static ICrossProcessSemaphore CreateCrossProcessSemaphore(string objectName) { + if (string.IsNullOrEmpty(objectName)) { + ASF.ArchiLogger.LogNullError(nameof(objectName)); + + return null; + } + + string resourceName = GetOsResourceName(objectName); + + try { + return new CrossProcessSemaphore(resourceName); + } catch (PlatformNotSupportedException e) { + // CrossProcessSemaphore is currently available only for Windows platforms, we use alternative synchronization for other OSes + ASF.ArchiLogger.LogGenericDebuggingException(e); + + return new CrossProcessMutexBasedSemaphore(resourceName); + } + } + internal static void Init(bool systemRequired, GlobalConfig.EOptimizationMode optimizationMode) { if (!Enum.IsDefined(typeof(GlobalConfig.EOptimizationMode), optimizationMode)) { ASF.ArchiLogger.LogNullError(nameof(optimizationMode)); @@ -94,7 +114,7 @@ namespace ArchiSteamFarm { return false; } - string uniqueName = "Global\\" + SharedInfo.AssemblyName + "-" + Convert.ToBase64String(Encoding.UTF8.GetBytes(Directory.GetCurrentDirectory())); + string uniqueName = "Global\\" + GetOsResourceName(nameof(SingleInstance)) + "-" + Convert.ToBase64String(Encoding.UTF8.GetBytes(Directory.GetCurrentDirectory())); Mutex singleInstance = new Mutex(true, uniqueName, out bool result); @@ -137,6 +157,16 @@ namespace ArchiSteamFarm { SingleInstance = null; } + private static string GetOsResourceName(string objectName) { + if (string.IsNullOrEmpty(objectName)) { + ASF.ArchiLogger.LogNullError(nameof(objectName)); + + return null; + } + + return SharedInfo.AssemblyName + "-" + objectName; + } + private static void WindowsDisableQuickEditMode() { if (!IsWindows) { return;