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.

257 lines
9.0 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. #if NET4
  14. using Task = System.Threading.Tasks.Task;
  15. using TaskEx = System.Threading.Tasks.Task;
  16. #endif
  17. namespace IPA.Config
  18. {
  19. internal static class ConfigRuntime
  20. {
  21. private class DirInfoEqComparer : IEqualityComparer<DirectoryInfo>
  22. {
  23. public bool Equals(DirectoryInfo x, DirectoryInfo y)
  24. => x?.FullName == y?.FullName;
  25. public int GetHashCode(DirectoryInfo obj)
  26. => obj?.GetHashCode() ?? 0;
  27. }
  28. private static readonly ConcurrentBag<Config> configs = new ConcurrentBag<Config>();
  29. private static readonly AutoResetEvent configsChangedWatcher = new AutoResetEvent(false);
  30. private static readonly ConcurrentDictionary<DirectoryInfo, FileSystemWatcher> watchers
  31. = new ConcurrentDictionary<DirectoryInfo, FileSystemWatcher>(new DirInfoEqComparer());
  32. private static readonly ConcurrentDictionary<FileSystemWatcher, ConcurrentBag<Config>> watcherTrackConfigs
  33. = new ConcurrentDictionary<FileSystemWatcher, ConcurrentBag<Config>>();
  34. private static SingleThreadTaskScheduler loadScheduler = null;
  35. private static TaskFactory loadFactory = null;
  36. private static Thread saveThread = null;
  37. private static void TryStartRuntime()
  38. {
  39. if (loadScheduler == null || !loadScheduler.IsRunning)
  40. {
  41. loadFactory = null;
  42. loadScheduler = new SingleThreadTaskScheduler();
  43. loadScheduler.Start();
  44. }
  45. if (loadFactory == null)
  46. loadFactory = new TaskFactory(loadScheduler);
  47. if (saveThread == null || !saveThread.IsAlive)
  48. {
  49. saveThread = new Thread(SaveThread);
  50. saveThread.Start();
  51. }
  52. AppDomain.CurrentDomain.ProcessExit -= ShutdownRuntime;
  53. AppDomain.CurrentDomain.ProcessExit += ShutdownRuntime;
  54. }
  55. private static void ShutdownRuntime(object sender, EventArgs e)
  56. {
  57. watcherTrackConfigs.Clear();
  58. var watchList = watchers.ToArray();
  59. watchers.Clear();
  60. foreach (var pair in watchList)
  61. pair.Value.EnableRaisingEvents = false;
  62. loadScheduler.Join(); // we can wait for the loads to finish
  63. saveThread.Abort(); // eww, but i don't like any of the other potential solutions
  64. SaveAll();
  65. }
  66. public static void RegisterConfig(Config cfg)
  67. {
  68. lock (configs)
  69. { // we only lock this segment, so that this only waits on other calls to this
  70. if (configs.ToArray().Contains(cfg))
  71. throw new InvalidOperationException("Config already registered to runtime!");
  72. configs.Add(cfg);
  73. }
  74. configsChangedWatcher.Set();
  75. TryStartRuntime();
  76. AddConfigToWatchers(cfg);
  77. }
  78. public static void ConfigChanged()
  79. {
  80. configsChangedWatcher.Set();
  81. }
  82. private static void AddConfigToWatchers(Config config)
  83. {
  84. var dir = config.File.Directory;
  85. if (!watchers.TryGetValue(dir, out var watcher))
  86. { // create the watcher
  87. watcher = new FileSystemWatcher(dir.FullName, "");
  88. var newWatcher = watchers.GetOrAdd(dir, watcher);
  89. if (watcher != newWatcher)
  90. { // if someone else beat us to adding, delete ours and switch to that new one
  91. watcher.Dispose();
  92. watcher = newWatcher;
  93. }
  94. watcher.NotifyFilter =
  95. NotifyFilters.FileName
  96. | NotifyFilters.LastWrite;
  97. watcher.Changed += FileChangedEvent;
  98. watcher.Created += FileChangedEvent;
  99. watcher.Renamed += FileChangedEvent;
  100. watcher.Deleted += FileChangedEvent;
  101. }
  102. TryStartRuntime();
  103. watcher.EnableRaisingEvents = false; // disable while we do shit
  104. var bag = watcherTrackConfigs.GetOrAdd(watcher, w => new ConcurrentBag<Config>());
  105. // we don't need to check containment because this function will only be called once per config ever
  106. bag.Add(config);
  107. watcher.EnableRaisingEvents = true;
  108. }
  109. private static void EnsureWritesSane(Config config)
  110. {
  111. // compare exchange loop to be sane
  112. var writes = config.Writes;
  113. while (writes < 0)
  114. writes = Interlocked.CompareExchange(ref config.Writes, 0, writes);
  115. }
  116. private static void FileChangedEvent(object sender, FileSystemEventArgs e)
  117. {
  118. var watcher = sender as FileSystemWatcher;
  119. if (!watcherTrackConfigs.TryGetValue(watcher, out var bag)) return;
  120. var config = bag.FirstOrDefault(c => c.File.FullName == e.FullPath);
  121. if (config != null && Interlocked.Decrement(ref config.Writes) + 1 > 0)
  122. {
  123. EnsureWritesSane(config);
  124. TriggerFileLoad(config);
  125. }
  126. }
  127. public static Task TriggerFileLoad(Config config) => loadFactory.StartNew(() => LoadTask(config));
  128. public static Task TriggerLoadAll() =>
  129. TaskEx.WhenAll(configs.Select(TriggerFileLoad));
  130. /// <summary>
  131. /// this is synchronous, unlike <see cref="TriggerFileLoad(Config)"/>
  132. /// </summary>
  133. /// <param name="config"></param>
  134. public static void Save(Config config)
  135. {
  136. var store = config.Store;
  137. try
  138. {
  139. using var readLock = Synchronization.LockRead(store.WriteSyncObject);
  140. lock (config.Provider)
  141. {
  142. config.Provider.File = config.File;
  143. EnsureWritesSane(config);
  144. Interlocked.Increment(ref config.Writes);
  145. store.WriteTo(config.Provider);
  146. }
  147. }
  148. catch (ThreadAbortException)
  149. {
  150. throw;
  151. }
  152. catch (Exception e)
  153. {
  154. Logger.config.Error($"{nameof(IConfigStore)} for {config.File} errored while writing to disk");
  155. Logger.config.Error(e);
  156. }
  157. }
  158. /// <summary>
  159. /// this is synchronous, unlike <see cref="TriggerLoadAll"/>
  160. /// </summary>
  161. public static void SaveAll()
  162. {
  163. foreach (var config in configs)
  164. Save(config);
  165. }
  166. private static void LoadTask(Config config)
  167. { // these tasks will always be running in the same thread as each other
  168. try
  169. {
  170. var store = config.Store;
  171. using var writeLock = Synchronization.LockWrite(store.WriteSyncObject);
  172. lock (config.Provider)
  173. {
  174. config.Provider.File = config.File;
  175. store.ReadFrom(config.Provider);
  176. }
  177. }
  178. catch (Exception e)
  179. {
  180. Logger.config.Error($"{nameof(IConfigStore)} for {config.File} errored while reading from the {nameof(IConfigProvider)}");
  181. Logger.config.Error(e);
  182. }
  183. }
  184. private static void SaveThread()
  185. {
  186. try
  187. {
  188. while (true)
  189. {
  190. var configArr = configs.Where(c => c.Store != null).ToArray();
  191. int index = -1;
  192. try
  193. {
  194. var waitHandles = configArr.Select(c => c.Store.SyncObject)
  195. .Prepend(configsChangedWatcher)
  196. .ToArray();
  197. index = WaitHandle.WaitAny(waitHandles);
  198. }
  199. catch (ThreadAbortException)
  200. {
  201. break;
  202. }
  203. catch (Exception e)
  204. {
  205. Logger.config.Error($"Error waiting for in-memory updates");
  206. Logger.config.Error(e);
  207. Thread.Sleep(TimeSpan.FromSeconds(1));
  208. }
  209. if (index <= 0)
  210. { // we got a signal that the configs collection changed, loop around, or errored
  211. continue;
  212. }
  213. // otherwise, we have a thing that changed in a store
  214. Save(configArr[index - 1]);
  215. }
  216. }
  217. catch (ThreadAbortException)
  218. {
  219. // we got aborted :(
  220. }
  221. }
  222. }
  223. }