Files
UnrealEngine/Engine/Source/Programs/UnrealGameSync/UnrealGameSyncShared/WorkspaceLock.cs
2025-05-18 13:04:45 +08:00

239 lines
5.3 KiB
C#

// 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
{
/// <summary>
/// Encapsulates the state of cross-process workspace lock
/// </summary>
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<Action> _acquireActions = new BlockingCollection<Action>();
readonly CancellationTokenSource _acquireCancellationSource = new CancellationTokenSource();
Thread? _monitorThread;
readonly ManualResetEvent _cancelMonitorEvent = new ManualResetEvent(false);
/// <summary>
/// Callback for the lock state changing
/// </summary>
public event Action<bool>? OnChange;
/// <summary>
/// Constructor
/// </summary>
/// <param name="rootDir">Root directory for the workspace</param>
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();
}
/// <inheritdoc/>
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();
}
/// <summary>
/// Determines if the lock is held by any
/// </summary>
/// <returns>True if the lock is held by any process</returns>
public bool IsLocked() => _locked;
/// <summary>
/// Determines if the lock is held by another process
/// </summary>
/// <returns>True if the lock is held by another process</returns>
public bool IsLockedByOtherProcess() => _acquireCount == 0 && IsLocked();
/// <summary>
/// Attempt to acquire the mutext
/// </summary>
/// <returns></returns>
public async Task<bool> TryAcquireAsync()
{
TaskCompletionSource<bool> result = new TaskCompletionSource<bool>();
_acquireActions.Add(() => TryAcquireInternal(result));
return await result.Task;
}
void TryAcquireInternal(TaskCompletionSource<bool> 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));
}
/// <summary>
/// Release the current mutext
/// </summary>
public async Task ReleaseAsync()
{
TaskCompletionSource<bool> resultTcs = new TaskCompletionSource<bool>();
_acquireActions.Add(() => ReleaseInternal(resultTcs));
await resultTcs.Task;
}
private void ReleaseInternal(TaskCompletionSource<bool> 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");
}
}