using System.Text.Json; using Google.Protobuf; namespace DynamicProtobuf.Runtime; public static class DynamicFieldRegistry { #region Local State private static FileSystemWatcher? _fsWatcher; private static ProtoJsonRegistry Registry { get; set; } = new(); private static HashSet<(string, string)> MissingFieldSet { get; set; } = []; private static Timer? _reloadTimer; private static readonly Lock _reloadLock = new(); #endregion Local State #region Actions public static Action OnMissingField { get; set; } = msg => Console.Error.WriteLine("[DynamicProtobuf] [WARN]: " + msg); public static Action OnError { get; set; } = msg => Console.Error.WriteLine("[DynamicProtobuf] [ERROR]: " + msg); public static event Action? OnHotReloaded; #endregion Actions #region Codegen Helper public static uint GetXorConst(string fullName, string fieldName) { return GetField(fullName, fieldName)?.XorConst ?? 0; } public static bool HasField(string fullName, string fieldName) { return GetField(fullName, fieldName) != null; } public static int GetFieldNumber(string fullName, string fieldName) { return GetField(fullName, fieldName)?.FieldNumber ?? 0; } public static uint GetTag(string fullName, string fieldName, bool packed = false) { MessageField? field = GetField(fullName, fieldName); if (field == null) return 0; return (uint)((field.FieldNumber << 3) | (int)field.WireType); } public static int GetTagSize(string fullName, string fieldName) { return CodedOutputStream.ComputeTagSize(GetFieldNumber(fullName, fieldName)); } private static MessageField? GetField(string fullName, string fieldName) { if (Registry.Messages.TryGetValue(fullName, out var fields) && fields.TryGetValue(fieldName, out var field)) { return field; } if (MissingFieldSet.Add((fullName, fieldName))) { OnMissingField?.Invoke($"no such field \"{fieldName}\" at message \"{fullName}\""); } return null; } #endregion Codegen Helper public static void ListenFromFile(string path) { LoadFromFile(path); var directory = Path.GetDirectoryName(path); if (string.IsNullOrEmpty(directory)) directory = "."; var fileName = Path.GetFileName(path); _fsWatcher = new FileSystemWatcher(directory, Path.GetFileName(path)) { NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size, EnableRaisingEvents = true }; _fsWatcher.Error += (sender, e) => { OnError?.Invoke(e.GetException().Message); }; _fsWatcher.Changed += (sender, e) => { if (e.ChangeType != WatcherChangeTypes.Changed) return; lock (_reloadLock) { _reloadTimer?.Dispose(); _reloadTimer = new Timer(_ => { try { while (IsFileLocked(path)) Thread.Sleep(50); LoadFromFile(path); } catch (Exception ex) { OnError?.Invoke(ex.Message); } }, null, 250, Timeout.Infinite); } }; } public static void LoadFromFile(string path) { if (!File.Exists(path)) { OnError?.Invoke($"proto json registry file not found: {path}"); return; } try { MissingFieldSet.Clear(); var json = File.ReadAllText(path); var reg = JsonSerializer.Deserialize(json); if (reg != null) { Registry = reg; OnHotReloaded?.Invoke(Registry); } } catch (Exception ex) { OnError?.Invoke($"failed to load proto json registry: {ex.Message}"); } } private static bool IsFileLocked(string path) { try { using FileStream file = File.Open(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None); return false; } catch (Exception) { return true; } } }