File: src\Aspire.Tray\Mac\SingleInstance.cs
Web Access
Project: src\tests\Aspire.Tray.Tests\Aspire.Tray.Tests.csproj (Aspire.Tray.Tests)
// 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.Runtime.InteropServices;
using System.Runtime.Versioning;
using Aspire.Shared;
using Microsoft.Win32.SafeHandles;
 
namespace Aspire.Tray;
 
[SupportedOSPlatform("macos")]
internal static partial class SingleInstance
{
    private const int WouldBlock = 35;
    private const int LockExclusiveNonblocking = 2 | 4;
 
    // Use the lock's user directory, not TMPDIR: terminal/editor launches can have
    // different temporary directories but must still activate the same global tray.
    public static string ActivationPipeName => Path.Combine(DirectoryPath, "control-v1.sock");
 
    public static string DirectoryPath
        => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
            ".aspire", "tray", "runtime");
 
    public static string LegacyStateDirectoryPath
        => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
            "Library", "Application Support", "Aspire", "Tray");
 
    public static FileStream? TryAcquire()
        => TryAcquire(DirectoryPath);
 
    internal static FileStream? TryAcquire(string directory)
    {
        // NativeAOT's Unix named Mutex implementation is process-local:
        // https://github.com/dotnet/runtime/issues/110348
        DirectoryHelper.CreateWithOwnerOnlyPermissions(directory);
 
        FileStream stream;
        try
        {
            stream = new FileStream(Path.Combine(directory, "instance.lock"), new FileStreamOptions
            {
                Mode = FileMode.OpenOrCreate,
                Access = FileAccess.ReadWrite,
                Share = FileShare.None,
                UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite
            });
        }
        catch (IOException ex) when (ex.HResult == WouldBlock)
        {
            // FileShare.None uses flock; contention is surfaced as the raw Darwin EWOULDBLOCK errno.
            // https://github.com/dotnet/runtime/blob/v10.0.0/src/libraries/Common/src/Interop/Unix/Interop.IOErrors.cs
            return null;
        }
 
        // FileStream's advisory lock is best-effort on unsupported filesystems. Require flock
        // to succeed rather than silently allowing multiple tray instances in that case.
        if (Lock(stream.SafeFileHandle, LockExclusiveNonblocking) != 0)
        {
            var error = Marshal.GetLastPInvokeError();
            stream.Dispose();
            throw new Win32Exception(error, "Could not acquire the tray's single-instance lock.");
        }
 
        // Keep the file, even after exit. Unlinking it lets concurrent starters lock different
        // inodes. Closing the handle (including on a crash) releases the OS lock automatically.
        return stream;
    }
 
    [LibraryImport("/usr/lib/libSystem.B.dylib", EntryPoint = "flock", SetLastError = true)]
    private static partial int Lock(SafeFileHandle handle, int operation);
}