dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.91k stars 4.63k forks source link

[API Proposal]: New overloads for named wait handles that enable creating/opening user-specific synchronization primitives #102682

Open kouvel opened 3 months ago

kouvel commented 3 months ago

Background and motivation

API Proposal

namespace System.Threading
{
    [Flags]
    public enum NamedWaitHandleOptions
    {
        /// <summary>
        /// The named wait handle is restricted to the current user. Unless excluded by other options, processes running as the
        /// current user can open the same named wait handle. Processes running as other users can't open the same named wait
        /// handle.
        /// </summary>
        CurrentUserOnly = 1 << 0,

        /// <summary>
        /// The named wait handle is not restricted to a user. Unless excluded by other options, processes running as any user
        /// can open the same named wait handle.
        /// </summary>
        AllUsers = 1 << 1,

        /// <summary>
        /// The named wait handle is restricted to the current session. Unless excluded by other options, processes running in
        /// the current session can open the same named wait handle. Processes running in other sessions can't open the same
        /// named wait handle.
        /// </summary>
        CurrentSessionOnly = 1 << 2,

        /// <summary>
        /// The named wait handle is not restricted to a session. Unless excluded by other options, processes running in any
        /// session can open the same named wait handle.
        /// </summary>
        AllSessions = 1 << 3
    }

    public sealed partial class Mutex : WaitHandle
    {
        //
        // Proposed
        //

        public Mutex(string? name, NamedWaitHandleOptions options) { }
        public Mutex(bool initiallyOwned, string? name, NamedWaitHandleOptions options) { }
        public Mutex(bool initiallyOwned, string? name, NamedWaitHandleOptions options, out bool createdNew) { }
        public static Mutex OpenExisting(string name, NamedWaitHandleOptions options) { }
        public static bool TryOpenExisting(string name, NamedWaitHandleOptions options, [NotNullWhen(true)] out Mutex? result) { }

        //
        // Existing
        //

        public Mutex(bool initiallyOwned, string? name) { }
        public Mutex(bool initiallyOwned, string? name, out bool createdNew) { }
        public static Mutex OpenExisting(string name) { }
        public static bool TryOpenExisting(string name, [NotNullWhen(true)] out Mutex? result) { }
    }

    public sealed partial class Semaphore : WaitHandle
    {
        //
        // Proposed
        //

        public Semaphore(int initialCount, int maximumCount, string? name, NamedWaitHandleOptions options) { }
        public Semaphore(int initialCount, int maximumCount, string? name, NamedWaitHandleOptions options, out bool createdNew) { }
        [SupportedOSPlatform("windows")]
        public static Semaphore OpenExisting(string name, NamedWaitHandleOptions options) { }
        [SupportedOSPlatform("windows")]
        public static bool TryOpenExisting(string name, NamedWaitHandleOptions options, [NotNullWhen(true)] out Semaphore? result) { }

        //
        // Existing
        //

        public Semaphore(int initialCount, int maximumCount, string? name) { }
        public Semaphore(int initialCount, int maximumCount, string? name, out bool createdNew) { }
        [SupportedOSPlatform("windows")]
        public static Semaphore OpenExisting(string name) { }
        [SupportedOSPlatform("windows")]
        public static bool TryOpenExisting(string name, [NotNullWhen(true)] out Semaphore? result) { }
    }

    public partial class EventWaitHandle : WaitHandle
    {
        //
        // Proposed
        //

        public EventWaitHandle(bool initialState, EventResetMode mode, string? name, NamedWaitHandleOptions options) { }
        public EventWaitHandle(bool initialState, EventResetMode mode, string? name, NamedWaitHandleOptions options, out bool createdNew) { }
        [SupportedOSPlatform("windows")]
        public static EventWaitHandle OpenExisting(string name, NamedWaitHandleOptions options) { }
        [SupportedOSPlatform("windows")]
        public static bool TryOpenExisting(string name, NamedWaitHandleOptions options, [NotNullWhen(true)] out EventWaitHandle? result) { }

        //
        // Existing
        //

        public EventWaitHandle(bool initialState, EventResetMode mode, string? name) { }
        public EventWaitHandle(bool initialState, EventResetMode mode, string? name, out bool createdNew) { }
        [SupportedOSPlatform("windows")]
        public static EventWaitHandle OpenExisting(string name) { }
        [SupportedOSPlatform("windows")]
        public static bool TryOpenExisting(string name, [NotNullWhen(true)] out EventWaitHandle? result) { }
    }
}

Notes about some behaviors

API Usage

        var mutex = new Mutex("MyMutex", NamedWaitHandleOptions.CurrentUserOnly | NamedWaitHandleOptions.AllSessions);
        var mutex = new Mutex(@"Global\MyMutex", NamedWaitHandleOptions.CurrentUserOnly);
        var mutex = Mutex.OpenExisting("MyMutex", NamedWaitHandleOptions.CurrentUserOnly | NamedWaitHandleOptions.AllUsers);
        if (Mutex.TryOpenExisting("MyMutex", NamedWaitHandleOptions.CurrentUserOnly | NamedWaitHandleOptions.AllUsers, out Mutex mutex))
        {
        }

Similarly for other enum values and other named wait handles.

Alternative Designs

Risks

dotnet-policy-service[bot] commented 3 months ago

Tagging subscribers to this area: @mangod9 See info in area-owners.md if you want to be subscribed.

jkotas commented 3 months ago

Do we have any customers asking for emulation of named Windows synchronization APIs beyond Mutexes on non-Windows platforms?

Only named mutexes are supported on Unixes, but similar APIs are also proposed for other named wait handles.

Are the reliability guarantees of the Unix emulation of the other named wait handles going to match Windows in case of abnormal process termination?

kouvel commented 3 months ago

Do we have any customers asking for emulation of named Windows synchronization APIs beyond Mutexes on non-Windows platforms?

Not that I've heard of.

Are the reliability guarantees of the Unix emulation of the other named wait handles going to match Windows in case of abnormal process termination?

It might be feasible on Linux where pthread process-shared robust mutexes and process-shared conditions are available. On OSX/BSD it may be more challenging or may need the use of some other primitive.

jkotas commented 3 months ago

Not that I've heard of.

I do not think we should be expanding the set of emulated named synchronization primitives outside Windows then. It is very niche functionality and fixing the security and reliability problems in the existing named mutex costed us a lot over time.

kouvel commented 3 months ago

I do not think we should be expanding the set of emulated named synchronization primitives outside Windows then.

I'm not suggesting expanding the implementations of named events and semaphores into Unixes, I'm only suggesting expanding their APIs similarly, mostly for Windows, as even there it could still be beneficial to have an easy and explicit way of specifying the user scope of a named wait handle.

bartonjs commented 2 months ago

Video

namespace System.Threading
{
    public struct NamedWaitHandleOptions
    {
        private bool _allUsers;
        private bool _allSessions;

        // Note that for default(NamedWaitHandleOptions), both of these properties will return `true`.

        public bool CurrentUserOnly { get => !_allUsers; set => _allUsers = !value; }
        public bool CurrentSessionOnly { get => !_allSessions; set => _allSessions = !value; }
    }

    public sealed partial class Mutex : WaitHandle
    {
        //
        // Proposed
        //

        public Mutex(string? name, NamedWaitHandleOptions options) { }
        public Mutex(bool initiallyOwned, string? name, NamedWaitHandleOptions options) { }
        public Mutex(bool initiallyOwned, string? name, NamedWaitHandleOptions options, out bool createdNew) { }
        public static Mutex OpenExisting(string name, NamedWaitHandleOptions options) { }
        public static bool TryOpenExisting(string name, NamedWaitHandleOptions options, [NotNullWhen(true)] out Mutex? result) { }

        //
        // Existing
        //
        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public Mutex(bool initiallyOwned, string? name) { }
        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public Mutex(bool initiallyOwned, string? name, out bool createdNew) { }
        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public static Mutex OpenExisting(string name) { }
        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public static bool TryOpenExisting(string name, [NotNullWhen(true)] out Mutex? result) { }
    }

    public sealed partial class Semaphore : WaitHandle
    {
        //
        // Proposed
        //

        public Semaphore(int initialCount, int maximumCount, string? name, NamedWaitHandleOptions options) { }
        public Semaphore(int initialCount, int maximumCount, string? name, NamedWaitHandleOptions options, out bool createdNew) { }
        [SupportedOSPlatform("windows")]
        public static Semaphore OpenExisting(string name, NamedWaitHandleOptions options) { }
        [SupportedOSPlatform("windows")]
        public static bool TryOpenExisting(string name, NamedWaitHandleOptions options, [NotNullWhen(true)] out Semaphore? result) { }

        //
        // Existing
        //

        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public Semaphore(int initialCount, int maximumCount, string? name) { }
        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public Semaphore(int initialCount, int maximumCount, string? name, out bool createdNew) { }
        [SupportedOSPlatform("windows")]
        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public static Semaphore OpenExisting(string name) { }
        [SupportedOSPlatform("windows")]
        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public static bool TryOpenExisting(string name, [NotNullWhen(true)] out Semaphore? result) { }
    }

    public partial class EventWaitHandle : WaitHandle
    {
        //
        // Proposed
        //

        public EventWaitHandle(bool initialState, EventResetMode mode, string? name, NamedWaitHandleOptions options) { }
        public EventWaitHandle(bool initialState, EventResetMode mode, string? name, NamedWaitHandleOptions options, out bool createdNew) { }
        [SupportedOSPlatform("windows")]
        public static EventWaitHandle OpenExisting(string name, NamedWaitHandleOptions options) { }
        [SupportedOSPlatform("windows")]
        public static bool TryOpenExisting(string name, NamedWaitHandleOptions options, [NotNullWhen(true)] out EventWaitHandle? result) { }

        //
        // Existing
        //

        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public EventWaitHandle(bool initialState, EventResetMode mode, string? name) { }
        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public EventWaitHandle(bool initialState, EventResetMode mode, string? name, out bool createdNew) { }
        [SupportedOSPlatform("windows")]
        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public static EventWaitHandle OpenExisting(string name) { }
        [SupportedOSPlatform("windows")]
        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public static bool TryOpenExisting(string name, [NotNullWhen(true)] out EventWaitHandle? result) { }
    }
}
KalleOlaviNiemitalo commented 2 months ago
    public struct NamedWaitHandleOptions
    {
        private bool _allUsers;
        private bool _allSessions;

        // Note that for default(NamedWaitHandleOptions), both of these properties will return `true`.

        public bool CurrentUserOnly { get => !_allUsers; set => _allUsers = !value; }
        public bool CurrentSessionOnly { get => !_allSessions; set => _allSessions = !value; }
    }

How would the application initialize a non-default NamedWaitHandleOptions value -- is a constructor missing from this API?

Never mind, I didn't notice that the properties have setters.