diff --git a/src/Gemstone/ArrayExtensions/ArrayExtensions.cs b/src/Gemstone/ArrayExtensions/ArrayExtensions.cs index 688d9a43a2..bd9b807ab9 100644 --- a/src/Gemstone/ArrayExtensions/ArrayExtensions.cs +++ b/src/Gemstone/ArrayExtensions/ArrayExtensions.cs @@ -59,6 +59,21 @@ namespace Gemstone.ArrayExtensions; /// public static class ArrayExtensions { + /// + /// Zero the given buffer in a way that will not be optimized away. + /// + /// Buffer to zero. + /// of array. + public static void Zero(this T[] buffer) + { + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + + // Zero buffer + for (int i = 0; i < buffer.Length; i++) + buffer[i] = default!; + } + /// /// Validates that the specified and are valid within the given . /// @@ -101,7 +116,7 @@ private static void RaiseValidationError(T[]? array, int startIndex, int leng /// Source array. /// Offset into array. /// Length of array to copy at offset. - /// A array of data copied from the specified portion of the source array. + /// An array of data copied from the specified portion of the source array. /// /// /// Returned array will be extended as needed to make it the specified , but @@ -272,7 +287,10 @@ public static T[] Combine(this T[] source, int sourceOffset, int sourceCount, /// /// /// of array. - public static T[] Combine(this T[] source, T[] other1, T[] other2) => new[] { source, other1, other2 }.Combine(); + public static T[] Combine(this T[] source, T[] other1, T[] other2) + { + return new[] { source, other1, other2 }.Combine(); + } /// /// Combines arrays together into a single array. @@ -294,7 +312,10 @@ public static T[] Combine(this T[] source, int sourceOffset, int sourceCount, /// /// /// of array. - public static T[] Combine(this T[] source, T[] other1, T[] other2, T[] other3) => new[] { source, other1, other2, other3 }.Combine(); + public static T[] Combine(this T[] source, T[] other1, T[] other2, T[] other3) + { + return new[] { source, other1, other2, other3 }.Combine(); + } /// /// Combines arrays together into a single array. @@ -317,7 +338,10 @@ public static T[] Combine(this T[] source, int sourceOffset, int sourceCount, /// /// /// of array. - public static T[] Combine(this T[] source, T[] other1, T[] other2, T[] other3, T[] other4) => new[] { source, other1, other2, other3, other4 }.Combine(); + public static T[] Combine(this T[] source, T[] other1, T[] other2, T[] other3, T[] other4) + { + return new[] { source, other1, other2, other3, other4 }.Combine(); + } /// /// Combines array of arrays together into a single array. @@ -447,33 +471,33 @@ public static int IndexOfSequence(this T[] array, T[] sequenceToFind, int sta // Search for first item in the sequence, if this doesn't exist then sequence doesn't exist int index = Array.IndexOf(array, sequenceToFind[0], startIndex, length); - if (sequenceToFind.Length > 1) - { - bool foundSequence = false; + if (sequenceToFind.Length <= 1) + return index; - while (index > -1 && !foundSequence) + bool foundSequence = false; + + while (index > -1 && !foundSequence) + { + // See if next bytes in sequence match + for (int x = 1; x < sequenceToFind.Length; x++) { - // See if next bytes in sequence match - for (int x = 1; x < sequenceToFind.Length; x++) + // Make sure there's enough array remaining to accommodate this item + if (index + x < startIndex + length) { - // Make sure there's enough array remaining to accommodate this item - if (index + x < startIndex + length) - { - // If sequence doesn't match, search for next first-item - if (array[index + x].CompareTo(sequenceToFind[x]) != 0) - { - index = Array.IndexOf(array, sequenceToFind[0], index + 1, startIndex + length - (index + 1)); - break; - } - - // If each item to find matched, we found the sequence - foundSequence = x == sequenceToFind.Length - 1; - } - else + // If sequence doesn't match, search for next first-item + if (array[index + x].CompareTo(sequenceToFind[x]) != 0) { - // Ran out of array, return -1 - index = -1; + index = Array.IndexOf(array, sequenceToFind[0], index + 1, startIndex + length - (index + 1)); + break; } + + // If each item to find matched, we found the sequence + foundSequence = x == sequenceToFind.Length - 1; + } + else + { + // Ran out of array, return -1 + index = -1; } } } @@ -566,11 +590,11 @@ public static int CountOfSequence(this T[] array, T[] sequenceToCount, int st if (index < 0) return 0; - // Occurances counter + // Occurrences counter int foundCount = 0; // Search when the first array element is found, and the sequence can fit in the search range - bool searching = (index > -1) && (sequenceToCount.Length <= startIndex + searchLength - index); + bool searching = sequenceToCount.Length <= startIndex + searchLength - index; while (searching) { @@ -595,7 +619,7 @@ public static int CountOfSequence(this T[] array, T[] sequenceToCount, int st } // Continue searching if the array remaining can accommodate the sequence to find - searching = (index > -1) && (sequenceToCount.Length <= startIndex + searchLength - index); + searching = index > -1 && sequenceToCount.Length <= startIndex + searchLength - index; } return foundCount; @@ -653,7 +677,7 @@ public static int CompareTo(this T[]? source, T[]? other) where T : IComparab int length1 = source.Length; int length2 = other.Length; - // If array lengths are unequal, array with largest number of elements is assumed to be largest + // If array lengths are unequal, array with the largest number of elements is assumed to be largest if (length1 != length2) return length1.CompareTo(length2); @@ -787,7 +811,10 @@ public static int CompareTo(this T[]? source, int sourceOffset, T[]? other, i /// to use the Linq function if you simply need to /// iterate over the combined buffers. /// - public static byte[] Combine(this byte[] source, byte[] other1, byte[] other2) => new[] { source, other1, other2 }.Combine(); + public static byte[] Combine(this byte[] source, byte[] other1, byte[] other2) + { + return new[] { source, other1, other2 }.Combine(); + } /// /// Combines buffers together as a single image. @@ -803,7 +830,10 @@ public static int CompareTo(this T[]? source, int sourceOffset, T[]? other, i /// to use the Linq function if you simply need to /// iterate over the combined buffers. /// - public static byte[] Combine(this byte[] source, byte[] other1, byte[] other2, byte[] other3) => new[] { source, other1, other2, other3 }.Combine(); + public static byte[] Combine(this byte[] source, byte[] other1, byte[] other2, byte[] other3) + { + return new[] { source, other1, other2, other3 }.Combine(); + } /// /// Combines buffers together as a single image. @@ -820,7 +850,10 @@ public static int CompareTo(this T[]? source, int sourceOffset, T[]? other, i /// to use the Linq function if you simply need to /// iterate over the combined buffers. /// - public static byte[] Combine(this byte[] source, byte[] other1, byte[] other2, byte[] other3, byte[] other4) => new[] { source, other1, other2, other3, other4 }.Combine(); + public static byte[] Combine(this byte[] source, byte[] other1, byte[] other2, byte[] other3, byte[] other4) + { + return new[] { source, other1, other2, other3, other4 }.Combine(); + } /// /// Combines an array of buffers together as a single image. diff --git a/src/Gemstone/Caching/MemoryCache.cs b/src/Gemstone/Caching/MemoryCache.cs new file mode 100644 index 0000000000..672082a831 --- /dev/null +++ b/src/Gemstone/Caching/MemoryCache.cs @@ -0,0 +1,125 @@ +//****************************************************************************************************** +// MemoryCache.cs - Gbtc +// +// Copyright © 2024, 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://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/17/2024 - Ritchie Carroll +// Generated original version of source code. +// +//****************************************************************************************************** +// ReSharper disable StaticMemberInGenericType + +using System; +using System.Runtime.Caching; +using Gemstone.TypeExtensions; + +namespace Gemstone.Caching; + +/// +/// Represents a generic memory cache for a specific type . +/// +/// Type of value to cache. +/// +/// Each type T should be unique unless cache can be safely shared. +/// +internal static class MemoryCache +{ + // Desired use case is one static MemoryCache per type T: + private static readonly MemoryCache s_memoryCache; + + static MemoryCache() + { + // Reflected type name is used to ensure unique cache name for generic types + string cacheName = $"{nameof(Gemstone)}Cache:{typeof(T).GetReflectedTypeName()}"; + s_memoryCache = new MemoryCache(cacheName); + } + + /// + /// Try to get a value from the memory cache. + /// + /// Name to use as cache key -- this should be unique per . + /// Value from cache if already cached; otherwise, default value for . + /// + public static bool TryGet(string cacheName, out T? value) + { + if (s_memoryCache.Get(cacheName) is not Lazy cachedValue) + { + value = default; + return false; + } + + value = cachedValue.Value; + return true; + } + + /// + /// Gets or adds a value, based on result of , to the memory cache. Cache defaults to a 1-minute expiration. + /// + /// Name to use as cache key -- this should be unique per . + /// Function to generate value to add to cache -- only called if value is not already cached. + /// + /// Value from cache if already cached; otherwise, new value generated by . + /// + public static T GetOrAdd(string cacheName, Func valueFactory) + { + return GetOrAdd(cacheName, 1.0D, valueFactory); + } + + /// + /// Gets or adds a value, based on result of , to the memory cache. + /// + /// Name to use as cache key -- this should be unique per . + /// Expiration time, in minutes, for cached value. + /// Function to generate value to add to cache -- only called if value is not already cached. + /// + /// Value from cache if already cached; otherwise, new value generated by . + /// + public static T GetOrAdd(string cacheName, double expirationTime, Func valueFactory) + { + Lazy newValue = new(valueFactory); + Lazy? oldValue; + + try + { + // Race condition exists here such that memory cache being referenced may + // be disposed between access and method invocation - hence the try/catch + oldValue = s_memoryCache.AddOrGetExisting(cacheName, newValue, new CacheItemPolicy { SlidingExpiration = TimeSpan.FromMinutes(expirationTime) }) as Lazy; + } + catch + { + oldValue = null; + } + + try + { + return (oldValue ?? newValue).Value; + } + catch + { + s_memoryCache.Remove(cacheName); + throw; + } + } + + /// + /// Removes a value from the memory cache. + /// + /// Specific named memory cache instance to remove from cache. + public static void Remove(string cacheName) + { + s_memoryCache.Remove(cacheName); + } +} diff --git a/src/Gemstone/Collections/HashHelpers.cs b/src/Gemstone/Collections/HashHelpers.cs index c4bf9fdd50..949e887a8d 100644 --- a/src/Gemstone/Collections/HashHelpers.cs +++ b/src/Gemstone/Collections/HashHelpers.cs @@ -85,14 +85,14 @@ public static bool IsPrime(int candidate) for (int divisor = 3; divisor <= limit; divisor += 2) { - if ((candidate % divisor) == 0) + if (candidate % divisor == 0) return false; } return true; } - return (candidate == 2); + return candidate == 2; } public static int GetPrime(int min) @@ -110,9 +110,9 @@ public static int GetPrime(int min) // outside of our predefined table. // compute the hard way. - for (int i = (min | 1); i < int.MaxValue; i += 2) + for (int i = min | 1; i < int.MaxValue; i += 2) { - if (IsPrime(i) && ((i - 1) % HashPrime != 0)) + if (IsPrime(i) && (i - 1) % HashPrime != 0) return i; } diff --git a/src/Gemstone/Common.cs b/src/Gemstone/Common.cs index bdf65222f1..3e608da272 100644 --- a/src/Gemstone/Common.cs +++ b/src/Gemstone/Common.cs @@ -46,8 +46,10 @@ using System; using System.ComponentModel; +using System.Diagnostics; using System.Globalization; using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Gemstone.Console; @@ -85,6 +87,8 @@ public enum UpdateType /// public static class Common { + private static string? s_applicationName; + /// /// Determines if the current system is a POSIX style environment. /// @@ -95,12 +99,19 @@ public static class Common /// /// /// This property will return true for both MacOSX and Unix environments. Use the Platform property - /// of the to determine more specific platform type, e.g., + /// of the to determine more specific platform type, e.g., /// MacOSX or Unix. /// /// public static readonly bool IsPosixEnvironment = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + /// + /// Gets the name of the current application. + /// + public static string ApplicationName => + s_applicationName ??= Assembly.GetEntryAssembly()?.GetName().Name ?? Process.GetCurrentProcess().ProcessName; + /// /// Converts to a using an appropriate . /// diff --git a/src/Gemstone/Configuration/AppSettings/AppSettingsExtensions.cs b/src/Gemstone/Configuration/AppSettings/AppSettingsExtensions.cs new file mode 100644 index 0000000000..1fbf579a26 --- /dev/null +++ b/src/Gemstone/Configuration/AppSettings/AppSettingsExtensions.cs @@ -0,0 +1,170 @@ +//****************************************************************************************************** +// AppSettingsExtensions.cs - Gbtc +// +// Copyright © 2023, 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://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: +// ---------------------------------------------------------------------------------------------------- +// 06/13/2020 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.Extensions.Configuration; + +namespace Gemstone.Configuration.AppSettings +{ + /// + /// Defines extensions for managing app settings. + /// + public static class AppSettingsExtensions + { + private const string InitialValueKey = "__AppSettings:InitialValue"; + private const string DescriptionKey = "__AppSettings:Description"; + + /// + /// Adds an for app settings to the given . + /// + /// The configuration builder. + /// The action to build app settings. + /// The configuration builder. + /// + /// This extension provides a simple way to add default values as well as descriptions for app settings + /// directly into an application. The source for these is a simple in-memory collection, and additional + /// key/value pairs are added so that the initial value and descriptions of these settings can still be + /// retrieved even if the settings themselves get overridden by another configuration source. + /// + public static IConfigurationBuilder AddAppSettings(this IConfigurationBuilder configurationBuilder, Action buildAction) + { + IAppSettingsBuilder appSettingsBuilder = new AppSettingsBuilder(); + buildAction(appSettingsBuilder); + + IEnumerable> appSettings = appSettingsBuilder.Build(); + configurationBuilder.AddInMemoryCollection(appSettings); + return configurationBuilder; + } + + /// + /// Gets the initial value of the app setting with the given name. + /// + /// The configuration that contains the app setting. + /// The name of the app setting. + /// The initial value of the app setting. + public static string? GetAppSettingInitialValue(this IConfiguration configuration, string name) + { + string key = ToInitialValueKey(name); + return configuration[key]; + } + + /// + /// Gets the description of the app setting with the given name. + /// + /// The configuration that contains the app setting. + /// The name of the app setting. + /// The initial value of the app setting. + public static string? GetAppSettingDescription(this IConfiguration configuration, string name) + { + string key = ToDescriptionKey(name); + return configuration[key]; + } + + /// + /// Gets the initial value of the given app setting. + /// + /// The app setting. + /// The initial value of the app setting. + public static string? GetAppSettingInitialValue(this IConfigurationSection setting) => + setting[InitialValueKey]; + + /// + /// Gets the description of the given app setting. + /// + /// The app setting. + /// The description of the app setting. + public static string? GetAppSettingDescription(this IConfigurationSection setting) => + setting[DescriptionKey]; + + private static string ToInitialValueKey(string appSettingName) => + $"{appSettingName}:{InitialValueKey}"; + + private static string ToDescriptionKey(string appSettingName) => + $"{appSettingName}:{DescriptionKey}"; + + // Implementation of IAppSettingsBuilder that works + // with the extension methods defined in this class. + private class AppSettingsBuilder : IAppSettingsBuilder + { + private class AppSetting + { + public string Name { get; } + public string Value { get; } + public string Description { get; } + + public AppSetting(string name, string value, string description) + { + Name = name; + Value = value; + Description = description; + } + + public KeyValuePair ToKeyValuePair() => new(Name, Value); + + public KeyValuePair ToInitialValuePair() + { + string key = ToInitialValueKey(Name); + return new KeyValuePair(key, Value); + } + + public KeyValuePair ToDescriptionPair() + { + string key = ToDescriptionKey(Name); + return new KeyValuePair(key, Description); + } + } + + private Dictionary AppSettingLookup { get; } + + public AppSettingsBuilder() => + AppSettingLookup = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public IAppSettingsBuilder Add(string name, string value, string description) + { + if (AppSettingLookup.ContainsKey(name)) + { + //throw new ArgumentException($"Unable to add duplicate app setting: {name}", nameof(name)); + Debug.WriteLine($"Duplicate app setting encountered: {name}\" - this can be normal."); + return this; + } + + AppSetting appSetting = new(name, value, description); + AppSettingLookup.Add(name, appSetting); + return this; + } + + public IEnumerable> Build() + { + return AppSettingLookup.Values.SelectMany(appSetting => new[] + { + appSetting.ToKeyValuePair(), + appSetting.ToInitialValuePair(), + appSetting.ToDescriptionPair() + }); + } + } + } +} diff --git a/src/Gemstone/Configuration/AppSettings/IAppSettingsBuilder.cs b/src/Gemstone/Configuration/AppSettings/IAppSettingsBuilder.cs new file mode 100644 index 0000000000..49fceb15eb --- /dev/null +++ b/src/Gemstone/Configuration/AppSettings/IAppSettingsBuilder.cs @@ -0,0 +1,49 @@ +//****************************************************************************************************** +// IAppSettingsBuilder.cs - Gbtc +// +// Copyright © 2023, 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://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: +// ---------------------------------------------------------------------------------------------------- +// 06/13/2020 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.Collections.Generic; + +namespace Gemstone.Configuration.AppSettings +{ + /// + /// Builder for app settings with descriptions. + /// + public interface IAppSettingsBuilder + { + /// + /// Adds an app setting to the builder. + /// + /// The name of the setting. + /// The value of the setting. + /// A description of the setting. + /// The app settings builder. + /// is a duplicate of a previously added app setting + public IAppSettingsBuilder Add(string name, string value, string description); + + /// + /// Converts the app settings into a collection of key/value pairs. + /// + /// The collection of key/value pairs. + public IEnumerable> Build(); + } +} diff --git a/src/Gemstone/Configuration/AppSettings/NamespaceDoc.cs b/src/Gemstone/Configuration/AppSettings/NamespaceDoc.cs new file mode 100644 index 0000000000..9f631ec651 --- /dev/null +++ b/src/Gemstone/Configuration/AppSettings/NamespaceDoc.cs @@ -0,0 +1,36 @@ +//****************************************************************************************************** +// NamespaceDoc.cs - Gbtc +// +// Copyright © 2023, 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://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: +// ---------------------------------------------------------------------------------------------------- +// 06/14/2020 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.Runtime.CompilerServices; + +namespace Gemstone.Configuration.AppSettings +{ + /// + /// The namespace contains extension functions related + /// to the definition of default app settings for Gemstone projects. + /// + [CompilerGenerated] + class NamespaceDoc + { + } +} diff --git a/src/Gemstone/Configuration/IDefineSettings.cs b/src/Gemstone/Configuration/IDefineSettings.cs new file mode 100644 index 0000000000..6cf3b04aaa --- /dev/null +++ b/src/Gemstone/Configuration/IDefineSettings.cs @@ -0,0 +1,41 @@ +//****************************************************************************************************** +// IDefineSettings.cs - Gbtc +// +// Copyright © 2024, 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://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/28/2024 - Ritchie Carroll +// Generated original version of source code. +// +//****************************************************************************************************** + +namespace Gemstone.Configuration; + +/// +/// Defines as interface that specifies that this object can define settings for a config file. +/// +public interface IDefineSettings +{ + /// + /// Establishes default settings for the config file. + /// + /// Settings instance used to hold configuration. + /// The config file settings category under which the settings are defined. +#if NET + static abstract void DefineSettings(Settings settings, string settingsCategory); +#else + static void DefineSettings(Settings settings, string settingsCategory) { } +#endif +} diff --git a/src/Gemstone/Configuration/INIConfigurationExtensions/INIConfigurationExtensions.cs b/src/Gemstone/Configuration/INIConfigurationExtensions/INIConfigurationExtensions.cs new file mode 100644 index 0000000000..e2ad649d70 --- /dev/null +++ b/src/Gemstone/Configuration/INIConfigurationExtensions/INIConfigurationExtensions.cs @@ -0,0 +1,160 @@ +//****************************************************************************************************** +// INIConfigurationExtensions.cs - Gbtc +// +// Copyright © 2023, 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://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: +// ---------------------------------------------------------------------------------------------------- +// 06/14/2020 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Collections.Generic; +using System.Linq; +using Gemstone.Configuration.AppSettings; +using Microsoft.Extensions.Configuration; + +namespace Gemstone.Configuration.INIConfigurationExtensions; + +/// +/// Defines extensions for setting up configuration defaults for Gemstone projects. +/// +public static class INIConfigurationExtensions +{ + /// + /// Generates the contents of an INI file based on the configuration settings. + /// + /// Source configuration. + /// Flag that determines whether the actual value (instead of default only) of each setting should be written to the INI file. + /// Flag that determines whether long description lines should be split into multiple lines. + /// Generated INI file contents. + public static string GenerateINIFileContents(this IConfiguration configuration, bool writeValue, bool splitDescriptionLines) + { + static IEnumerable Split(string str, int maxLineLength) + { + string[] lines = str.Split(["\r\n", "\n"], StringSplitOptions.None); + + foreach (string line in lines) + { + string leftover = line.TrimStart(); + + // Lines in the original text that contain + // only whitespace will be returned as-is + if (leftover.Length == 0) + yield return line; + + while (leftover.Length > 0) + { + char[] chars = leftover + .Take(maxLineLength + 1) + .Reverse() + .SkipWhile(c => !char.IsWhiteSpace(c)) + .SkipWhile(char.IsWhiteSpace) + .Reverse() + .ToArray(); + + if (!chars.Any()) + { + // Tokens that are longer than the maximum length will + // be returned (in their entirety) on their own line; + // maxLineLength is just a suggestion + chars = leftover + .TakeWhile(c => !char.IsWhiteSpace(c)) + .ToArray(); + } + + string splitLine = new(chars); + leftover = leftover.Substring(splitLine.Length).TrimStart(); + yield return splitLine; + } + } + } + + static bool HasAppSetting(IConfiguration section) => + section.GetChildren().Any(HasAppSettingDescription); + + static bool HasAppSettingDescription(IConfigurationSection setting) => + setting.GetAppSettingDescription() is not null; + + static string ConvertSettingToINI(IConfigurationSection setting, bool writeValue, bool splitDescriptionLines) + { + string key = setting.Key; + string value = setting.Value ?? ""; + string initialValue = setting.GetAppSettingInitialValue() ?? ""; + string description = setting.GetAppSettingDescription() ?? ""; + + // Break up long descriptions to be more readable in the INI file + IEnumerable descriptionLines = (splitDescriptionLines ? + Split(description, 78) : + description.Split(["\r\n", "\n"], StringSplitOptions.None)) + .Select(line => $"; {line}"); + + string multilineDescription = string.Join(Environment.NewLine, descriptionLines); + + string[] lines; + + if (writeValue && value != initialValue) + { + lines = + [ + $"{multilineDescription}", + $"{key}={value}" + ]; + } + else + { + lines = + [ + $"{multilineDescription}", + $";{key}={initialValue}" + ]; + } + + return string.Join(Environment.NewLine, lines); + } + + static string ConvertConfigToINI(IConfiguration config, bool writeValue, bool splitDescriptionLines) + { + IEnumerable settings = config.GetChildren() + .Where(HasAppSettingDescription) + .OrderBy(setting => setting.Key) + .Select(setting => ConvertSettingToINI(setting, writeValue, splitDescriptionLines)); + + string settingSeparator = string.Format("{0}{0}", Environment.NewLine); + string settingsText = string.Join(settingSeparator, settings); + + // The root section has no heading + if (config is not ConfigurationSection section) + return settingsText; + + return string.Join(Environment.NewLine, $"[{section.Key}]", settingsText); + } + + // Root MUST go before all other sections, so the order is important: + // 1. Sort by section key + // 2. Prepend root + // 3. Filter out sections without any app settings + IEnumerable appSettingsSections = configuration.AsEnumerable() + .Select(kvp => configuration.GetSection(kvp.Key)) + .OrderBy(section => section.Key) + .Prepend(configuration) + .Where(HasAppSetting) + .Select(config => ConvertConfigToINI(config, writeValue, splitDescriptionLines)); + + string sectionSeparator = string.Format("{0}{0}", Environment.NewLine); + return string.Join(sectionSeparator, appSettingsSections); + } +} diff --git a/src/Gemstone/Configuration/INIConfigurationExtensions/NamespaceDoc.cs b/src/Gemstone/Configuration/INIConfigurationExtensions/NamespaceDoc.cs new file mode 100644 index 0000000000..15c3838690 --- /dev/null +++ b/src/Gemstone/Configuration/INIConfigurationExtensions/NamespaceDoc.cs @@ -0,0 +1,34 @@ +//****************************************************************************************************** +// NamespaceDoc.cs - Gbtc +// +// Copyright © 2024, 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://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: +// ---------------------------------------------------------------------------------------------------- +// 04/02/2024 - Ritchie Carroll +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.Runtime.CompilerServices; + +namespace Gemstone.Configuration.INIConfigurationExtensions; + +/// +/// Contains helper and extension methods for INI configuration files. +/// +[CompilerGenerated] +class NamespaceDoc +{ +} diff --git a/src/Gemstone/Configuration/INIConfigurationHelpers.cs b/src/Gemstone/Configuration/INIConfigurationHelpers.cs new file mode 100644 index 0000000000..f13ab090eb --- /dev/null +++ b/src/Gemstone/Configuration/INIConfigurationHelpers.cs @@ -0,0 +1,61 @@ +//****************************************************************************************************** +// INIConfigurationHelpers.cs - Gbtc +// +// Copyright © 2024, 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://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: +// ---------------------------------------------------------------------------------------------------- +// 06/14/2020 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.IO; + +namespace Gemstone.Configuration; + +/// +/// Defines helper functions for working with INI configuration files. +/// +internal static class INIConfigurationHelpers +{ + /// + /// Gets file path for INI configuration file. + /// + /// Target file INI file name. + /// INI file path. + public static string GetINIFilePath(string fileName) + { + Environment.SpecialFolder specialFolder = Environment.SpecialFolder.CommonApplicationData; + string appDataPath = Environment.GetFolderPath(specialFolder); + return Path.Combine(appDataPath, Common.ApplicationName, fileName); + } + + /// + /// Gets an INI file writer for the specified path. + /// + /// Path for INI file. + /// INI file write at specified path. + public static TextWriter GetINIFileWriter(string path) + { + if (File.Exists(path)) + return File.CreateText(path); + + string directoryPath = Path.GetDirectoryName(path) ?? string.Empty; + Directory.CreateDirectory(directoryPath); + + return File.CreateText(path); + } +} diff --git a/src/Gemstone/Configuration/IPersistSettings.cs b/src/Gemstone/Configuration/IPersistSettings.cs new file mode 100644 index 0000000000..42d367f6c0 --- /dev/null +++ b/src/Gemstone/Configuration/IPersistSettings.cs @@ -0,0 +1,58 @@ +//****************************************************************************************************** +// IPersistSettings.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/2007 - Pinal C. Patel +// Generated original version of source code. +// 09/16/2008 - Pinal C. Patel +// Converted code to C#. +// 09/29/2008 - Pinal C. Patel +// Reviewed code comments. +// 09/14/2009 - Stephen C. Wills +// Added new header and license agreement +// 12/14/2012 - Starlynn Danyelle Gilliam +// Modified Header. +// +//****************************************************************************************************** + +namespace Gemstone.Configuration; + +/// +/// Defines as interface that specifies that this object can persist settings to a config file. +/// +public interface IPersistSettings : IDefineSettings +{ + /// + /// Determines whether the object settings are to be persisted to the config file. + /// + bool PersistSettings { get; set; } + + /// + /// Gets or sets the category name under which the object settings are persisted in the config file. + /// + string SettingsCategory { get; set; } + + /// + /// Saves settings to the config file. + /// + void SaveSettings(); + + /// + /// Loads saved settings from the config file. + /// + void LoadSettings(); +} diff --git a/src/Gemstone/Configuration/ReadOnly/NamespaceDoc.cs b/src/Gemstone/Configuration/ReadOnly/NamespaceDoc.cs new file mode 100644 index 0000000000..a36a74dff3 --- /dev/null +++ b/src/Gemstone/Configuration/ReadOnly/NamespaceDoc.cs @@ -0,0 +1,39 @@ +//****************************************************************************************************** +// NamespaceDoc.cs - Gbtc +// +// Copyright © 2023, 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://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: +// ---------------------------------------------------------------------------------------------------- +// 06/14/2020 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Configuration; + +namespace Gemstone.Configuration.ReadOnly +{ + /// + /// The namespace contains a wrapper for + /// to prevent calls to + /// from + /// reaching the underlying . + /// + [CompilerGenerated] + class NamespaceDoc + { + } +} diff --git a/src/Gemstone/Configuration/ReadOnly/ReadOnlyConfigurationExtensions.cs b/src/Gemstone/Configuration/ReadOnly/ReadOnlyConfigurationExtensions.cs new file mode 100644 index 0000000000..19788079ee --- /dev/null +++ b/src/Gemstone/Configuration/ReadOnly/ReadOnlyConfigurationExtensions.cs @@ -0,0 +1,119 @@ +//****************************************************************************************************** +// ReadOnlyConfigurationExtensions.cs - Gbtc +// +// Copyright © 2023, 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://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: +// ---------------------------------------------------------------------------------------------------- +// 06/12/2020 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Configuration; + +namespace Gemstone.Configuration.ReadOnly +{ + /// + /// Defines extensions for adding read-only configuration providers. + /// + public static class ReadOnlyConfigurationExtensions + { + private class ReferenceEqualityComparer : IEqualityComparer + { + public new bool Equals(object? x, object? y) => + ReferenceEquals(x, y); + + public int GetHashCode(object obj) => + RuntimeHelpers.GetHashCode(obj); + } + + /// + /// Configures an with read-only configuration sources. + /// + /// The configuration builder. + /// The action to set up configuration sources that will be made read-only. + /// The configuration builder. + /// + /// + /// This method is intended to encapsulate the builder action that creates a group of read-only providers. + /// + /// + /// + /// IConfiguration configuration = new ConfigurationBuilder() + /// .ConfigureReadOnly(readOnlyBuilder => readOnlyBuilder + /// .AddInMemoryCollection(defaultSettings) + /// .AddIniFile("usersettings.ini")) + /// .AddSQLite() + /// .Build(); + /// + /// // This will only update the SQLite configuration provider + /// configuration["Hello"] = "World"; + /// + /// + public static IConfigurationBuilder ConfigureReadOnly(this IConfigurationBuilder builder, Action builderAction) + { + ReferenceEqualityComparer referenceEqualityComparer = new(); + HashSet originalSources = new(builder.Sources, referenceEqualityComparer); + builderAction(builder); + + for (int i = 0; i < builder.Sources.Count; i++) + { + IConfigurationSource source = builder.Sources[i]; + + if (originalSources.Contains(source)) + continue; + + IConfigurationSource readOnlySource = new ReadOnlyConfigurationSource(source); + builder.Sources[i] = readOnlySource; + } + + return builder; + } + + /// + /// Converts the most recently added configuration source into a read-only configuration source. + /// + /// The configuration builder. + /// The configuration builder. + /// + /// + /// This method is intended to be chained after each source that needs to be made read-only. + /// + /// + /// + /// IConfiguration configuration = new ConfigurationBuilder() + /// .AddInMemoryCollection(defaultSettings).AsReadOnly() + /// .AddIniFile("usersettings.ini").AsReadOnly() + /// .AddSQLite() + /// .Build(); + /// + /// // This will only update the SQLite configuration provider + /// configuration["Hello"] = "World"; + /// + /// + /// + public static IConfigurationBuilder AsReadOnly(this IConfigurationBuilder builder) + { + int index = builder.Sources.Count - 1; + IConfigurationSource source = builder.Sources[index]; + IConfigurationSource readOnlySource = new ReadOnlyConfigurationSource(source); + builder.Sources[index] = readOnlySource; + return builder; + } + } +} diff --git a/src/Gemstone/Configuration/ReadOnly/ReadOnlyConfigurationProvider.cs b/src/Gemstone/Configuration/ReadOnly/ReadOnlyConfigurationProvider.cs new file mode 100644 index 0000000000..0a1034e76c --- /dev/null +++ b/src/Gemstone/Configuration/ReadOnly/ReadOnlyConfigurationProvider.cs @@ -0,0 +1,86 @@ +//****************************************************************************************************** +// ReadOnlyConfigurationProvider.cs - Gbtc +// +// Copyright © 2023, 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://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: +// ---------------------------------------------------------------------------------------------------- +// 06/12/2020 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Primitives; + +namespace Gemstone.Configuration.ReadOnly +{ + /// + /// Wrapper for to block calls + /// to . + /// + /// + public class ReadOnlyConfigurationProvider : IConfigurationProvider + { + internal IConfigurationProvider Provider { get; } + + /// + /// Creates a new instance of the class. + /// + /// + public ReadOnlyConfigurationProvider(IConfigurationProvider provider) => + Provider = provider; + + /// + /// Returns the immediate descendant configuration keys for a given parent path based + /// on this s data and the set of keys returned by + /// all the preceding s. + /// + /// The child keys returned by the preceding providers for the same parent path. + /// The parent path. + /// The child keys. + public IEnumerable GetChildKeys(IEnumerable earlierKeys, string? parentPath) => + Provider.GetChildKeys(earlierKeys, parentPath); + + /// + /// Returns a change token if this provider supports change tracking, null otherwise. + /// + /// The change token. + public IChangeToken GetReloadToken() => + Provider.GetReloadToken(); + + /// + /// Loads configuration values from the source represented by this . + /// + public void Load() => + Provider.Load(); + + /// + /// Sets a configuration value for the specified key. + /// + /// The key. + /// The value. + public void Set(string key, string value) { } + + /// + /// Tries to get a configuration value for the specified key. + /// + /// The key. + /// The value. + /// True if a value for the specified key was found, otherwise false. + public bool TryGet(string key, out string value) => + Provider.TryGet(key, out value); + } +} diff --git a/src/Gemstone/Configuration/ReadOnly/ReadOnlyConfigurationSource.cs b/src/Gemstone/Configuration/ReadOnly/ReadOnlyConfigurationSource.cs new file mode 100644 index 0000000000..f8543ef94b --- /dev/null +++ b/src/Gemstone/Configuration/ReadOnly/ReadOnlyConfigurationSource.cs @@ -0,0 +1,62 @@ +//****************************************************************************************************** +// ReadOnlyConfigurationSource.cs - Gbtc +// +// Copyright © 2023, 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://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: +// ---------------------------------------------------------------------------------------------------- +// 06/12/2020 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using Microsoft.Extensions.Configuration; + +namespace Gemstone.Configuration.ReadOnly +{ + /// + /// Wrapper for to block calls + /// to . + /// + /// + /// Configuration providers are typically designed to load configuration into an in-memory + /// dictionary from their configuration source. Subsequently, the in-memory dictionary can be + /// modified programmatically via the indexer. + /// This class blocks calls to + /// on the underlying configuration source's provider so that static defaults won't be + /// modified when updating configuration. + /// + public class ReadOnlyConfigurationSource : IConfigurationSource + { + private IConfigurationSource Source { get; } + + /// + /// Creates a new instance of the class. + /// + /// The source to be made read-only. + public ReadOnlyConfigurationSource(IConfigurationSource source) => + Source = source; + + /// + /// Builds the for this source. + /// + /// The configuration builder + /// The read-only configuration provider. + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + IConfigurationProvider provider = Source.Build(builder); + return new ReadOnlyConfigurationProvider(provider); + } + } +} diff --git a/src/Gemstone/Configuration/Settings.cs b/src/Gemstone/Configuration/Settings.cs new file mode 100644 index 0000000000..7f6ab06f6e --- /dev/null +++ b/src/Gemstone/Configuration/Settings.cs @@ -0,0 +1,323 @@ +//****************************************************************************************************** +// Settings.cs - Gbtc +// +// Copyright © 2020, 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://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/14/2024 - J. Ritchie Carroll +// Generated original version of source code. +// +//****************************************************************************************************** +// ReSharper disable NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using System.Dynamic; +using System.IO; +using System.Linq; +using Gemstone.Configuration.AppSettings; +using Gemstone.Configuration.INIConfigurationExtensions; +using Gemstone.Configuration.ReadOnly; +using Gemstone.Threading.SynchronizedOperations; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Ini; +using static Gemstone.Configuration.INIConfigurationHelpers; + +namespace Gemstone.Configuration; + +/// +/// Defines system settings for an application. +/// +public class Settings : DynamicObject +{ + /// + /// Defines the configuration section name for system settings. + /// + public const string SystemSettingsCategory = nameof(System); + + private readonly ConcurrentDictionary m_sections = new(StringComparer.OrdinalIgnoreCase); + private readonly List<(string key, object? defaultValue, string description, string[]? switchMappings)> m_definedSettings = []; + private readonly List m_configurationProviders = []; + private readonly ShortSynchronizedOperation m_saveOperation; + + /// + /// Creates a new instance. + /// + public Settings() + { + Instance ??= this; + m_saveOperation = new ShortSynchronizedOperation(SaveSections, ex => LibraryEvents.OnSuppressedException(this, ex)); + } + + /// + /// Gets or sets the source for settings. + /// + public IConfiguration? Configuration { get; set; } + + /// + /// Gets or sets flag that determines if INI file should be used for settings. + /// + public bool UseINIFile { get; set; } + + /// + /// Gets or sets flag that determines if SQLite should be used for settings. + /// + public bool UseSQLite { get; set; } + + /// + /// Gets or sets flag that determines if INI description lines should be split into multiple lines. + /// + public bool SplitINIDescriptionLines { get; set; } + + /// + /// Gets the names for the settings sections. + /// + public string[] SectionNames => m_sections.Keys.ToArray(); + + /// + /// Gets the sections count for the settings. + /// + public int Count => m_sections.Count; + + /// + /// Gets the command line switch mappings for . + /// + public Dictionary SwitchMappings { get; } = new(StringComparer.OrdinalIgnoreCase); + + /// + public override IEnumerable GetDynamicMemberNames() => m_sections.Keys; + + /// + /// Gets the for the specified key. + /// + /// Section key. + public SettingsSection this[string key] => m_sections.GetOrAdd(key, _ => new SettingsSection(this, key)); + + /// + /// Gets flag that determines if any settings have been changed. + /// + public bool IsDirty + { + get => m_sections.Values.Any(section => section.IsDirty); + private set + { + foreach (SettingsSection section in m_sections.Values) + section.IsDirty = value; + } + } + + /// + /// Attempts to bind the instance to configuration values by matching property + /// names against configuration keys recursively. + /// + /// Configuration builder used to bind settings. + public void Bind(IConfigurationBuilder builder) + { + // Build a new configuration with keys and values from the set of providers + // registered in builder sources - we call this instead of directly using + // the 'Build()' method on the config builder so providers can be cached + foreach (IConfigurationSource source in builder.Sources) + { + IConfigurationProvider provider = source.Build(builder); + m_configurationProviders.Add(provider); + } + + // Cache configuration root + Configuration = new ConfigurationRoot(m_configurationProviders); + + // Load settings from configuration sources hierarchy + foreach (IConfigurationSection configSection in Configuration.GetChildren()) + { + SettingsSection section = this[configSection.Key]; + + foreach (IConfigurationSection entry in configSection.GetChildren()) + section[entry.Key] = entry.Value; + + section.ConfigurationSection = configSection; + section.IsDirty = false; + } + } + + /// + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + string key = binder.Name; + + // If you try to get a value of a property that is + // not defined in the class, this method is called. + result = m_sections.GetOrAdd(key, _ => new SettingsSection(this, key)); + + return true; + } + + /// + public override bool TrySetMember(SetMemberBinder binder, object? value) + { + // If you try to set a value of a property that is + // not defined in the class, this method is called. + if (value is not SettingsSection section) + return false; + + m_sections[binder.Name] = section; + + return true; + } + + /// + public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) + { + if (indexes.Length != 1) + throw new ArgumentException($"{nameof(Settings)} indexer requires a single index representing string name of settings section."); + + string key = indexes[0].ToString()!; + + result = m_sections.GetOrAdd(key, _ => new SettingsSection(this, key)); + + return true; + } + + /// + public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object? value) + { + if (indexes.Length != 1) + throw new ArgumentException($"{nameof(Settings)} indexer requires a single index representing string name of settings section."); + + if (value is not SettingsSection section) + return false; + + m_sections[indexes[0].ToString()!] = section; + + return true; + } + + /// + /// Configures the for . + /// + /// Builder used to configure settings. + public void ConfigureAppSettings(IAppSettingsBuilder builder) + { + foreach ((string key, object? defaultValue, string description, string[]? switchMappings) in m_definedSettings) + { + builder.Add(key, defaultValue?.ToString() ?? "", description); + + if (switchMappings is null || switchMappings.Length == 0) + continue; + + foreach (string switchMapping in switchMappings) + SwitchMappings[switchMapping] = key; + } + } + + // Defines application settings for the specified section key + internal void DefineSetting(string key, string defaultValue, string description, string[]? switchMappings) + { + m_definedSettings.Add((key, defaultValue, description, switchMappings)); + } + + /// + /// Saves any changed settings. + /// + /// Determines if save operation should wait for completion. + public void Save(bool waitForSave) + { + if (!IsDirty) + return; + + if (waitForSave) + m_saveOperation.Run(true); + else + m_saveOperation.RunAsync(); + + IsDirty = false; + } + + private void SaveSections() + { + try + { + foreach (IConfigurationProvider provider in m_configurationProviders) + { + if (provider is ReadOnlyConfigurationProvider readOnlyProvider) + { + if (readOnlyProvider.Provider is not IniConfigurationProvider) + continue; + + // Handle INI file as a special case, writing entire file contents on save + string contents = Configuration!.GenerateINIFileContents(true, SplitINIDescriptionLines); + string iniFilePath = GetINIFilePath("settings.ini"); + using TextWriter writer = GetINIFileWriter(iniFilePath); + writer.Write(contents); + } + else + { + foreach (SettingsSection section in m_sections.Values) + { + if (!section.IsDirty) + continue; + + // Update configuration provider with each setting - in the case of + // SQLite, this will update the configuration database contents + foreach (string key in section.Keys) + provider.Set($"{section.Name}:{key}", section.ConfigurationSection[key]); + } + } + } + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed while trying to save configuration: {ex.Message}", ex); + } + } + + /// + /// Saves any changed settings. + /// + /// Settings instance. + /// + /// This method will not return until the save operation has completed. + /// + public static void Save(Settings? settings = null) + { + (settings ?? Instance).Save(true); + } + + /// + /// Gets the default instance of . + /// + public static Settings Instance { get; private set; } = default!; + + /// + /// Gets the default instance of as a dynamic object. + /// + /// Default instance of as a dynamic object. + /// Settings have not been initialized. + public static dynamic Default => Instance ?? throw new InvalidOperationException("Settings have not been initialized."); + + /// + /// Updates the default instance of . + /// + /// New default instance of . + /// + /// This changes the default singleton instance of to the specified instance. + /// Use this method with caution as it can lead to unexpected behavior if the default instance is changed. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static void UpdateInstance(Settings settings) + { + Instance = settings; + } +} diff --git a/src/Gemstone/Configuration/SettingsSection.cs b/src/Gemstone/Configuration/SettingsSection.cs new file mode 100644 index 0000000000..4442a0cc3d --- /dev/null +++ b/src/Gemstone/Configuration/SettingsSection.cs @@ -0,0 +1,404 @@ +//****************************************************************************************************** +// SettingsSection.cs - Gbtc +// +// Copyright © 2024, 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://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/14/2024 - J. Ritchie Carroll +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Collections.Concurrent; +using System.Dynamic; +using System.Linq; +using Gemstone.TypeExtensions; +using Microsoft.Extensions.Configuration; + +namespace Gemstone.Configuration; + +/// +/// Defines a dynamic section with typed values. +/// +public class SettingsSection : DynamicObject +{ + private readonly Settings m_parent; + private readonly ConcurrentDictionary m_keyValues = new(StringComparer.OrdinalIgnoreCase); + private IConfigurationSection? m_configurationSection; + + internal SettingsSection(Settings parent, string sectionName) + { + m_parent = parent; + Name = sectionName; + } + + internal IConfigurationSection ConfigurationSection + { + get => m_configurationSection ??= m_parent.Configuration?.GetSection(Name) ?? throw new InvalidOperationException("Configuration has not been set."); + set => m_configurationSection = value; + } + + /// + /// Gets the name of the settings section. + /// + public string Name { get; } + + /// + /// Gets the keys for the settings section. + /// + public string[] Keys => m_keyValues.Keys.ToArray(); + + /// + /// Gets flag that determines if the settings section has been modified. + /// + public bool IsDirty { get; internal set; } + + /// + /// Gets the typed value for the specified key. + /// + /// Setting key name. + public object? this[string key] + { + get + { + if (m_keyValues.TryGetValue(key, out object? cachedValue)) + return cachedValue; + + if (m_configurationSection is not null) + return m_keyValues[key] = FromTypedValue(ConfigurationSection[key]).value; + + throw new InvalidOperationException($"Configuration section \"{Name}\" has not been defined."); + } + set + { + if (value is null) + throw new ArgumentNullException(nameof(value)); + + object? updatedValue; + + if (value is string stringValue) + { + (Type valueType, object parsedValue, bool typePrefixed) = FromTypedValue(stringValue); + + if (typePrefixed) + { + updatedValue = parsedValue; + } + else + { + if (m_keyValues.TryGetValue(key, out object? initialValue)) + valueType = initialValue.GetType(); + + if (valueType == typeof(string)) + { + updatedValue = value; + } + else + { + if (string.IsNullOrWhiteSpace(stringValue)) + updatedValue = Activator.CreateInstance(valueType) ?? string.Empty; + else + updatedValue = Common.TypeConvertFromString(stringValue, valueType) ?? Activator.CreateInstance(valueType) ?? string.Empty; + } + } + } + else + { + updatedValue = value; + } + + if (!m_keyValues.TryGetValue(key, out object? currentValue) || currentValue != updatedValue) + { + m_keyValues[key] = updatedValue; + IsDirty = true; + } + + ConfigurationSection[key] = ToTypedValue(updatedValue); + } + } + + /// + /// Defines a setting for the section. + /// + /// Key name of setting to get, case-insensitive. + /// Default value if key does not exist. + /// Description of the setting. + /// Optional array of switch mappings for the setting. + public void Define(string key, object? defaultValue, string description, string[]? switchMappings = null) + { + m_keyValues.TryAdd(key, defaultValue ?? string.Empty); + + string typedValue = ToTypedValue(defaultValue); + + if (m_configurationSection is not null) + ConfigurationSection[key] ??= typedValue; + + m_parent.DefineSetting($"{Name}:{key}", typedValue, description, switchMappings); + } + + /// + /// Gets the value of the setting with the specified key, if it exists; + /// otherwise, the default value for the parameter. + /// + /// Key name of setting to get, case-insensitive. + /// Default value if key does not exist. + /// Description of the setting. + /// Optional array of switch mappings for the setting. + /// + /// Value of the setting with the specified key, if it exists; otherwise, + /// the default value for the parameter. + /// + public object? GetOrAdd(string key, object? defaultValue, string description, string[]? switchMappings = null) + { + Define(key, defaultValue, description, switchMappings); + return this[key]; + } + + /// + public override bool TryGetMember(GetMemberBinder binder, out object? result) + { + result = this[binder.Name]; + return true; + } + + /// + public override bool TrySetMember(SetMemberBinder binder, object? value) + { + return TrySetKeyValue(binder.Name, value); + } + + /// + public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object? result) + { + string key = indexes[0].ToString()!; + + // This allows dynamic access to section values with ability to add default values, descriptions and switch mappings + // For example: + // string hostURLs = settings.Web["HostURLs", "http://localhost:8180", "Defines the URLs the hosted service will listen on.", "-u"] + // string hostCertificate = settings.Web["HostCertificate", "", "Defines the certificate used to host the service.", "-s"] + switch (indexes.Length) + { + case 1: + result = this[key]; + return true; + case 2: + result = GetOrAdd(key, indexes[1], ""); + return true; + case 3: + result = GetOrAdd(key, indexes[1], indexes[2].ToString()!); + return true; + case 4: + result = GetOrAdd(key, indexes[1], indexes[2].ToString()!, [indexes[3].ToString()]); + return true; + case 5: + result = GetOrAdd(key, indexes[1], indexes[2].ToString()!, [indexes[3].ToString(), indexes[4].ToString()]); + return true; + case 6: + result = GetOrAdd(key, indexes[1], indexes[2].ToString()!, [indexes[3].ToString(), indexes[4].ToString(), indexes[5].ToString()]); + return true; + } + + throw new InvalidOperationException("Invalid number of index parameters."); + } + + /// + public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object? value) + { + return TrySetKeyValue(indexes[0].ToString()!, value); + } + + private bool TrySetKeyValue(string key, object? value) + { + // This allows dynamic setting definitions with values, descriptions and optional switch mappings + // For example: + // Settings.Default.WebHosting.HostURLs = ("http://localhost:8180", "Defines the URLs the hosted service will listen on.", "-u", "--HostURLs"); + // Settings.Default.WebHosting.HostCertificate = ("", "Defines the certificate used to host the service.", "-s", "--HostCertificate"); + // + // Non-tuple values just handle setting value assignment: + // Settings.Default.WebHosting.HostURLs = "http://localhost:5000"; + switch (value) + { + case ({ } defaultValue, string description, string[] switchMappings): + Define(key, defaultValue, description, switchMappings); + return true; + case ({ } defaultValue, string description, string switchMapping): + Define(key, defaultValue, description, [switchMapping]); + return true; + case ({ } defaultValue, string description, string switchMapping1, string switchMapping2): + Define(key, defaultValue, description, [switchMapping1, switchMapping2]); + return true; + case ({ } defaultValue, string description, string switchMapping1, string switchMapping2, string switchMapping3): + Define(key, defaultValue, description, [switchMapping1, switchMapping2, switchMapping3]); + return true; + case ({ } defaultValue, string description): + Define(key, defaultValue, description); + return true; + default: + this[key] = value; + return true; + } + } + + /// + /// Gets the parsed type and value of a configuration setting value. + /// + /// Configuration setting value that can be prefixed with a type. + /// Tuple containing the parsed type, value and flag determining if setting was type prefixed. + /// Failed to load specified type. + /// + /// + /// Type name is parsed from the beginning of the setting value, if it exists, and is enclosed in brackets, + /// e.g.: [int]:123 or [System.Int32]:123. If no type name is specified, the assumed default + /// type will always be . + /// + /// + /// Common C# names like long and DateTime can be used as type names, these will not be + /// case-sensitive. Custom type names will be case-sensitive and require a full type name. + /// + /// + public static (Type type, object value, bool typePrefixed) FromTypedValue(string? setting) + { + if (setting is null) + return (typeof(string), string.Empty, false); + + string[] parts = setting.Split(':'); + + if (parts.Length < 2) + return (typeof(string), setting, false); + + string typeName = parts[0].Trim(); + + // Make sure type name is enclosed in brackets, only this indicates a type name + if (typeName.StartsWith('[') && typeName.EndsWith(']')) + typeName = typeName[1..^1].Trim(); + else + return (typeof(string), setting, false); + + string value = setting[(parts[0].Length + 1)..].Trim(); + + // Parse common C# type names + return typeName.ToLowerInvariant() switch + { + "string" => (typeof(string), value, true), + "bool" => (typeof(bool), Convert.ToBoolean(value), true), + "byte" => (typeof(byte), Convert.ToByte(value), true), + "sbyte" => (typeof(sbyte), Convert.ToSByte(value), true), + "short" => (typeof(short), Convert.ToInt16(value), true), + "ushort" => (typeof(ushort), Convert.ToUInt16(value), true), + "int" => (typeof(int), Convert.ToInt32(value), true), + "uint" => (typeof(uint), Convert.ToUInt32(value), true), + "long" => (typeof(long), Convert.ToInt64(value), true), + "ulong" => (typeof(ulong), Convert.ToUInt64(value), true), + "float" => (typeof(float), Convert.ToSingle(value), true), + "double" => (typeof(double), Convert.ToDouble(value), true), + "decimal" => (typeof(decimal), Convert.ToDecimal(value), true), + "char" => (typeof(char), Convert.ToChar(value), true), + "datetime" => (typeof(DateTime), Convert.ToDateTime(value), true), + "timespan" => (typeof(TimeSpan), TimeSpan.Parse(value), true), + "guid" => (typeof(Guid), Guid.Parse(value), true), + "uri" => (typeof(Uri), new Uri(value), true), + "version" => (typeof(Version), new Version(value), true), + "type" => (typeof(Type), Type.GetType(value, true) ?? throw new InvalidOperationException($"Failed to load type \"{value}\"."), true), + _ => getParsedTypeAndValue() + }; + + // Parse custom type names - custom names will be case-sensitive and require full type name + (Type, object, bool) getParsedTypeAndValue() + { + Type parsedType = Type.GetType(typeName, true) ?? throw new InvalidOperationException($"Failed to load type \"{typeName}\"."); + object parsedValue = Common.TypeConvertFromString(value, parsedType) ?? Activator.CreateInstance(parsedType) ?? string.Empty; + + return (parsedType, parsedValue, true); + } + } + + /// + /// Converts a value to a typed string representation. + /// + /// Value to convert to a typed representation. + /// String formatted as a type-prefixed value, e.g.: [int]:123. + public static string ToTypedValue(object? value) + { + if (value is null) + return string.Empty; + + Type valueType = value.GetType(); + + // Handle common C# type names + if (valueType == typeof(string)) + return value.ToString()!; + + if (valueType == typeof(bool)) + return $"[bool]:{value}"; + + if (valueType == typeof(byte)) + return $"[byte]:{value}"; + + if (valueType == typeof(sbyte)) + return $"[sbyte]:{value}"; + + if (valueType == typeof(short)) + return $"[short]:{value}"; + + if (valueType == typeof(ushort)) + return $"[ushort]:{value}"; + + if (valueType == typeof(int)) + return $"[int]:{value}"; + + if (valueType == typeof(uint)) + return $"[uint]:{value}"; + + if (valueType == typeof(long)) + return $"[long]:{value}"; + + if (valueType == typeof(ulong)) + return $"[ulong]:{value}"; + + if (valueType == typeof(float)) + return $"[float]:{value}"; + + if (valueType == typeof(double)) + return $"[double]:{value}"; + + if (valueType == typeof(decimal)) + return $"[decimal]:{value}"; + + if (valueType == typeof(char)) + return $"[char]:{value}"; + + if (valueType == typeof(DateTime)) + return $"[DateTime]:{value}"; + + if (valueType == typeof(TimeSpan)) + return $"[TimeSpan]:{value}"; + + if (valueType == typeof(Guid)) + return $"[Guid]:{value}"; + + if (valueType == typeof(Uri)) + return $"[Uri]:{value}"; + + if (valueType == typeof(Version)) + return $"[Version]:{value}"; + + if (valueType == typeof(Type)) + return $"[Type]:{value}"; + + // Handle custom type names + return $"[{valueType.GetReflectedTypeName()}]:{value}"; + } +} diff --git a/src/Gemstone/Console/Arguments.cs b/src/Gemstone/Console/Arguments.cs index abf79e0ea6..dd52e3659c 100644 --- a/src/Gemstone/Console/Arguments.cs +++ b/src/Gemstone/Console/Arguments.cs @@ -323,7 +323,7 @@ public virtual string? this[string argument] /// /// Gets a boolean value that indicates whether the command-line command contains request for displaying help. /// - public virtual bool ContainsHelpRequest => (m_arguments.ContainsKey("?") || m_arguments.ContainsKey("Help")); + public virtual bool ContainsHelpRequest => m_arguments.ContainsKey("?") || m_arguments.ContainsKey("Help"); /// /// Gets the dictionary containing all of the arguments present in the command-line command. diff --git a/src/Gemstone/Gemstone.Common.csproj b/src/Gemstone/Gemstone.Common.csproj index a361a3e9c8..582c5a1cac 100644 --- a/src/Gemstone/Gemstone.Common.csproj +++ b/src/Gemstone/Gemstone.Common.csproj @@ -42,6 +42,8 @@ Full + False + $(DefineConstants);DEBUG @@ -60,11 +62,17 @@ + + + + + + diff --git a/src/Gemstone/IO/FilePath.cs b/src/Gemstone/IO/FilePath.cs index 77ab934072..c4cabea98f 100644 --- a/src/Gemstone/IO/FilePath.cs +++ b/src/Gemstone/IO/FilePath.cs @@ -120,20 +120,20 @@ static FilePath() /// /// Connects to a network share with the specified user's credentials. /// - /// UNC share name to connect to. - /// User name to use for connection. + /// UNC share name to connect to. + /// Username to use for connection. /// Password to use for connection. /// Domain name to use for connection. Specify the computer name for local system accounts. - public static void ConnectToNetworkShare(string sharename, string userName, string password, string domain) + public static void ConnectToNetworkShare(string shareName, string userName, string password, string domain) { // TODO: Add #include implementation for POSIX environment, see Gemstone.POSIX library if (IsPosixEnvironment) - throw new NotImplementedException("Failed to connect to network share \"" + sharename + "\" as user " + userName + " - not implemented in POSIX environment"); + throw new NotImplementedException("Failed to connect to network share \"" + shareName + "\" as user " + userName + " - not implemented in POSIX environment"); NETRESOURCE resource = new() { dwType = RESOURCETYPE_DISK, - lpRemoteName = sharename + lpRemoteName = shareName }; if (domain.Length > 0) @@ -142,33 +142,33 @@ public static void ConnectToNetworkShare(string sharename, string userName, stri int result = WNetAddConnection2(ref resource, password, userName, 0); if (result != 0) - throw new InvalidOperationException("Failed to connect to network share \"" + sharename + "\" as user " + userName + " - " + WindowsApi.GetErrorMessage(result)); + throw new InvalidOperationException("Failed to connect to network share \"" + shareName + "\" as user " + userName + " - " + WindowsApi.GetErrorMessage(result)); } /// /// Disconnects the specified network share. /// - /// UNC share name to disconnect from. - public static void DisconnectFromNetworkShare(string sharename) + /// UNC share name to disconnect from. + public static void DisconnectFromNetworkShare(string shareName) { - DisconnectFromNetworkShare(sharename, true); + DisconnectFromNetworkShare(shareName, true); } /// /// Disconnects the specified network share. /// - /// UNC share name to disconnect from. + /// UNC share name to disconnect from. /// true to force a disconnect; otherwise false. - public static void DisconnectFromNetworkShare(string sharename, bool force) + public static void DisconnectFromNetworkShare(string shareName, bool force) { // TODO: Add #include implementation for POSIX environment, see Gemstone.POSIX library if (IsPosixEnvironment) - throw new NotImplementedException("Failed to disconnect from network share \"" + sharename + "\" - not implemented in POSIX environment"); + throw new NotImplementedException("Failed to disconnect from network share \"" + shareName + "\" - not implemented in POSIX environment"); - int result = WNetCancelConnection2(sharename, 0, force); + int result = WNetCancelConnection2(shareName, 0, force); if (result != 0) - throw new InvalidOperationException("Failed to disconnect from network share \"" + sharename + "\" - " + WindowsApi.GetErrorMessage(result)); + throw new InvalidOperationException("Failed to disconnect from network share \"" + shareName + "\" - " + WindowsApi.GetErrorMessage(result)); } /// diff --git a/src/Gemstone/IO/InterprocessCache.cs b/src/Gemstone/IO/InterprocessCache.cs new file mode 100644 index 0000000000..5836d05446 --- /dev/null +++ b/src/Gemstone/IO/InterprocessCache.cs @@ -0,0 +1,652 @@ +//****************************************************************************************************** +// InterprocessCache.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. +// +//****************************************************************************************************** + +using System; +using System.Collections; +using System.IO; +using System.Threading; +using System.Timers; +using Gemstone.Collections.CollectionExtensions; +using Gemstone.IO.StreamExtensions; +using Gemstone.Threading; +using Gemstone.Threading.SynchronizedOperations; +using Timer = System.Timers.Timer; + +namespace Gemstone.IO; + +/// +/// Represents a serialized data cache that can be saved or read from multiple applications using inter-process synchronization. +/// +/// +/// +/// Note that all file data in this class gets serialized to and from memory, as such, the design intention for this class is for +/// use with smaller data sets such as serialized lists or dictionaries that need inter-process synchronized loading and saving. +/// +/// +/// The uses a to synchronize access to cache as an inter-process shared resource. +/// On POSIX systems, the exhibits kernel persistence, meaning instances will remain active beyond the lifespan of +/// the creating process. The named semaphore must be explicitly removed by invoking when the last +/// interprocess cache instance is no longer needed. Kernel persistence necessitates careful design consideration regarding process +/// responsibility for invoking the method. Since the common use case for named semaphores is across +/// multiple applications, it is advisable for the last exiting process to handle the cleanup. In cases where an application may crash before +/// calling the method, the semaphore persists in the system, potentially leading to resource leakage. +/// Implementations should include strategies to address and mitigate this risk. +/// +/// +public class InterprocessCache : IDisposable +{ + #region [ Members ] + + // Constants + private const int WriteEvent = 0; + private const int ReadEvent = 1; + + /// + /// Default maximum retry attempts allowed for loading . + /// + public const int DefaultMaximumRetryAttempts = 5; + + /// + /// Default wait interval, in milliseconds, before retrying load of . + /// + public const double DefaultRetryDelayInterval = 1000.0D; + + // Fields + private string? m_fileName; // Path and file name of file needing inter-process synchronization + private byte[]? m_fileData; // Data loaded or to be saved + private readonly LongSynchronizedOperation m_loadOperation; // Synchronized operation to asynchronously load data from the file + private readonly LongSynchronizedOperation m_saveOperation; // Synchronized operation to asynchronously save data to the file + private InterprocessReaderWriterLock? m_fileLock; // Inter-process reader/writer lock used to synchronize file access + private readonly ManualResetEventSlim m_loadIsReady; // Wait handle used so that system will wait for file data load + private readonly ManualResetEventSlim m_saveIsReady; // Wait handle used so that system will wait for file data save + private SafeFileWatcher? m_fileWatcher; // Optional file watcher used to reload changes + private readonly BitArray m_retryQueue; // Retry event queue + private readonly Timer m_retryTimer; // File I/O retry timer + private long m_lastRetryTime; // Time of last retry attempt + private int m_retryCount; // Total number of retries attempted so far + private bool m_disposed; // Class disposed flag + + #endregion + + #region [ Constructors ] + + /// + /// Creates a new instance of the . + /// + public InterprocessCache() : + this(InterprocessReaderWriterLock.DefaultMaximumConcurrentLocks) + { + } + + /// + /// Creates a new instance of the with the specified number of . + /// + /// Maximum concurrent reader locks to allow. + public InterprocessCache(int maximumConcurrentLocks) + { + // Initialize field values + m_loadOperation = new LongSynchronizedOperation(SynchronizedRead) { IsBackground = true }; + m_saveOperation = new LongSynchronizedOperation(SynchronizedWrite); + m_loadIsReady = new ManualResetEventSlim(false); + m_saveIsReady = new ManualResetEventSlim(true); + MaximumConcurrentLocks = maximumConcurrentLocks; + MaximumRetryAttempts = DefaultMaximumRetryAttempts; + m_retryQueue = new BitArray(2); + m_fileData = Array.Empty(); + + // Setup retry timer + m_retryTimer = new Timer(); + m_retryTimer.Elapsed += m_retryTimer_Elapsed; + m_retryTimer.AutoReset = false; + m_retryTimer.Interval = DefaultRetryDelayInterval; + } + + /// + /// Releases the unmanaged resources before the object is reclaimed by . + /// + ~InterprocessCache() => Dispose(false); + + #endregion + + #region [ Properties ] + + /// + /// Path and file name for the cache needing inter-process synchronization. + /// + public string FileName + { + get + { + return m_fileName ?? string.Empty; + } + // Disallowing set accessor for this property as enabling would require re-initialization + // of inter-process lock when file name was changed. This would also require a call to + // "ReleaseInterprocessResources" on the old file name and would make responsibility for + // inter-process lock management related to "ReleaseInterprocessResources" ambiguous. + init + { + if (value is null) + throw new NullReferenceException("FileName cannot be null"); + + if (m_fileLock is not null) + throw new InvalidOperationException("FileName cannot be changed after inter-process lock has been initialized"); + + m_fileName = FilePath.GetAbsolutePath(value); + + m_fileLock = new InterprocessReaderWriterLock(m_fileName, MaximumConcurrentLocks); + } + } + + /// + /// Gets or sets file data for the cache to be saved or that has been loaded. + /// + /// + /// Setting value to null will create a zero-length file. + /// + public byte[]? FileData + { + get + { + // Calls to this property are blocked until data is available + WaitForLoad(); + + byte[] fileData = Interlocked.CompareExchange(ref m_fileData, default, default) ?? Array.Empty(); + + return fileData.Copy(0, fileData.Length); + } + set + { + if (m_fileName is null) + throw new NullReferenceException("FileName property must be defined before setting FileData"); + + bool dataChanged = false; + + // If value is null, assume user means zero-length file + value ??= Array.Empty(); + + byte[]? fileData = Interlocked.Exchange(ref m_fileData, value); + + if (AutoSave) + dataChanged = (fileData!.CompareTo(value) != 0); + + // Initiate save if data has changed + if (!dataChanged) + return; + + m_saveIsReady.Reset(); + m_saveOperation.RunAsync(); + } + } + + /// + /// Gets or sets flag that determines if should automatically initiate a save when has been updated. + /// + public bool AutoSave { get; set; } + + /// + /// Gets or sets flag that enables system to monitor for changes in and automatically reload . + /// + public bool ReloadOnChange + { + get + { + return m_fileWatcher is not null; + } + set + { + switch (value) + { + case true when m_fileWatcher is null: + { + if (m_fileName is null) + throw new NullReferenceException("FileName property must be defined before enabling ReloadOnChange"); + + // Setup file watcher to monitor for external updates + m_fileWatcher = new SafeFileWatcher + { + Path = FilePath.GetDirectoryName(m_fileName), + Filter = FilePath.GetFileName(m_fileName), + EnableRaisingEvents = true + }; + + m_fileWatcher.Changed += m_fileWatcher_Changed; + break; + } + case false when m_fileWatcher is not null: + // Disable file watcher + m_fileWatcher.Changed -= m_fileWatcher_Changed; + m_fileWatcher.Dispose(); + m_fileWatcher = null; + break; + } + } + } + + /// + /// Gets the maximum concurrent reader locks allowed. + /// + public int MaximumConcurrentLocks { get; } + + /// + /// Maximum retry attempts allowed for loading or saving cache file data. + /// + public int MaximumRetryAttempts { get; set; } + + /// + /// Wait interval, in milliseconds, before retrying load or save of cache file data. + /// + public double RetryDelayInterval + { + get => m_retryTimer.Interval; + set => m_retryTimer.Interval = value; + } + + #endregion + + #region [ Methods ] + + /// + /// Releases all the resources used by the object. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (m_disposed) + return; + + try + { + if (!disposing) + return; + + if (m_fileWatcher is not null) + { + m_fileWatcher.Changed -= m_fileWatcher_Changed; + m_fileWatcher.Dispose(); + m_fileWatcher = null; + } + + m_retryTimer.Elapsed -= m_retryTimer_Elapsed; + m_retryTimer.Dispose(); + + m_loadIsReady.Dispose(); + m_saveIsReady.Dispose(); + + if (m_fileLock is not null) + { + m_fileLock.Dispose(); + m_fileLock = null; + } + + m_fileName = null; + } + finally + { + m_disposed = true; // Prevent duplicate dispose. + } + } + + /// + /// Releases the inter-process resources used by the . + /// + /// + /// On POSIX systems, calling this method removes the named semaphore used by the inter-process cache. + /// The semaphore name is removed immediately and is destroyed once all other processes that have the + /// semaphore open close it. Calling this method on Windows systems does nothing. + /// + public void ReleaseInterprocessResources() + { + m_fileLock?.ReleaseInterprocessResources(); + } + + /// + /// Initiates inter-process synchronized cache file save. + /// + /// + /// Subclasses should always call before calling this method. + /// + public virtual void Save() + { + if (m_disposed) + throw new ObjectDisposedException(nameof(InterprocessCache)); + + if (m_fileName is null) + throw new NullReferenceException("FileName is null, cannot initiate save"); + + if (m_fileData is null) + throw new NullReferenceException("FileData is null, cannot initiate save"); + + m_saveIsReady.Reset(); + m_saveOperation.RunAsync(); + } + + /// + /// Initiates inter-process synchronized cache file load. + /// + /// + /// Subclasses should always call before calling this method. + /// + public virtual void Load() + { + if (m_disposed) + throw new ObjectDisposedException(nameof(InterprocessCache)); + + if (m_fileName is null) + throw new NullReferenceException("FileName is null, cannot initiate load"); + + m_loadIsReady.Reset(); + m_loadOperation.RunAsync(); + } + + /// + /// Blocks current thread and waits for any pending load to complete; wait time is * . + /// + public virtual void WaitForLoad() => WaitForLoad((int)(RetryDelayInterval * MaximumRetryAttempts)); + + /// + /// Blocks current thread and waits for specified for any pending load to complete. + /// + /// The number of milliseconds to wait, or (-1) to wait indefinitely. + public virtual void WaitForLoad(int millisecondsTimeout) + { + if (m_disposed) + throw new ObjectDisposedException(nameof(InterprocessCache)); + + // Calls to this method are blocked until data is available + if (!m_loadIsReady.IsSet && !m_loadIsReady.Wait(millisecondsTimeout)) + throw new TimeoutException($"Timeout waiting to read data from {m_fileName}"); + } + + /// + /// Blocks current thread and waits for any pending save to complete; wait time is * . + /// + public virtual void WaitForSave() => WaitForSave((int)(RetryDelayInterval * MaximumRetryAttempts)); + + /// + /// Blocks current thread and waits for specified for any pending save to complete. + /// + /// The number of milliseconds to wait, or (-1) to wait indefinitely. + public virtual void WaitForSave(int millisecondsTimeout) + { + if (m_disposed) + throw new ObjectDisposedException(nameof(InterprocessCache)); + + // Calls to this method are blocked until data is saved + if (!m_saveIsReady.IsSet && !m_saveIsReady.Wait(millisecondsTimeout)) + throw new TimeoutException($"Timeout waiting to save data to {m_fileName}"); + } + + /// + /// Handles serialization of file to disk; virtual method allows customization (e.g., pre-save encryption and/or data merge). + /// + /// used to serialize data. + /// File data to be serialized. + /// + /// Consumers overriding this method should not directly call property to avoid potential dead-locks. + /// + protected virtual void SaveFileData(FileStream fileStream, byte[] fileData) => + fileStream.Write(fileData, 0, fileData.Length); + + /// + /// Handles deserialization of file from disk; virtual method allows customization (e.g., pre-load decryption and/or data merge). + /// + /// used to deserialize data. + /// Deserialized file data. + /// + /// Consumers overriding this method should not directly call property to avoid potential dead-locks. + /// + protected virtual byte[] LoadFileData(FileStream fileStream) => + fileStream.ReadStream(); + + /// + /// Synchronously writes file data when no reads are active. + /// + private void SynchronizedWrite() + { + try + { + if (m_disposed) + return; + + if (m_fileLock?.TryEnterWriteLock((int)m_retryTimer.Interval) ?? false) + { + FileStream? fileStream = null; + + try + { + fileStream = new FileStream(m_fileName!, FileMode.Create, FileAccess.Write, FileShare.None); + + try + { + // Disable file watch notification before update + if (m_fileWatcher is not null) + m_fileWatcher.EnableRaisingEvents = false; + + byte[]? fileData = Interlocked.CompareExchange(ref m_fileData, default, default); + SaveFileData(fileStream, fileData!); + + // Release any threads waiting for file save + m_saveIsReady.Set(); + } + finally + { + // Re-enable file watch notification + if (m_fileWatcher is not null) + m_fileWatcher.EnableRaisingEvents = true; + } + } + catch (IOException ex) + { + RetrySynchronizedEvent(ex, WriteEvent); + } + finally + { + m_fileLock?.ExitWriteLock(); + fileStream?.Close(); + } + } + else + { + RetrySynchronizedEvent(new TimeoutException($"Timeout waiting to acquire write lock for {m_fileName}"), WriteEvent); + } + } + catch (ThreadAbortException) + { + // Release any threads waiting for file save in case of thread abort + m_saveIsReady.Set(); + throw; + } + catch (UnauthorizedAccessException) + { + // Release any threads waiting for file save in case of I/O or locking failures during write attempt + m_saveIsReady.Set(); + throw; + } + catch (Exception ex) + { + // Other exceptions can happen, e.g., NullReferenceException if thread resumes and the class is disposed middle way through this method + // or other serialization issues in call to SaveFileData, in these cases, release any threads waiting for file save + m_saveIsReady.Set(); + LibraryEvents.OnSuppressedException(this, new Exception($"Synchronized write exception: {ex.Message}", ex)); + } + } + + /// + /// Synchronously reads file data when no writes are active. + /// + private void SynchronizedRead() + { + try + { + if (m_disposed) + return; + + if (File.Exists(m_fileName)) + { + if (m_fileLock?.TryEnterReadLock((int)m_retryTimer.Interval) ?? false) + { + FileStream? fileStream = null; + + try + { + fileStream = new FileStream(m_fileName!, FileMode.Open, FileAccess.Read, FileShare.Read); + Interlocked.Exchange(ref m_fileData, LoadFileData(fileStream)); + + // Release any threads waiting for file data + m_loadIsReady.Set(); + } + catch (IOException ex) + { + RetrySynchronizedEvent(ex, ReadEvent); + } + finally + { + m_fileLock?.ExitReadLock(); + fileStream?.Close(); + } + } + else + { + RetrySynchronizedEvent(new TimeoutException($"Timeout waiting to acquire read lock for {m_fileName}"), ReadEvent); + } + } + else + { + // File doesn't exist, create an empty array representing a zero-length file + m_fileData = Array.Empty(); + + // Release any threads waiting for file data + m_loadIsReady.Set(); + } + } + catch (ThreadAbortException) + { + // Release any threads waiting for file data in case of thread abort + m_loadIsReady.Set(); + throw; + } + catch (UnauthorizedAccessException) + { + // Release any threads waiting for file load in case of I/O or locking failures during read attempt + m_loadIsReady.Set(); + throw; + } + catch (Exception ex) + { + // Other exceptions can happen, e.g., NullReferenceException if thread resumes and the class is disposed middle way through this method + // or other deserialization issues in call to LoadFileData, in these cases, release any threads waiting for file load + m_loadIsReady.Set(); + LibraryEvents.OnSuppressedException(this, new Exception($"Synchronized read exception: {ex.Message}", ex)); + } + } + + /// + /// Initiates a retry for specified event type. + /// + /// Exception causing retry. + /// Event type to retry. + private void RetrySynchronizedEvent(Exception ex, int eventType) + { + if (m_disposed) + return; + + // A retry is only being initiating for basic file I/O or locking errors - monitor these failures occurring + // in quick succession so that retry activity is not allowed to go on forever... + if (DateTime.UtcNow.Ticks - m_lastRetryTime > (long)Ticks.FromMilliseconds(m_retryTimer.Interval * MaximumRetryAttempts)) + { + // Significant time has passed since last retry, so we reset counter + m_retryCount = 0; + m_lastRetryTime = DateTime.UtcNow.Ticks; + } + else + { + m_retryCount++; + + if (m_retryCount >= MaximumRetryAttempts) + throw new UnauthorizedAccessException($"Failed to {(eventType == WriteEvent ? "write data to " : "read data from ")}{m_fileName} after {MaximumRetryAttempts} attempts: {ex.Message}", ex); + } + + // Technically the inter-process mutex will handle serialized access to the file, but if the OS or other process + // not participating with the mutex has the file locked, all we can do is queue up a retry for this event. + lock (m_retryQueue) + m_retryQueue[eventType] = true; + + m_retryTimer.Start(); + } + + /// + /// Retries specified serialize or deserialize event in case of file I/O failures. + /// + private void m_retryTimer_Elapsed(object? sender, ElapsedEventArgs e) + { + if (m_disposed) + return; + + LongSynchronizedOperation? operation = null; + + lock (m_retryQueue) + { + // Reads should always occur first since you may need to load any + // newly written data before saving new data. Users can override + // load and save behavior to "merge" data sets if needed. + if (m_retryQueue[ReadEvent]) + { + operation = m_loadOperation; + m_retryQueue[ReadEvent] = false; + } + else if (m_retryQueue[WriteEvent]) + { + operation = m_saveOperation; + m_retryQueue[WriteEvent] = false; + } + + // If any events remain queued for retry, start timer for next event + if (m_retryQueue.Any(true)) + m_retryTimer.Start(); + } + + operation?.TryRunAsync(); + } + + /// + /// Reload file upon external modification. + /// + /// The object that triggered the event. + /// An object which provides data for directory events. + private void m_fileWatcher_Changed(object? sender, FileSystemEventArgs e) + { + if (e.ChangeType == WatcherChangeTypes.Changed) + Load(); + } + + #endregion +} diff --git a/src/Gemstone/IO/RunTimeLog.cs b/src/Gemstone/IO/RunTimeLog.cs index b372f93454..d2bd5f4595 100644 --- a/src/Gemstone/IO/RunTimeLog.cs +++ b/src/Gemstone/IO/RunTimeLog.cs @@ -123,10 +123,7 @@ public RunTimeLog() /// public string FileName { - get - { - return m_fileName; - } + get => m_fileName; set { if (string.IsNullOrWhiteSpace(value)) @@ -227,7 +224,9 @@ public void Dispose() /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { - if (IsDisposed) return; + if (IsDisposed) + return; + try { if (disposing) @@ -334,7 +333,7 @@ protected void ReadLog() } /// - /// Writes the run-time log - times are in a human readable format. + /// Writes the run-time log - times are in a human-readable format. /// protected void WriteLog() { diff --git a/src/Gemstone/IO/SafeFileWatcher.cs b/src/Gemstone/IO/SafeFileWatcher.cs new file mode 100644 index 0000000000..aac1cada50 --- /dev/null +++ b/src/Gemstone/IO/SafeFileWatcher.cs @@ -0,0 +1,408 @@ +//****************************************************************************************************** +// SafeFileWatcher.cs - Gbtc +// +// Copyright © 2015, 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://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: +// ---------------------------------------------------------------------------------------------------- +// 07/28/2015 - J. Ritchie Carroll +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.ComponentModel; +using System.IO; +using System.Security; +using Gemstone.EventHandlerExtensions; + +namespace Gemstone.IO; + +/// +/// Represents a wrapper around the native .NET that avoids problems with +/// dangling references when using a file watcher instance as a class member that never gets disposed. +/// +/// +/// +/// The design goal of the SafeFileWatcher is to avoid accidental memory leaks caused by use of .NET's native +/// file system watcher when used as a member of a class that consumers fail to properly dispose. If a class +/// has a reference to a file watcher as a member variable and attaches to the file watcher's events, the +/// file watcher will maintain a reference the parent class so it can call its event handler. If the parent +/// class is not disposed properly, the file watcher will thus not be disposed and will maintain the +/// reference to the parent class - the garbage collector will never collect the parent because it has a +/// valid reference and no collection means the parent finalizer will never get called and the file system +/// watcher will never get disposed. Creating multiple instances of parent class and not disposing of them +/// will cause a memory leak even with a properly designed disposable pattern. Using the SafeFileWatcher +/// instead of directly using the FileSystemWatcher will resolve this potential issue. +/// +/// +/// Note that component model implementation is not fully replicated - if you are using a file system watcher +/// on a design surface, this safety wrapper will usually not be needed. This class has benefit when a class +/// will dynamically use a file watcher and needs to make sure any unmanaged resources get properly released +/// even if a consumer neglects to call the dispose function. +/// +/// +[SecurityCritical] +public class SafeFileWatcher : IDisposable +{ + #region [ Members ] + + // Events + + /// + /// Occurs when a file or directory in the specified is changed. + /// + public event FileSystemEventHandler? Changed; + + /// + /// Occurs when a file or directory in the specified is created. + /// + public event FileSystemEventHandler? Created; + + /// + /// Occurs when a file or directory in the specified is deleted. + /// + public event FileSystemEventHandler? Deleted; + + /// + /// Occurs when a file or directory in the specified is renamed. + /// + public event RenamedEventHandler? Renamed; + + /// + /// Occurs when the internal buffer overflows. + /// + public event ErrorEventHandler? Error; + + // Fields + private readonly FileSystemWatcher m_fileSystemWatcher; + private bool m_disposed; + + #endregion + + #region [ Constructors ] + + /// + /// Initializes a new instance of the class. + /// + public SafeFileWatcher() + { + m_fileSystemWatcher = new FileSystemWatcher(); + InitializeFileSystemWatcher(); + } + + /// + /// Initializes a new instance of the class, given the specified directory to monitor. + /// + /// The directory to monitor, in standard or Universal Naming Convention (UNC) notation. + /// + /// The parameter is null. + /// + /// The parameter is an empty string (""). -or- + /// The path specified through the parameter does not exist. + /// + public SafeFileWatcher(string path) + { + m_fileSystemWatcher = new FileSystemWatcher(path); + InitializeFileSystemWatcher(); + } + + /// + /// Initializes a new instance of the class, given the specified directory and type of files to monitor. + /// + /// The directory to monitor, in standard or Universal Naming Convention (UNC) notation. + /// The type of files to watch. For example, "*.txt" watches for changes to all text files. + /// + /// The parameter is null. -or- + /// The parameter is null. + /// + /// + /// The parameter is an empty string (""). -or- + /// The path specified through the parameter does not exist. + /// + public SafeFileWatcher(string path, string filter) + { + m_fileSystemWatcher = new FileSystemWatcher(path, filter); + InitializeFileSystemWatcher(); + } + + // Attach to file system watcher events via lambda function using a weak reference to this class instance + // connected through static method so that file watcher cannot hold onto a reference to this class - this + // way even if consumer neglects to dispose this class, it will get properly garbage collected and finalized + // because there will be no remaining references to this class instance. Also, even though the following + // intermediate lambda classes that get created will be attached to the file system watcher event handlers, + // they will also be freed because this class will make sure the file system watcher instance is handled + // like an unmanaged resource, i.e., it always gets disposed, via the finalizer if need be. + private void InitializeFileSystemWatcher() + { + WeakReference reference = new(this); + + m_fileSystemWatcher.Changed += (_, e) => OnChanged(reference, e); + m_fileSystemWatcher.Created += (_, e) => OnCreated(reference, e); + m_fileSystemWatcher.Deleted += (_, e) => OnDeleted(reference, e); + m_fileSystemWatcher.Renamed += (_, e) => OnRenamed(reference, e); + m_fileSystemWatcher.Error += (_, e) => OnError(reference, e); + } + + /// + /// Terminates instance making sure to release unmanaged resources. + /// + ~SafeFileWatcher() + { + // Finalizer desired because we want to dispose FileSystemWatcher to force release of pinned buffers + // and thus release reference to any associated event delegates allowing garbage collection + Dispose(false); + } + + #endregion + + #region [ Properties ] + + /// + /// Gets or sets the path of the directory to watch. + /// + /// + /// The path to monitor. The default is an empty string (""). + /// + /// + /// The specified path does not exist or could not be found. -or- + /// The specified path contains wildcard characters. -or- + /// The specified path contains invalid path characters. + /// + public string Path + { + get => m_fileSystemWatcher.Path; + set => m_fileSystemWatcher.Path = value; + } + + /// + /// Gets or sets the filter string used to determine what files are monitored in a directory. + /// + /// + /// The filter string. The default is "*.*" (Watches all files.) + /// + public string Filter + { + get => m_fileSystemWatcher.Filter; + set => m_fileSystemWatcher.Filter = value; + } + + /// + /// Gets or sets the type of changes to watch for. + /// + /// + /// One of the values. The default is the bitwise OR combination of LastWrite, FileName, and DirectoryName. + /// The value is not a valid bitwise OR combination of the values. + /// The value that is being set is not valid. + public NotifyFilters NotifyFilter + { + get => m_fileSystemWatcher.NotifyFilter; + set => m_fileSystemWatcher.NotifyFilter = value; + } + + /// + /// Gets or sets a value indicating whether the component is enabled. + /// + /// + /// true if the component is enabled; otherwise, false. The default is false. If you are using the component on a designer in Visual Studio 2005, the default is true. + /// The object has been disposed. + /// The current operating system is not Microsoft Windows NT or later. + /// The directory specified in could not be found. + /// has not been set or is invalid. + public bool EnableRaisingEvents + { + get => m_fileSystemWatcher.EnableRaisingEvents; + set => m_fileSystemWatcher.EnableRaisingEvents = value; + } + + /// + /// Gets or sets a value indicating whether subdirectories within the specified path should be monitored. + /// + /// + /// true if you want to monitor subdirectories; otherwise, false. The default is false. + /// + public bool IncludeSubdirectories + { + get => m_fileSystemWatcher.IncludeSubdirectories; + set => m_fileSystemWatcher.IncludeSubdirectories = value; + } + + /// + /// Gets or sets the size of the internal buffer. + /// + /// + /// The internal buffer size. The default is 8192 (8K). + /// + public int InternalBufferSize + { + get => m_fileSystemWatcher.InternalBufferSize; + set => m_fileSystemWatcher.InternalBufferSize = value; + } + + /// + /// Gets or sets the object used to marshal the event handler calls issued as a result of a directory change. + /// + /// + /// The that represents the object used to marshal the event handler calls issued as a result of a directory change. The default is null. + /// + public ISynchronizeInvoke? SynchronizingObject + { + get => m_fileSystemWatcher.SynchronizingObject; + set => m_fileSystemWatcher.SynchronizingObject = value; + } + + /// + /// Gets or sets an for the . + /// + /// + /// An for the . + /// + public ISite? Site + { + get => m_fileSystemWatcher.Site; + set => m_fileSystemWatcher.Site = value; + } + + #endregion + + #region [ Methods ] + + /// + /// Releases all the resources used by the object. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (m_disposed) + return; + + try + { + // Treating file system watcher like an unmanaged resource + m_fileSystemWatcher.Dispose(); + } + finally + { + m_disposed = true; + } + } + + /// + /// A synchronous method that returns a structure that contains specific information on the change that occurred, given the type of change you want to monitor. + /// + /// A that contains specific information on the change that occurred. + /// The to watch for. + public WaitForChangedResult WaitForChanged(WatcherChangeTypes changeType) + { + return m_fileSystemWatcher.WaitForChanged(changeType); + } + + /// + /// A synchronous method that returns a structure that contains specific information on the change that occurred, given the type of change you want to monitor and the time (in milliseconds) to wait before timing out. + /// + /// A that contains specific information on the change that occurred. + /// The to watch for. + /// The time (in milliseconds) to wait before timing out. + public WaitForChangedResult WaitForChanged(WatcherChangeTypes changeType, int timeout) + { + return m_fileSystemWatcher.WaitForChanged(changeType, timeout); + } + + /// + /// Begins the initialization of a used on a form or used by another component. The initialization occurs at run time. + /// + public void BeginInit() + { + m_fileSystemWatcher.BeginInit(); + } + + /// + /// Ends the initialization of a used on a form or used by another component. The initialization occurs at run time. + /// + public void EndInit() + { + m_fileSystemWatcher.EndInit(); + } + + private void OnChanged(FileSystemEventArgs e) + { + Changed?.SafeInvoke(this, e); + } + + private void OnCreated(FileSystemEventArgs e) + { + Created?.SafeInvoke(this, e); + } + + private void OnDeleted(FileSystemEventArgs e) + { + Deleted?.SafeInvoke(this, e); + } + + private void OnRenamed(RenamedEventArgs e) + { + Renamed?.SafeInvoke(this, e); + } + + private void OnError(ErrorEventArgs e) + { + Error?.SafeInvoke(this, e); + } + + #endregion + + #region [ Static ] + + // Static Methods + private static void OnChanged(WeakReference reference, FileSystemEventArgs e) + { + if (reference.TryGetTarget(out SafeFileWatcher? instance)) + instance.OnChanged(e); + } + + private static void OnCreated(WeakReference reference, FileSystemEventArgs e) + { + if (reference.TryGetTarget(out SafeFileWatcher? instance)) + instance.OnCreated(e); + } + + private static void OnDeleted(WeakReference reference, FileSystemEventArgs e) + { + if (reference.TryGetTarget(out SafeFileWatcher? instance)) + instance.OnDeleted(e); + } + + private static void OnRenamed(WeakReference reference, RenamedEventArgs e) + { + if (reference.TryGetTarget(out SafeFileWatcher? instance)) + instance.OnRenamed(e); + } + + private static void OnError(WeakReference reference, ErrorEventArgs e) + { + if (reference.TryGetTarget(out SafeFileWatcher? instance)) + instance.OnError(e); + } + + #endregion +} diff --git a/src/Gemstone/Interop/VBArrayDescriptor.cs b/src/Gemstone/Interop/VBArrayDescriptor.cs index f09b137c26..fa35257c0a 100644 --- a/src/Gemstone/Interop/VBArrayDescriptor.cs +++ b/src/Gemstone/Interop/VBArrayDescriptor.cs @@ -126,8 +126,8 @@ public int GenerateBinaryImage(byte[] buffer, int startIndex) for (int i = 0; i < m_arrayDimensionDescriptors.Count; i++) { - Buffer.BlockCopy(BitConverter.GetBytes(m_arrayDimensionDescriptors[i].Length), 0, buffer, startIndex + (i * DimensionDescriptor.BinaryLength) + 2, 4); - Buffer.BlockCopy(BitConverter.GetBytes(m_arrayDimensionDescriptors[i].LowerBound), 0, buffer, startIndex + (i * DimensionDescriptor.BinaryLength) + 6, 4); + Buffer.BlockCopy(BitConverter.GetBytes(m_arrayDimensionDescriptors[i].Length), 0, buffer, startIndex + i * DimensionDescriptor.BinaryLength + 2, 4); + Buffer.BlockCopy(BitConverter.GetBytes(m_arrayDimensionDescriptors[i].LowerBound), 0, buffer, startIndex + i * DimensionDescriptor.BinaryLength + 6, 4); } return length; diff --git a/src/Gemstone/PrecisionTimer.cs b/src/Gemstone/PrecisionTimer.cs index 84e18ac29f..9bce0879e0 100644 --- a/src/Gemstone/PrecisionTimer.cs +++ b/src/Gemstone/PrecisionTimer.cs @@ -319,16 +319,16 @@ public bool AutoReset get { if (m_disposed) - throw (new ObjectDisposedException("PrecisionTimer")); + throw new ObjectDisposedException("PrecisionTimer"); - return (m_mode == TimerMode.Periodic); + return m_mode == TimerMode.Periodic; } set { if (m_disposed) - throw (new ObjectDisposedException("PrecisionTimer")); + throw new ObjectDisposedException("PrecisionTimer"); - m_mode = (value ? TimerMode.Periodic : TimerMode.OneShot); + m_mode = value ? TimerMode.Periodic : TimerMode.OneShot; if (IsRunning && m_mode == TimerMode.Periodic) { @@ -427,7 +427,7 @@ public void Start(EventArgs? userArgs) try { m_timer.Interval = m_period; - m_timer.AutoReset = (m_mode == TimerMode.Periodic); + m_timer.AutoReset = m_mode == TimerMode.Periodic; m_timer.Start(); m_running = true; @@ -609,4 +609,4 @@ public static void ClearMinimumTimerResolution(int period) private static extern int timeEndPeriod(int period); #endregion -} \ No newline at end of file +} diff --git a/src/Gemstone/ProcessProgress.cs b/src/Gemstone/ProcessProgress.cs new file mode 100644 index 0000000000..ced8b38cf5 --- /dev/null +++ b/src/Gemstone/ProcessProgress.cs @@ -0,0 +1,74 @@ +//****************************************************************************************************** +// ProcessProgress.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: +// ---------------------------------------------------------------------------------------------------- +// 05/23/2007 - Pinal C. Patel +// Generated original version of source code. +// 09/09/2008 - J. Ritchie Carroll +// Converted to C#. +// 09/26/2008 - J. Ritchie Carroll +// Added a ProcessProgress.Handler class to allow functions with progress delegate +// to update progress information using the ProcessProgress class. +// 09/14/2009 - Stephen C. Wills +// Added new header and license agreement. +// 12/14/2012 - Starlynn Danyelle Gilliam +// Modified Header. +// +//****************************************************************************************************** + +using System; + +namespace Gemstone; + +/// +/// Represents current process progress for an operation. +/// +/// +/// Used to track total progress of an identified operation. +/// +/// Unit of progress used (long, double, int, etc.) +/// +/// Constructs a new instance of the class using specified process name. +/// +/// Name of process for which progress is being monitored. +[Serializable] +public class ProcessProgress(string processName) where TUnit : struct +{ + #region [ Properties ] + + /// + /// Gets or sets name of process for which progress is being monitored. + /// + public string ProcessName { get; set; } = processName; + + /// + /// Gets or sets current progress message (e.g., current file being copied, etc.) + /// + public string ProgressMessage { get; set; } = string.Empty; + + /// + /// Gets or sets total number of units to be processed. + /// + public TUnit Total { get; set; } + + /// + /// Gets or sets number of units completed processing so far. + /// + public TUnit Complete { get; set; } + + #endregion +} diff --git a/src/Gemstone/ProcessProgressHandler.cs b/src/Gemstone/ProcessProgressHandler.cs new file mode 100644 index 0000000000..c72f69ee8e --- /dev/null +++ b/src/Gemstone/ProcessProgressHandler.cs @@ -0,0 +1,123 @@ +//****************************************************************************************************** +// ProcessProgressHandler.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: +// ---------------------------------------------------------------------------------------------------- +// 11/04/2008 - J. Ritchie Carroll +// Generated original version of source code. +// 09/14/2009 - Stephen C. Wills +// Added new header and license agreement. +// 12/14/2012 - Starlynn Danyelle Gilliam +// Modified Header. +// +//****************************************************************************************************** + +using System; + +namespace Gemstone; + +/// +/// Defines a delegate handler for a instance. +/// +/// +/// This handler is used by methods with an delegate parameter (e.g., Action<ProcessProgress<long>>) +/// providing a simple callback mechanism for reporting progress on a long operation. +/// +/// Unit of progress used (long, double, int, etc.) +public class ProcessProgressHandler where TUnit : struct +{ + #region [ Constructors ] + + /// + /// Constructs a new process progress handler for the specified parameters. + /// + /// Delegate callback to invoke as process progresses. + /// Descriptive name of process, if useful. + public ProcessProgressHandler(Action> progressHandler, string processName) + { + ProgressHandler = progressHandler; + ProcessProgress = new ProcessProgress(processName); + } + + /// + /// Constructs a new process progress handler for the specified parameters. + /// + /// Delegate callback to invoke as process progresses. + /// Descriptive name of process, if useful. + /// Total number of units to be processed. + public ProcessProgressHandler(Action> progressHandler, string processName, TUnit total) + : this(progressHandler, processName) + { + ProcessProgress.Total = total; + } + + #endregion + + #region [ Properties ] + + /// + /// Gets instance of used to track progress for this handler. + /// + public ProcessProgress ProcessProgress { get; } + + /// + /// Gets or sets reference to delegate handler used as a callback to report process progress. + /// + public Action> ProgressHandler { get; set; } + + /// + /// Gets or sets current process progress (i.e., number of units completed processing so far) - note that when this + /// property value is assigned, the callback function is automatically called with updated + /// instance so consumer can track progress. + /// + /// Number of units completed processing so far. + public TUnit Complete + { + get => ProcessProgress.Complete; + set => UpdateProgress(value); + } + + /// + /// Gets or sets total number of units to be processed. + /// + public TUnit Total + { + get => ProcessProgress.Total; + set => ProcessProgress.Total = value; + } + + #endregion + + #region [ Methods ] + + /// + /// Calls callback function with updated instance so consumer can track progress. + /// + /// Number of units completed processing so far. + /// + /// Note that assigning a value to the property will have the same effect as calling this method. + /// + public void UpdateProgress(TUnit completed) + { + // Update bytes completed + ProcessProgress.Complete = completed; + + // Call user function + ProgressHandler(ProcessProgress); + } + + #endregion +} diff --git a/src/Gemstone/Reflection/IsExternalInit.cs b/src/Gemstone/Reflection/IsExternalInit.cs new file mode 100644 index 0000000000..c9eedcf9cd --- /dev/null +++ b/src/Gemstone/Reflection/IsExternalInit.cs @@ -0,0 +1,12 @@ +#if !NET + +// This allows { get; init; } to be used in .NET Standard + +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices; + +internal class IsExternalInit +{ +} + +#endif diff --git a/src/Gemstone/Scheduling/Schedule.cs b/src/Gemstone/Scheduling/Schedule.cs index 09175bcf33..0835e8a7cb 100644 --- a/src/Gemstone/Scheduling/Schedule.cs +++ b/src/Gemstone/Scheduling/Schedule.cs @@ -109,7 +109,7 @@ public class Schedule : IProvideStatus /// Initializes a new instance of the class. /// public Schedule() - : this($"Schedule{(++s_instances)}") + : this($"Schedule{++s_instances}") { } @@ -486,7 +486,7 @@ private DateTime ToScheduleTimeZone(DateTime dateTime) ? DateTimeKind.Local : DateTimeKind.Utc; - DateTime specifiedDateTime = (dateTime.Kind == DateTimeKind.Unspecified) + DateTime specifiedDateTime = dateTime.Kind == DateTimeKind.Unspecified ? DateTime.SpecifyKind(dateTime, targetKind) : dateTime; @@ -563,4 +563,4 @@ private static DateTime NextMinute(DateTime dt) => NextInterval(dt, TimeSpan.FromMinutes(1.0D)); #endregion -} \ No newline at end of file +} diff --git a/src/Gemstone/Security/Cryptography/Cipher.cs b/src/Gemstone/Security/Cryptography/Cipher.cs deleted file mode 100644 index ee87cfed4d..0000000000 --- a/src/Gemstone/Security/Cryptography/Cipher.cs +++ /dev/null @@ -1,104 +0,0 @@ -//****************************************************************************************************** -// Cipher.cs - Gbtc -// -// Copyright © 2017, 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://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: -// ---------------------------------------------------------------------------------------------------- -// 04/11/2017 - Ritchie Carroll -// Generated partial version of class in GSF.Shared. -// -//****************************************************************************************************** - -using System; -using System.Security.Cryptography; -using Microsoft.Win32; - -// ReSharper disable InconsistentNaming -namespace Gemstone.Security.Cryptography; - -/// -/// Provides general use cryptographic functions. -/// -/// -/// This class exists to simplify usage of basic cryptography functionality. -/// -public class Cipher -{ - /// - /// Gets a flag that determines if system will allow use of managed, i.e., non-FIPS compliant, security algorithms. - /// - public bool SystemAllowsManagedEncryption { get; } - - /// - /// Creates a new class. - /// - public Cipher() - { - if (Common.IsPosixEnvironment) - { - SystemAllowsManagedEncryption = true; - } - else - { - const string FipsKeyOld = "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Lsa"; - const string FipsKeyNew = "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Lsa\\FipsAlgorithmPolicy"; - - // Determine if the user needs to use FIPS-compliant algorithms - try - { -#pragma warning disable CA1416 - SystemAllowsManagedEncryption = - (Registry.GetValue(FipsKeyNew, "Enabled", 0) ?? - Registry.GetValue(FipsKeyOld, "FIPSAlgorithmPolicy", 0))?.ToString() == "0"; -#pragma warning restore CA1416 - } - catch (Exception ex) - { - SystemAllowsManagedEncryption = true; - LibraryEvents.OnSuppressedException(this, new Exception($"Cipher FIPS compliance lookup exception: {ex.Message}", ex)); - } - } - } - - /// - /// Creates a hashing algorithm that respects current FIPS setting. - /// - /// New hashing algorithm that respects current FIPS setting. - public SHA1 CreateSHA1() => SHA1.Create(); // SystemAllowsManagedEncryption ? new SHA1Managed() : new SHA1CryptoServiceProvider() as SHA1; - - /// - /// Creates a hashing algorithm that respects current FIPS setting. - /// - /// New hashing algorithm that respects current FIPS setting. - public SHA256 CreateSHA256() => SHA256.Create(); // SystemAllowsManagedEncryption ? new SHA256Managed() : new SHA256CryptoServiceProvider() as SHA256; - - /// - /// Creates a hashing algorithm that respects current FIPS setting. - /// - /// New hashing algorithm that respects current FIPS setting. - public SHA384 CreateSHA384() => SHA384.Create(); // SystemAllowsManagedEncryption ? new SHA384Managed() : new SHA384CryptoServiceProvider() as SHA384; - - /// - /// Creates a hashing algorithm that respects current FIPS setting. - /// - /// New hashing algorithm that respects current FIPS setting. - public SHA512 CreateSHA512() => SHA512.Create(); // SystemAllowsManagedEncryption ? new SHA512Managed() : new SHA512CryptoServiceProvider() as SHA512; - - /// - /// Creates an encryption algorithm that respects current FIPS setting. - /// - /// New encryption algorithm that respects current FIPS setting. - public Aes CreateAes() => Aes.Create(); // SystemAllowsManagedEncryption ? new AesManaged() : new AesCryptoServiceProvider() as Aes; -} \ No newline at end of file diff --git a/src/Gemstone/Security/Cryptography/DataProtection.cs b/src/Gemstone/Security/Cryptography/DataProtection.cs new file mode 100644 index 0000000000..0c32a545e5 --- /dev/null +++ b/src/Gemstone/Security/Cryptography/DataProtection.cs @@ -0,0 +1,210 @@ +//****************************************************************************************************** +// DataProtection.cs - Gbtc +// +// Copyright © 2023, 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://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: +// ---------------------------------------------------------------------------------------------------- +// 02/26/2023 - J. Ritchie Carroll +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Security.Principal; +using System.Threading; +using Gemstone.Caching; +using Gemstone.Configuration; +using Microsoft.AspNetCore.DataProtection; + +namespace Gemstone.Security.Cryptography +{ + /// + /// Provides methods for encrypting and decrypting data. + /// + /// + /// This is a safety wrapper around the class such that it can be used with + /// LocalMachine scope regardless of current user. This is especially important for applications + /// that may be running as user account that has no association to the current user, e.g., an Azure AD + /// user or database account when authenticated using AdoSecurityProvider. + /// + public static class DataProtection + { + /// + /// Folder name for data protection keys. + /// + public const string DataProtectionKeysFolder = "DataProtectionKeys"; + + /// + /// Default settings category for cryptography services. + /// + public const string DefaultSettingsCategory = "CryptographyServices"; + + /// + /// Default timeout, in minutes, for user-specific data protection provider. + /// + public const double DefaultUserDataProtectionTimeout = 5.0D; + + private static IDataProtector? s_localMachineDataProtector; + + /// + /// Encrypts the data in a specified byte array and returns a byte array that contains the encrypted data. + /// + /// A byte array representing the encrypted data. + /// A byte array that contains data to encrypt. + /// An optional additional byte array used to increase the complexity of the encryption, or null for no additional complexity. + /// Set to true to protect data to the local machine; otherwise, set to false to protect data to the current user. + /// The config file settings category under which the settings are defined. + /// The parameter is null. + /// The encryption failed. + public static byte[] Protect(byte[] unencryptedData, byte[]? optionalEntropy = null, bool protectToLocalMachine = true, string settingsCategory = DefaultSettingsCategory) + { + if (unencryptedData is null) + throw new ArgumentNullException(nameof(unencryptedData)); + + if (!protectToLocalMachine) + return ProtectData(GetDataProtector(false, settingsCategory), unencryptedData, optionalEntropy); + + IPrincipal? principal = Thread.CurrentPrincipal; + byte[] protectedBytes; + + try + { + Thread.CurrentPrincipal = null; + protectedBytes = ProtectData(GetDataProtector(true, settingsCategory), unencryptedData, optionalEntropy); + } + finally + { + Thread.CurrentPrincipal = principal; + } + + return protectedBytes; + } + + /// + /// Decrypts the data in a specified byte array and returns a byte array that contains the decrypted data. + /// + /// A byte array representing the decrypted data. + /// A byte array containing data encrypted using the method. + /// An optional additional byte array that was used to encrypt the data, or null if the additional byte array was not used. + /// Set to true to protect data to the local machine; otherwise, set to false to protect data to the current user. + /// The config file settings category under which the settings are defined. + /// The parameter is null. + /// The decryption failed. + public static byte[] Unprotect(byte[] encryptedData, byte[]? optionalEntropy = null, bool protectToLocalMachine = true, string settingsCategory = DefaultSettingsCategory) + { + if (encryptedData is null) + throw new ArgumentNullException(nameof(encryptedData)); + + if (!protectToLocalMachine) + return UnprotectData(GetDataProtector(false, settingsCategory), encryptedData, optionalEntropy); + + IPrincipal? principal = Thread.CurrentPrincipal; + byte[] unprotectedBytes; + + try + { + Thread.CurrentPrincipal = null; + unprotectedBytes = UnprotectData(GetDataProtector(true, settingsCategory), encryptedData, optionalEntropy); + } + finally + { + Thread.CurrentPrincipal = principal; + } + + return unprotectedBytes; + } + + private static byte[] ProtectData(IDataProtector dataProtector, byte[] data, byte[]? optionalEntropy) + { + if (optionalEntropy is null || optionalEntropy.Length == 0) + return dataProtector.Protect(data); + + data = [..optionalEntropy, ..data]; + + return dataProtector.Protect(data); + } + + private static byte[] UnprotectData(IDataProtector dataProtector, byte[] data, byte[]? optionalEntropy) + { + if (optionalEntropy is null || optionalEntropy.Length == 0) + return dataProtector.Unprotect(data); + + byte[] unprotectedData = dataProtector.Unprotect(data); + + if (unprotectedData.Length < optionalEntropy.Length || !optionalEntropy.SequenceEqual(unprotectedData[..optionalEntropy.Length])) + throw new CryptographicException("Data was not protected using the specified optional entropy."); + + return unprotectedData[optionalEntropy.Length..]; + } + + private static IDataProtector GetDataProtector(bool protectToLocalMachine, string settingsCategory) + { + Lazy applicationName = new(() => Settings.Default[settingsCategory].ApplicationName ?? Common.ApplicationName); + + if (protectToLocalMachine) + return s_localMachineDataProtector ??= CreateDataProtector(true, applicationName.Value, "Machine"); + + ClaimsPrincipal? currentPrincipal = ClaimsPrincipal.Current; + + currentPrincipal ??= Thread.CurrentPrincipal as ClaimsPrincipal; + + string userName = currentPrincipal?.Identity?.Name ?? Environment.UserName; + double cacheTimeout = Settings.Default[settingsCategory].UserDataProtectionTimeout ?? DefaultUserDataProtectionTimeout; + + return MemoryCache.GetOrAdd(userName, cacheTimeout, () => CreateDataProtector(false, applicationName.Value, $"User:{userName.ToLowerInvariant()}")); + } + + private static IDataProtector CreateDataProtector(bool protectToLocalMachine, string applicationName, string target) + { + string keyFilePath = GetKeyFilePath(DataProtectionKeysFolder, applicationName, protectToLocalMachine); + + IDataProtectionProvider dataProtectionProvider = DataProtectionProvider.Create( + new DirectoryInfo(keyFilePath), + configuration => + { + configuration.SetApplicationName(applicationName); + + #pragma warning disable CA1416 + if (!Common.IsPosixEnvironment) + configuration.ProtectKeysWithDpapi(protectToLocalMachine); + #pragma warning restore CA1416 + }); + + return dataProtectionProvider.CreateProtector($"{nameof(Gemstone)}:{applicationName}:{target}"); + } + + private static string GetKeyFilePath(string keyFolder, string applicationName, bool protectToLocalMachine) + { + string appDataPath = Environment.GetFolderPath(protectToLocalMachine ? + Environment.SpecialFolder.CommonApplicationData : + Environment.SpecialFolder.LocalApplicationData); + + return Path.Combine(appDataPath, applicationName, keyFolder); + } + + /// + public static void DefineSettings(Settings settings, string settingsCategory = DefaultSettingsCategory) + { + dynamic section = settings[settingsCategory]; + + section.ApplicationName = (Common.ApplicationName, "Name of the application using the data protection provider used for key discrimination."); + section.UserDataProtectionTimeout = (DefaultUserDataProtectionTimeout, "Timeout, in minutes, for cached user-specific data protection provider."); + } + } +} diff --git a/src/Gemstone/Security/Cryptography/HashAlgorithmExtensions/HashAlgorithmExtensions.cs b/src/Gemstone/Security/Cryptography/HashAlgorithmExtensions/HashAlgorithmExtensions.cs index 54efa2a141..fc9e002af6 100644 --- a/src/Gemstone/Security/Cryptography/HashAlgorithmExtensions/HashAlgorithmExtensions.cs +++ b/src/Gemstone/Security/Cryptography/HashAlgorithmExtensions/HashAlgorithmExtensions.cs @@ -38,6 +38,10 @@ public static class HashAlgorithmExtensions /// to use for encryption. /// String value to hash. /// Base64 encoded hash of provided string . - public static string GetStringHash(this HashAlgorithm algorithm, string? value) => - string.IsNullOrEmpty(value) ? string.Empty : Convert.ToBase64String(algorithm.ComputeHash(Encoding.UTF8.GetBytes(value))); -} \ No newline at end of file + public static string GetStringHash(this HashAlgorithm algorithm, string? value) + { + return string.IsNullOrEmpty(value) + ? string.Empty + : Convert.ToBase64String(algorithm.ComputeHash(Encoding.UTF8.GetBytes(value))); + } +} diff --git a/src/Gemstone/StringExtensions/StringExtensions.cs b/src/Gemstone/StringExtensions/StringExtensions.cs index 46f7f61d14..9908bb3239 100644 --- a/src/Gemstone/StringExtensions/StringExtensions.cs +++ b/src/Gemstone/StringExtensions/StringExtensions.cs @@ -120,8 +120,7 @@ public static class StringExtensions /// for the specified . public static StringComparer GetComparer(this StringComparison comparison) => s_comparisonComparers[comparison]; -#if NET6_0_OR_GREATER - +#if NET /// /// Throws an if is null -or- /// an if is Empty. @@ -140,7 +139,6 @@ public static void ThrowIfNullOrEmpty([NotNull] this string? argument, [CallerAr [DoesNotReturn] // This allows ThrowIfNullOrEmpty to be inlined private static void ThrowArgumentEmptyException(string? paramName) => throw new ArgumentException("Argument cannot be empty", paramName); - #endif /// diff --git a/src/Gemstone/Threading/INamedSemaphore.cs b/src/Gemstone/Threading/INamedSemaphore.cs new file mode 100644 index 0000000000..b1903f844c --- /dev/null +++ b/src/Gemstone/Threading/INamedSemaphore.cs @@ -0,0 +1,59 @@ +//****************************************************************************************************** +// INamedSemaphore.cs - Gbtc +// +// Copyright © 2023, 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://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: +// ---------------------------------------------------------------------------------------------------- +// 11/09/2023 - Ritchie Carroll +// Generated original version of source code. +// +//****************************************************************************************************** +// ReSharper disable UnusedMember.Global + +using System; +using Microsoft.Win32.SafeHandles; + +namespace Gemstone.Threading +{ + internal enum OpenExistingResult + { + Success, + NameNotFound, + NameInvalid, + PathTooLong, + AccessDenied + } + + internal interface INamedSemaphore : IDisposable + { + SafeWaitHandle? SafeWaitHandle { get; set; } + + void CreateSemaphoreCore(int initialCount, int maximumCount, string name, out bool createdNew); + + int ReleaseCore(int releaseCount); + + void Close(); + + bool WaitOne(); + + bool WaitOne(TimeSpan timeout); + + bool WaitOne(int millisecondsTimeout); + + bool WaitOne(TimeSpan timeout, bool exitContext); + + bool WaitOne(int millisecondsTimeout, bool exitContext); + } +} diff --git a/src/Gemstone/Threading/InterprocessLock.cs b/src/Gemstone/Threading/InterprocessLock.cs new file mode 100644 index 0000000000..db8eab2fdb --- /dev/null +++ b/src/Gemstone/Threading/InterprocessLock.cs @@ -0,0 +1,236 @@ +//****************************************************************************************************** +// 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.Cryptography; +using System.Threading; +using Gemstone.Identity; +using Gemstone.Security.Cryptography.HashAlgorithmExtensions; + +namespace Gemstone.Threading; + +/// +/// Defines helper methods related to inter-process locking. +/// +public static class InterprocessLock +{ + /// + /// Default value for global flag. + /// + public const bool DefaultMutexGlobal = true; + + /// + /// Default value for maximum count. + /// + public const int DefaultSemaphoreMaximumCount = 10; + + /// + /// Default value for initial count. + /// + public const int DefaultSemaphoreInitialCount = -1; + + /// + /// Default value for global flag. + /// + public const bool DefaultSemaphoreGlobal = true; + + 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 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) + { + Assembly entryAssembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); + GuidAttribute? attribute = entryAssembly.GetCustomAttributes(typeof(GuidAttribute), true).FirstOrDefault() as GuidAttribute; + string? name = attribute?.Value ?? entryAssembly.GetName().Name; + + if (perUser) + name += UserInfo.CurrentUserID; + + return GetNamedMutex(name!, !perUser); + } + + /// + /// 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). + /// Determines if mutex should be marked as global; set value to false for local. + /// 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. + /// + /// + /// 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, bool global = DefaultMutexGlobal) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullException(nameof(name), "Argument cannot be empty, null or white space."); + + // When requested, prefix mutex name with "Global\" such that mutex + // will apply to all active application sessions + string mutexName = $"{(global ? "Global" : "Local")}\\{GetHashedName(name, MutexHash)}"; + + if (!Mutex.TryOpenExisting(mutexName, out Mutex? mutex)) + mutex = new Mutex(false, mutexName); + + return mutex; + } + + /// + /// 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. + /// + /// + /// On POSIX systems, the exhibits kernel persistence, meaning instances will remain active beyond the lifespan of the + /// creating process. Named semaphores must be explicitly removed by invoking when they are no longer needed. + /// Kernel persistence necessitates careful design consideration regarding the responsibility for invoking . + /// Since the common use case for named semaphores is across multiple applications, it is advisable for the last exiting process to handle the + /// cleanup. In cases where an application may crash before calling , the semaphore persists in the system, + /// potentially leading to resource leakage. Implementations should include strategies to address and mitigate this risk. + /// + /// + /// The named semaphore exists, but the user does not have the minimum needed security access rights to use it. + [MethodImpl(MethodImplOptions.Synchronized)] + public static NamedSemaphore GetNamedSemaphore(bool perUser, int maximumCount = DefaultSemaphoreMaximumCount, int initialCount = DefaultSemaphoreInitialCount) + { + Assembly entryAssembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); + GuidAttribute? attribute = entryAssembly.GetCustomAttributes(typeof(GuidAttribute), true).FirstOrDefault() as GuidAttribute; + string? name = attribute?.Value ?? entryAssembly.GetName().Name; + + if (perUser) + name += UserInfo.CurrentUserID; + + return GetNamedSemaphore(name!, maximumCount, initialCount, !perUser); + } + + /// + /// 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 . + /// Determines if semaphore should be marked as global; set value to false for local. + /// 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. + /// + /// + /// On POSIX systems, the exhibits kernel persistence, meaning instances will remain active beyond the lifespan of the + /// creating process. Named semaphores must be explicitly removed by invoking when they are no longer needed. + /// Kernel persistence necessitates careful design consideration regarding the responsibility for invoking . + /// Since the common use case for named semaphores is across multiple applications, it is advisable for the last exiting process to handle the + /// cleanup. In cases where an application may crash before calling , the semaphore persists in the system, + /// potentially leading to resource leakage. Implementations should include strategies to address and mitigate this risk. + /// + /// + /// 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 NamedSemaphore GetNamedSemaphore(string name, int maximumCount = DefaultSemaphoreMaximumCount, int initialCount = DefaultSemaphoreInitialCount, bool global = DefaultSemaphoreGlobal) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullException(nameof(name), "Argument cannot be empty, null or white space."); + + if (initialCount < 0) + initialCount = maximumCount; + + // When requested, prefix semaphore name with "Global\" such that semaphore + // will apply to all active application sessions + string semaphoreName = $"{(global ? "Global" : "Local")}\\{GetHashedName(name, SemaphoreHash)}"; + + if (!NamedSemaphore.TryOpenExisting(semaphoreName, out NamedSemaphore? semaphore)) + semaphore = new NamedSemaphore(initialCount, maximumCount, semaphoreName); + + return semaphore; + } + + internal static string GetHashedName(string name, int hashIndex) + { + // Create a name that is specific to an object (e.g., a path and file name). + // Note that we use a SHA hash 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/semaphore name requirements. + // This is not being used for security, just to create a unique name, so SHA is fine. + SHA256 hash = SHA256.Create(); + return $"{hash.GetStringHash($"{name.ToLowerInvariant()}{hashIndex}").Replace('\\', '-')}"; + } +} diff --git a/src/Gemstone/Threading/InterprocessReaderWriterLock.cs b/src/Gemstone/Threading/InterprocessReaderWriterLock.cs new file mode 100644 index 0000000000..326275a810 --- /dev/null +++ b/src/Gemstone/Threading/InterprocessReaderWriterLock.cs @@ -0,0 +1,338 @@ +//****************************************************************************************************** +// InterprocessReaderWriterLock.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. +// 12/14/2012 - Starlynn Danyelle Gilliam +// Modified Header. +// +//****************************************************************************************************** + +using System; +using System.Threading; + +namespace Gemstone.Threading; + +/// +/// Represents an inter-process reader/writer lock using and +/// native locking mechanisms. +/// +/// +/// +/// The uses a to synchronize access to an inter-process shared resource. +/// On POSIX systems, the exhibits kernel persistence, meaning instances will remain active beyond the lifespan of +/// the creating process. The named semaphore must be explicitly removed by invoking when the last +/// reader-writer lock instance is no longer needed. Kernel persistence necessitates careful design consideration regarding process +/// responsibility for invoking the method. Since the common use case for named semaphores is across +/// multiple applications, it is advisable for the last exiting process to handle the cleanup. In cases where an application may crash before +/// calling the method, the semaphore persists in the system, potentially leading to resource leakage. +/// Implementations should include strategies to address and mitigate this risk. +/// +/// +public class InterprocessReaderWriterLock : IDisposable +{ + #region [ Members ] + + // Constants + + /// + /// Default maximum concurrent locks allowed for . + /// + public const int DefaultMaximumConcurrentLocks = 10; + + // Fields + private readonly Mutex m_semaphoreLock; // Mutex used to synchronize access to Semaphore + private readonly NamedSemaphore m_concurrencyLock; // Semaphore used for reader/writer lock on consumer object + private bool m_disposed; + + #endregion + + #region [ Constructors ] + + /// + /// Creates a new instance of the 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). + /// Determines if semaphore and mutex used by should be marked as global; set value to false for local. + public InterprocessReaderWriterLock(string name, bool global = true) : + this(name, DefaultMaximumConcurrentLocks, global) + { + } + + /// + /// Creates a new instance of the 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). + /// Maximum concurrent reader locks to allow. + /// Determines if semaphore and mutex used by should be marked as global; set value to false for local. + /// + /// If more reader locks are requested than the , excess reader locks will simply + /// wait until a lock is available (i.e., one of the existing reads completes). + /// + public InterprocessReaderWriterLock(string name, int maximumConcurrentLocks, bool global = true) + { + MaximumConcurrentLocks = maximumConcurrentLocks; + m_semaphoreLock = InterprocessLock.GetNamedMutex(name, global); + m_concurrencyLock = InterprocessLock.GetNamedSemaphore(name, MaximumConcurrentLocks, global: global); + } + + /// + /// Releases the unmanaged resources before the object is reclaimed by . + /// + ~InterprocessReaderWriterLock() + { + Dispose(false); + } + + #endregion + + #region [ Properties ] + + /// + /// Gets the maximum concurrent reader locks allowed. + /// + public int MaximumConcurrentLocks { get; } + + #endregion + + #region [ Methods ] + + /// + /// Releases all the resources used by the object. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (m_disposed) + return; + + try + { + if (!disposing) + return; + + m_concurrencyLock.Close(); + m_semaphoreLock.Close(); + } + finally + { + m_disposed = true; // Prevent duplicate dispose. + } + } + + /// + /// Tries to enter the lock in read mode. + /// + /// + /// Upon successful acquisition of a read lock, use the finally block of a try/finally statement to call . + /// One should be called for each or . + /// + public void EnterReadLock() + { + TryEnterReadLock(Timeout.Infinite); + } + + /// + /// Tries to enter the lock in write mode. + /// + /// + /// Upon successful acquisition of a write lock, use the finally block of a try/finally statement to call . + /// One should be called for each or . + /// + public void EnterWriteLock() + { + TryEnterWriteLock(Timeout.Infinite); + } + + /// + /// Exits read mode and returns the prior read lock count. + /// + /// + /// Upon successful acquisition of a read lock, use the finally block of a try/finally statement to call . + /// One should be called for each or . + /// + public int ExitReadLock() + { + // Release the semaphore lock and restore the slot + return m_concurrencyLock.Release(); + } + + /// + /// Exits write mode. + /// + /// + /// Upon successful acquisition of a write lock, use the finally block of a try/finally statement to call . + /// One should be called for each or . + /// + public void ExitWriteLock() + { + // Release semaphore synchronization mutex lock + m_semaphoreLock.ReleaseMutex(); + } + + /// + /// Tries to enter the lock in read mode, with an optional time-out. + /// + /// The number of milliseconds to wait, or -1 () to wait indefinitely. + /// true if the calling thread entered read mode, otherwise, false. + /// + /// + /// Upon successful acquisition of a read lock, use the finally block of a try/finally statement to call . + /// One should be called for each or . + /// + /// + /// Note that this function may wait as long as 2 * since the function first waits for synchronous access + /// to the semaphore, then waits again on an available semaphore slot. + /// + /// + public bool TryEnterReadLock(int millisecondsTimeout) + { + bool success; + + try + { + // Wait for system level mutex lock to synchronize access to semaphore + if (!m_semaphoreLock.WaitOne(millisecondsTimeout)) + return false; + } + catch (AbandonedMutexException) + { + // Abnormal application terminations can leave a mutex abandoned, in + // this case we now own the mutex, so we just ignore the exception + } + + try + { + // Wait for a semaphore slot to become available + success = m_concurrencyLock.WaitOne(millisecondsTimeout); + } + finally + { + // Release mutex so others can access the semaphore + m_semaphoreLock.ReleaseMutex(); + } + + return success; + } + + /// + /// Tries to enter the lock in write mode, with an optional time-out. + /// + /// The number of milliseconds to wait, or -1 () to wait indefinitely. + /// true if the calling thread entered write mode, otherwise, false. + /// + /// + /// Upon successful acquisition of a write lock, use the finally block of a try/finally statement to call . + /// One should be called for each or . + /// + /// + /// Note that this function may wait as long as 2 * since the function first waits for synchronous access + /// to the semaphore, then waits again on an available semaphore slot. + /// + /// + public bool TryEnterWriteLock(int millisecondsTimeout) + { + bool success = false; + + try + { + // Wait for system level mutex lock to synchronize access to semaphore + if (!m_semaphoreLock.WaitOne(millisecondsTimeout)) + return false; + } + catch (AbandonedMutexException) + { + // Abnormal application terminations can leave a mutex abandoned, in + // this case we now own the mutex, so we just ignore the exception + } + + try + { + // At this point no other threads can acquire read or write access since we own the mutex. + // Other threads may be busy reading, so we wait until all semaphore slots become available. + // The only way to get a semaphore slot count is to execute a successful wait and release. + long startTime = DateTime.UtcNow.Ticks; + + success = m_concurrencyLock.WaitOne(millisecondsTimeout); + + if (success) + { + int count = m_concurrencyLock.Release(); + int adjustedTimeout = millisecondsTimeout; + + // After a successful wait and release the returned semaphore slot count will be -1 of the + // actual count since we owned one slot after a successful wait. + while (success && count != MaximumConcurrentLocks - 1) + { + // Sleep to allow any remaining reads to complete + Thread.Sleep(1); + + // Continue to adjust remaining time to accommodate user specified millisecond timeout + if (millisecondsTimeout > 0) + adjustedTimeout = millisecondsTimeout - (int)Ticks.ToMilliseconds(DateTime.UtcNow.Ticks - startTime); + + if (adjustedTimeout < 0) + adjustedTimeout = 0; + + success = m_concurrencyLock.WaitOne(adjustedTimeout); + + if (success) + count = m_concurrencyLock.Release(); + } + } + } + finally + { + // If lock failed, release mutex so others can access the semaphore + if (!success) + m_semaphoreLock.ReleaseMutex(); + } + + // Successfully entering write lock leaves state of semaphore locking mutex "owned", it is critical that + // consumer call ExitWriteLock upon completion of write action to release mutex regardless of whether + // their code succeeds or fails, that is, consumer should use the "finally" clause of a "try/finally" + // expression to ExitWriteLock. + return success; + } + + /// + /// Releases inter-process resources used by the . + /// + /// + /// On POSIX systems, calling this method removes the named semaphore used by the reader-writer lock. + /// The semaphore name is removed immediately and is destroyed once all other processes that have the + /// semaphore open close it. Calling this method on Windows systems does nothing. + /// + public void ReleaseInterprocessResources() + { + m_concurrencyLock.Unlink(); + } + + #endregion +} diff --git a/src/Gemstone/Threading/NamedSemaphore.cs b/src/Gemstone/Threading/NamedSemaphore.cs new file mode 100644 index 0000000000..1761a66cac --- /dev/null +++ b/src/Gemstone/Threading/NamedSemaphore.cs @@ -0,0 +1,386 @@ +//****************************************************************************************************** +// NamedSemaphore.cs - Gbtc +// +// Copyright © 2023, 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://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: +// ---------------------------------------------------------------------------------------------------- +// 11/09/2023 - J. Ritchie Carroll +// Generated original version of source code. +// +//****************************************************************************************************** +// ReSharper disable InconsistentNaming +// ReSharper disable OutParameterValueIsAlwaysDiscarded.Local +// ReSharper disable UnusedMember.Global +#pragma warning disable CA1416 + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics; +using System.IO; +using System.Threading; +using Microsoft.Win32.SafeHandles; + +namespace Gemstone.Threading; + +/// +/// Represents a cross-platform, interprocess named semaphore, which limits the number of threads that can concurrently +/// access a resource or a pool of resources. +/// +/// +/// +/// A is a synchronization object that can be utilized across multiple processes. +/// +/// +/// On POSIX systems, the exhibits kernel persistence, meaning instances will remain +/// active beyond the lifespan of the creating process. Named semaphores must be explicitly removed by invoking +/// when they are no longer needed. Kernel persistence necessitates careful design consideration +/// regarding the responsibility for invoking . Since the common use case for named semaphores is +/// across multiple applications, it is advisable for the last exiting process to handle the cleanup. In cases where +/// an application may crash before calling , the semaphore persists in the system, potentially +/// leading to resource leakage. Implementations should include strategies to address and mitigate this risk. +/// +/// +public class NamedSemaphore : WaitHandle +{ + private readonly INamedSemaphore m_semaphore; + + /// + /// Initializes a new instance of the class, specifying the initial number of entries, + /// the maximum number of concurrent entries, and the name of a system semaphore object. + /// + /// The initial number of requests for the semaphore that can be granted concurrently. + /// The maximum number of requests for the semaphore that can be granted concurrently. + /// + /// The unique name identifying the semaphore. This name is case-sensitive. Use a backslash (\\) to specify a + /// namespace, but avoid it elsewhere in the name. On Unix-based systems, the name should conform to valid file + /// naming conventions, excluding slashes except for an optional namespace backslash. The name length is limited + /// to 250 characters after any optional namespace. + /// + /// + /// The may be prefixed with Global\ or Local\ to specify a namespace. + /// When the Global namespace is specified, the synchronization object may be shared with any processes on the system. + /// When the Local namespace is specified, which is also the default when no namespace is specified, the synchronization + /// object may be shared with processes in the same session. On Windows, a session is a login session, and services + /// typically run in a different non-interactive session. On Unix-like operating systems, each shell has its own session. + /// Session-local synchronization objects may be appropriate for synchronizing between processes with a parent/child + /// relationship where they all run in the same session. + /// + public NamedSemaphore(int initialCount, int maximumCount, string name) : + this(initialCount, maximumCount, name, out _) + { + } + + /// + /// Initializes a new instance of the class, specifying the initial number of entries, + /// the maximum number of concurrent entries, the name of a system semaphore object, and specifying a variable that + /// receives a value indicating whether a new system semaphore was created. + /// + /// The initial number of requests for the semaphore that can be granted concurrently. + /// The maximum number of requests for the semaphore that can be granted concurrently. + /// + /// The unique name identifying the semaphore. This name is case-sensitive. Use a backslash (\\) to specify a + /// namespace, but avoid it elsewhere in the name. On Unix-based systems, the name should conform to valid file + /// naming conventions, excluding slashes except for an optional namespace backslash. The name length is limited + /// to 250 characters after any optional namespace. + /// + /// + /// When method returns, contains true if the specified named system semaphore was created; otherwise, + /// false if the semaphore already existed. + /// + /// + /// The may be prefixed with Global\ or Local\ to specify a namespace. + /// When the Global namespace is specified, the synchronization object may be shared with any processes on the system. + /// When the Local namespace is specified, which is also the default when no namespace is specified, the synchronization + /// object may be shared with processes in the same session. On Windows, a session is a login session, and services + /// typically run in a different non-interactive session. On Unix-like operating systems, each shell has its own session. + /// Session-local synchronization objects may be appropriate for synchronizing between processes with a parent/child + /// relationship where they all run in the same session. + /// + public NamedSemaphore(int initialCount, int maximumCount, string name, out bool createdNew) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullException(nameof(name), "Name cannot be null, empty or whitespace."); + + m_semaphore = Common.IsPosixEnvironment ? + new NamedSemaphoreUnix() : + new NamedSemaphoreWindows(); + + m_semaphore.CreateSemaphoreCore(initialCount, maximumCount, name, out createdNew); + Name = name; + } + + private NamedSemaphore(INamedSemaphore semaphore, string name) + { + m_semaphore = semaphore; + Name = name; + } + + /// + /// Gets or sets the native operating system handle. + /// + public new SafeWaitHandle SafeWaitHandle + { + get => m_semaphore.SafeWaitHandle ?? new SafeWaitHandle(InvalidHandle, false); + set => base.SafeWaitHandle = m_semaphore.SafeWaitHandle = value; + } + + /// + /// Gets the name of the . + /// + public string Name { get; } + + /// + /// When overridden in a derived class, releases the unmanaged resources used by the , + /// and optionally releases the managed resources. + /// + /// + /// true to release both managed and unmanaged resources; otherwise, false to release only + /// unmanaged resources. + /// + protected override void Dispose(bool explicitDisposing) + { + try + { + base.Dispose(explicitDisposing); + } + finally + { + m_semaphore.Dispose(); + } + } + + /// + /// Releases all resources held by the current . + /// + public override void Close() + { + m_semaphore.Close(); + } + + /// + /// Blocks the current thread until the current receives a signal. + /// + /// + /// true if the current instance receives a signal. If the current instance is never signaled, + /// never returns. + /// + public override bool WaitOne() + { + return m_semaphore.WaitOne(); + } + + /// + /// Blocks the current thread until the current instance receives a signal, using a + /// to specify the time interval. + /// + /// + /// A that represents the number of milliseconds to wait, or a + /// that represents -1 milliseconds to wait indefinitely. + /// + /// true if the current instance receives a signal; otherwise, false. + public override bool WaitOne(TimeSpan timeout) + { + return m_semaphore.WaitOne(timeout); + } + + /// + /// Blocks the current thread until the current instance receives a signal, using a 32-bit signed integer to + /// specify the time interval in milliseconds. + /// + /// + /// The number of milliseconds to wait, or (-1) to wait indefinitely. + /// + /// true if the current instance receives a signal; otherwise, false. + public override bool WaitOne(int millisecondsTimeout) + { + return m_semaphore.WaitOne(millisecondsTimeout); + } + + /// + /// Blocks the current thread until the current instance receives a signal, using a + /// to specify the time interval and specifying whether to exit the synchronization domain before the wait. + /// + /// + /// A that represents the number of milliseconds to wait, or a + /// that represents -1 milliseconds to wait indefinitely. + /// + /// + /// true to exit the synchronization domain for the context before the wait (if in a synchronized context), + /// and reacquire it afterward; otherwise, false. + /// + /// true if the current instance receives a signal; otherwise, false. + public override bool WaitOne(TimeSpan timeout, bool exitContext) + { + return m_semaphore.WaitOne(timeout, exitContext); + } + + /// + /// Blocks the current thread until the current receives a signal, using a + /// 32-bit signed integer to specify the time interval and specifying whether to exit the synchronization + /// domain before the wait. + /// + /// + /// The number of milliseconds to wait, or (-1) to wait indefinitely. + /// + /// + /// true to exit the synchronization domain for the context before the wait (if in a synchronized context), + /// and reacquire it afterward; otherwise, false. + /// + /// true if the current instance receives a signal; otherwise, false. + public override bool WaitOne(int millisecondsTimeout, bool exitContext) + { + return m_semaphore.WaitOne(millisecondsTimeout, exitContext); + } + + /// + /// Exits the semaphore and returns the previous count. + /// + /// The count on the semaphore before the method was called. + public int Release() + { + return m_semaphore.ReleaseCore(1); + } + + /// + /// Exits the semaphore a specified number of times and returns the previous count. + /// + /// The number of times to exit the semaphore. + /// The count on the semaphore before the method was called. + public int Release(int releaseCount) + { + if (releaseCount < 1) + throw new ArgumentOutOfRangeException(nameof(releaseCount), "Non-negative number required."); + + return m_semaphore.ReleaseCore(releaseCount); + } + + /// + /// Removes a named semaphore. + /// + /// + /// On POSIX systems, calling this method removes the named semaphore referred to by . + /// The semaphore name is removed immediately and is destroyed once all other processes that have the semaphore + /// open close it. Calling this method on Windows systems does nothing. + /// + public void Unlink() + { + Unlink(Name); + } + + private static OpenExistingResult OpenExistingWorker(string name, out INamedSemaphore? semaphore) + { + return Common.IsPosixEnvironment ? + NamedSemaphoreUnix.OpenExistingWorker(name, out semaphore) : + NamedSemaphoreWindows.OpenExistingWorker(name, out semaphore); + } + + /// + /// Opens an existing named semaphore. + /// + /// + /// The unique name identifying the semaphore. This name is case-sensitive. Use a backslash (\\) to specify a + /// namespace, but avoid it elsewhere in the name. On Unix-based systems, the name should conform to valid file + /// naming conventions, excluding slashes except for an optional namespace backslash. The name length is limited + /// to 250 characters after any optional namespace. + /// + /// + /// An object that represents the opened named semaphore. + /// + /// + /// The may be prefixed with Global\ or Local\ to specify a namespace. + /// When the Global namespace is specified, the synchronization object may be shared with any processes on the system. + /// When the Local namespace is specified, which is also the default when no namespace is specified, the synchronization + /// object may be shared with processes in the same session. On Windows, a session is a login session, and services + /// typically run in a different non-interactive session. On Unix-like operating systems, each shell has its own session. + /// Session-local synchronization objects may be appropriate for synchronizing between processes with a parent/child + /// relationship where they all run in the same session. + /// + public static NamedSemaphore OpenExisting(string name) + { + switch (OpenExistingWorker(name, out INamedSemaphore? result)) + { + case OpenExistingResult.NameNotFound: + throw new WaitHandleCannotBeOpenedException(); + case OpenExistingResult.NameInvalid: + throw new WaitHandleCannotBeOpenedException($"Semaphore with name '{name}' cannot be created."); + case OpenExistingResult.PathTooLong: + throw new IOException($"Path too long for semaphore with name '{name}'."); + case OpenExistingResult.AccessDenied: + throw new UnauthorizedAccessException($"Access to the semaphore with name '{name}' is denied."); + default: + Debug.Assert(result is not null, "result should be non-null on success"); + return new NamedSemaphore(result, name); + } + } + + /// + /// Opens the specified named semaphore, if it already exists, and returns a value that indicates whether the + /// operation succeeded. + /// + /// + /// The unique name identifying the semaphore. This name is case-sensitive. Use a backslash (\\) to specify a + /// namespace, but avoid it elsewhere in the name. On Unix-based systems, the name should conform to valid file + /// naming conventions, excluding slashes except for an optional namespace backslash. The name length is limited + /// to 250 characters after any optional namespace. + /// + /// + /// When this method returns, contains a object that represents the named semaphore + /// if the call succeeded, or null if the call failed. This parameter is treated as uninitialized. + /// + /// + /// true if the named semaphore was opened successfully; otherwise, false. In some cases, + /// false may be returned for invalid names. + /// + /// + /// The may be prefixed with Global\ or Local\ to specify a namespace. + /// When the Global namespace is specified, the synchronization object may be shared with any processes on the system. + /// When the Local namespace is specified, which is also the default when no namespace is specified, the synchronization + /// object may be shared with processes in the same session. On Windows, a session is a login session, and services + /// typically run in a different non-interactive session. On Unix-like operating systems, each shell has its own session. + /// Session-local synchronization objects may be appropriate for synchronizing between processes with a parent/child + /// relationship where they all run in the same session. + /// + public static bool TryOpenExisting(string name, [NotNullWhen(true)] out NamedSemaphore? semaphore) + { + if (OpenExistingWorker(name, out INamedSemaphore? result) == OpenExistingResult.Success) + { + semaphore = new NamedSemaphore(result!, name); + return true; + } + + semaphore = null; + return false; + } + + /// + /// Removes a named semaphore. + /// + /// + /// The unique name identifying the semaphore. This name is case-sensitive. Use a backslash (\\) to specify a + /// namespace, but avoid it elsewhere in the name. On Unix-based systems, the name should conform to valid file + /// naming conventions, excluding slashes except for an optional namespace backslash. The name length is limited + /// to 250 characters after any optional namespace. + /// + /// + /// On POSIX systems, calling this method removes the named semaphore referred to by . + /// The semaphore name is removed immediately and is destroyed once all other processes that have the semaphore + /// open close it. Calling this method on Windows systems does nothing. + /// + public static void Unlink(string name) + { + if (Common.IsPosixEnvironment) + NamedSemaphoreUnix.Unlink(name); + } +} diff --git a/src/Gemstone/Threading/NamedSemaphoreUnix.cs b/src/Gemstone/Threading/NamedSemaphoreUnix.cs new file mode 100644 index 0000000000..3fe10db5a0 --- /dev/null +++ b/src/Gemstone/Threading/NamedSemaphoreUnix.cs @@ -0,0 +1,355 @@ +//****************************************************************************************************** +// NamedSemaphoreUnix.cs - Gbtc +// +// Copyright © 2023, 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://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: +// ---------------------------------------------------------------------------------------------------- +// 11/09/2023 - Ritchie Carroll +// Generated original version of source code. +// +//****************************************************************************************************** +// ReSharper disable IdentifierTypo +// ReSharper disable InconsistentNaming + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using Microsoft.Win32.SafeHandles; + +namespace Gemstone.Threading; + +internal partial class NamedSemaphoreUnix : INamedSemaphore +{ + // DllImport code is in Gemstone.POSIX.c + private const string ImportFileName = "./Gemstone.POSIX.so"; + + private readonly ref struct ErrorNo + { + public const int ENOENT = 2; + public const int EINTR= 4; + public const int EAGAIN = 11; + public const int ENOMEM = 12; + public const int EACCES = 13; + public const int EINVAL = 22; + public const int ENFILE = 23; + public const int EMFILE = 24; + public const int ENAMETOOLONG = 36; + public const int EOVERFLOW = 75; + public const int ETIMEDOUT = 110; + } + + private sealed partial class SemaphorePtr : SafeHandleZeroOrMinusOneIsInvalid + { + public SemaphorePtr(nint handle) : base(true) + { + SetHandle(handle); + } + + protected override bool ReleaseHandle() + { + return CloseSemaphore(handle) == 0; + } + + #if NET + [LibraryImport(ImportFileName)] + private static partial int CloseSemaphore(nint semaphore); + #else + [DllImport(ImportFileName)] + private static extern int CloseSemaphore(nint semaphore); + #endif + } + + private SemaphorePtr? m_semaphore; + private int m_maximumCount; + + // We can ignore any user assigned SafeWaitHandle since we are using a custom SafeHandle implementation. Internal + // to this class we assign a new SafeWaitHandle around our semaphore pointer that the parent class then passes to + // the WaitHandle base class so that the parent class can be used by standard WaitHandle class methods. + public SafeWaitHandle? SafeWaitHandle { get; set; } + + public void CreateSemaphoreCore(int initialCount, int maximumCount, string name, out bool createdNew) + { + if (initialCount < 0) + throw new ArgumentOutOfRangeException(nameof(initialCount), "Non-negative number required."); + + if (maximumCount < 1) + throw new ArgumentOutOfRangeException(nameof(maximumCount), "Positive number required."); + + if (initialCount > maximumCount) + throw new ArgumentException("The initial count for the semaphore must be greater than or equal to zero and less than the maximum count."); + + m_maximumCount = maximumCount; + + (bool result, ArgumentException? ex) = ParseSemaphoreName(name, out string? namespaceName, out string? semaphoreName); + + if (!result) + throw ex!; + + int retVal = CreateSemaphore(semaphoreName!, namespaceName == "Global", initialCount, out createdNew, out nint semaphoreHandle); + + switch (retVal) + { + case 0 when semaphoreHandle > 0: + m_semaphore = new SemaphorePtr(semaphoreHandle); + SafeWaitHandle = new SafeWaitHandle(semaphoreHandle, false); + return; + case ErrorNo.EACCES: + throw new UnauthorizedAccessException("The named semaphore exists, but the user does not have the security access required to use it."); + case ErrorNo.EINVAL: + throw new ArgumentOutOfRangeException(nameof(initialCount), "The value was greater than SEM_VALUE_MAX."); + case ErrorNo.EMFILE: + throw new IOException("The per-process limit on the number of open file descriptors has been reached."); + case ErrorNo.ENAMETOOLONG: + throw new PathTooLongException("The 'name' is too long. Length restrictions may depend on the operating system or configuration."); + case ErrorNo.ENFILE: + throw new IOException("The system limit on the total number of open files has been reached."); + case ErrorNo.ENOENT: + throw new WaitHandleCannotBeOpenedException("The 'name' is not well formed."); + case ErrorNo.ENOMEM: + throw new OutOfMemoryException("Insufficient memory to create the named semaphore."); + default: + if (semaphoreHandle == 0) + throw new WaitHandleCannotBeOpenedException("The semaphore handle is invalid."); + + throw new InvalidOperationException($"An unknown error occurred while creating the named semaphore. Error code: {retVal}"); + } + } + + public void Dispose() + { + m_semaphore?.Dispose(); + m_semaphore = null; + } + + public static OpenExistingResult OpenExistingWorker(string name, out INamedSemaphore? semaphore) + { + semaphore = null; + + if (!ParseSemaphoreName(name, out string? semaphoreName)) + return OpenExistingResult.NameInvalid; + + int retVal = OpenExistingSemaphore(semaphoreName, out nint semaphoreHandle); + + switch (retVal) + { + case 0 when semaphoreHandle > 0: + semaphore = new NamedSemaphoreUnix + { + m_semaphore = new SemaphorePtr(semaphoreHandle), + SafeWaitHandle = new SafeWaitHandle(semaphoreHandle, false) + }; + + return OpenExistingResult.Success; + case ErrorNo.ENAMETOOLONG: + return OpenExistingResult.PathTooLong; + default: + // Just return NameNotFound for all other errors + return OpenExistingResult.NameNotFound; + } + } + + private static bool ParseSemaphoreName(string name, [NotNullWhen(true)] out string? semaphoreName) + { + return ParseSemaphoreName(name, out _, out semaphoreName).result; + } + + private static (bool result, ArgumentException? ex) ParseSemaphoreName(string name, out string? namespaceName, out string? semaphoreName) + { + namespaceName = null; + semaphoreName = null; + + int namespaceSeparatorIndex = name.IndexOf('\\'); + + if (namespaceSeparatorIndex > 0) + { + namespaceName = name[..namespaceSeparatorIndex]; + + if (namespaceName != "Global" && namespaceName != "Local") + return (false, new ArgumentException("""When using a namespace, the name of the semaphore must be prefixed with either "Global\" or "Local\".""")); + + semaphoreName = name[(namespaceSeparatorIndex + 1)..]; + } + else + { + semaphoreName = name; + } + + if (semaphoreName.Length > 250) + return (false, new ArgumentException("The name of the semaphore must be less than 251 characters.")); + + if (semaphoreName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) + return (false, new ArgumentException("The name of the semaphore contains invalid characters.")); + + // Linux named semaphores must start with a forward slash + semaphoreName = $"/{semaphoreName}"; + + return (true, null); + } + + public int ReleaseCore(int releaseCount) + { + if (m_semaphore is null) + throw new ObjectDisposedException(nameof(NamedSemaphoreUnix)); + + if (releaseCount < 1) + throw new ArgumentOutOfRangeException(nameof(releaseCount), "Non-negative number required."); + + int retVal = GetSemaphoreCount(m_semaphore, out int previousCount); + + if (retVal == ErrorNo.EINVAL) + throw new InvalidOperationException("The named semaphore is invalid."); + + if (retVal != 0) + throw new InvalidOperationException($"An unknown error occurred while getting current count for the named semaphore. Error code: {retVal}"); + + if (previousCount >= m_maximumCount) + throw new SemaphoreFullException("The semaphore count is already at the maximum value."); + + for (int i = 0; i < releaseCount; i++) + { + retVal = ReleaseSemaphore(m_semaphore); + + switch (retVal) + { + case 0: + continue; + case ErrorNo.EOVERFLOW: + throw new SemaphoreFullException("The maximum count for the semaphore would be exceeded."); + case ErrorNo.EINVAL: + throw new InvalidOperationException("The named semaphore is invalid."); + default: + throw new InvalidOperationException($"An unknown error occurred while releasing the named semaphore. Error code: {retVal}"); + } + } + + return previousCount; + } + + public void Close() + { + Dispose(); + } + + public bool WaitOne() + { + return WaitOne(Timeout.Infinite); + } + + public bool WaitOne(TimeSpan timeout) + { + long totalMilliseconds = (long)timeout.TotalMilliseconds; + + int millisecondsTimeout = totalMilliseconds switch + { + < -1 => throw new ArgumentOutOfRangeException(nameof(timeout), "Argument milliseconds must be either non-negative and less than or equal to Int32.MaxValue or -1"), + > int.MaxValue => throw new ArgumentOutOfRangeException(nameof(timeout), "Argument milliseconds must be less than or equal to Int32.MaxValue."), + _ => (int)totalMilliseconds + }; + + return WaitOne(millisecondsTimeout); + } + + public bool WaitOne(int millisecondsTimeout) + { + if (m_semaphore is null) + return false; + + int retVal = WaitSemaphore(m_semaphore, millisecondsTimeout); + + return retVal switch + { + 0 => true, + ErrorNo.EINTR => false, + ErrorNo.EAGAIN => false, + ErrorNo.ETIMEDOUT => false, + ErrorNo.EINVAL => throw new InvalidOperationException("The named semaphore is invalid."), + _ => throw new InvalidOperationException($"An unknown error occurred while waiting on the named semaphore. Error code: {retVal}") + }; + } + + public bool WaitOne(int millisecondsTimeout, bool exitContext) + { + return WaitOne(millisecondsTimeout); + } + + public bool WaitOne(TimeSpan timeout, bool exitContext) + { + return WaitOne(timeout); + } + + public static void Unlink(string name) + { + (bool result, ArgumentException? ex) = ParseSemaphoreName(name, out _, out string? semaphoreName); + + if (!result) + throw ex!; + + int retVal = UnlinkSemaphore(semaphoreName!); + + switch (retVal) + { + case 0: + return; + case ErrorNo.ENAMETOOLONG: + throw new PathTooLongException("The 'name' is too long. Length restrictions may depend on the operating system or configuration."); + case ErrorNo.ENOENT: + throw new FileNotFoundException("There is no semaphore with the given 'name'."); + case ErrorNo.EACCES: + throw new UnauthorizedAccessException("The 'name' exists, but the user does not have the security access required to unlink it."); + default: + throw new InvalidOperationException($"An unknown error occurred while unlinking the named semaphore. Error code: {retVal}"); + } + } + +#if NET + [LibraryImport(ImportFileName, StringMarshalling = StringMarshalling.Utf8)] + private static partial int CreateSemaphore(string name, [MarshalAs(UnmanagedType.I4)] bool useGlobalScope, int initialCount, [MarshalAs(UnmanagedType.I4)] out bool createdNew, out nint semaphoreHandle); + + [LibraryImport(ImportFileName, StringMarshalling = StringMarshalling.Utf8)] + private static partial int OpenExistingSemaphore(string name, out nint semaphoreHandle); + + [LibraryImport(ImportFileName)] + private static partial int GetSemaphoreCount(SemaphorePtr semaphore, out int count); + + [LibraryImport(ImportFileName)] + private static partial int ReleaseSemaphore(SemaphorePtr semaphore); + + [LibraryImport(ImportFileName)] + private static partial int WaitSemaphore(SemaphorePtr semaphore, int timeout); + + [LibraryImport(ImportFileName, StringMarshalling = StringMarshalling.Utf8)] + private static partial int UnlinkSemaphore(string name); +#else + [DllImport(ImportFileName)] + private static extern int CreateSemaphore(string name, bool useGlobalScope, int initialCount, out bool createdNew, out nint semaphoreHandle); + + [DllImport(ImportFileName)] + private static extern int OpenExistingSemaphore(string name, out nint semaphoreHandle); + + [DllImport(ImportFileName)] + private static extern int GetSemaphoreCount(SemaphorePtr semaphore, out int count); + + [DllImport(ImportFileName)] + private static extern int ReleaseSemaphore(SemaphorePtr semaphore); + + [DllImport(ImportFileName)] + private static extern int WaitSemaphore(SemaphorePtr semaphore, int timeout); + + [DllImport(ImportFileName)] + private static extern int UnlinkSemaphore(string name); +#endif +} diff --git a/src/Gemstone/Threading/NamedSemaphoreWindows.cs b/src/Gemstone/Threading/NamedSemaphoreWindows.cs new file mode 100644 index 0000000000..74bcdb69e7 --- /dev/null +++ b/src/Gemstone/Threading/NamedSemaphoreWindows.cs @@ -0,0 +1,136 @@ +//****************************************************************************************************** +// NamedSemaphoreWindows.cs - Gbtc +// +// Copyright © 2023, 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://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: +// ---------------------------------------------------------------------------------------------------- +// 11/09/2023 - Ritchie Carroll +// Generated original version of source code. +// +//****************************************************************************************************** +// ReSharper disable UnusedMember.Global + +using System; +using System.IO; +using System.Runtime.Versioning; +using System.Threading; +using Microsoft.Win32.SafeHandles; + +namespace Gemstone.Threading +{ + internal class NamedSemaphoreWindows : INamedSemaphore + { + private Semaphore? m_semaphore; + + public SafeWaitHandle? SafeWaitHandle + { + get => m_semaphore?.SafeWaitHandle; + set + { + if (m_semaphore is null) + return; + + m_semaphore.SafeWaitHandle = value; + } + } + + public void CreateSemaphoreCore(int initialCount, int maximumCount, string name, out bool createdNew) + { + m_semaphore = new Semaphore(initialCount, maximumCount, name, out createdNew); + } + + public void Dispose() + { + m_semaphore?.Dispose(); + } + + #if NET + [SupportedOSPlatform("windows")] + #endif + public static OpenExistingResult OpenExistingWorker(string name, out INamedSemaphore? semaphore) + { + semaphore = null; + + try + { + semaphore = new NamedSemaphoreWindows { m_semaphore = Semaphore.OpenExisting(name) }; + + return OpenExistingResult.Success; + } + catch (ArgumentException) + { + return OpenExistingResult.NameInvalid; + } + catch (PathTooLongException) + { + return OpenExistingResult.PathTooLong; + } + catch (IOException) + { + return OpenExistingResult.NameInvalid; + } + catch (WaitHandleCannotBeOpenedException ex) + { + return string.IsNullOrWhiteSpace(ex.Message) + ? OpenExistingResult.NameNotFound + : OpenExistingResult.NameInvalid; + } + catch (Exception) + { + return OpenExistingResult.NameNotFound; + } + } + + public int ReleaseCore(int releaseCount) + { + return m_semaphore?.Release(releaseCount) ?? 0; + } + + public void Close() + { + m_semaphore?.Close(); + } + + public bool WaitOne() + { + return m_semaphore?.WaitOne() ?? false; + } + + public bool WaitOne(TimeSpan timeout) + { + return m_semaphore?.WaitOne(timeout) ?? false; + } + + public bool WaitOne(int millisecondsTimeout) + { + return m_semaphore?.WaitOne(millisecondsTimeout) ?? false; + } + + public bool WaitOne(TimeSpan timeout, bool exitContext) + { + return m_semaphore?.WaitOne(timeout, exitContext) ?? false; + } + + public bool WaitOne(int millisecondsTimeout, bool exitContext) + { + return m_semaphore?.WaitOne(millisecondsTimeout, exitContext) ?? false; + } + + public static void Unlink(string _) + { + // This function does nothing on Windows + } + } +} diff --git a/src/Gemstone/Threading/SynchronizedOperations/DelayedSynchronizedOperation.cs b/src/Gemstone/Threading/SynchronizedOperations/DelayedSynchronizedOperation.cs new file mode 100644 index 0000000000..23e937a071 --- /dev/null +++ b/src/Gemstone/Threading/SynchronizedOperations/DelayedSynchronizedOperation.cs @@ -0,0 +1,226 @@ +//****************************************************************************************************** +// DelayedSynchronizedOperation.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: +// ---------------------------------------------------------------------------------------------------- +// 02/06/2012 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.ComponentModel; +using System.Threading; +using Gemstone.ActionExtensions; + +namespace Gemstone.Threading.SynchronizedOperations; + +/// +/// Represents a short-running synchronized operation that cannot run while it is already +/// in progress. Async operations will execute on the thread-pool after the specified +/// in milliseconds. +/// +/// +/// By default, the action performed by the +/// is executed on the when running the operation asynchronously. +/// When the operation is set to pending, the action is executed in an asynchronous loop on +/// the thread pool until all pending operations have been completed. Since the action is +/// executed on the thread pool, it is best if it can be executed quickly, without +/// blocking the thread or putting it to sleep. If completion of the operation is +/// critical, such as when saving data to a file, this type of operation should not +/// be used since thread pool threads are background threads and will not prevent the +/// program from ending before the operation is complete. +/// +public class DelayedSynchronizedOperation : SynchronizedOperationBase, ISynchronizedOperation +{ + #region [ Members ] + + // Constants + + /// + /// Defines the default value for the property. + /// + public const int DefaultDelay = 1000; + + // Fields + private readonly Action m_delayedAction; + + #endregion + + #region [ Constructors ] + + /// + /// Creates a new instance of the class. + /// + /// The action to be performed during this operation. + public DelayedSynchronizedOperation(Action action) : this(action, null) + { + } + + /// + /// Creates a new instance of the class. + /// + /// The cancellable action to be performed during this operation. + /// + /// Cancellable synchronized operation is useful in cases where actions should be terminated + /// during dispose and/or shutdown operations. + /// + public DelayedSynchronizedOperation(Action action) : this(action, null) + { + } + + /// + /// Creates a new instance of the class. + /// + /// The action to be performed during this operation. + /// The action to be performed if an exception is thrown from the action. + public DelayedSynchronizedOperation(Action action, Action? exceptionAction) : this(_ => action(), exceptionAction) + { + } + + /// + /// Creates a new instance of the class. + /// + /// The cancellable action to be performed during this operation. + /// The action to be performed if an exception is thrown from the action. + /// + /// Cancellable synchronized operation is useful in cases where actions should be terminated + /// during dispose and/or shutdown operations. + /// + public DelayedSynchronizedOperation(Action action, Action? exceptionAction) : base(action, exceptionAction) + { + m_delayedAction = _ => + { + if (ExecuteAction()) + ExecuteActionAsync(); + }; + } + + #endregion + + #region [ Properties ] + + /// + /// Gets or sets the amount of time to wait before execution, in milliseconds, + /// for any asynchronous calls. Zero value will execute immediately. + /// + /// + /// Non asynchronous calls will not be delayed. + /// + public int Delay { get; set; } = DefaultDelay; + + #endregion + + #region [ Methods ] + + /// + /// Executes the action on another thread after the specified in milliseconds or marks + /// the operation as pending if the operation is already running. Method same as for + /// . + /// + /// + /// Defines synchronization mode for running any pending operation; must be false for + /// . + /// + /// + /// + /// For , actions will always run on another thread so this method is + /// hidden from intellisense. + /// + /// + /// When the operation is marked as pending, it will run again after the operation that is currently running + /// has completed. This is useful if an update has invalidated the operation that is currently running and + /// will therefore need to be run again. + /// + /// + /// + /// must be false for . + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public new void Run(bool runPendingSynchronously = false) // Method shadowed to provide updated documentation and hide from intellisense + { + if (runPendingSynchronously) + throw new InvalidOperationException($"{nameof(runPendingSynchronously)} must be false for {nameof(DelayedSynchronizedOperation)}"); + + base.Run(); + } + + void ISynchronizedOperation.Run(bool runPendingSynchronously) => Run(runPendingSynchronously); + + /// + /// Attempts to execute the action on another thread after the specified in milliseconds. + /// Does nothing if the operation is already running. Method same as for + /// . + /// + /// + /// Defines synchronization mode for running any pending operation; must be false for + /// . + /// + /// + /// For , actions will always run on another thread so this method is + /// hidden from intellisense. + /// + /// + /// must be false for . + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public new void TryRun(bool runPendingSynchronously = false) // Method shadowed to provide updated documentation and hide from intellisense + { + if (runPendingSynchronously) + throw new InvalidOperationException($"{nameof(runPendingSynchronously)} must be false for {nameof(DelayedSynchronizedOperation)}"); + + base.TryRun(); + } + + void ISynchronizedOperation.TryRun(bool runPendingSynchronously) => TryRun(runPendingSynchronously); + + /// + /// Executes the action on another thread after the specified in milliseconds or marks + /// the operation as pending if the operation is already running + /// + /// + /// When the operation is marked as pending, operation will run again after currently running operation has + /// completed. This is useful if an update has invalidated the operation that is currently running and will + /// therefore need to be run again. + /// + public new void RunAsync() => base.RunAsync(); // Method shadowed to provide updated documentation + + /// + /// Attempts to execute the action on another thread after the specified in milliseconds. + /// Does nothing if the operation is already running. + /// + public new void TryRunAsync() => base.TryRunAsync(); // Method shadowed to provide updated documentation + + /// + /// Executes the action on a separate thread after the specified . + /// + protected override void ExecuteActionAsync() => m_delayedAction.DelayAndExecute(Delay, CancellationToken, ProcessException); + + #endregion + + #region [ Static ] + + // Static Methods + + /// + /// Factory method to match the signature. + /// + /// The action to be performed by the . + /// A new instance of with of 1000 milliseconds. + public static ISynchronizedOperation Factory(Action action) => new DelayedSynchronizedOperation(action); + + #endregion +} diff --git a/src/Gemstone/Threading/SynchronizedOperations/ISynchronizedOperation.cs b/src/Gemstone/Threading/SynchronizedOperations/ISynchronizedOperation.cs new file mode 100644 index 0000000000..e4cf9e0c32 --- /dev/null +++ b/src/Gemstone/Threading/SynchronizedOperations/ISynchronizedOperation.cs @@ -0,0 +1,124 @@ +//****************************************************************************************************** +// ISynchronizedOperation.cs - Gbtc +// +// Copyright © 2014, 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/2014 - Stephen C. Wills +// Generated original version of source code. +// 10/14/2019 - J. Ritchie Carroll +// Simplified calling model to Run, TryRun, RunAsync, and TryRunAsync. +// +//****************************************************************************************************** + +using System; +using System.Threading; + +// ReSharper disable UnusedMemberInSuper.Global +namespace Gemstone.Threading.SynchronizedOperations; + +/// +/// Factory method for creating synchronized operations. +/// +/// The action to be synchronized by the operation. +/// The operation that synchronizes the given action. +public delegate ISynchronizedOperation SynchronizedOperationFactory(Action action); + +/// +/// Represents the available types of synchronized operations. +/// +public enum SynchronizedOperationType +{ + /// + /// + /// + Short, + + /// + /// + /// + Long, + + /// + /// with IsBackground set to true + /// + LongBackground +} + +/// +/// Represents an operation that cannot run while it is already in progress. +/// +public interface ISynchronizedOperation +{ + /// + /// Gets flag indicating if the synchronized operation is currently executing its action. + /// + bool IsRunning { get; } + + /// + /// Gets flag indicating if the synchronized operation has an additional operation that is pending + /// execution after the currently running action has completed. + /// + bool IsPending { get; } + + /// + /// Gets or sets to use for canceling actions. + /// + CancellationToken CancellationToken { get; set; } + + /// + /// Executes the action on current thread or marks the operation as pending if the operation is already running. + /// + /// Defines synchronization mode for running any pending operation. + /// + /// + /// When the operation is marked as pending, operation will run again after currently running operation has + /// completed. This is useful if an update has invalidated the operation that is currently running and will + /// therefore need to be run again. + /// + /// + /// When is true, this method will not guarantee that control + /// will be returned to the thread that called it; if other threads continuously mark the operation as pending, + /// this thread will continue to run the operation indefinitely on the calling thread. + /// + /// + void Run(bool runPendingSynchronously = false); + + /// + /// Attempts to execute the action on current thread. Does nothing if the operation is already running. + /// + /// Defines synchronization mode for running any pending operation. + /// + /// When is true, this method will not guarantee that control + /// will be returned to the thread that called it; if other threads continuously mark the operation as pending, + /// this thread will continue to run the operation indefinitely on the calling thread. + /// + void TryRun(bool runPendingSynchronously = false); + + /// + /// Executes the action on another thread or marks the operation as pending if the operation is already running. + /// + /// + /// When the operation is marked as pending, it will run again after the operation that is currently running + /// has completed. This is useful if an update has invalidated the operation that is currently running and + /// will therefore need to be run again. + /// + void RunAsync(); + + /// + /// Attempts to execute the action on another thread. Does nothing if the operation is already running. + /// + void TryRunAsync(); +} diff --git a/src/Gemstone/Threading/SynchronizedOperations/LongSynchronizedOperation.cs b/src/Gemstone/Threading/SynchronizedOperations/LongSynchronizedOperation.cs new file mode 100644 index 0000000000..ebcff23750 --- /dev/null +++ b/src/Gemstone/Threading/SynchronizedOperations/LongSynchronizedOperation.cs @@ -0,0 +1,156 @@ +//****************************************************************************************************** +// LongSynchronizedOperation.cs - Gbtc +// +// Copyright © 2014, 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/2014 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Gemstone.Threading.SynchronizedOperations; + +/// +/// Represents a long-running synchronized operation that cannot run while it is already in progress. +/// +/// +/// The action performed by the is executed on +/// its own dedicated thread when running the operation in the foreground asynchronously. +/// When running on its own thread, the action is executed in a tight loop until all +/// pending operations have been completed. This type of synchronized operation should +/// be preferred if operations may take a long time, block the thread, or put it to sleep. +/// It is also recommended to prefer this type of operation if the speed of the operation +/// is not critical or if completion of the operation is critical, such as when saving data +/// to a file. +/// +public class LongSynchronizedOperation : SynchronizedOperationBase +{ + #region [ Constructors ] + + /// + /// Creates a new instance of the class. + /// + /// The action to be performed during this operation. + public LongSynchronizedOperation(Action action) : base(action) + { + } + + /// + /// Creates a new instance of the class. + /// + /// The cancellable action to be performed during this operation. + /// + /// Cancellable synchronized operation is useful in cases where actions should be terminated + /// during dispose and/or shutdown operations. + /// + public LongSynchronizedOperation(Action action) : base(action) + { + } + + /// + /// Creates a new instance of the class. + /// + /// The action to be performed during this operation. + /// The action to be performed if an exception is thrown from the action. + public LongSynchronizedOperation(Action action, Action? exceptionAction) : base(action, exceptionAction) + { + } + + /// + /// Creates a new instance of the class. + /// + /// The action to be performed during this operation. + /// The cancellable action to be performed if an exception is thrown from the action. + /// + /// Cancellable synchronized operation is useful in cases where actions should be terminated + /// during dispose and/or shutdown operations. + /// + public LongSynchronizedOperation(Action action, Action? exceptionAction) : base(action, exceptionAction) + { + } + + #endregion + + #region [ Properties ] + + /// + /// Gets or sets whether the thread executing the action is a background thread. + /// + /// + /// This defaults to false, be aware that foreground thread will prevent shutdown + /// while task is running. If a task keeps getting marked as pending, application will not + /// shut down; consider a cancellable action for + /// instances that use a foreground thread. + /// + public bool IsBackground { get; init; } + + #endregion + + #region [ Methods ] + + /// + /// Executes the action on a separate thread. + /// + protected override void ExecuteActionAsync() + { + if (IsBackground) + ExecuteActionAsyncBackground(); + else + ExecuteActionAsyncForeground(); + } + + private void ExecuteActionAsyncBackground() + { + void TaskAction() + { + if (ExecuteAction()) + ExecuteActionAsync(); + } + + Task.Factory.StartNew(TaskAction, TaskCreationOptions.LongRunning); + } + + private void ExecuteActionAsyncForeground() + { + void ThreadAction() + { + while (ExecuteAction()) + { + } + } + + new Thread(ThreadAction).Start(); + } + + #endregion + + #region [ Static ] + + // Static Methods + + /// + /// Factory method to match the signature. + /// + /// The action to be performed by the . + /// A new instance of . + public static ISynchronizedOperation Factory(Action action) => new LongSynchronizedOperation(action); + + #endregion +} diff --git a/src/Gemstone/Threading/SynchronizedOperations/NamespaceDoc.cs b/src/Gemstone/Threading/SynchronizedOperations/NamespaceDoc.cs new file mode 100644 index 0000000000..fd0fdfb0d6 --- /dev/null +++ b/src/Gemstone/Threading/SynchronizedOperations/NamespaceDoc.cs @@ -0,0 +1,36 @@ +//****************************************************************************************************** +// NamespaceDoc.cs - Gbtc +// +// Copyright © 2019, 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://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: +// ---------------------------------------------------------------------------------------------------- +// 10/14/2019 - J. Ritchie Carroll +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.Runtime.CompilerServices; + +namespace Gemstone.Threading.SynchronizedOperations; + +/// +/// The namespace provides classes and interfaces for +/// synchronized operations, which are operations that cannot run while another is already in progress, e.g., +/// and . +/// +[CompilerGenerated] +internal class NamespaceDoc +{ +} diff --git a/src/Gemstone/Threading/SynchronizedOperations/ShortSynchronizedOperation.cs b/src/Gemstone/Threading/SynchronizedOperations/ShortSynchronizedOperation.cs new file mode 100644 index 0000000000..025ecb38d0 --- /dev/null +++ b/src/Gemstone/Threading/SynchronizedOperations/ShortSynchronizedOperation.cs @@ -0,0 +1,96 @@ +//****************************************************************************************************** +// ShortSynchronizedOperation.cs - Gbtc +// +// Copyright © 2014, 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/2014 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Gemstone.Threading.SynchronizedOperations; + +/// +/// Represents a short-running synchronized operation that cannot run while it is already in progress. +/// +/// +/// By default, the action performed by the +/// is executed on the when running the operation asynchronously. +/// When the operation is set to pending, the action is executed in an asynchronous loop +/// on the thread pool until all pending operations have been completed. Since the action +/// is executed on the thread pool, it is best if it can be executed quickly, without +/// blocking the thread or putting it to sleep. If completion of the operation is +/// critical, such as when saving data to a file, this type of operation should not +/// be used since thread pool threads are background threads and will not prevent the +/// program from ending before the operation is complete. +/// +public class ShortSynchronizedOperation : SynchronizedOperationBase +{ + #region [ Constructors ] + + /// + /// Creates a new instance of the class. + /// + /// The action to be performed during this operation. + public ShortSynchronizedOperation(Action action) : base(action) + { + } + + /// + /// Creates a new instance of the class. + /// + /// The action to be performed during this operation. + /// The action to be performed if an exception is thrown from the action. + public ShortSynchronizedOperation(Action action, Action? exceptionAction) : base(action, exceptionAction) + { + } + + #endregion + + #region [ Methods ] + + /// + /// Executes the action in an asynchronous loop on + /// the thread pool, as long as the operation is pending. + /// + protected override void ExecuteActionAsync() + { + Task.Run(() => + { + if (ExecuteAction()) + ExecuteActionAsync(); + }); + } + + #endregion + + #region [ Static ] + + // Static Methods + + /// + /// Factory method to match the signature. + /// + /// The action to be performed by the . + /// A new instance of . + public static ISynchronizedOperation Factory(Action action) => new ShortSynchronizedOperation(action); + + #endregion +} diff --git a/src/Gemstone/Threading/SynchronizedOperations/SynchronizedOperationBase.cs b/src/Gemstone/Threading/SynchronizedOperations/SynchronizedOperationBase.cs new file mode 100644 index 0000000000..8f4d45463d --- /dev/null +++ b/src/Gemstone/Threading/SynchronizedOperations/SynchronizedOperationBase.cs @@ -0,0 +1,321 @@ +//****************************************************************************************************** +// SynchronizedOperationBase.cs - Gbtc +// +// Copyright © 2014, 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/2014 - Stephen C. Wills +// Generated original version of source code. +// 10/14/2019 - J. Ritchie Carroll +// Simplified calling model to Run, TryRun, RunAsync, and TryRunAsync. +// +//****************************************************************************************************** + +using System; +using System.Threading; + +namespace Gemstone.Threading.SynchronizedOperations; + +/// +/// Base class for operations that cannot run while they is already in progress. +/// +/// +/// +/// This class handles the synchronization between the methods defined in the +/// interface. Implementers should only need to implement the method to provide a +/// mechanism for executing the action on a separate thread. +/// +/// +/// If subclass implementations get constructed without an exception handler, applications should attach to the static +/// event so that any unhandled exceptions can be exposed to a log. +/// +/// +public abstract class SynchronizedOperationBase : ISynchronizedOperation +{ + #region [ Members ] + + // Constants + private const int NotRunning = 0; + private const int Running = 1; + private const int Pending = 2; + + // Fields + private readonly Action m_action; + private readonly Action? m_exceptionAction; + private int m_state; + + #endregion + + #region [ Constructors ] + + /// + /// Creates a new instance of the class. + /// + /// The action to be performed during this operation. + protected SynchronizedOperationBase(Action action) : this(action, null) + { + } + + /// + /// Creates a new instance of the class. + /// + /// The cancellable action to be performed during this operation. + /// + /// Cancellable synchronized operation is useful in cases where actions should be terminated + /// during dispose and/or shutdown operations. + /// + protected SynchronizedOperationBase(Action action) : this(action, null) + { + } + + /// + /// Creates a new instance of the class. + /// + /// The action to be performed during this operation. + /// The action to be performed if an exception is thrown from the action. + protected SynchronizedOperationBase(Action action, Action? exceptionAction) : this(_ => action(), exceptionAction) + { + } + + /// + /// Creates a new instance of the class. + /// + /// The cancellable action to be performed during this operation. + /// The action to be performed if an exception is thrown from the action. + /// + /// Cancellable synchronized operation is useful in cases where actions should be terminated + /// during dispose and/or shutdown operations. + /// + protected SynchronizedOperationBase(Action action, Action? exceptionAction) + { + m_action = action ?? throw new ArgumentNullException(nameof(action)); + m_exceptionAction = exceptionAction; + } + + #endregion + + #region [ Properties ] + + /// + /// Gets flag indicating if the synchronized operation is currently executing its action. + /// + public bool IsRunning => Interlocked.CompareExchange(ref m_state, NotRunning, NotRunning) != NotRunning; + + /// + /// Gets flag indicating if the synchronized operation has an additional operation that is pending + /// execution after the currently running action has completed. + /// + public bool IsPending => Interlocked.CompareExchange(ref m_state, NotRunning, NotRunning) == Pending; + + /// + /// Gets or sets to use for cancelling actions. + /// + public CancellationToken CancellationToken { get; set; } = CancellationToken.None; + + #endregion + + #region [ Methods ] + + /// + /// Executes the action on current thread or marks the operation as pending if the operation is already running. + /// + /// Defines synchronization mode for running any pending operation. + /// + /// + /// When the operation is marked as pending, operation will run again after currently running operation has + /// completed. This is useful if an update has invalidated the operation that is currently running and will + /// therefore need to be run again. + /// + /// + /// When is true, this method will not guarantee that control + /// will be returned to the thread that called it; if other threads continuously mark the operation as pending, + /// this thread will continue to run the operation indefinitely on the calling thread. + /// + /// + public void Run(bool runPendingSynchronously = false) + { + // if (m_state == NotRunning) + // TryRun(runPendingAsync); + // else if (m_state == Running) + // m_state = Pending; + + if (Interlocked.CompareExchange(ref m_state, Pending, Running) == NotRunning) + TryRun(runPendingSynchronously); + } + + /// + /// Attempts to execute the action on current thread. Does nothing if the operation is already running. + /// + /// Defines synchronization mode for running any pending operation. + /// + /// When is true, this method will not guarantee that control + /// will be returned to the thread that called it; if other threads continuously mark the operation as pending, + /// this thread will continue to run the operation indefinitely on the calling thread. + /// + public void TryRun(bool runPendingSynchronously = false) + { + // if (m_state == NotRunning) + // { + // m_state = Running; + // + // if (runPendingAsync) + // { + // while (ExecuteAction()) + // { + // } + // } + // else + // { + // if (ExecuteAction()) + // ExecuteActionAsync(); + // } + // } + + if (Interlocked.CompareExchange(ref m_state, Running, NotRunning) == NotRunning) + { + if (runPendingSynchronously) + { + while (ExecuteAction()) + { + } + } + else + { + if (ExecuteAction()) + ExecuteActionAsync(); + } + } + } + + /// + /// Executes the action on another thread or marks the operation as pending if the operation is already running. + /// + /// + /// When the operation is marked as pending, operation will run again after currently running operation has + /// completed. This is useful if an update has invalidated the operation that is currently running and will + /// therefore need to be run again. + /// + public void RunAsync() + { + // if (m_state == NotRunning) + // TryRunOnceAsync(); + // else if (m_state == Running) + // m_state = Pending; + + if (Interlocked.CompareExchange(ref m_state, Pending, Running) == NotRunning) + TryRunAsync(); + } + + /// + /// Attempts to execute the action on another thread. Does nothing if the operation is already running. + /// + public void TryRunAsync() + { + // if (m_state == NotRunning) + // { + // m_state = Running; + // ExecuteActionAsync(); + // } + + if (Interlocked.CompareExchange(ref m_state, Running, NotRunning) == NotRunning) + ExecuteActionAsync(); + } + + /// + /// Executes the action once on the current thread. + /// + /// true if the action was pending and needs to run again; otherwise, false. + protected bool ExecuteAction() + { + try + { + if (!CancellationToken.IsCancellationRequested) + m_action(CancellationToken); + } + catch (Exception ex) + { + ProcessException(ex); + } + + // if (m_state == Pending) + // { + // m_state = Running; + // return true; + // } + // else if (m_state == Running) + // { + // m_state = NotRunning; + // } + + if (Interlocked.CompareExchange(ref m_state, NotRunning, Running) == Pending) + { + // There is no race condition here because if m_state is Pending, + // then it cannot be changed by any other line of code except this one + Interlocked.Exchange(ref m_state, Running); + + return true; + } + + return false; + } + + /// + /// Executes the action on a separate thread. + /// + /// + /// Implementers should call on a separate thread and check the return value. + /// If it returns true, that means it needs to run again. The following is a sample implementation using + /// a regular dedicated thread: + /// + /// protected override void ExecuteActionAsync() + /// { + /// Thread actionThread = new Thread(() => + /// { + /// while (ExecuteAction()) + /// { + /// } + /// }); + /// + /// actionThread.Start(); + /// } + /// + /// + protected abstract void ExecuteActionAsync(); + + /// + /// Processes an exception thrown by an operation. + /// + /// to be processed. + protected void ProcessException(Exception ex) + { + if (m_exceptionAction is null) + { + LibraryEvents.OnSuppressedException(this, new Exception($"Synchronized operation exception: {ex.Message}", ex)); + } + else + { + try + { + m_exceptionAction(ex); + } + catch (Exception handlerEx) + { + LibraryEvents.OnSuppressedException(this, new Exception($"Synchronized operation exception handler exception: {handlerEx.Message}", new AggregateException(handlerEx, ex))); + } + } + } + + #endregion +} diff --git a/src/Gemstone/Threading/SynchronizedOperations/TaskSynchronizedOperation.cs b/src/Gemstone/Threading/SynchronizedOperations/TaskSynchronizedOperation.cs new file mode 100644 index 0000000000..3df92dda8d --- /dev/null +++ b/src/Gemstone/Threading/SynchronizedOperations/TaskSynchronizedOperation.cs @@ -0,0 +1,203 @@ +//****************************************************************************************************** +// TaskSynchronizedOperation.cs - Gbtc +// +// Copyright © 2021, 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://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: +// ---------------------------------------------------------------------------------------------------- +// 02/26/2021 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Gemstone.Threading.SynchronizedOperations; + +/// +/// Represents a task-based synchronized operation +/// that cannot run while it is already in progress. +/// +/// +/// +/// The action performed by the is executed using +/// . Pending actions run when the task returned by the +/// asynchronous action is completed. This synchronized operation only supports the async +/// methods on the interface because the async action +/// cannot be executed synchronously. +/// +/// +/// +/// The following example shows how to use to +/// implement a notifier that receives notification requests ad-hoc but sends notifications +/// no more than once every 15 seconds. +/// +/// +/// +/// public ExampleClass() => +/// SynchronizedOperation = new TaskSynchronizedOperation(NotifyAsync); +/// +/// public void SendNotification() => +/// SynchronizedOperation.RunOnceAsync(); +/// +/// private async Task NotifyAsync() +/// { +/// Notify(); +/// await Task.Delay(15000); +/// } +/// +/// +public class TaskSynchronizedOperation : ISynchronizedOperation +{ + #region [ Members ] + + // Nested Types + private class SynchronizedOperation : SynchronizedOperationBase + { + private Func AsyncAction { get; } + + private Action? ExceptionHandler { get; } + + public SynchronizedOperation(Func asyncAction, Action? exceptionHandler) + : base(() => { }) + { + AsyncAction = asyncAction ?? throw new ArgumentNullException(nameof(asyncAction)); + ExceptionHandler = exceptionHandler; + } + + protected override void ExecuteActionAsync() + { + _ = Task.Run(async () => + { + try { await AsyncAction(CancellationToken); } + catch (Exception ex) { TryHandleException(ex); } + + if (ExecuteAction()) + ExecuteActionAsync(); + }); + } + + private void TryHandleException(Exception ex) + { + void SuppressException(Exception suppressedException) + { + string message = $"Synchronized operation exception handler exception: {suppressedException.Message}"; + AggregateException aggregateException = new(message, suppressedException, ex); + LibraryEvents.OnSuppressedException(this, aggregateException); + } + + try { ExceptionHandler?.Invoke(ex); } + catch (Exception handlerException) { SuppressException(handlerException); } + } + } + + #endregion + + #region [ Constructors ] + + /// + /// Creates a new instance of the class. + /// + /// The action to be performed during this operation. + public TaskSynchronizedOperation(Func asyncAction) + : this(asyncAction, null) + { + } + + /// + /// Creates a new instance of the class. + /// + /// The action to be performed during this operation. + public TaskSynchronizedOperation(Func asyncAction) + : this(asyncAction, null) + { + } + + /// + /// Creates a new instance of the class. + /// + /// The action to be performed during this operation. + /// The action to be performed if an exception is thrown from the action. + public TaskSynchronizedOperation(Func asyncAction, Action? exceptionAction) + : this(_ => asyncAction(), exceptionAction) + { + } + + /// + /// Creates a new instance of the class. + /// + /// The action to be performed during this operation. + /// The action to be performed if an exception is thrown from the action. + public TaskSynchronizedOperation(Func asyncAction, Action? exceptionAction) => + InternalSynchronizedOperation = new SynchronizedOperation(asyncAction, exceptionAction); + + #endregion + + #region [ Properties ] + + /// + /// Gets a value to indicate whether the synchronized + /// operation is currently executing its action. + /// + public bool IsRunning => InternalSynchronizedOperation.IsRunning; + + /// + /// Gets a value to indicate whether the synchronized operation + /// has an additional operation that is pending execution after + /// the currently running action has completed. + /// + public bool IsPending => InternalSynchronizedOperation.IsPending; + + /// + /// Gets or sets to use for canceling actions. + /// + public CancellationToken CancellationToken + { + get => InternalSynchronizedOperation.CancellationToken; + set => InternalSynchronizedOperation.CancellationToken = value; + } + + private SynchronizedOperation InternalSynchronizedOperation { get; } + + #endregion + + #region [ Methods ] + + /// + /// Executes the action on another thread or marks the + /// operation as pending if the operation is already running. + /// + /// + /// When the operation is marked as pending, it will run again after the + /// operation that is currently running has completed. This is useful if + /// an update has invalidated the operation that is currently running and + /// will therefore need to be run again. + /// + public void RunAsync() => + InternalSynchronizedOperation.RunAsync(); + + /// + /// Attempts to execute the action on another thread. + /// Does nothing if the operation is already running. + /// + public void TryRunAsync() => + InternalSynchronizedOperation.TryRunAsync(); + + void ISynchronizedOperation.Run(bool runPendingSynchronously) => RunAsync(); + void ISynchronizedOperation.TryRun(bool runPendingSynchronously) => TryRunAsync(); + + #endregion +} diff --git a/src/Gemstone/Ticks.cs b/src/Gemstone/Ticks.cs index e4b07b1f97..dba608a019 100644 --- a/src/Gemstone/Ticks.cs +++ b/src/Gemstone/Ticks.cs @@ -652,10 +652,10 @@ public override bool Equals(object? obj) { return obj switch { - long lng => (Value == lng), - Ticks ticks => (Value == ticks.Value), - DateTime dateTime => (Value == dateTime.Ticks), - TimeSpan timeSpan => (Value == timeSpan.Ticks), + long lng => Value == lng, + Ticks ticks => Value == ticks.Value, + DateTime dateTime => Value == dateTime.Ticks, + TimeSpan timeSpan => Value == timeSpan.Ticks, _ => false }; } @@ -1408,4 +1408,4 @@ public static Ticks ToSecondDistribution(Ticks timestamp, double samplesPerSecon } #endregion -} \ No newline at end of file +} diff --git a/src/Gemstone/TypeExtensions/TypeExtensions.cs b/src/Gemstone/TypeExtensions/TypeExtensions.cs index 3803757d60..bb2eeece6b 100644 --- a/src/Gemstone/TypeExtensions/TypeExtensions.cs +++ b/src/Gemstone/TypeExtensions/TypeExtensions.cs @@ -46,6 +46,8 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; using Gemstone.IO; using Gemstone.Reflection.AssemblyExtensions; @@ -57,7 +59,22 @@ namespace Gemstone.TypeExtensions; public static class TypeExtensions { // Native data types that represent numbers - private static readonly Type[] s_numericTypes = { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) }; + private static readonly Type[] s_numericTypes = + { + typeof(sbyte), + typeof(byte), + typeof(short), + typeof(ushort), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong), + typeof(float), + typeof(double), + typeof(decimal) + }; + + private static readonly Regex s_validCSharpIdentifierRegex = new(@"[\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}\p{Nl}\p{Nd}\p{Pc}\p{Mn}\p{Mc}\p{Cf}]", RegexOptions.Compiled); /// /// Determines if the specified type is a native structure that represents a numeric value. @@ -119,6 +136,91 @@ public static Type GetRootType(this Type type) } } + /// + /// Gets a C#-compatible proper type name, resolving generic type names using reflection with no backticks (`), for the specified . + /// + /// The whose name is to be resolved. + /// Flag that indicates if namespaces should be included in the type name. + /// + /// A C#-compatible proper type name, resolving generic type names using reflection with no backticks (`), for the specified . + /// + /// + /// This method will return a C#-compatible proper type name, resolving generic type names using reflection, which creates a valid, + /// usable type name versus what returns. For example, will return something like: + /// System.Collections.Generic.List`1[[System.String, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e] + /// with a backtick (`) to indicate a generic type and noisy assembly info. Even Type.Name returns List`1 with a backtick (`). For the + /// same type, this method would instead return: System.Collections.Generic.List<System.String> which is a valid, usable C# type name. + /// You can also set the parameter to false to remove namespaces from the type name, which yields: + /// List<String> for the same example. + /// + public static string GetReflectedTypeName(this Type type, bool includeNamespaces = true) + { + return type.GetReflectedTypeName(includeNamespaces, null, null); + } + + private static string GetReflectedTypeName(this Type type, bool includeNamespaces, Stack? genericArgs, StringBuilder? arrayBrackets) + { + StringBuilder code = new(); + Type? declaringType = type.DeclaringType; + bool arrayBracketsWasNull = arrayBrackets == null; + + genericArgs ??= new Stack(type.GetGenericArguments()); + + int currentTypeGenericArgsCount = genericArgs.Count; + + if (declaringType != null) + currentTypeGenericArgsCount -= declaringType.GetGenericArguments().Length; + + Type[] currentTypeGenericArgs = new Type[currentTypeGenericArgsCount]; + + for (int i = currentTypeGenericArgsCount - 1; i >= 0; i--) + currentTypeGenericArgs[i] = genericArgs.Pop(); + + if (declaringType != null) + code.Append(declaringType.GetReflectedTypeName(includeNamespaces, genericArgs, null)).Append('.'); + + if (type.IsArray) + { + arrayBrackets ??= new StringBuilder(); + arrayBrackets.Append('['); + arrayBrackets.Append(',', type.GetArrayRank() - 1); + arrayBrackets.Append(']'); + + Type elementType = type.GetElementType()!; + code.Insert(0, elementType.GetReflectedTypeName(includeNamespaces, null, arrayBrackets)); + } + else + { + static bool isValidCSharpIdentifier(char identifier) => + s_validCSharpIdentifierRegex.IsMatch(identifier.ToString()); + + code.Append(new string(type.Name.TakeWhile(isValidCSharpIdentifier).ToArray())); + + if (currentTypeGenericArgsCount > 0) + { + code.Append('<'); + + for (int i = 0; i < currentTypeGenericArgsCount; i++) + { + code.Append(currentTypeGenericArgs[i].GetReflectedTypeName(includeNamespaces, null, null)); + + if (i < currentTypeGenericArgsCount - 1) + code.Append(','); + } + + code.Append('>'); + } + + if (declaringType == null && !string.IsNullOrEmpty(type.Namespace) && includeNamespaces) + code.Insert(0, '.').Insert(0, type.Namespace); + } + + if (arrayBracketsWasNull && arrayBrackets != null) + code.Append(arrayBrackets); + + return code.ToString(); + } + /// /// Loads public types from assemblies in the application binaries directory that implement the specified /// either through class inheritance or interface implementation. @@ -206,4 +308,4 @@ public static List LoadImplementations(this Type type, string binariesDire return types; // Return the matching types found. } -} \ No newline at end of file +}