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.

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