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.

300 lines
10 KiB

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