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.

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