File: src\runtime\src\libraries\System.Private.CoreLib\src\System\Threading\NamedMutex.Unix.cs
Web Access
Project: System.Private.CoreLib.csproj (System.Private.CoreLib)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

namespace System.Threading
{
    internal sealed class NamedMutexOwnershipChain(Thread thread)
    {
        private readonly Thread _thread = thread;
        private NamedMutexProcessDataBase? _head;

        public void Add(NamedMutexProcessDataBase namedMutex)
        {
            SharedMemoryManager<NamedMutexProcessDataBase>.Instance.VerifyCreationDeletionProcessLockIsLocked();
            namedMutex.NextOwnedNamedMutex = _head;
            _head = namedMutex;
        }

        public void Remove(NamedMutexProcessDataBase namedMutex)
        {
            SharedMemoryManager<NamedMutexProcessDataBase>.Instance.VerifyCreationDeletionProcessLockIsLocked();
            if (_head == namedMutex)
            {
                _head = namedMutex.NextOwnedNamedMutex;
            }
            else
            {
                NamedMutexProcessDataBase? previous = _head;
                while (previous?.NextOwnedNamedMutex != namedMutex)
                {
                    previous = previous?.NextOwnedNamedMutex;
                }

                previous?.NextOwnedNamedMutex = namedMutex.NextOwnedNamedMutex;
            }

            namedMutex.NextOwnedNamedMutex = null;
        }

        public void Abandon()
        {
            WaitSubsystem.LockHolder scope = SharedMemoryManager<NamedMutexProcessDataBase>.Instance.AcquireCreationDeletionProcessLock();
            try
            {
                while (true)
                {
                    NamedMutexProcessDataBase? namedMutex = _head;
                    if (namedMutex == null)
                    {
                        break;
                    }

                    namedMutex.Abandon(this, _thread);
                    Debug.Assert(_head != namedMutex);
                }
            }
            finally
            {
                scope.Dispose();
            }
        }
    }

    internal abstract class NamedMutexProcessDataBase(SharedMemoryProcessDataHeader<NamedMutexProcessDataBase> header) : ISharedMemoryProcessData
    {
        // The shared data layout must stay compatible with the CoreCLR PAL implementation used by .NET 10 and earlier
        // https://github.com/dotnet/runtime/blob/release/10.0/src/coreclr/pal/src/synchobj/mutex.cpp, which
        // uses the same shared memory files and the same SyncSystemVersion.
        // Processes from both implementations may open the same named mutex at the same time.
        private const byte SyncSystemVersion = 1;
        protected const int PollLoopMaximumSleepMilliseconds = 100;
        protected const uint InvalidProcessId = unchecked((uint)-1);
        protected const ulong InvalidThreadId = unchecked((ulong)-1);

        // Use PThread mutex-backed named mutexes if possible.
        // macOS has support for the features we need in the pthread mutexes on arm64
        // but not in the Rosetta 2 x64 emulation layer.
        // Until Rosetta 2 is removed, we need to use the non-PThread mutexes on Apple platforms.
        // On FreeBSD, pthread process-shared robust mutexes cannot be placed in shared memory mapped
        // independently by the processes involved. See https://github.com/dotnet/runtime/issues/10519.
        // On OpenBSD and OpenHarmony, cross process mutexes are not supported in the pthread implementation.
        // On Haiku, robust mutexes are WIP. See https://github.com/dotnet/runtime/pull/126701#issuecomment-4334338213.
        // On Linux arm and arm64, we do not use PThread mutex-backed named mutexes for compatibility with previous .NET versions.
        private static bool UsePThreadMutexes =>
#if (TARGET_ARM || TARGET_ARM64)
            !OperatingSystem.IsLinux() &&
#endif
            !OperatingSystem.IsApplePlatform() && !OperatingSystem.IsFreeBSD() && !OperatingSystem.IsOpenBSD() && !OperatingSystem.IsHaiku() && !OperatingSystem.IsOpenHarmony();

        private readonly SharedMemoryProcessDataHeader<NamedMutexProcessDataBase> _processDataHeader = header;
        protected nuint _lockCount;
        private Thread? _lockOwnerThread;

        public SharedMemoryId Id => _processDataHeader._id;

        public NamedMutexProcessDataBase? NextOwnedNamedMutex { get; set; }

        public bool IsLockOwnedByCurrentThread => IsLockOwnedByThreadInThisProcess(Thread.CurrentThread);
        protected abstract bool IsLockOwnedByThreadInThisProcess(Thread thread);
        public bool IsLockOwnedByAnyThreadInThisProcess => _lockOwnerThread is not null;

        protected abstract void SetLockOwnerToCurrentThread();

        public UnrecordedMutexAcquisition TryAcquireLock(int timeoutMilliseconds)
        {
            if (timeoutMilliseconds != 0)
            {
                // We can't have the creation/deletion process lock held while trying to acquire a lock with a non-zero timeout
                // as this can lead to a deadlock or livelock situation between multiple different cross-process mutexes
                // used across multiple threads within the same process and across processes.
                SharedMemoryManager<NamedMutexProcessDataBase>.Instance.VerifyCreationDeletionProcessLockIsNotLocked();
            }

            return new UnrecordedMutexAcquisition(this, AcquireLockCore(timeoutMilliseconds));
        }

        public void ReleaseLock()
        {
            if (!IsLockOwnedByCurrentThread)
            {
                throw new InvalidOperationException(SR.Arg_InvalidOperationException_CannotReleaseUnownedMutex);
            }

            --_lockCount;
            if (_lockCount != 0)
            {
                return;
            }

            WaitSubsystem.LockHolder scope = SharedMemoryManager<NamedMutexProcessDataBase>.Instance.AcquireCreationDeletionProcessLock();
            try
            {
                Thread.CurrentThread.WaitInfo.NamedMutexOwnershipChain.Remove(this);
                _lockOwnerThread = null;
                ReleaseLockCore(abandoning: false);
                // Remove the refcount from the thread's wait info.
                _processDataHeader.DecrementRefCount();
            }
            finally
            {
                scope.Dispose();
            }
        }

        public void Abandon(NamedMutexOwnershipChain chain, Thread abandonedThread)
        {
            SharedMemoryManager<NamedMutexProcessDataBase>.Instance.VerifyCreationDeletionProcessLockIsLocked();

            // This method can be called from one of two threads:
            // 1. The dead thread that owns the mutex.
            //   In the first case, we know that no one else can acquire the lock, so we can safely
            //   set the lock count to 0 and abandon the mutex.
            // 2. The finalizer thread after the owning thread has died.
            //   In this case, its possible for the mutex to have been acquired by another thread
            //   after the owning thread died if it is backed by a pthread mutex.
            //   A pthread mutex doesn't need our Abandon() call to be abandoned, it will be automatically abandoned
            //   when the owning thread dies.
            //   In this case, we don't want to do anything. Our named mutex implementation will handle the abandonment
            //   as part of acquisition.
            Debug.Assert(IsLockOwnedByCurrentThread || (!abandonedThread.IsAlive && Thread.CurrentThreadIsFinalizerThread()),
                "Abandon can only be called from the thread that owns the lock or from the finalizer thread when the owning thread is dead.");

            if (!IsLockOwnedByThreadInThisProcess(abandonedThread))
            {
                Debug.Assert(Thread.CurrentThreadIsFinalizerThread());
                // Lock is owned by a different thread already, so we don't need to do anything.
                // Just remove it from the list of owned named mutexes.
                chain.Remove(this);
                return;
            }

            IsAbandoned = true;

            _lockCount = 0;

            Debug.Assert(_lockOwnerThread is not null);

            ReleaseLockCore(abandoning: true);

            chain.Remove(this);
            _lockOwnerThread = null;
            // Remove the refcount from the thread's wait info.
            _processDataHeader.DecrementRefCount();
        }

        protected abstract MutexTryAcquireLockResult AcquireLockCore(int timeoutMilliseconds);

        protected abstract bool IsAbandoned { get; set; }

        protected abstract void ReleaseLockCore(bool abandoning);

        internal static unsafe SharedMemoryProcessDataHeader<NamedMutexProcessDataBase>? CreateOrOpen(string name, bool isUserScope, bool createIfNotExist, bool acquireLockIfCreated, out bool created)
        {
            WaitSubsystem.LockHolder creationDeletionProcessLock = SharedMemoryManager<NamedMutexProcessDataBase>.Instance.AcquireCreationDeletionProcessLock();
            try
            {

                SharedMemoryProcessDataHeader<NamedMutexProcessDataBase>? processDataHeader = SharedMemoryProcessDataHeader<NamedMutexProcessDataBase>.CreateOrOpen(
                    name,
                    isUserScope,
                    new SharedMemorySharedDataHeader(SharedMemoryType.Mutex, SyncSystemVersion),
                    UsePThreadMutexes ? NamedMutexProcessDataWithPThreads.SharedDataSize : (nuint)sizeof(NamedMutexProcessDataNoPThreads.SharedData),
                    createIfNotExist,
                    acquireLockIfCreated,
                    out created,
                    out AutoReleaseFileLock creationDeletionLockFileScope);

                if (processDataHeader is null)
                {
                    return null;
                }

                using (creationDeletionLockFileScope)
                {
                    if (created)
                    {
                        InitializeSharedData(SharedMemoryProcessDataHeader<NamedMutexProcessDataBase>.GetDataPointer(processDataHeader));
                    }

                    if (processDataHeader._processData is null)
                    {
                        if (UsePThreadMutexes)
                        {
                            processDataHeader._processData = new NamedMutexProcessDataWithPThreads(processDataHeader);
                        }
                        else
                        {
                            processDataHeader._processData = new NamedMutexProcessDataNoPThreads(processDataHeader, created);
                        }

                        if (created && acquireLockIfCreated)
                        {
                            NamedMutexProcessDataBase processData = processDataHeader._processData;
                            MutexTryAcquireLockResult acquireResult = processDataHeader._processData.TryAcquireLock(timeoutMilliseconds: 0).RecordMutexAcquisition(Thread.CurrentThread.WaitInfo);
                            if (acquireResult != MutexTryAcquireLockResult.AcquiredLock)
                            {
                                // We created the mutex so we should be able to acquire it immediately.
                                throw new ApplicationException();
                            }
                        }
                    }

                    return processDataHeader;
                }
            }
            finally
            {
                creationDeletionProcessLock.Dispose();
            }
        }

        private static unsafe void InitializeSharedData(void* v)
        {
            if (UsePThreadMutexes)
            {
                if (Interop.Sys.LowLevelCrossProcessMutex_Init(v) != 0)
                {
                    Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
                    throw Interop.GetExceptionForIoErrno(errorInfo, SR.Arg_FailedToInitializePThreadMutex);
                }
            }
            else
            {
                NamedMutexProcessDataNoPThreads.SharedData* sharedData = (NamedMutexProcessDataNoPThreads.SharedData*)v;
                sharedData->LockOwnerProcessId = InvalidProcessId;
                sharedData->LockOwnerThreadId = InvalidThreadId;
                sharedData->IsAbandoned = false;
                sharedData->TimedWaiterCount = 0;
            }
        }

        public virtual void Close(bool releaseSharedData)
        {
            if (IsLockOwnedByCurrentThread)
            {
                Thread.CurrentThread.WaitInfo.NamedMutexOwnershipChain.Remove(this);
            }
        }

        public struct UnrecordedMutexAcquisition(NamedMutexProcessDataBase mutexData, MutexTryAcquireLockResult rawAcquireResult)
        {
            public MutexTryAcquireLockResult RecordMutexAcquisition(WaitSubsystem.ThreadWaitInfo waitInfo)
            {
                if (rawAcquireResult == MutexTryAcquireLockResult.AcquiredLockRecursively)
                {
                    return MutexTryAcquireLockResult.AcquiredLock;
                }

                if (rawAcquireResult == MutexTryAcquireLockResult.TimedOut)
                {
                    return MutexTryAcquireLockResult.TimedOut;
                }

                SharedMemoryManager<NamedMutexProcessDataBase>.Instance.VerifyCreationDeletionProcessLockIsLocked();

                mutexData.SetLockOwnerToCurrentThread();
                mutexData._lockCount = 1;
                mutexData._lockOwnerThread = waitInfo.Thread;
                // Add the ref count for the thread's wait info.
                mutexData._processDataHeader.IncrementRefCount();
                waitInfo.NamedMutexOwnershipChain.Add(mutexData);

                if (mutexData.IsAbandoned)
                {
                    mutexData.IsAbandoned = false;
                    return MutexTryAcquireLockResult.AcquiredLockButMutexWasAbandoned;
                }

                return rawAcquireResult;
            }
        }
    }

    internal sealed unsafe class NamedMutexProcessDataWithPThreads(SharedMemoryProcessDataHeader<NamedMutexProcessDataBase> processDataHeader) : NamedMutexProcessDataBase(processDataHeader)
    {
        public static nuint SharedDataSize { get; } = (nuint)Interop.Sys.LowLevelCrossProcessMutex_Size();
        private readonly void* _sharedData = SharedMemoryProcessDataHeader<NamedMutexProcessDataBase>.GetDataPointer(processDataHeader);

        protected override bool IsLockOwnedByThreadInThisProcess(Thread thread)
        {
            Interop.Sys.LowLevelCrossProcessMutex_GetOwnerProcessAndThreadId(_sharedData, out uint ownerProcessId, out ulong ownerThreadId);
            return ownerProcessId == (uint)Environment.ProcessId &&
                   ownerThreadId == (uint)thread.ManagedThreadId;
        }

        protected override void SetLockOwnerToCurrentThread()
        {
            Interop.Sys.LowLevelCrossProcessMutex_SetOwnerProcessAndThreadId(
                _sharedData,
                (uint)Environment.ProcessId,
                (uint)Thread.CurrentThread.ManagedThreadId);
        }

        protected override bool IsAbandoned
        {
            get => Interop.Sys.LowLevelCrossProcessMutex_IsAbandoned(_sharedData);
            set => Interop.Sys.LowLevelCrossProcessMutex_SetAbandoned(_sharedData, value);
        }

        protected override MutexTryAcquireLockResult AcquireLockCore(int timeoutMilliseconds)
        {
            Interop.Error lockResult = (Interop.Error)Interop.Sys.LowLevelCrossProcessMutex_Acquire(_sharedData, timeoutMilliseconds);

            MutexTryAcquireLockResult result = lockResult switch
            {
                Interop.Error.SUCCESS => MutexTryAcquireLockResult.AcquiredLock,
                Interop.Error.EBUSY => MutexTryAcquireLockResult.TimedOut,
                Interop.Error.ETIMEDOUT => MutexTryAcquireLockResult.TimedOut,
                Interop.Error.EOWNERDEAD => MutexTryAcquireLockResult.AcquiredLockButMutexWasAbandoned,
                Interop.Error.EAGAIN => throw new OutOfMemoryException(),
                _ => throw new Win32Exception((int)lockResult)
            };

            if (result == MutexTryAcquireLockResult.TimedOut)
            {
                return MutexTryAcquireLockResult.TimedOut;
            }

            if (result == MutexTryAcquireLockResult.AcquiredLockButMutexWasAbandoned)
            {
                // If the underlying pthread mutex was abandoned, treat this as an abandoned mutex.
                // Its possible that the underlying pthread mutex was abandoned, but the shared data and process data
                // is out of sync. This can happen if the dead thread did not call Abandon() before it exited
                // and is relying on the finalizer to clean it up.
                return result;
            }

            if (_lockCount != 0)
            {
                Debug.Assert(IsLockOwnedByCurrentThread);
                Debug.Assert(!IsAbandoned);
                try
                {
                    checked
                    {
                        _lockCount++;
                    }
                    return MutexTryAcquireLockResult.AcquiredLockRecursively;
                }
                finally
                {
                    // The lock is released upon acquiring a recursive lock from the thread that already owns the lock
                    Interop.Sys.LowLevelCrossProcessMutex_Release(_sharedData);
                }
            }

            return result;
        }

        protected override void ReleaseLockCore(bool abandoning)
        {
            Debug.Assert(_lockCount == 0);
            Interop.Sys.LowLevelCrossProcessMutex_SetOwnerProcessAndThreadId(
                _sharedData,
                InvalidProcessId,
                InvalidThreadId);

            Interop.Sys.LowLevelCrossProcessMutex_Release(_sharedData);
        }

        public override void Close(bool releaseSharedData)
        {
            base.Close(releaseSharedData);

            if (releaseSharedData)
            {
                // Release the pthread mutex.
                Interop.Sys.LowLevelCrossProcessMutex_Destroy(_sharedData);
            }
        }
    }

    internal sealed unsafe class NamedMutexProcessDataNoPThreads : NamedMutexProcessDataBase
    {
        private const string SharedMemoryLockFilesDirectoryName = "lockfiles";
        private readonly Mutex _processLevelMutex = new Mutex(initiallyOwned: false);
        private readonly SafeFileHandle _sharedLockFileHandle;

        private readonly SharedData* _sharedData;

        public NamedMutexProcessDataNoPThreads(SharedMemoryProcessDataHeader<NamedMutexProcessDataBase> processDataHeader, bool created) : base(processDataHeader)
        {
            _sharedData = (SharedData*)SharedMemoryProcessDataHeader<NamedMutexProcessDataBase>.GetDataPointer(processDataHeader);
            string lockFileDirectory = Path.Combine(
                SharedMemoryHelpers.SharedFilesPath,
                Id.GetRuntimeTempDirectoryName(),
                SharedMemoryLockFilesDirectoryName
            );

            if (created)
            {
                SharedMemoryHelpers.EnsureDirectoryExists(lockFileDirectory, Id, isGlobalLockAcquired: true);
            }

            string sessionDirectory = Path.Combine(lockFileDirectory, Id.GetSessionDirectoryName());

            if (created)
            {
                SharedMemoryHelpers.EnsureDirectoryExists(sessionDirectory, Id, isGlobalLockAcquired: true);
            }

            string lockFilePath = Path.Combine(sessionDirectory, Id.Name);
            _sharedLockFileHandle = SharedMemoryHelpers.CreateOrOpenFile(lockFilePath, Id, created, out _);
        }

        protected override bool IsLockOwnedByThreadInThisProcess(Thread thread)
        {
            return _sharedData->LockOwnerProcessId == (uint)Environment.ProcessId &&
                   _sharedData->LockOwnerThreadId == (uint)thread.ManagedThreadId;
        }

        protected override void SetLockOwnerToCurrentThread()
        {
            _sharedData->LockOwnerProcessId = (uint)Environment.ProcessId;
            _sharedData->LockOwnerThreadId = (uint)Thread.CurrentThread.ManagedThreadId;
        }

        private bool IsLockOwnedByAnyThread
        {
            get
            {
                return _sharedData->LockOwnerProcessId != InvalidProcessId &&
                       _sharedData->LockOwnerThreadId != InvalidThreadId;
            }
        }

        protected override void ReleaseLockCore(bool abandoning)
        {
            Debug.Assert(_lockCount == 0);
            _sharedData->LockOwnerProcessId = InvalidProcessId;
            _sharedData->LockOwnerThreadId = InvalidThreadId;

            Interop.Sys.FLock(_sharedLockFileHandle, Interop.Sys.LockOperations.LOCK_UN);
            if (!abandoning)
            {
                // We're going to abandon this mutex, so don't release it.
                // We might not be on the owning thread, so we can't be sure we can release it.
                _processLevelMutex.ReleaseMutex();
            }
        }

        protected override bool IsAbandoned
        {
            get => _sharedData->IsAbandoned;
            set => _sharedData->IsAbandoned = value;
        }

        public override void Close(bool releaseSharedData)
        {
            base.Close(releaseSharedData);

            _sharedLockFileHandle.Dispose();
            _processLevelMutex.Dispose();
        }

        protected override unsafe MutexTryAcquireLockResult AcquireLockCore(int timeoutMilliseconds)
        {
            int startTime = 0;
            if (timeoutMilliseconds > 0)
            {
                startTime = Environment.TickCount;
            }

            // Acquire the process lock. A file lock can only be acquired once per file descriptor, so to synchronize the threads of
            // this process, the process lock is used.
            bool releaseProcessLock = true;
            try
            {
                if (!_processLevelMutex.WaitOne(timeoutMilliseconds))
                {
                    return MutexTryAcquireLockResult.TimedOut;
                }
            }
            catch (AbandonedMutexException)
            {
                // If the process-level lock was abandoned, the shared mutex will also have been abandoned.
                // We don't need to do anything here. We'll acquire the shared lock below and throw for abandonment as expected.
            }

            try
            {
                if (_lockCount > 0)
                {
                    Debug.Assert(IsLockOwnedByCurrentThread);
                    // The lock is already owned by the current thread.
                    checked
                    {
                        _lockCount++;
                    }
                    return MutexTryAcquireLockResult.AcquiredLockRecursively;
                }

                switch (timeoutMilliseconds)
                {
                    case -1:
                        bool acquiredLock = false;
                        while (_sharedData->TimedWaiterCount > 0)
                        {
                            if (SharedMemoryHelpers.TryAcquireFileLock(_sharedLockFileHandle, nonBlocking: true))
                            {
                                acquiredLock = true;
                                break;
                            }
                            Thread.Sleep(PollLoopMaximumSleepMilliseconds);
                        }

                        if (acquiredLock)
                        {
                            break;
                        }

                        acquiredLock = SharedMemoryHelpers.TryAcquireFileLock(_sharedLockFileHandle, nonBlocking: false);
                        Debug.Assert(acquiredLock);
                        break;
                    case 0:
                        if (!SharedMemoryHelpers.TryAcquireFileLock(_sharedLockFileHandle, nonBlocking: true))
                        {
                            return MutexTryAcquireLockResult.TimedOut;
                        }
                        break;
                    default:
                    {
                        if (SharedMemoryHelpers.TryAcquireFileLock(_sharedLockFileHandle, nonBlocking: true))
                        {
                            break;
                        }

                        _sharedData->TimedWaiterCount++;

                        do
                        {
                            int elapsedMilliseconds = Environment.TickCount - startTime;
                            if (elapsedMilliseconds >= timeoutMilliseconds)
                            {
                                _sharedData->TimedWaiterCount--;
                                return MutexTryAcquireLockResult.TimedOut;
                            }

                            int remainingTimeoutMilliseconds = timeoutMilliseconds - elapsedMilliseconds;
                            int sleepMilliseconds = Math.Min(PollLoopMaximumSleepMilliseconds, remainingTimeoutMilliseconds);
                            Thread.Sleep(sleepMilliseconds);
                        } while (!SharedMemoryHelpers.TryAcquireFileLock(_sharedLockFileHandle, nonBlocking: true));
                        _sharedData->TimedWaiterCount--;
                        break;
                    }
                }
                releaseProcessLock = false;

                // Detect abandoned lock that isn't marked as abandoned.
                if (IsLockOwnedByAnyThread)
                    return MutexTryAcquireLockResult.AcquiredLockButMutexWasAbandoned;

                return MutexTryAcquireLockResult.AcquiredLock;
            }
            finally
            {
                if (releaseProcessLock)
                {
                    _processLevelMutex.ReleaseMutex();
                }
            }
        }

        // Must match the file lock variant of NamedMutexSharedData in the CoreCLR PAL used by .NET 10 and earlier
        // (src/coreclr/pal/src/include/pal/mutex.hpp): the owner thread ID is 64 bits wide and _isAbandoned follows it.
        [StructLayout(LayoutKind.Sequential)]
        internal ref struct SharedData
        {
            private uint _timedWaiterCount;
            private uint _lockOwnerProcessId;
            private ulong _lockOwnerThreadId;
            private byte _isAbandoned;

            public uint TimedWaiterCount { get => _timedWaiterCount; set => _timedWaiterCount = value; }
            public uint LockOwnerProcessId
            {
                get
                {
                    return _lockOwnerProcessId;
                }
                set
                {
                    _lockOwnerProcessId = value;
                }
            }

            public ulong LockOwnerThreadId
            {
                get
                {
                    return _lockOwnerThreadId;
                }
                set
                {
                    _lockOwnerThreadId = value;
                }
            }

            public bool IsAbandoned
            {
                get
                {
                    return _isAbandoned != 0;
                }
                set
                {
                    _isAbandoned = value ? (byte)1 : (byte)0;
                }
            }
        }
    }

    internal enum MutexTryAcquireLockResult : byte
    {
        AcquiredLock,
        AcquiredLockButMutexWasAbandoned,
        TimedOut,
        AcquiredLockRecursively,
    }

    internal static partial class WaitSubsystem
    {
        private sealed class NamedMutex : IWaitableObject
        {
            private readonly SharedMemoryProcessDataHeader<NamedMutexProcessDataBase> _processDataHeader;

            public NamedMutex(SharedMemoryProcessDataHeader<NamedMutexProcessDataBase> processDataHeader)
            {
                _processDataHeader = processDataHeader;
            }

            public int Wait_Locked(ThreadWaitInfo waitInfo, int timeoutMilliseconds, bool interruptible, ref LockHolder lockHolder)
            {
                lockHolder.Dispose();

                NamedMutexProcessDataBase.UnrecordedMutexAcquisition acquisition = _processDataHeader._processData!.TryAcquireLock(timeoutMilliseconds);
                LockHolder scope = SharedMemoryManager<NamedMutexProcessDataBase>.Instance.AcquireCreationDeletionProcessLock();
                try
                {
                    MutexTryAcquireLockResult result = acquisition.RecordMutexAcquisition(waitInfo);

                    return result switch
                    {
                        MutexTryAcquireLockResult.AcquiredLock => WaitHandle.WaitSuccess,
                        MutexTryAcquireLockResult.AcquiredLockButMutexWasAbandoned => WaitHandle.WaitAbandoned,
                        MutexTryAcquireLockResult.TimedOut => WaitHandle.WaitTimeout,
                        _ => throw new UnreachableException()
                    };
                }
                finally
                {
                    scope.Dispose();
                }
            }

            public void Signal(int count, ref LockHolder lockHolder)
            {
                lockHolder.Dispose();
                _processDataHeader._processData!.ReleaseLock();
            }

            public void OnDeleteHandle()
            {
                LockHolder scope = SharedMemoryManager<NamedMutexProcessDataBase>.Instance.AcquireCreationDeletionProcessLock();
                try
                {
                    _processDataHeader.DecrementRefCount();
                }
                finally
                {
                    scope.Dispose();
                }
            }

            public static NamedMutex? CreateNamedMutex(string name, bool isUserScope, bool initiallyOwned, out bool createdNew)
            {
                var namedMutexProcessData = NamedMutexProcessDataBase.CreateOrOpen(name, isUserScope, createIfNotExist: true, acquireLockIfCreated: initiallyOwned, out createdNew);
                if (namedMutexProcessData is null)
                {
                    createdNew = false;
                    return null;
                }

                Debug.Assert(namedMutexProcessData._processData is not null);
                return new NamedMutex(namedMutexProcessData);
            }

            public static OpenExistingResult OpenNamedMutex(string name, bool isUserScope, out NamedMutex? result)
            {
                var namedMutexProcessData = NamedMutexProcessDataBase.CreateOrOpen(name, isUserScope, createIfNotExist: false, acquireLockIfCreated: false, out _);
                if (namedMutexProcessData is null)
                {
                    result = null;
                    return OpenExistingResult.NameNotFound;
                }

                Debug.Assert(namedMutexProcessData._processData is not null);
                result = new NamedMutex(namedMutexProcessData);
                return OpenExistingResult.Success;
            }
        }
    }
}