Files
caliverse_server/ServerCore/Config/ConfigManager.cs
2025-11-28 16:54:56 +09:00

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