//****************************************************************************************************** // InterprocessLock.cs - Gbtc // // Copyright © 2012, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://www.opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 03/21/2011 - J. Ritchie Carroll // Generated original version of source code. // 06/30/2011 - Stephen Wills // Applying changes from Jian (Ryan) Zuo: updated to allow unauthorized users to attempt to grant // themselves lower than full access to existing mutexes and semaphores. // 08/12/2011 - J. Ritchie Carroll // Modified creation methods such that locking natives are created in a synchronized fashion. // 09/21/2011 - J. Ritchie Carroll // Added Mono implementation exception regions. // 12/14/2012 - Starlynn Danyelle Gilliam // Modified Header. // 12/18/2013 - J. Ritchie Carroll // Improved operational behavior. // //****************************************************************************************************** using System; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security.AccessControl; using System.Security.Principal; using System.Threading; using GSF.Diagnostics; using GSF.Identity; using GSF.Security.Cryptography; namespace GSF.Threading { /// /// Defines helper methods related to inter-process locking. /// public static class InterprocessLock { private const int MutexHash = 0; private const int SemaphoreHash = 1; /// /// Gets a uniquely named inter-process associated with the running application, typically used to detect whether an instance /// of the application is already running. /// /// Indicates whether to generate a different name for the dependent upon the user running the application. /// A uniquely named inter-process specific to the application; is created if it does not exist. /// /// /// This function uses a hash of the assembly's GUID when creating the , if it is available. If it is not available, it uses a hash /// of the simple name of the assembly. Although the name is hashed to help guarantee uniqueness, it is still entirely possible that another application /// may use that name with the same hashing algorithm to generate its name. Therefore, it is best to ensure that the /// is defined in the AssemblyInfo of your application. /// /// /// The created is "Global" meaning that it will be accessible to all active application sessions including terminal service /// sessions. This is accomplished internally by prefixing the name with "Global\". Do not use this helper function if you need /// to create a specifically named or non-global , such as when you need to interact with another application using a /// that does not use this function. /// /// /// The named mutex exists, but the user does not have the minimum needed security access rights to use it. [MethodImpl(MethodImplOptions.Synchronized)] public static Mutex GetNamedMutex(bool perUser = true) { Assembly entryAssembly = Assembly.GetEntryAssembly(); GuidAttribute attribute = (GuidAttribute)entryAssembly.GetCustomAttributes(typeof(GuidAttribute), true).FirstOrDefault(); string name = attribute?.Value ?? entryAssembly.GetName().Name; if (perUser) name += UserInfo.CurrentUserID; return GetNamedMutex(name); } /// /// Gets a uniquely named inter-process associated with the specified that identifies a source object /// needing concurrency locking. /// /// Identifying name of source object needing concurrency locking (e.g., a path and file name). /// A uniquely named inter-process specific to ; is created if it does not exist. /// /// /// This function uses a hash of the when creating the , not the actual - this way /// restrictions on the length do not need to be a user concern. All processes needing an inter-process need /// to use this same function to ensure access to the same . /// /// /// The can be a string of any length (must not be empty, null or white space) and is not case-sensitive. All hashes of the /// used to create the global are first converted to lower case. /// /// /// The created is "Global" meaning that it will be accessible to all active application sessions including terminal service /// sessions. This is accomplished internally by prefixing the name with "Global\"; it is not necessary for the user to be /// concerned with the length or contents of the in this method as long as the same is used for /// each application. Do not use this helper function if you need to create a specifically named or non-global , such as when /// you need to interact with another application using a that does not use this function. /// /// /// Argument cannot be empty, null or white space. /// The named mutex exists, but the user does not have the minimum needed security access rights to use it. [MethodImpl(MethodImplOptions.Synchronized)] public static Mutex GetNamedMutex(string name) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name), "Argument cannot be empty, null or white space."); Mutex namedMutex; bool mutexWasCreated; // Create a mutex name that is specific to an object (e.g., a path and file name). // Note that we use GetPasswordHash to create a short common name for the name parameter // that was passed into the function - this allows the parameter to be very long, e.g., // a file path, and still meet minimum mutex name requirements. // Prefix mutex name with "Global\" such that mutex will apply to all active // application sessions in case terminal services is running. string mutexName = "Global\\" + Cipher.GetPasswordHash(name.ToLower(), MutexHash).Replace('\\', '-'); #if MONO // Mono Mutex implementations do not include ability to change access rules namedMutex = new Mutex(false, mutexName, out mutexWasCreated); #else bool doesNotExist = false; // Attempt to open the named mutex try { namedMutex = Mutex.OpenExisting(mutexName, MutexRights.Synchronize | MutexRights.Modify); } catch (WaitHandleCannotBeOpenedException ex) { Logger.SwallowException(ex, "Common Exception", "Exception expected when mutex does not already exist."); namedMutex = null; doesNotExist = true; } // If mutex does not exist we attempt to create it if (doesNotExist) { try { MutexSecurity security = new MutexSecurity(); security.AddAccessRule(new MutexAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null), MutexRights.Synchronize | MutexRights.Modify, AccessControlType.Allow)); namedMutex = new Mutex(false, mutexName, out mutexWasCreated, security); } catch (UnauthorizedAccessException ex) { Logger.SwallowException(ex, "Common Exception", "Accommodated race condition on mutex creation."); // Named mutex exists now but current user doesn't have full control, attempt to open with minimum needed rights namedMutex = Mutex.OpenExisting(mutexName, MutexRights.Synchronize | MutexRights.Modify); } } #endif return namedMutex; } /// /// Gets a uniquely named inter-process associated with the running application, typically used to detect whether some number of /// instances of the application are already running. /// /// Indicates whether to generate a different name for the dependent upon the user running the application. /// The maximum number of requests for the semaphore that can be granted concurrently. /// The initial number of requests for the semaphore that can be granted concurrently, or -1 to default to . /// A uniquely named inter-process specific to entry assembly; is created if it does not exist. /// /// /// This function uses a hash of the assembly's GUID when creating the , if it is available. If it is not available, it uses a hash /// of the simple name of the assembly. Although the name is hashed to help guarantee uniqueness, it is still entirely possible that another application /// may use that name with the same hashing algorithm to generate its name. Therefore, it is best to ensure that the /// is defined in the AssemblyInfo of your application. /// /// /// The created is "Global" meaning that it will be accessible to all active application sessions including terminal service /// sessions. This is accomplished internally by prefixing the name with "Global\". Do not use this helper function if you need /// to create a specifically named or non-global , such as when you need to interact with another application using a /// that does not use this function. /// /// /// The named semaphore exists, but the user does not have the minimum needed security access rights to use it. [MethodImpl(MethodImplOptions.Synchronized)] public static Semaphore GetNamedSemaphore(bool perUser = true, int maximumCount = 10, int initialCount = -1) { Assembly entryAssembly = Assembly.GetEntryAssembly(); GuidAttribute attribute = (GuidAttribute)entryAssembly.GetCustomAttributes(typeof(GuidAttribute), true).FirstOrDefault(); string name = attribute?.Value ?? entryAssembly.GetName().Name; if (perUser) name += UserInfo.CurrentUserID; return GetNamedSemaphore(name, maximumCount, initialCount); } /// /// Gets a uniquely named inter-process associated with the specified that identifies a source object /// needing concurrency locking. /// /// Identifying name of source object needing concurrency locking (e.g., a path and file name). /// The maximum number of requests for the semaphore that can be granted concurrently. /// The initial number of requests for the semaphore that can be granted concurrently, or -1 to default to . /// A uniquely named inter-process specific to ; is created if it does not exist. /// /// /// This function uses a hash of the when creating the , not the actual - this way /// restrictions on the length do not need to be a user concern. All processes needing an inter-process need /// to use this same function to ensure access to the same . /// /// /// The can be a string of any length (must not be empty, null or white space) and is not case-sensitive. All hashes of the /// used to create the global are first converted to lower case. /// /// /// The created is "Global" meaning that it will be accessible to all active application sessions including terminal service /// sessions. This is accomplished internally by prefixing the name with "Global\"; it is not necessary for the user to be /// concerned with the length or contents of the in this method as long as the same is used for /// each application. Do not use this helper function if you need to create a specifically named or non-global , such as when /// you need to interact with another application using a that does not use this function. /// /// /// Argument cannot be empty, null or white space. /// The named semaphore exists, but the user does not have the minimum needed security access rights to use it. [MethodImpl(MethodImplOptions.Synchronized)] public static Semaphore GetNamedSemaphore(string name, int maximumCount = 10, int initialCount = -1) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name), "Argument cannot be empty, null or white space."); Semaphore namedSemaphore; bool semaphoreWasCreated; if (initialCount < 0) initialCount = maximumCount; // Create a semaphore name that is specific to an object (e.g., a path and file name). // Note that we use GetPasswordHash to create a short common name for the name parameter // that was passed into the function - this allows the parameter to be very long, e.g., // a file path, and still meet minimum semaphore name requirements. // Prefix semaphore name with "Global\" such that semaphore will apply to all active // application sessions in case terminal services is running. Note that this is necessary // even though .NET documentation does not state it - IL disassembly shows direct calls // to WinAPI OpenSemaphore and CreateSemaphore which clearly document this: // http://msdn.microsoft.com/en-us/library/windows/desktop/ms684326(v=vs.85).aspx string semaphoreName = "Global\\" + Cipher.GetPasswordHash(name.ToLower(), SemaphoreHash).Replace('\\', '-'); #if MONO // Mono Semaphore implementations do not include ability to change access rules namedSemaphore = new Semaphore(initialCount, maximumCount, semaphoreName, out semaphoreWasCreated); #else bool doesNotExist = false; // Attempt to open the named semaphore with minimum needed rights try { namedSemaphore = Semaphore.OpenExisting(semaphoreName, SemaphoreRights.Synchronize | SemaphoreRights.Modify); } catch (WaitHandleCannotBeOpenedException ex) { Logger.SwallowException(ex, "Common Exception", "Exception expected when semaphore does not already exist."); namedSemaphore = null; doesNotExist = true; } // If semaphore does not exist we attempt to create it if (doesNotExist) { try { SemaphoreSecurity security = new SemaphoreSecurity(); security.AddAccessRule(new SemaphoreAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null), SemaphoreRights.Synchronize | SemaphoreRights.Modify, AccessControlType.Allow)); namedSemaphore = new Semaphore(initialCount, maximumCount, semaphoreName, out semaphoreWasCreated, security); } catch (UnauthorizedAccessException ex) { Logger.SwallowException(ex, "Common Exception", "Accommodated race condition on semaphore creation."); // Named semaphore exists now but current user doesn't have full control, attempt to open with minimum needed rights namedSemaphore = Semaphore.OpenExisting(semaphoreName, SemaphoreRights.Synchronize | SemaphoreRights.Modify); } } #endif return namedSemaphore; } } }