Skip to content

Commit

Permalink
Move DataFile auto-reload to a separate class
Browse files Browse the repository at this point in the history
  • Loading branch information
JimmyCushnie committed Sep 28, 2024
1 parent 05c01e5 commit 5651cee
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 158 deletions.
17 changes: 12 additions & 5 deletions JECS/DataFiles/Abstractions/IDataFileOnDisk.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
using System;
using System.IO;

namespace JECS.Abstractions
{
internal interface IDataFileOnDisk : IDisposable
public interface IDataFileOnDisk
{
/// <summary> The absolute path of the file this object corresponds to. </summary>
string FilePath { get; }
Expand All @@ -13,10 +14,16 @@ internal interface IDataFileOnDisk : IDisposable
/// <summary> Size of this file on disk in bytes. If there is unsaved data in memory it will not be counted. </summary>
long SizeOnDisk { get; }

/// <summary> If true, the DataFile will automatically reload its data when the file changes on disk. If false, you can still call ReloadAllData() manually. </summary>
bool AutoReload { get; set; }
void ReloadAllData();


object FileSystemReadWriteLock { get; }
DateTime LastKnownWriteTimeUTC { get; }
}

/// <summary> Invoked every time the file is auto-reloaded. This only happens when AutoReload is set to true. </summary>
event Action OnAutoReload;
internal static class IDataFileOnDiskExtensions
{
public static DateTime GetCurrentLastWriteTimeUTC(this IDataFileOnDisk dataFileOnDisk)
=> File.GetLastWriteTimeUtc(dataFileOnDisk.FilePath);
}
}
83 changes: 5 additions & 78 deletions JECS/DataFiles/DataFile.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using JECS.Abstractions;
using System;
using System.IO;
using System.Timers;

namespace JECS
{
Expand Down Expand Up @@ -40,7 +39,7 @@ protected override string GetSavedText()
{
if (File.Exists(FilePath))
{
LastKnownWriteTimeUTC = GetCurrentLastWriteTimeUTC();
LastKnownWriteTimeUTC = this.GetCurrentLastWriteTimeUTC();
return File.ReadAllText(FilePath);
}

Expand All @@ -54,7 +53,7 @@ protected override void SetSavedText(string text)
lock (FileSystemReadWriteLock)
{
File.WriteAllText(FilePath, text);
LastKnownWriteTimeUTC = GetCurrentLastWriteTimeUTC();
LastKnownWriteTimeUTC = this.GetCurrentLastWriteTimeUTC();
}
}

Expand All @@ -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.
Expand All @@ -75,85 +72,15 @@ protected override void SetSavedText(string text)
public string FileName => Path.GetFileNameWithoutExtension(FilePath);
/// <inheritdoc/>
public long SizeOnDisk => new FileInfo(FilePath).Length;
/// <inheritdoc/>
public event Action OnAutoReload;

/// <inheritdoc/>
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
}
Expand Down
79 changes: 79 additions & 0 deletions JECS/DataFiles/DataFileDiskAutoReloader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using JECS.Abstractions;
using System;
using System.Timers;

namespace JECS
{
/// <summary>
/// Utility class that watches for a DataFile has been changed on disk, i.e. by a user editing the file manually.
/// Subscribe to <see cref="OnAutoReload"/> then call <see cref="StartWatchingForDiskChanges"/>.
/// Make sure to call <see cref="Dispose"/> when you're done with the instance.
/// </summary>
public class DataFileDiskAutoReloader : IDisposable
{
public IDataFileOnDisk DataFileOnDisk { get; }

/// <summary>
/// This will fire when <see cref="DataFileOnDisk"/> has been changed by an external program, after its data has been reloaded from disk.
/// </summary>
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();
}
}
}
}
80 changes: 5 additions & 75 deletions JECS/DataFiles/ReadOnlyDataFile.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using JECS.Abstractions;
using System;
using System.IO;
using System.Timers;

namespace JECS
{
Expand Down Expand Up @@ -40,7 +39,7 @@ protected override string GetSavedText()
{
if (File.Exists(FilePath))
{
LastKnownWriteTimeUTC = GetCurrentLastWriteTimeUTC();
LastKnownWriteTimeUTC = this.GetCurrentLastWriteTimeUTC();
return File.ReadAllText(FilePath);
}

Expand All @@ -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.
Expand All @@ -62,85 +62,15 @@ protected override string GetSavedText()
public string FileName => Path.GetFileNameWithoutExtension(FilePath);
/// <inheritdoc/>
public long SizeOnDisk => new FileInfo(FilePath).Length;
/// <inheritdoc/>
public event Action OnAutoReload;

/// <inheritdoc/>
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
}
Expand Down

0 comments on commit 5651cee

Please sign in to comment.