2024-03-17 00:06:13 +01:00
// ----------------------------------------------------------------------------------------------
2020-05-21 23:02:04 +02:00
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
2024-03-17 00:06:13 +01:00
// ----------------------------------------------------------------------------------------------
2024-03-17 02:35:40 +01:00
// |
2024-01-08 11:33:28 +01:00
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
2020-05-21 23:02:04 +02:00
// Contact: JustArchi@JustArchi.net
2024-03-17 02:35:40 +01:00
// |
2020-05-21 23:02:04 +02:00
// 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
2024-03-17 02:35:40 +01:00
// |
2020-05-21 23:02:04 +02:00
// http://www.apache.org/licenses/LICENSE-2.0
2024-03-17 02:35:40 +01:00
// |
2020-05-21 23:02:04 +02:00
// 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 ;
2021-11-10 19:03:05 +01:00
using System.Security.AccessControl ;
2020-05-21 23:02:04 +02:00
using System.Threading ;
using System.Threading.Tasks ;
2021-05-08 01:37:22 +02:00
using ArchiSteamFarm.Core ;
2020-05-21 23:02:04 +02:00
2021-11-10 21:23:24 +01:00
namespace ArchiSteamFarm.Helpers ;
2020-05-21 23:02:04 +02:00
2022-06-19 18:24:52 +02:00
internal sealed class CrossProcessFileBasedSemaphore : IAsyncDisposable , ICrossProcessSemaphore , IDisposable {
2023-11-10 19:02:03 +01:00
private const byte SpinLockDelay = 200 ; // In milliseconds
2020-05-21 23:02:04 +02:00
2021-11-10 21:23:24 +01:00
private readonly string FilePath ;
private readonly SemaphoreSlim LocalSemaphore = new ( 1 , 1 ) ;
2020-05-21 23:02:04 +02:00
2021-11-10 21:23:24 +01:00
private FileStream ? FileLock ;
2020-05-21 23:02:04 +02:00
2021-11-10 21:23:24 +01:00
internal CrossProcessFileBasedSemaphore ( string name ) {
2023-11-14 19:12:33 +01:00
ArgumentException . ThrowIfNullOrEmpty ( name ) ;
2020-05-21 23:02:04 +02:00
2021-11-10 21:23:24 +01:00
FilePath = Path . Combine ( Path . GetTempPath ( ) , SharedInfo . ASF , name ) ;
2020-05-21 23:02:04 +02:00
2021-11-10 21:23:24 +01:00
EnsureFileExists ( ) ;
}
2020-05-21 23:02:04 +02:00
2021-11-10 21:23:24 +01:00
public void Dispose ( ) {
2022-06-19 18:24:52 +02:00
// Those are objects that are always being created if constructor doesn't throw exception
2021-11-10 21:23:24 +01:00
LocalSemaphore . Dispose ( ) ;
2020-05-21 23:02:04 +02:00
2022-06-19 18:24:52 +02:00
// Those are objects that might be null and the check should be in-place
2021-11-10 21:23:24 +01:00
FileLock ? . Dispose ( ) ;
}
2022-06-19 18:24:52 +02:00
public async ValueTask DisposeAsync ( ) {
// Those are objects that are always being created if constructor doesn't throw exception
LocalSemaphore . Dispose ( ) ;
// Those are objects that might be null and the check should be in-place
if ( FileLock ! = null ) {
await FileLock . DisposeAsync ( ) . ConfigureAwait ( false ) ;
}
}
2021-11-10 21:23:24 +01:00
void ICrossProcessSemaphore . Release ( ) {
// ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
lock ( LocalSemaphore ) {
if ( FileLock = = null ) {
throw new InvalidOperationException ( nameof ( FileLock ) ) ;
2020-05-21 23:02:04 +02:00
}
2021-11-10 21:23:24 +01:00
FileLock . Dispose ( ) ;
FileLock = null ;
2020-05-21 23:02:04 +02:00
}
2021-11-10 21:23:24 +01:00
LocalSemaphore . Release ( ) ;
}
2023-11-14 20:58:02 +01:00
async Task ICrossProcessSemaphore . WaitAsync ( CancellationToken cancellationToken ) {
await LocalSemaphore . WaitAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
2020-05-21 23:02:04 +02:00
2021-11-10 21:23:24 +01:00
bool success = false ;
2020-05-21 23:02:04 +02:00
2021-11-10 21:23:24 +01:00
try {
while ( true ) {
try {
// ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
lock ( LocalSemaphore ) {
if ( FileLock ! = null ) {
throw new InvalidOperationException ( nameof ( FileLock ) ) ;
}
2020-05-21 23:02:04 +02:00
2021-11-10 21:23:24 +01:00
EnsureFileExists ( ) ;
2020-05-21 23:02:04 +02:00
2021-11-10 21:23:24 +01:00
FileLock = new FileStream ( FilePath , FileMode . OpenOrCreate , FileAccess . Read , FileShare . None ) ;
success = true ;
2020-05-21 23:02:04 +02:00
2021-11-10 21:23:24 +01:00
return ;
2020-05-21 23:02:04 +02:00
}
2021-11-10 21:23:24 +01:00
} catch ( IOException ) {
2023-11-14 20:58:02 +01:00
await Task . Delay ( SpinLockDelay , cancellationToken ) . ConfigureAwait ( false ) ;
2020-05-21 23:02:04 +02:00
}
}
2021-11-10 21:23:24 +01:00
} finally {
if ( ! success ) {
LocalSemaphore . Release ( ) ;
}
2020-05-21 23:02:04 +02:00
}
2021-11-10 21:23:24 +01:00
}
2020-05-21 23:02:04 +02:00
2023-11-14 20:58:02 +01:00
async Task < bool > ICrossProcessSemaphore . WaitAsync ( int millisecondsTimeout , CancellationToken cancellationToken ) {
2021-11-10 21:23:24 +01:00
Stopwatch stopwatch = Stopwatch . StartNew ( ) ;
2020-05-21 23:02:04 +02:00
2023-11-14 20:58:02 +01:00
if ( ! await LocalSemaphore . WaitAsync ( millisecondsTimeout , cancellationToken ) . ConfigureAwait ( false ) ) {
2021-11-10 21:23:24 +01:00
stopwatch . Stop ( ) ;
2020-05-21 23:02:04 +02:00
2021-11-10 21:23:24 +01:00
return false ;
}
2020-05-21 23:02:04 +02:00
2021-11-10 21:23:24 +01:00
bool success = false ;
2020-05-22 10:45:48 +02:00
2021-11-10 21:23:24 +01:00
try {
stopwatch . Stop ( ) ;
2020-05-21 23:02:04 +02:00
2023-11-10 19:02:03 +01:00
if ( stopwatch . ElapsedMilliseconds > = millisecondsTimeout ) {
2022-04-02 16:00:07 +02:00
return false ;
}
2021-11-10 21:23:24 +01:00
millisecondsTimeout - = ( int ) stopwatch . ElapsedMilliseconds ;
2020-05-21 23:02:04 +02:00
2021-11-10 21:23:24 +01:00
while ( true ) {
try {
// ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
lock ( LocalSemaphore ) {
if ( FileLock ! = null ) {
throw new InvalidOperationException ( nameof ( FileLock ) ) ;
}
2020-05-21 23:08:02 +02:00
2021-11-10 21:23:24 +01:00
EnsureFileExists ( ) ;
2020-05-21 23:02:04 +02:00
2021-11-10 21:23:24 +01:00
FileLock = new FileStream ( FilePath , FileMode . OpenOrCreate , FileAccess . Read , FileShare . None ) ;
success = true ;
2020-05-22 10:45:48 +02:00
2021-11-10 21:23:24 +01:00
return true ;
2020-05-21 23:08:02 +02:00
}
2021-11-10 21:23:24 +01:00
} catch ( IOException ) {
if ( millisecondsTimeout < = SpinLockDelay ) {
return false ;
}
2023-11-14 20:58:02 +01:00
await Task . Delay ( SpinLockDelay , cancellationToken ) . ConfigureAwait ( false ) ;
2021-11-10 21:23:24 +01:00
millisecondsTimeout - = SpinLockDelay ;
2020-05-21 23:02:04 +02:00
}
}
2021-11-10 21:23:24 +01:00
} finally {
if ( ! success ) {
LocalSemaphore . Release ( ) ;
}
2020-05-21 23:02:04 +02:00
}
2021-11-10 21:23:24 +01:00
}
2020-05-21 23:02:04 +02:00
2021-11-10 21:23:24 +01:00
private void EnsureFileExists ( ) {
if ( File . Exists ( FilePath ) ) {
return ;
}
2020-05-21 23:02:04 +02:00
2021-11-10 21:23:24 +01:00
string? directoryPath = Path . GetDirectoryName ( FilePath ) ;
2020-05-21 23:31:07 +02:00
2021-11-10 21:23:24 +01:00
if ( string . IsNullOrEmpty ( directoryPath ) ) {
2022-04-13 23:16:36 +02:00
ASF . ArchiLogger . LogNullError ( directoryPath ) ;
2020-05-21 23:31:07 +02:00
2021-11-10 21:23:24 +01:00
return ;
}
2020-05-21 23:31:07 +02:00
2023-11-14 20:01:29 +01:00
if ( ! Directory . Exists ( directoryPath ) ) {
DirectoryInfo directoryInfo = Directory . CreateDirectory ( directoryPath ) ;
2020-05-21 23:31:07 +02:00
2021-11-10 21:23:24 +01:00
if ( OperatingSystem . IsWindows ( ) ) {
try {
DirectorySecurity directorySecurity = new ( directoryPath , AccessControlSections . All ) ;
2020-06-16 10:04:51 +02:00
2021-11-10 21:23:24 +01:00
directoryInfo . SetAccessControl ( directorySecurity ) ;
} catch ( PrivilegeNotHeldException e ) {
// Non-critical, user might have no rights to manage the resource
ASF . ArchiLogger . LogGenericDebuggingException ( e ) ;
2020-05-21 23:31:07 +02:00
}
2021-11-10 21:23:24 +01:00
} else if ( OperatingSystem . IsFreeBSD ( ) | | OperatingSystem . IsLinux ( ) | | OperatingSystem . IsMacOS ( ) ) {
2023-02-09 02:25:11 +01:00
directoryInfo . UnixFileMode | = UnixFileMode . UserRead | UnixFileMode . UserWrite | UnixFileMode . UserExecute | UnixFileMode . GroupRead | UnixFileMode . GroupWrite | UnixFileMode . GroupExecute | UnixFileMode . OtherRead | UnixFileMode . OtherWrite | UnixFileMode . OtherExecute ;
2020-05-21 23:31:07 +02:00
}
2021-11-10 21:23:24 +01:00
}
2020-05-21 23:31:07 +02:00
2021-11-10 21:23:24 +01:00
try {
new FileStream ( FilePath , FileMode . CreateNew ) . Dispose ( ) ;
2020-05-21 23:02:04 +02:00
2023-02-09 02:25:11 +01:00
FileInfo fileInfo = new ( FilePath ) ;
2020-05-21 23:02:04 +02:00
2023-02-09 02:25:11 +01:00
if ( OperatingSystem . IsWindows ( ) ) {
2021-11-10 21:23:24 +01:00
try {
FileSecurity fileSecurity = new ( FilePath , AccessControlSections . All ) ;
2020-06-16 10:04:51 +02:00
2021-11-10 21:23:24 +01:00
fileInfo . SetAccessControl ( fileSecurity ) ;
} catch ( PrivilegeNotHeldException e ) {
// Non-critical, user might have no rights to manage the resource
ASF . ArchiLogger . LogGenericDebuggingException ( e ) ;
2020-05-21 23:02:04 +02:00
}
2021-11-10 21:23:24 +01:00
} else if ( OperatingSystem . IsFreeBSD ( ) | | OperatingSystem . IsLinux ( ) | | OperatingSystem . IsMacOS ( ) ) {
2023-02-09 02:25:11 +01:00
fileInfo . UnixFileMode | = UnixFileMode . UserRead | UnixFileMode . UserWrite | UnixFileMode . UserExecute | UnixFileMode . GroupRead | UnixFileMode . GroupWrite | UnixFileMode . GroupExecute | UnixFileMode . OtherRead | UnixFileMode . OtherWrite | UnixFileMode . OtherExecute ;
2020-05-21 23:02:04 +02:00
}
2021-11-10 21:23:24 +01:00
} catch ( IOException ) {
// Ignored, if the file was already created in the meantime by another instance, this is fine
2020-05-21 23:02:04 +02:00
}
}
}