From 0f1b3f05f970f0bfc42be7a43d1dfe9f5e2e9abe Mon Sep 17 00:00:00 2001 From: JustArchi Date: Thu, 21 May 2020 23:02:04 +0200 Subject: [PATCH] Implement CrossProcessFileBasedSemaphore in place of CrossProcessMutexBasedSemaphore Hopefully this one works better --- ArchiSteamFarm/ASF.cs | 2 +- ArchiSteamFarm/ArchiSteamFarm.csproj | 1 + .../Helpers/CrossProcessFileBasedSemaphore.cs | 173 +++++++++++++++++ .../CrossProcessMutexBasedSemaphore.cs | 177 ------------------ .../Helpers/NonCrossProcessLocalSemaphore.cs | 49 ----- ArchiSteamFarm/OS.cs | 41 ++-- 6 files changed, 193 insertions(+), 250 deletions(-) create mode 100644 ArchiSteamFarm/Helpers/CrossProcessFileBasedSemaphore.cs delete mode 100644 ArchiSteamFarm/Helpers/CrossProcessMutexBasedSemaphore.cs delete mode 100644 ArchiSteamFarm/Helpers/NonCrossProcessLocalSemaphore.cs diff --git a/ArchiSteamFarm/ASF.cs b/ArchiSteamFarm/ASF.cs index efd09a7a7..58175a9a3 100644 --- a/ArchiSteamFarm/ASF.cs +++ b/ArchiSteamFarm/ASF.cs @@ -305,7 +305,7 @@ namespace ArchiSteamFarm { string executable = Path.Combine(SharedInfo.HomeDirectory, SharedInfo.AssemblyName); if (File.Exists(executable)) { - OS.UnixSetFileAccessExecutable(executable); + OS.UnixSetFileAccess(executable, OS.EUnixPermission.Combined755); } } diff --git a/ArchiSteamFarm/ArchiSteamFarm.csproj b/ArchiSteamFarm/ArchiSteamFarm.csproj index 1c4b90e69..5a836e0fb 100644 --- a/ArchiSteamFarm/ArchiSteamFarm.csproj +++ b/ArchiSteamFarm/ArchiSteamFarm.csproj @@ -69,6 +69,7 @@ + diff --git a/ArchiSteamFarm/Helpers/CrossProcessFileBasedSemaphore.cs b/ArchiSteamFarm/Helpers/CrossProcessFileBasedSemaphore.cs new file mode 100644 index 000000000..d9d04c222 --- /dev/null +++ b/ArchiSteamFarm/Helpers/CrossProcessFileBasedSemaphore.cs @@ -0,0 +1,173 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// 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.IO; +using System.Security.AccessControl; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace ArchiSteamFarm.Helpers { + internal sealed class CrossProcessFileBasedSemaphore : ICrossProcessSemaphore { + private const int SpinLockDelay = 1000; // In milliseconds + + private readonly string FilePath; + private readonly SemaphoreSlim LocalSemaphore = new SemaphoreSlim(1, 1); + + private FileStream FileLock; + + internal CrossProcessFileBasedSemaphore([NotNull] string name) { + if (string.IsNullOrEmpty(name)) { + throw new ArgumentNullException(nameof(name)); + } + + FilePath = Path.Combine(Path.GetTempPath(), SharedInfo.ASF, name); + + EnsureFileExists(); + } + + public void Dispose() { + LocalSemaphore.Dispose(); + + FileLock?.Dispose(); + } + + void ICrossProcessSemaphore.Release() { + lock (LocalSemaphore) { + if (FileLock == null) { + throw new ArgumentNullException(nameof(FileLock)); + } + + FileLock.Dispose(); + FileLock = null; + } + + LocalSemaphore.Release(); + } + + async Task ICrossProcessSemaphore.WaitAsync() { + await LocalSemaphore.WaitAsync().ConfigureAwait(false); + + bool success = false; + + try { + lock (LocalSemaphore) { + if (FileLock != null) { + throw new ArgumentNullException(nameof(FileLock)); + } + + while (true) { + EnsureFileExists(); + + try { + FileLock = new FileStream(FilePath, FileMode.OpenOrCreate, FileAccess.Read, FileShare.None); + success = true; + + return; + } catch (IOException) { + Thread.Sleep(SpinLockDelay); + } + } + } + } finally { + if (!success) { + LocalSemaphore.Release(); + } + } + } + + async Task ICrossProcessSemaphore.WaitAsync(int millisecondsTimeout) { + Stopwatch stopwatch = Stopwatch.StartNew(); + + if (!await LocalSemaphore.WaitAsync(millisecondsTimeout).ConfigureAwait(false)) { + stopwatch.Stop(); + + return false; + } + + stopwatch.Stop(); + + bool success = false; + + try { + millisecondsTimeout -= (int) stopwatch.ElapsedMilliseconds; + + if (millisecondsTimeout <= 0) { + return false; + } + + lock (LocalSemaphore) { + if (FileLock != null) { + throw new ArgumentNullException(nameof(FileLock)); + } + + while (true) { + EnsureFileExists(); + + try { + FileLock = new FileStream(FilePath, FileMode.OpenOrCreate, FileAccess.Read, FileShare.None); + success = true; + + return true; + } catch (IOException) { + if (millisecondsTimeout <= SpinLockDelay) { + return false; + } + + Thread.Sleep(SpinLockDelay); + millisecondsTimeout -= SpinLockDelay; + } + } + } + } finally { + if (!success) { + LocalSemaphore.Release(); + } + } + } + + private void EnsureFileExists() { + Directory.CreateDirectory(Path.GetDirectoryName(FilePath)); + + FileInfo fileInfo = new FileInfo(FilePath); + + if (fileInfo.Exists) { + return; + } + + try { + using (new FileStream(FilePath, FileMode.CreateNew)) { } + + if (OS.IsUnix) { + OS.UnixSetFileAccess(FilePath, OS.EUnixPermission.Combined777); + } else { + FileSecurity fileSecurity = new FileSecurity(FilePath, AccessControlSections.All); + + fileInfo.SetAccessControl(fileSecurity); + } + } catch (IOException) { + // Ignored, if the file was already created in the meantime by another instance, this is fine + } + } + } +} diff --git a/ArchiSteamFarm/Helpers/CrossProcessMutexBasedSemaphore.cs b/ArchiSteamFarm/Helpers/CrossProcessMutexBasedSemaphore.cs deleted file mode 100644 index 87a8033c8..000000000 --- a/ArchiSteamFarm/Helpers/CrossProcessMutexBasedSemaphore.cs +++ /dev/null @@ -1,177 +0,0 @@ -// _ _ _ ____ _ _____ -// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ -// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ -// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | -// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| -// | -// 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, other ASF process has been closed - } - - ReleasedTask = new TaskCompletionSource(); - Mutex = mutex; - - return true; - } - } - } finally { - LocalSemaphore.Release(); - } - } - } -} diff --git a/ArchiSteamFarm/Helpers/NonCrossProcessLocalSemaphore.cs b/ArchiSteamFarm/Helpers/NonCrossProcessLocalSemaphore.cs deleted file mode 100644 index 0e0a07a68..000000000 --- a/ArchiSteamFarm/Helpers/NonCrossProcessLocalSemaphore.cs +++ /dev/null @@ -1,49 +0,0 @@ -// _ _ _ ____ _ _____ -// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ -// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ -// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | -// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| -// | -// 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 NonCrossProcessLocalSemaphore : ICrossProcessSemaphore { - private readonly SemaphoreSlim LocalSemaphore; - - internal NonCrossProcessLocalSemaphore([NotNull] string name) { - if (string.IsNullOrEmpty(name)) { - throw new ArgumentNullException(nameof(name)); - } - - LocalSemaphore = new SemaphoreSlim(1, 1); - } - - public void Dispose() => LocalSemaphore.Dispose(); - - void ICrossProcessSemaphore.Release() => LocalSemaphore.Release(); - - [NotNull] - Task ICrossProcessSemaphore.WaitAsync() => LocalSemaphore.WaitAsync(); - - [NotNull] - Task ICrossProcessSemaphore.WaitAsync(int millisecondsTimeout) => LocalSemaphore.WaitAsync(millisecondsTimeout); - } -} diff --git a/ArchiSteamFarm/OS.cs b/ArchiSteamFarm/OS.cs index f358429fe..fb0d53308 100644 --- a/ArchiSteamFarm/OS.cs +++ b/ArchiSteamFarm/OS.cs @@ -76,10 +76,7 @@ namespace ArchiSteamFarm { // CrossProcessSemaphore is currently available only for Windows platforms, we use alternative synchronization for other OSes ASF.ArchiLogger.LogGenericDebuggingException(e); - // CrossProcessMutexBasedSemaphore disabled until reason for segfault on Linux is found - //return new CrossProcessMutexBasedSemaphore(resourceName); - - return new NonCrossProcessLocalSemaphore(resourceName); + return new CrossProcessFileBasedSemaphore(resourceName); } } @@ -132,7 +129,7 @@ namespace ArchiSteamFarm { return true; } - internal static void UnixSetFileAccessExecutable(string path) { + internal static void UnixSetFileAccess(string path, EUnixPermission permission) { if (string.IsNullOrEmpty(path) || !File.Exists(path)) { ASF.ArchiLogger.LogNullError(nameof(path)); @@ -144,7 +141,7 @@ namespace ArchiSteamFarm { } // Chmod() returns 0 on success, -1 on failure - if (NativeMethods.Chmod(path, (int) NativeMethods.UnixExecutePermission) != 0) { + if (NativeMethods.Chmod(path, (int) permission) != 0) { ASF.ArchiLogger.LogGenericError(string.Format(Strings.WarningFailedWithError, Marshal.GetLastWin32Error())); } } @@ -206,11 +203,25 @@ namespace ArchiSteamFarm { } } + [Flags] + internal enum EUnixPermission : ushort { + OtherExecute = 0x1, + OtherWrite = 0x2, + OtherRead = 0x4, + GroupExecute = 0x8, + GroupWrite = 0x10, + GroupRead = 0x20, + UserExecute = 0x40, + UserWrite = 0x80, + UserRead = 0x100, + Combined755 = UserRead | UserWrite | UserExecute | GroupRead | GroupExecute | OtherRead | OtherExecute, + Combined777 = UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherWrite | OtherExecute + } + private static class NativeMethods { internal const EExecutionState AwakeExecutionState = EExecutionState.SystemRequired | EExecutionState.AwayModeRequired | EExecutionState.Continuous; internal const uint EnableQuickEditMode = 0x0040; internal const sbyte StandardInputHandle = -10; - internal const EUnixPermission UnixExecutePermission = EUnixPermission.UserRead | EUnixPermission.UserWrite | EUnixPermission.UserExecute | EUnixPermission.GroupRead | EUnixPermission.GroupExecute | EUnixPermission.OtherRead | EUnixPermission.OtherExecute; [DllImport("libc", EntryPoint = "chmod", SetLastError = true)] internal static extern int Chmod(string path, int mode); @@ -234,22 +245,6 @@ namespace ArchiSteamFarm { AwayModeRequired = 0x00000040, Continuous = 0x80000000 } - - [Flags] - internal enum EUnixPermission : ushort { - OtherExecute = 0x1, - OtherRead = 0x4, - GroupExecute = 0x8, - GroupRead = 0x20, - UserExecute = 0x40, - UserWrite = 0x80, - UserRead = 0x100 - - /* - OtherWrite = 0x2 - GroupWrite = 0x10 - */ - } } } }