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.

212 lines
7.4 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 ConcurrentDictionary<FileSystemWatcher, ConcurrentBag<Config>> watcherTrackConfigs
  29. = new ConcurrentDictionary<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.GetOrAdd(watcher, w => new ConcurrentBag<Config>());
  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. #if NET3
  103. TaskEx
  104. #else
  105. Task
  106. #endif
  107. .WhenAll(configs.Select(TriggerFileLoad));
  108. /// <summary>
  109. /// this is synchronous, unlike <see cref="TriggerFileLoad(Config)"/>
  110. /// </summary>
  111. /// <param name="config"></param>
  112. public static void Save(Config config)
  113. {
  114. var store = config.Store;
  115. try
  116. {
  117. using var readLock = Synchronization.LockRead(store.WriteSyncObject);
  118. lock (config.Provider)
  119. {
  120. config.Provider.File = config.File;
  121. store.WriteTo(config.Provider);
  122. }
  123. }
  124. catch (Exception e)
  125. {
  126. Logger.config.Error($"{nameof(IConfigStore)} for {config.File} errored while writing to disk");
  127. Logger.config.Error(e);
  128. }
  129. }
  130. /// <summary>
  131. /// this is synchronous, unlike <see cref="TriggerLoadAll"/>
  132. /// </summary>
  133. public static void SaveAll()
  134. {
  135. foreach (var config in configs)
  136. Save(config);
  137. }
  138. private static void LoadTask(Config config)
  139. { // these tasks will always be running in the same thread as each other
  140. try
  141. {
  142. var store = config.Store;
  143. using var writeLock = Synchronization.LockWrite(store.WriteSyncObject);
  144. lock (config.Provider)
  145. {
  146. config.Provider.File = config.File;
  147. store.ReadFrom(config.Provider);
  148. }
  149. }
  150. catch (Exception e)
  151. {
  152. Logger.config.Error($"{nameof(IConfigStore)} for {config.File} errored while reading from the {nameof(IConfigProvider)}");
  153. Logger.config.Error(e);
  154. }
  155. }
  156. private static void SaveThread()
  157. {
  158. while (true)
  159. {
  160. var configArr = configs.Where(c => c.Store != null).ToArray();
  161. int index = -1;
  162. try
  163. {
  164. var waitHandles = configArr.Select(c => c.Store.SyncObject)
  165. .Prepend(configsChangedWatcher)
  166. .ToArray();
  167. index = WaitHandle.WaitAny(waitHandles);
  168. }
  169. catch (Exception e)
  170. {
  171. Logger.config.Error($"Error waiting for in-memory updates");
  172. Logger.config.Error(e);
  173. Thread.Sleep(TimeSpan.FromSeconds(1));
  174. }
  175. if (index <= 0)
  176. { // we got a signal that the configs collection changed, loop around, or errored
  177. continue;
  178. }
  179. // otherwise, we have a thing that changed in a store
  180. Save(configArr[index - 1]);
  181. }
  182. }
  183. }
  184. }