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.

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