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.

305 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?.FullName.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. internal static FileSystemWatcher[] GetWatchers()
  125. {
  126. return watcherTrackConfigs.Keys.ToArray();
  127. }
  128. private static void EnsureWritesSane(Config config)
  129. {
  130. // compare exchange loop to be sane
  131. var writes = config.Writes;
  132. while (writes < 0)
  133. writes = Interlocked.CompareExchange(ref config.Writes, 0, writes);
  134. }
  135. private static void FileChangedEvent(object sender, FileSystemEventArgs e)
  136. {
  137. var watcher = sender as FileSystemWatcher;
  138. if (!watcherTrackConfigs.TryGetValue(watcher, out var bag)) return;
  139. var config = bag.FirstOrDefault(c => c.File.FullName == e.FullPath);
  140. if (config != null && Interlocked.Decrement(ref config.Writes) + 1 <= 0)
  141. {
  142. EnsureWritesSane(config);
  143. TriggerFileLoad(config);
  144. }
  145. }
  146. public static Task TriggerFileLoad(Config config)
  147. => loadFactory.StartNew(() => LoadTask(config));
  148. public static Task TriggerLoadAll()
  149. => TaskEx.WhenAll(configs.Select(TriggerFileLoad));
  150. /// <summary>
  151. /// this is synchronous, unlike <see cref="TriggerFileLoad(Config)"/>
  152. /// </summary>
  153. /// <param name="config"></param>
  154. public static void Save(Config config)
  155. {
  156. var store = config.Store;
  157. try
  158. {
  159. using var readLock = Synchronization.LockRead(store.WriteSyncObject);
  160. EnsureWritesSane(config);
  161. Interlocked.Increment(ref config.Writes);
  162. store.WriteTo(config.configProvider);
  163. }
  164. catch (ThreadAbortException)
  165. {
  166. throw;
  167. }
  168. catch (Exception e)
  169. {
  170. Logger.Config.Error($"{nameof(IConfigStore)} for {config.File} errored while writing to disk");
  171. Logger.Config.Error(e);
  172. }
  173. }
  174. /// <summary>
  175. /// this is synchronous, unlike <see cref="TriggerLoadAll"/>
  176. /// </summary>
  177. public static void SaveAll()
  178. {
  179. foreach (var config in configs)
  180. Save(config);
  181. }
  182. private static void LoadTask(Config config)
  183. { // these tasks will always be running in the same thread as each other
  184. try
  185. {
  186. var store = config.Store;
  187. using var writeLock = Synchronization.LockWrite(store.WriteSyncObject);
  188. store.ReadFrom(config.configProvider);
  189. }
  190. catch (Exception e)
  191. {
  192. Logger.Config.Error($"{nameof(IConfigStore)} for {config.File} errored while reading from the {nameof(IConfigProvider)}");
  193. Logger.Config.Error(e);
  194. }
  195. }
  196. private static void SaveThread()
  197. {
  198. if (requiresSave == null)
  199. {
  200. return;
  201. }
  202. try
  203. {
  204. foreach (var item in requiresSave.GetConsumingEnumerable())
  205. {
  206. try
  207. {
  208. Save(configs.First((c) => c.Store != null && ReferenceEquals(c.Store.WriteSyncObject, item.WriteSyncObject)));
  209. }
  210. catch (ThreadAbortException)
  211. {
  212. break;
  213. }
  214. catch (Exception e)
  215. {
  216. Logger.Config.Error($"Error waiting for in-memory updates");
  217. Logger.Config.Error(e);
  218. Thread.Sleep(TimeSpan.FromSeconds(1));
  219. }
  220. }
  221. }
  222. catch (ThreadAbortException)
  223. {
  224. // we got aborted :(
  225. }
  226. }
  227. private static void LegacySaveThread()
  228. {
  229. try
  230. {
  231. while (true)
  232. {
  233. var configArr = configs.Where(c => c.Store?.SyncObject != null).ToArray();
  234. int index = -1;
  235. try
  236. {
  237. var waitHandles = configArr.Select(c => c.Store.SyncObject)
  238. .Prepend(configsChangedWatcher)
  239. .ToArray();
  240. index = WaitHandle.WaitAny(waitHandles);
  241. }
  242. catch (ThreadAbortException)
  243. {
  244. break;
  245. }
  246. catch (Exception e)
  247. {
  248. Logger.Config.Error($"Error waiting for in-memory updates");
  249. Logger.Config.Error(e);
  250. Thread.Sleep(TimeSpan.FromSeconds(1));
  251. }
  252. if (index <= 0)
  253. { // we got a signal that the configs collection changed, loop around, or errored
  254. continue;
  255. }
  256. // otherwise, we have a thing that changed in a store
  257. Save(configArr[index - 1]);
  258. }
  259. }
  260. catch (ThreadAbortException)
  261. {
  262. // we got aborted :(
  263. }
  264. }
  265. }
  266. }