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;
}
}