279 lines
7.7 KiB
C#
279 lines
7.7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
using YamlDotNet.Serialization;
|
|
using YamlDotNet.Serialization.NamingConventions;
|
|
|
|
|
|
namespace ServerCore;
|
|
|
|
|
|
|
|
// HANDOVER: Config 정보를 Json & Yaml 기반으로 관리해 주는 클래스 이다.
|
|
|
|
public sealed class ConfigManager
|
|
{
|
|
private readonly Dictionary<uint, (string path, string type)> m_key_to_source = new();
|
|
private readonly Dictionary<string, JObject> m_path_to_jobject = new();
|
|
private readonly Dictionary<string, FileSystemWatcher> m_watchers = new();
|
|
|
|
private bool m_is_detect_duplicates = true;
|
|
|
|
public ConfigManager() { }
|
|
|
|
public void dnableDuplicateDetection(bool enable)
|
|
{
|
|
m_is_detect_duplicates = enable;
|
|
}
|
|
|
|
public ConfigManager addJson(uint key, string path)
|
|
{
|
|
loadJson(path);
|
|
watchFile(path, "json");
|
|
|
|
m_key_to_source[key] = (path, "json");
|
|
return this;
|
|
}
|
|
|
|
public ConfigManager addYaml(uint key, string path)
|
|
{
|
|
loadYaml(path);
|
|
watchFile(path, "yaml");
|
|
|
|
m_key_to_source[key] = (path, "yaml");
|
|
return this;
|
|
}
|
|
|
|
private void loadJson(string path)
|
|
{
|
|
if (!File.Exists(path))
|
|
throw new FileNotFoundException($"Not found Json File !!! : path:{path}");
|
|
|
|
var json_text = File.ReadAllText(path);
|
|
var jObject = JObject.Parse(json_text);
|
|
m_path_to_jobject[path] = jObject;
|
|
}
|
|
|
|
private void loadYaml(string path)
|
|
{
|
|
if (!File.Exists(path))
|
|
throw new FileNotFoundException($"Not found Yaml File !!! : path:{path}");
|
|
|
|
var yamlText = File.ReadAllText(path);
|
|
var deserializer = new DeserializerBuilder()
|
|
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
|
.Build();
|
|
|
|
var yamlObject = deserializer.Deserialize<object>(yamlText);
|
|
var serializer = new SerializerBuilder()
|
|
.JsonCompatible()
|
|
.Build();
|
|
|
|
var json = serializer.Serialize(yamlObject);
|
|
var jObject = JObject.Parse(json);
|
|
m_path_to_jobject[path] = jObject;
|
|
}
|
|
|
|
private void watchFile(string path, string type)
|
|
{
|
|
if (m_watchers.ContainsKey(path))
|
|
return;
|
|
|
|
var directory_name = Path.GetDirectoryName(path);
|
|
NullReferenceCheckHelper.throwIfNull(directory_name, () => $"directory_name is null !!! : path:{path}, format:{type}");
|
|
|
|
var watcher = new FileSystemWatcher(directory_name!)
|
|
{
|
|
Filter = Path.GetFileName(path),
|
|
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.Size
|
|
};
|
|
|
|
watcher.Changed += (sender, e) => reloadAll();
|
|
watcher.Created += (sender, e) => reloadAll();
|
|
watcher.Renamed += (sender, e) => reloadAll();
|
|
watcher.EnableRaisingEvents = true;
|
|
|
|
m_watchers[path] = watcher;
|
|
}
|
|
private void reloadAll()
|
|
{
|
|
Log.getLogger().error("Detected changes in config file !!!, Reloading...");
|
|
|
|
m_path_to_jobject.Clear();
|
|
|
|
foreach (var key in m_key_to_source.Keys)
|
|
{
|
|
if (false == reloadConfig(key))
|
|
{
|
|
Log.getLogger().error($"Failed to reload config for key={key} during full reload");
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool reloadConfig(uint key)
|
|
{
|
|
if (!m_key_to_source.TryGetValue(key, out var entry))
|
|
return false;
|
|
|
|
try
|
|
{
|
|
if (entry.type == "json")
|
|
loadJson(entry.path);
|
|
else if (entry.type == "yaml")
|
|
loadYaml(entry.path);
|
|
|
|
Log.getLogger().info($"Reloaded config for key={key}, path={entry.path}");
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.getLogger().error($"Failed to reload config: key={key}, path={entry.path}, error={ex.Message}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public T bind<T>(string sectionName)
|
|
{
|
|
if (m_path_to_jobject.Count == 0)
|
|
throw new InvalidOperationException("No config sources have been loaded !!!");
|
|
|
|
var merged = new JObject();
|
|
|
|
foreach (var source in m_path_to_jobject.Values)
|
|
{
|
|
if (m_is_detect_duplicates)
|
|
detectDuplicateKeys(merged, source);
|
|
|
|
merged.Merge(source, new JsonMergeSettings
|
|
{
|
|
MergeArrayHandling = MergeArrayHandling.Replace,
|
|
MergeNullValueHandling = MergeNullValueHandling.Merge
|
|
});
|
|
}
|
|
|
|
return extractSection<T>(merged, sectionName, "merged");
|
|
}
|
|
|
|
public T bindFromPath<T>(string path, string sectionName)
|
|
{
|
|
if (!m_path_to_jobject.TryGetValue(path, out var jObject))
|
|
throw new ArgumentException($"Config file not loaded or not found: path={path}");
|
|
|
|
return extractSection<T>(jObject, sectionName, $"path={path}");
|
|
}
|
|
|
|
public T bindFromKey<T>(uint key, string sectionName)
|
|
{
|
|
if (!m_key_to_source.TryGetValue(key, out var entry))
|
|
throw new ArgumentException($"Key not found: key={key}");
|
|
|
|
if (!m_path_to_jobject.TryGetValue(entry.path, out var jObject))
|
|
throw new Exception($"JObject not found for path associated with key={key}");
|
|
|
|
return extractSection<T>(jObject, sectionName, $"key={key}, path={entry.path}");
|
|
}
|
|
|
|
public bool tryBindFromKey<T>(uint key, string sectionName, out T? result)
|
|
{
|
|
result = default;
|
|
|
|
try
|
|
{
|
|
result = bindFromKey<T>(key, sectionName);
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public bool tryBindFromPath<T>(string path, string sectionName, out T? result)
|
|
{
|
|
result = default;
|
|
|
|
try
|
|
{
|
|
result = bindFromPath<T>(path, sectionName);
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public JObject? getRawJsonByKey(uint key)
|
|
{
|
|
if (!m_key_to_source.TryGetValue(key, out var entry))
|
|
return null;
|
|
|
|
return m_path_to_jobject.TryGetValue(entry.path, out var jObject)
|
|
? jObject
|
|
: null;
|
|
}
|
|
|
|
public JObject? getRawJsonByPath(string path)
|
|
{
|
|
return m_path_to_jobject.TryGetValue(path, out var jObject)
|
|
? jObject
|
|
: null;
|
|
}
|
|
|
|
public bool removeConfig(uint key)
|
|
{
|
|
if (!m_key_to_source.TryGetValue(key, out var entry))
|
|
return false;
|
|
|
|
m_key_to_source.Remove(key);
|
|
m_path_to_jobject.Remove(entry.path);
|
|
|
|
if (m_watchers.TryGetValue(entry.path, out var watcher))
|
|
{
|
|
watcher.EnableRaisingEvents = false;
|
|
watcher.Dispose();
|
|
m_watchers.Remove(entry.path);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public IEnumerable<uint> getAllKeys()
|
|
{
|
|
return m_key_to_source.Keys;
|
|
}
|
|
|
|
public IEnumerable<string> getAllPaths()
|
|
{
|
|
return m_path_to_jobject.Keys;
|
|
}
|
|
|
|
private T extractSection<T>(JObject jObject, string sectionName, string context)
|
|
{
|
|
if (!jObject.TryGetValue(sectionName, out var token))
|
|
{
|
|
token = jObject; // fallback
|
|
}
|
|
|
|
if (token == null)
|
|
throw new Exception($"Section not found: sectionName={sectionName}, context={context}");
|
|
|
|
return token.ToObject<T>()!;
|
|
}
|
|
|
|
private void detectDuplicateKeys(JObject existing, JObject incoming)
|
|
{
|
|
foreach (var prop in incoming.Properties())
|
|
{
|
|
if (existing.ContainsKey(prop.Name))
|
|
{
|
|
Log.getLogger().error($"Duplicate key detected !!!, overwritten by later value : propertyName:{prop.Name}");
|
|
}
|
|
}
|
|
}
|
|
}
|