You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

207 lines
7.3 KiB

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.Concurrent;
  4. using System.Linq;
  5. using System.Text;
  6. using System.Threading.Tasks;
  7. using System.Threading;
  8. using IPA.Utilities;
  9. using IPA.Utilities.Async;
  10. using System.IO;
  11. using System.Runtime.CompilerServices;
  12. using IPA.Logging;
  13. namespace IPA.Config
  14. {
  15. internal static class ConfigRuntime
  16. {
  17. private class DirInfoEqComparer : IEqualityComparer<DirectoryInfo>
  18. {
  19. public bool Equals(DirectoryInfo x, DirectoryInfo y)
  20. => x?.FullName == y?.FullName;
  21. public int GetHashCode(DirectoryInfo obj)
  22. => obj?.GetHashCode() ?? 0;
  23. }
  24. private static readonly ConcurrentBag<Config> configs = new ConcurrentBag<Config>();
  25. private static readonly AutoResetEvent configsChangedWatcher = new AutoResetEvent(false);
  26. private static readonly ConcurrentDictionary<DirectoryInfo, FileSystemWatcher> watchers
  27. = new ConcurrentDictionary<DirectoryInfo, FileSystemWatcher>(new DirInfoEqComparer());
  28. private static readonly ConditionalWeakTable<FileSystemWatcher, ConcurrentBag<Config>> watcherTrackConfigs
  29. = new ConditionalWeakTable<FileSystemWatcher, ConcurrentBag<Config>>();
  30. private static SingleThreadTaskScheduler loadScheduler = null;
  31. private static TaskFactory loadFactory = null;
  32. private static Thread saveThread = null;
  33. private static void TryStartRuntime()
  34. {
  35. if (loadScheduler == null || !loadScheduler.IsRunning)
  36. {
  37. loadFactory = null;
  38. loadScheduler = new SingleThreadTaskScheduler();
  39. loadScheduler.Start();
  40. }
  41. if (loadFactory == null)
  42. loadFactory = new TaskFactory(loadScheduler);
  43. if (saveThread == null || !saveThread.IsAlive)
  44. {
  45. saveThread = new Thread(SaveThread);
  46. saveThread.Start();
  47. }
  48. }
  49. public static void RegisterConfig(Config cfg)
  50. {
  51. lock (configs)
  52. { // we only lock this segment, so that this only waits on other calls to this
  53. if (configs.ToArray().Contains(cfg))
  54. throw new InvalidOperationException("Config already registered to runtime!");
  55. configs.Add(cfg);
  56. }
  57. configsChangedWatcher.Set();
  58. TryStartRuntime();
  59. AddConfigToWatchers(cfg);
  60. }
  61. public static void ConfigChanged()
  62. {
  63. configsChangedWatcher.Set();
  64. }
  65. private static void AddConfigToWatchers(Config config)
  66. {
  67. var dir = config.File.Directory;
  68. if (!watchers.TryGetValue(dir, out var watcher))
  69. { // create the watcher
  70. watcher = new FileSystemWatcher(dir.FullName, "");
  71. var newWatcher = watchers.GetOrAdd(dir, watcher);
  72. if (watcher != newWatcher)
  73. { // if someone else beat us to adding, delete ours and switch to that new one
  74. watcher.Dispose();
  75. watcher = newWatcher;
  76. }
  77. watcher.NotifyFilter =
  78. NotifyFilters.FileName
  79. | NotifyFilters.LastWrite;
  80. watcher.Changed += FileChangedEvent;
  81. watcher.Created += FileChangedEvent;
  82. watcher.Renamed += FileChangedEvent;
  83. watcher.Deleted += FileChangedEvent;
  84. }
  85. TryStartRuntime();
  86. watcher.EnableRaisingEvents = false; // disable while we do shit
  87. var bag = watcherTrackConfigs.GetOrCreateValue(watcher);
  88. // we don't need to check containment because this function will only be called once per config ever
  89. bag.Add(config);
  90. watcher.EnableRaisingEvents = true;
  91. }
  92. private static void FileChangedEvent(object sender, FileSystemEventArgs e)
  93. {
  94. var watcher = sender as FileSystemWatcher;
  95. if (!watcherTrackConfigs.TryGetValue(watcher, out var bag)) return;
  96. var config = bag.FirstOrDefault(c => c.File.FullName == e.FullPath);
  97. if (config != null)
  98. TriggerFileLoad(config);
  99. }
  100. public static Task TriggerFileLoad(Config config) => loadFactory.StartNew(() => LoadTask(config));
  101. public static Task TriggerLoadAll()
  102. => Task.WhenAll(configs.Select(TriggerFileLoad));
  103. /// <summary>
  104. /// this is synchronous, unlike <see cref="TriggerFileLoad(Config)"/>
  105. /// </summary>
  106. /// <param name="config"></param>
  107. public static void Save(Config config)
  108. {
  109. var store = config.Store;
  110. try
  111. {
  112. using var readLock = Synchronization.LockRead(store.WriteSyncObject);
  113. lock (config.Provider)
  114. {
  115. config.Provider.File = config.File;
  116. store.WriteTo(config.Provider);
  117. }
  118. }
  119. catch (Exception e)
  120. {
  121. Logger.config.Error($"{nameof(IConfigStore)} for {config.File} errored while writing to disk");
  122. Logger.config.Error(e);
  123. }
  124. }
  125. /// <summary>
  126. /// this is synchronous, unlike <see cref="TriggerLoadAll"/>
  127. /// </summary>
  128. public static void SaveAll()
  129. {
  130. foreach (var config in configs)
  131. Save(config);
  132. }
  133. private static void LoadTask(Config config)
  134. { // these tasks will always be running in the same thread as each other
  135. try
  136. {
  137. var store = config.Store;
  138. using var writeLock = Synchronization.LockWrite(store.WriteSyncObject);
  139. lock (config.Provider)
  140. {
  141. config.Provider.File = config.File;
  142. store.ReadFrom(config.Provider);
  143. }
  144. }
  145. catch (Exception e)
  146. {
  147. Logger.config.Error($"{nameof(IConfigStore)} for {config.File} errored while reading from the {nameof(IConfigProvider)}");
  148. Logger.config.Error(e);
  149. }
  150. }
  151. private static void SaveThread()
  152. {
  153. while (true)
  154. {
  155. var configArr = configs.Where(c => c.Store != null).ToArray();
  156. int index = -1;
  157. try
  158. {
  159. var waitHandles = configArr.Select(c => c.Store.SyncObject)
  160. .Prepend(configsChangedWatcher)
  161. .ToArray();
  162. index = WaitHandle.WaitAny(waitHandles);
  163. }
  164. catch (Exception e)
  165. {
  166. Logger.config.Error($"Error waiting for in-memory updates");
  167. Logger.config.Error(e);
  168. Thread.Sleep(TimeSpan.FromSeconds(1));
  169. }
  170. if (index <= 0)
  171. { // we got a signal that the configs collection changed, loop around, or errored
  172. continue;
  173. }
  174. // otherwise, we have a thing that changed in a store
  175. Save(configArr[index - 1]);
  176. }
  177. }
  178. }
  179. }