diff --git a/CHANGELOG.md b/CHANGELOG.md
index da0b438c4..7efa67758 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
+### Added
+
+- [SIL.Core] Added macOS support for `GlobalMutex`
+
## [14.1.1] - 2024-05-23
### Fixed
diff --git a/SIL.Core/Threading/GlobalMutex.cs b/SIL.Core/Threading/GlobalMutex.cs
index 5b6bb0804..da2a9bd3d 100644
--- a/SIL.Core/Threading/GlobalMutex.cs
+++ b/SIL.Core/Threading/GlobalMutex.cs
@@ -15,6 +15,10 @@ namespace SIL.Threading
///
/// This is needed because Mono does not support system-wide, named mutexes. Mono does implement the Mutex class,
/// but even when using the constructors with names, it only works within a single process.
+ ///
+ /// Note that in .NET 8.0 and later (and perhaps starting sooner than version 8), a named mutex can be used across
+ /// processes in Linux and macOS. However, software using the previous method of locking would not recognize the
+ /// new method of locking, so Linux continues to use the old, file-based approach internally.
///
public class GlobalMutex : DisposableBase
{
@@ -24,14 +28,18 @@ public class GlobalMutex : DisposableBase
///
/// Initializes a new instance of the class.
+ /// A object is only intended to work with other objects.
+ /// The name provided may not be the exact name/ID used by the internal OS object(s) used to enforce the mutex.
///
public GlobalMutex(string name)
{
_name = name;
- if (!Platform.IsWindows)
+ if (Platform.IsWindows)
+ _adapter = new WindowsGlobalMutexAdapter(name);
+ else if (Platform.IsLinux)
_adapter = new LinuxGlobalMutexAdapter(name);
else
- _adapter = new WindowsGlobalMutexAdapter(name);
+ _adapter = new ExplicitGlobalMutexAdapter(name);
}
///
@@ -285,5 +293,19 @@ protected override void DisposeManagedResources()
_mutex.Dispose();
}
}
+
+ ///
+ /// A .NET native Mutex object works cross-process on all OSes if we prepend its name with "Global\".
+ /// On multi-user systems (e.g., Terminal Server on Windows) this can cause one user to grab the lock that another user
+ /// would like to get. If this is an important scenario, then a login session ID and/or username could be included in
+ /// the name of the Mutex. Without prepending "Global\", though, named Mutexes don't work cross-process on OSes other
+ /// than Windows.
+ ///
+ private class ExplicitGlobalMutexAdapter: WindowsGlobalMutexAdapter
+ {
+ private const string GLOBAL = "Global\\";
+
+ public ExplicitGlobalMutexAdapter(string name) : base(name.StartsWith(GLOBAL) ? name : $"{GLOBAL}{name}") {}
+ }
}
}