// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Concurrent; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using EpicGames.Core; namespace UnrealGameSync { /// /// Encapsulates the state of cross-process workspace lock /// public sealed class WorkspaceLock : IDisposable { const string Prefix = @"Global\ugs-workspace"; readonly Mutex _mutex; readonly object _lockObject = new object(); readonly string _objectName; int _acquireCount; bool _locked; EventWaitHandle? _lockedEvent; Thread? _acquireThread; readonly BlockingCollection _acquireActions = new BlockingCollection(); readonly CancellationTokenSource _acquireCancellationSource = new CancellationTokenSource(); Thread? _monitorThread; readonly ManualResetEvent _cancelMonitorEvent = new ManualResetEvent(false); /// /// Callback for the lock state changing /// public event Action? OnChange; /// /// Constructor /// /// Root directory for the workspace public WorkspaceLock(DirectoryReference rootDir) { #pragma warning disable CA5351 // Do Not Use Broken Cryptographic Algorithms using (MD5 md5 = MD5.Create()) { byte[] idBytes = Encoding.UTF8.GetBytes(rootDir.FullName.ToUpperInvariant()); _objectName = StringUtils.FormatHexString(md5.ComputeHash(idBytes)); } #pragma warning restore CA5351 // Do Not Use Broken Cryptographic Algorithms _mutex = new Mutex(false, $"{Prefix}.{_objectName}.mutex"); _acquireThread = new Thread(AcquireThread); _acquireThread.Name = nameof(AcquireThread); _acquireThread.IsBackground = true; _acquireThread.Start(); _monitorThread = new Thread(MonitorThread); _monitorThread.Name = nameof(MonitorThread); _monitorThread.IsBackground = true; _monitorThread.Start(); } /// public void Dispose() { if (_acquireThread != null) { _acquireCancellationSource.Cancel(); _acquireThread.Join(); _acquireThread = null; } if (_monitorThread != null) { _cancelMonitorEvent.Set(); _monitorThread.Join(); _monitorThread = null; } _lockedEvent?.Dispose(); _acquireCancellationSource.Dispose(); _acquireActions.Dispose(); _cancelMonitorEvent.Dispose(); _mutex.Dispose(); } /// /// Determines if the lock is held by any /// /// True if the lock is held by any process public bool IsLocked() => _locked; /// /// Determines if the lock is held by another process /// /// True if the lock is held by another process public bool IsLockedByOtherProcess() => _acquireCount == 0 && IsLocked(); /// /// Attempt to acquire the mutext /// /// public async Task TryAcquireAsync() { TaskCompletionSource result = new TaskCompletionSource(); _acquireActions.Add(() => TryAcquireInternal(result)); return await result.Task; } void TryAcquireInternal(TaskCompletionSource resultTcs) { bool result; lock (_lockObject) { try { result = _mutex.WaitOne(0); } catch (AbandonedMutexException) { result = true; } if (result && ++_acquireCount == 1) { _lockedEvent = CreateLockedEvent(); _lockedEvent.Set(); } } Task.Run(() => resultTcs.TrySetResult(result)); } /// /// Release the current mutext /// public async Task ReleaseAsync() { TaskCompletionSource resultTcs = new TaskCompletionSource(); _acquireActions.Add(() => ReleaseInternal(resultTcs)); await resultTcs.Task; } private void ReleaseInternal(TaskCompletionSource resultTcs) { lock (_lockObject) { if (_acquireCount > 0) { _mutex.ReleaseMutex(); if (--_acquireCount == 0) { ReleaseLockedEvent(); } } } Task.Run(() => resultTcs.TrySetResult(true)); } private void ReleaseLockedEvent() { if (_lockedEvent != null) { _lockedEvent.Reset(); _lockedEvent.Dispose(); _lockedEvent = null; } } void AcquireThread() { for (; ; ) { try { _acquireActions.Take(_acquireCancellationSource.Token)(); } catch (OperationCanceledException) { break; } } for (; _acquireCount > 0; _acquireCount--) { _mutex.ReleaseMutex(); } ReleaseLockedEvent(); } void MonitorThread() { _locked = IsLocked(); for (; ; ) { if (_locked) { try { int idx = WaitHandle.WaitAny(new WaitHandle[] { _mutex, _cancelMonitorEvent }); if (idx == 1) { break; } } catch (AbandonedMutexException) { } _mutex.ReleaseMutex(); } else { using EventWaitHandle lockedEvent = CreateLockedEvent(); int idx = WaitHandle.WaitAny(new WaitHandle[] { lockedEvent, _cancelMonitorEvent }); if (idx == 1) { break; } } _locked ^= true; OnChange?.Invoke(!_locked); } } EventWaitHandle CreateLockedEvent() => new EventWaitHandle(false, EventResetMode.ManualReset, $"{Prefix}.{_objectName}.locked"); } }