From 5651cee2aaee783888137d39e996658fbf5b7293 Mon Sep 17 00:00:00 2001 From: Jimmy Cushnie Date: Sat, 28 Sep 2024 17:02:12 -0400 Subject: [PATCH] Move DataFile auto-reload to a separate class --- .../DataFiles/Abstractions/IDataFileOnDisk.cs | 17 ++-- JECS/DataFiles/DataFile.cs | 83 ++----------------- JECS/DataFiles/DataFileDiskAutoReloader.cs | 79 ++++++++++++++++++ JECS/DataFiles/ReadOnlyDataFile.cs | 80 ++---------------- 4 files changed, 101 insertions(+), 158 deletions(-) create mode 100644 JECS/DataFiles/DataFileDiskAutoReloader.cs diff --git a/JECS/DataFiles/Abstractions/IDataFileOnDisk.cs b/JECS/DataFiles/Abstractions/IDataFileOnDisk.cs index 236b3af..15b763d 100644 --- a/JECS/DataFiles/Abstractions/IDataFileOnDisk.cs +++ b/JECS/DataFiles/Abstractions/IDataFileOnDisk.cs @@ -1,8 +1,9 @@ using System; +using System.IO; namespace JECS.Abstractions { - internal interface IDataFileOnDisk : IDisposable + public interface IDataFileOnDisk { /// The absolute path of the file this object corresponds to. string FilePath { get; } @@ -13,10 +14,16 @@ internal interface IDataFileOnDisk : IDisposable /// Size of this file on disk in bytes. If there is unsaved data in memory it will not be counted. long SizeOnDisk { get; } - /// If true, the DataFile will automatically reload its data when the file changes on disk. If false, you can still call ReloadAllData() manually. - bool AutoReload { get; set; } + void ReloadAllData(); + + + object FileSystemReadWriteLock { get; } + DateTime LastKnownWriteTimeUTC { get; } + } - /// Invoked every time the file is auto-reloaded. This only happens when AutoReload is set to true. - event Action OnAutoReload; + internal static class IDataFileOnDiskExtensions + { + public static DateTime GetCurrentLastWriteTimeUTC(this IDataFileOnDisk dataFileOnDisk) + => File.GetLastWriteTimeUtc(dataFileOnDisk.FilePath); } } \ No newline at end of file diff --git a/JECS/DataFiles/DataFile.cs b/JECS/DataFiles/DataFile.cs index 1a3dbf7..a1f90b8 100644 --- a/JECS/DataFiles/DataFile.cs +++ b/JECS/DataFiles/DataFile.cs @@ -1,7 +1,6 @@ using JECS.Abstractions; using System; using System.IO; -using System.Timers; namespace JECS { @@ -40,7 +39,7 @@ protected override string GetSavedText() { if (File.Exists(FilePath)) { - LastKnownWriteTimeUTC = GetCurrentLastWriteTimeUTC(); + LastKnownWriteTimeUTC = this.GetCurrentLastWriteTimeUTC(); return File.ReadAllText(FilePath); } @@ -54,7 +53,7 @@ protected override void SetSavedText(string text) lock (FileSystemReadWriteLock) { File.WriteAllText(FilePath, text); - LastKnownWriteTimeUTC = GetCurrentLastWriteTimeUTC(); + LastKnownWriteTimeUTC = this.GetCurrentLastWriteTimeUTC(); } } @@ -63,8 +62,6 @@ protected override void SetSavedText(string text) - - #region IDataFileOnDisk implementation // this code is copied between DataFile and ReadOnlyDataFile. // todo: once we upgrade to c# 8, this can probably be abstracted to a default interface implementation. @@ -75,85 +72,15 @@ protected override void SetSavedText(string text) public string FileName => Path.GetFileNameWithoutExtension(FilePath); /// public long SizeOnDisk => new FileInfo(FilePath).Length; - /// - public event Action OnAutoReload; - - /// - public bool AutoReload - { - get => _AutoReload; - set - { - _AutoReload = value; - if (value == true) - { - EnsureTimerIsSetup(); - AutoReloadTimer.Start(); - } - else - { - AutoReloadTimer?.Stop(); - } - } - } - private bool _AutoReload = false; - - - // To make AutoReload work, we regularly check the last write time on the filesystem. - // We used to use FileSystemWatcher, but that class is a nasty bastard that loves to randomly not work, especially on Linux, and especially on Mono. - // It was also a problem because it's very hard to determine whether FileSystemWatcher is firing legitimately or just because this code has saved a - // new value to disk. - - private static readonly TimeSpan AutoReloadTimerInterval = TimeSpan.FromSeconds(1); - - private Timer AutoReloadTimer; - private readonly object TimerLock = new object(); - private void EnsureTimerIsSetup() - { - if (AutoReloadTimer == null) - { - AutoReloadTimer = new Timer(AutoReloadTimerInterval.TotalMilliseconds); - AutoReloadTimer.AutoReset = false; - AutoReloadTimer.Elapsed += AutoReloadTimerElapsed; - } - } - private void AutoReloadTimerElapsed(object _, ElapsedEventArgs __) - { - // Restart the timer manually, only after we finish ReloadIfChanged. - // Use the lock to ensure we can properly stop the timer when Disposing. - lock (TimerLock) - { - ReloadIfChanged(); - AutoReloadTimer.Start(); - } - } - private void ReloadIfChanged() - { - lock (FileSystemReadWriteLock) - { - if (GetCurrentLastWriteTimeUTC() != LastKnownWriteTimeUTC) - { - ReloadAllData(); - OnAutoReload?.Invoke(); - } - } - } + // For the below members, don't make them directly public; it'll just clutter the public API for this class. + object IDataFileOnDisk.FileSystemReadWriteLock => FileSystemReadWriteLock; private readonly object FileSystemReadWriteLock = new object(); + DateTime IDataFileOnDisk.LastKnownWriteTimeUTC => LastKnownWriteTimeUTC; private DateTime LastKnownWriteTimeUTC; - private DateTime GetCurrentLastWriteTimeUTC() => File.GetLastWriteTimeUtc(this.FilePath); - - public void Dispose() - { - lock (TimerLock) - { - AutoReloadTimer?.Stop(); - AutoReloadTimer?.Dispose(); - } - } #endregion } diff --git a/JECS/DataFiles/DataFileDiskAutoReloader.cs b/JECS/DataFiles/DataFileDiskAutoReloader.cs new file mode 100644 index 0000000..1dd8aba --- /dev/null +++ b/JECS/DataFiles/DataFileDiskAutoReloader.cs @@ -0,0 +1,79 @@ +using JECS.Abstractions; +using System; +using System.Timers; + +namespace JECS +{ + /// + /// Utility class that watches for a DataFile has been changed on disk, i.e. by a user editing the file manually. + /// Subscribe to then call . + /// Make sure to call when you're done with the instance. + /// + public class DataFileDiskAutoReloader : IDisposable + { + public IDataFileOnDisk DataFileOnDisk { get; } + + /// + /// This will fire when has been changed by an external program, after its data has been reloaded from disk. + /// + public event Action OnAutoReload; + + public DataFileDiskAutoReloader(IDataFileOnDisk dataFileOnDisk) + { + DataFileOnDisk = dataFileOnDisk; + } + + public void StartWatchingForDiskChanges() + { + if (AutoReloadTimer != null) + throw new Exception($"You can only call {nameof(StartWatchingForDiskChanges)} once!"); + + const float AutoReloadTimerIntervalSeconds = 1f; + AutoReloadTimer = new Timer(TimeSpan.FromSeconds(AutoReloadTimerIntervalSeconds).TotalMilliseconds); + AutoReloadTimer.AutoReset = false; + AutoReloadTimer.Elapsed += AutoReloadTimerElapsed; + + AutoReloadTimer.Start(); + } + + + // To make AutoReload work, we regularly check the last write time on the filesystem. + // We used to use FileSystemWatcher, but that class is a nasty bastard that loves to randomly not work, especially on Linux, and especially on Mono. + // It was also a problem because it's very hard to determine whether FileSystemWatcher is firing legitimately or just because this code has saved a + // new value to disk. + + private Timer AutoReloadTimer; + private readonly object TimerLock = new object(); + + private void AutoReloadTimerElapsed(object _, ElapsedEventArgs __) + { + // Restart the timer manually, only after we finish ReloadIfChanged. + // Use the lock to ensure we can properly stop the timer when Disposing. + lock (TimerLock) + { + ReloadIfChanged(); + AutoReloadTimer.Start(); + } + } + private void ReloadIfChanged() + { + lock (DataFileOnDisk.FileSystemReadWriteLock) + { + if (DataFileOnDisk.GetCurrentLastWriteTimeUTC() != DataFileOnDisk.LastKnownWriteTimeUTC) + { + DataFileOnDisk.ReloadAllData(); + OnAutoReload?.Invoke(); + } + } + } + + public void Dispose() + { + lock (TimerLock) + { + AutoReloadTimer.Stop(); + AutoReloadTimer.Dispose(); + } + } + } +} diff --git a/JECS/DataFiles/ReadOnlyDataFile.cs b/JECS/DataFiles/ReadOnlyDataFile.cs index c713b62..a0f87ef 100644 --- a/JECS/DataFiles/ReadOnlyDataFile.cs +++ b/JECS/DataFiles/ReadOnlyDataFile.cs @@ -1,7 +1,6 @@ using JECS.Abstractions; using System; using System.IO; -using System.Timers; namespace JECS { @@ -40,7 +39,7 @@ protected override string GetSavedText() { if (File.Exists(FilePath)) { - LastKnownWriteTimeUTC = GetCurrentLastWriteTimeUTC(); + LastKnownWriteTimeUTC = this.GetCurrentLastWriteTimeUTC(); return File.ReadAllText(FilePath); } @@ -52,6 +51,7 @@ protected override string GetSavedText() public override string Identifier => FilePath; + #region IDataFileOnDisk implementation // this code is copied between DataFile and ReadOnlyDataFile. // todo: once we upgrade to c# 8, this can probably be abstracted to a default interface implementation. @@ -62,85 +62,15 @@ protected override string GetSavedText() public string FileName => Path.GetFileNameWithoutExtension(FilePath); /// public long SizeOnDisk => new FileInfo(FilePath).Length; - /// - public event Action OnAutoReload; - /// - public bool AutoReload - { - get => _AutoReload; - set - { - _AutoReload = value; - if (value == true) - { - EnsureTimerIsSetup(); - AutoReloadTimer.Start(); - } - else - { - AutoReloadTimer?.Stop(); - } - } - } - private bool _AutoReload = false; - - - // To make AutoReload work, we regularly check the last write time on the filesystem. - // We used to use FileSystemWatcher, but that class is a nasty bastard that loves to randomly not work, especially on Linux, and especially on Mono. - // It was also a problem because it's very hard to determine whether FileSystemWatcher is firing legitimately or just because this code has saved a - // new value to disk. - - private static readonly TimeSpan AutoReloadTimerInterval = TimeSpan.FromSeconds(1); - - private Timer AutoReloadTimer; - private readonly object TimerLock = new object(); - private void EnsureTimerIsSetup() - { - if (AutoReloadTimer == null) - { - AutoReloadTimer = new Timer(AutoReloadTimerInterval.TotalMilliseconds); - AutoReloadTimer.AutoReset = false; - AutoReloadTimer.Elapsed += AutoReloadTimerElapsed; - } - } - - private void AutoReloadTimerElapsed(object _, ElapsedEventArgs __) - { - // Restart the timer manually, only after we finish ReloadIfChanged. - // Use the lock to ensure we can properly stop the timer when Disposing. - lock (TimerLock) - { - ReloadIfChanged(); - AutoReloadTimer.Start(); - } - } - private void ReloadIfChanged() - { - lock (FileSystemReadWriteLock) - { - if (GetCurrentLastWriteTimeUTC() != LastKnownWriteTimeUTC) - { - ReloadAllData(); - OnAutoReload?.Invoke(); - } - } - } + // For the below members, don't make them directly public; it'll just clutter the public API for this class. + object IDataFileOnDisk.FileSystemReadWriteLock => FileSystemReadWriteLock; private readonly object FileSystemReadWriteLock = new object(); + DateTime IDataFileOnDisk.LastKnownWriteTimeUTC => LastKnownWriteTimeUTC; private DateTime LastKnownWriteTimeUTC; - private DateTime GetCurrentLastWriteTimeUTC() => File.GetLastWriteTimeUtc(this.FilePath); - - public void Dispose() - { - lock (TimerLock) - { - AutoReloadTimer?.Stop(); - AutoReloadTimer?.Dispose(); - } - } #endregion }