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.

252 lines
8.8 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 = watchers.GetOrAdd(dir, dir => new FileSystemWatcher(dir.FullName));
  92. watcher.NotifyFilter =
  93. NotifyFilters.FileName
  94. | NotifyFilters.LastWrite
  95. | NotifyFilters.Size
  96. | NotifyFilters.LastAccess
  97. | NotifyFilters.Attributes
  98. | NotifyFilters.CreationTime;
  99. watcher.Changed += FileChangedEvent;
  100. watcher.Created += FileChangedEvent;
  101. watcher.Renamed += FileChangedEvent;
  102. watcher.Deleted += FileChangedEvent;
  103. }
  104. TryStartRuntime();
  105. watcher.EnableRaisingEvents = false; // disable while we do shit
  106. var bag = watcherTrackConfigs.GetOrAdd(watcher, w => new ConcurrentBag<Config>());
  107. // we don't need to check containment because this function will only be called once per config ever
  108. bag.Add(config);
  109. watcher.EnableRaisingEvents = true;
  110. }
  111. private static void EnsureWritesSane(Config config)
  112. {
  113. // compare exchange loop to be sane
  114. var writes = config.Writes;
  115. while (writes < 0)
  116. writes = Interlocked.CompareExchange(ref config.Writes, 0, writes);
  117. }
  118. private static void FileChangedEvent(object sender, FileSystemEventArgs e)
  119. {
  120. var watcher = sender as FileSystemWatcher;
  121. if (!watcherTrackConfigs.TryGetValue(watcher, out var bag)) return;
  122. var config = bag.FirstOrDefault(c => c.File.FullName == e.FullPath);
  123. if (config != null && Interlocked.Decrement(ref config.Writes) + 1 <= 0)
  124. {
  125. EnsureWritesSane(config);
  126. TriggerFileLoad(config);
  127. }
  128. }
  129. public static Task TriggerFileLoad(Config config)
  130. => loadFactory.StartNew(() => LoadTask(config));
  131. public static Task TriggerLoadAll()
  132. => TaskEx.WhenAll(configs.Select(TriggerFileLoad));
  133. /// <summary>
  134. /// this is synchronous, unlike <see cref="TriggerFileLoad(Config)"/>
  135. /// </summary>
  136. /// <param name="config"></param>
  137. public static void Save(Config config)
  138. {
  139. var store = config.Store;
  140. try
  141. {
  142. using var readLock = Synchronization.LockRead(store.WriteSyncObject);
  143. EnsureWritesSane(config);
  144. Interlocked.Increment(ref config.Writes);
  145. store.WriteTo(config.configProvider);
  146. }
  147. catch (ThreadAbortException)
  148. {
  149. throw;
  150. }
  151. catch (Exception e)
  152. {
  153. Logger.config.Error($"{nameof(IConfigStore)} for {config.File} errored while writing to disk");
  154. Logger.config.Error(e);
  155. }
  156. }
  157. /// <summary>
  158. /// this is synchronous, unlike <see cref="TriggerLoadAll"/>
  159. /// </summary>
  160. public static void SaveAll()
  161. {
  162. foreach (var config in configs)
  163. Save(config);
  164. }
  165. private static void LoadTask(Config config)
  166. { // these tasks will always be running in the same thread as each other
  167. try
  168. {
  169. var store = config.Store;
  170. using var writeLock = Synchronization.LockWrite(store.WriteSyncObject);
  171. store.ReadFrom(config.configProvider);
  172. }
  173. catch (Exception e)
  174. {
  175. Logger.config.Error($"{nameof(IConfigStore)} for {config.File} errored while reading from the {nameof(IConfigProvider)}");
  176. Logger.config.Error(e);
  177. }
  178. }
  179. private static void SaveThread()
  180. {
  181. try
  182. {
  183. while (true)
  184. {
  185. var configArr = configs.Where(c => c.Store != null).ToArray();
  186. int index = -1;
  187. try
  188. {
  189. var waitHandles = configArr.Select(c => c.Store.SyncObject)
  190. .Prepend(configsChangedWatcher)
  191. .ToArray();
  192. index = WaitHandle.WaitAny(waitHandles);
  193. }
  194. catch (ThreadAbortException)
  195. {
  196. break;
  197. }
  198. catch (Exception e)
  199. {
  200. Logger.config.Error($"Error waiting for in-memory updates");
  201. Logger.config.Error(e);
  202. Thread.Sleep(TimeSpan.FromSeconds(1));
  203. }
  204. if (index <= 0)
  205. { // we got a signal that the configs collection changed, loop around, or errored
  206. continue;
  207. }
  208. // otherwise, we have a thing that changed in a store
  209. Save(configArr[index - 1]);
  210. }
  211. }
  212. catch (ThreadAbortException)
  213. {
  214. // we got aborted :(
  215. }
  216. }
  217. }
  218. }