using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json; namespace Terminal.Gui.Configuration; /// /// Manages the Sources and provides the API for loading them. Source is a location where a configuration can be stored. Sources are defined in . /// public class SourcesManager { /// /// Provides a map from each of the to file system and resource paths that have been loaded by . /// public ConcurrentDictionary Sources { get; } = new (); /// INTERNAL: Loads into the specified . /// The Settings Scope object that will be loaded into. /// Json document to update the settings with. /// The source (filename/resource name) the Json document was read from. /// The Config Location corresponding to /// if the settingsScope was updated. [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] internal bool Load (SettingsScope? settingsScope, Stream stream, string source, ConfigLocations location) { if (settingsScope is null) { return false; } // Update the existing settings with the new settings. try { #if DEBUG string? json = new StreamReader (stream).ReadToEnd (); stream.Position = 0; Debug.Assert (json != null, "json != null"); #endif SettingsScope? scope = JsonSerializer.Deserialize (stream, typeof (SettingsScope), ConfigurationManager.SerializerContext.Options) as SettingsScope; settingsScope.UpdateFrom (scope!); ConfigurationManager.OnUpdated (); AddSource (location, source); Logging.Trace ($"Read configuration from \"{source}\" - ConfigLocation: {location}"); return true; } catch (JsonException e) { if (ConfigurationManager.ThrowOnJsonErrors ?? false) { throw; } ConfigurationManager.AddJsonError ($"Error reading {source}: {e.Message}"); } return false; } internal void AddSource (ConfigLocations location, string source) { // ConcurrentDictionary's AddOrUpdate is thread-safe Sources.AddOrUpdate (location, source, (key, oldValue) => source); } /// INTERNAL: Loads the `config.json` file a into the specified . /// The Settings Scope object that will be loaded into. /// Json document to update the settings with. /// The Config Location corresponding to /// if the settingsScope was updated. [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] internal bool Load (SettingsScope? settingsScope, string filePath, ConfigLocations location) { string realPath = filePath.Replace ("~", Environment.GetFolderPath (Environment.SpecialFolder.UserProfile)); if (!File.Exists (realPath)) { //Logging.Warning ($"\"{realPath}\" does not exist."); // Always add the source even if it doesn't exist. AddSource (location, filePath); return true; } int retryCount = 0; // Sometimes when the config file is written by an external agent, the change notification comes // before the file is closed. This works around that. while (retryCount < 2) { try { FileStream? stream = File.OpenRead (realPath); bool ret = Load (settingsScope, stream, filePath, location); stream.Close (); stream.Dispose (); return ret; } catch (IOException ioe) { Logging.Warning ($"{ioe.Message}. Retrying..."); Task.Delay (100); retryCount++; } } return false; } /// INTERNAL: Loads the Json document in into the specified . /// The Settings Scope object that will be loaded into. /// Json document to update the settings with. /// The source (filename/resource name) the Json document was read from. /// The Config Location corresponding to /// if the settingsScope was updated. [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] internal bool Load (SettingsScope? settingsScope, string? json, string source, ConfigLocations location) { Debug.Assert (location != ConfigLocations.All); if (string.IsNullOrEmpty (json)) { return false; } var stream = new MemoryStream (); var writer = new StreamWriter (stream); writer.Write (json); writer.Flush (); stream.Position = 0; return Load (settingsScope, stream, source, location); } /// INTERNAL: Loads the Json document from the resource named from into the specified . /// The Settings Scope object that will be loaded into. /// The assembly containing the resource. /// The name of the resource containing the Json document was read from. /// The Config Location corresponding to /// if the settingsScope was updated. [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] internal bool Load (SettingsScope? settingsScope, Assembly assembly, string resourceName, ConfigLocations location) { if (string.IsNullOrEmpty (resourceName)) { Logging.Warning ($"{resourceName} must not be null or empty."); return false; } using Stream? stream = assembly.GetManifestResourceStream (resourceName); if (stream is null) { Logging.Warning ($"Resource \"{resourceName}\" does not exist in \"{assembly.GetName ().Name}\"."); return false; } return Load (settingsScope, stream, $"resource://[{assembly.GetName ().Name}]/{resourceName}", location); } /// /// INTERNAL: Returns a JSON document with the configuration specified. /// /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] internal string ToJson (SettingsScope? scope) { //Logging.Debug ("ConfigurationManager.ToJson()"); return JsonSerializer.Serialize (scope, typeof (SettingsScope), ConfigurationManager.SerializerContext); } /// /// INTERNAL: Returns a stream with the configuration specified. /// /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] internal Stream ToStream (SettingsScope? scope) { string json = JsonSerializer.Serialize (scope, typeof (SettingsScope), ConfigurationManager.SerializerContext); // turn it into a stream var stream = new MemoryStream (); var writer = new StreamWriter (stream); writer.Write (json); writer.Flush (); stream.Position = 0; return stream; } }