Skip to content
6 changes: 6 additions & 0 deletions src/ColumnizerLib/ILogFileInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ public interface ILogFileInfo
/// </summary>
bool FileExists { get; }

/// <summary>
/// Gets the time when the file was last written, in UTC, or <see langword="null"/> when the file system plugin
/// cannot provide it.
/// </summary>
DateTime? LastWriteTimeUtc => null;

/// <summary>
/// The interval (in ms) LogExpert should check for file changes. The property is checked by LogExpert repeatedly in the loop which
/// checks for file changes. So you can adjust the poll interval as needed. E.g. you can lower the interval when many changes occur and
Expand Down
46 changes: 40 additions & 6 deletions src/LogExpert.Core/Classes/Log/RolloverFilenameHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class RolloverFilenameHandler
private readonly RolloverFilenameBuilder _filenameBuilder;
private readonly ILogFileInfo _logFileInfo;
private readonly MultiFileOptions _options;
private readonly TimeProvider _timeProvider;

#endregion

Expand All @@ -31,10 +32,12 @@ public class RolloverFilenameHandler
/// </summary>
/// <param name="logFileInfo">The complete path of the logfile</param>
/// <param name="options">Multifile option (e.g. format pattern)</param>
public RolloverFilenameHandler(ILogFileInfo logFileInfo, MultiFileOptions options)
/// <param name="timeProvider">Time source used to calculate the file-age cutoff.</param>
public RolloverFilenameHandler(ILogFileInfo logFileInfo, MultiFileOptions options, TimeProvider? timeProvider = null)
{
_options = options;
_logFileInfo = logFileInfo;
_timeProvider = timeProvider ?? TimeProvider.System;
_filenameBuilder = new RolloverFilenameBuilder(_options.FormatPattern);
_filenameBuilder.SetFileName(logFileInfo.FileName);
}
Expand Down Expand Up @@ -69,12 +72,18 @@ public LinkedList<string> GetNameList(IPluginRegistry pluginRegistry)
_filenameBuilder.Index += 1;
fileName = _filenameBuilder.BuildFileName();
filePath = _logFileInfo.DirectoryName + _logFileInfo.DirectorySeparatorChar + fileName;//TODO: Change to Directory.Combine
if (FileExists(filePath, pluginRegistry))
var result = GetCandidateResult(filePath, pluginRegistry);
if (result == CandidateResult.Include)
{
_ = fileList.AddFirst(filePath);
found = true;
continue;
}

if (result == CandidateResult.TooOld)
{
return fileList;
}
}

// if file with index isn't found or no index is in format pattern, decrement the current date
Expand All @@ -87,13 +96,19 @@ public LinkedList<string> GetNameList(IPluginRegistry pluginRegistry)
_filenameBuilder.DecrementDate();
fileName = _filenameBuilder.BuildFileName();
filePath = _logFileInfo.DirectoryName + _logFileInfo.DirectorySeparatorChar + fileName;//TODO: Change to Directory.Combine
if (FileExists(filePath, pluginRegistry))
var result = GetCandidateResult(filePath, pluginRegistry);
if (result == CandidateResult.Include)
{
_ = fileList.AddFirst(filePath);
found = true;
break;
}

if (result == CandidateResult.TooOld)
{
return fileList;
}

tryCounter++;
}
}
Expand All @@ -106,17 +121,36 @@ public LinkedList<string> GetNameList(IPluginRegistry pluginRegistry)

#region Private Methods

private bool FileExists(string filePath, IPluginRegistry pluginRegistry)
private CandidateResult GetCandidateResult(string filePath, IPluginRegistry pluginRegistry)
{
var fs = pluginRegistry.FindFileSystemForUri(filePath);

if(fs == null)
{
return false;
return CandidateResult.Missing;
}

var info = fs.GetLogfileInfo(filePath);
return info is not null && info.FileExists;
if (info is null || !info.FileExists)
{
return CandidateResult.Missing;
}

var maxFileAgeDays = MultiFileOptions.ClampMaxFileAgeDays(_options.MaxFileAgeDays);
if (maxFileAgeDays == 0 || info.LastWriteTimeUtc is not { } lastWriteTimeUtc)
{
return CandidateResult.Include;
}

var cutoffUtc = _timeProvider.GetUtcNow().UtcDateTime - TimeSpan.FromDays(maxFileAgeDays);
return lastWriteTimeUtc >= cutoffUtc ? CandidateResult.Include : CandidateResult.TooOld;
}

private enum CandidateResult
{
Missing,
Include,
TooOld
}

#endregion
Expand Down
2 changes: 2 additions & 0 deletions src/LogExpert.Core/Classes/Persister/PersistenceData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ public class PersistenceData

public int MultiFileMaxDays { get; set; }

public int MultiFileMaxFileAgeDays { get; set; }

public List<string> MultiFileNames { get; set; } = [];

public string MultiFilePattern { get; set; }
Expand Down
2 changes: 2 additions & 0 deletions src/LogExpert.Core/Classes/Persister/SessionFileComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public static PersistenceData Compose (SessionSnapshot snapshot)
FilterSaveListVisible = snapshot.FilterSaveListVisible,
MultiFile = snapshot.MultiFile,
MultiFileMaxDays = snapshot.MultiFileMaxDays,
MultiFileMaxFileAgeDays = snapshot.MultiFileMaxFileAgeDays,
MultiFilePattern = snapshot.MultiFilePattern,
TabName = snapshot.TabName,
HighlightGroupName = snapshot.HighlightGroupName,
Expand Down Expand Up @@ -83,6 +84,7 @@ public static SessionSnapshot Decompose (PersistenceData persistenceData)
FilterSaveListVisible = persistenceData.FilterSaveListVisible,
MultiFile = persistenceData.MultiFile,
MultiFileMaxDays = persistenceData.MultiFileMaxDays,
MultiFileMaxFileAgeDays = persistenceData.MultiFileMaxFileAgeDays,
MultiFilePattern = persistenceData.MultiFilePattern,
TabName = persistenceData.TabName,
HighlightGroupName = persistenceData.HighlightGroupName,
Expand Down
2 changes: 2 additions & 0 deletions src/LogExpert.Core/Classes/Persister/SessionSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public class SessionSnapshot

public int MultiFileMaxDays { get; set; }

public int MultiFileMaxFileAgeDays { get; set; }

public string MultiFilePattern { get; set; }

public string TabName { get; set; }
Expand Down
16 changes: 16 additions & 0 deletions src/LogExpert.Core/Entities/MultifileOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,24 @@ public class MultiFileOptions
{
#region Properties

/// <summary>
/// The largest file age exposed by the UI and accepted by the rollover reader, in days.
/// </summary>
public const int MAX_FILE_AGE_DAYS = 3650;

public int MaxDayTry { get; set; } = 3;

/// <summary>
/// Gets or sets the maximum age, in days, of rollover files loaded into a MultiFile. Zero disables the limit.
/// Values greater than <see cref="MAX_FILE_AGE_DAYS"/> are treated as that maximum.
/// </summary>
public int MaxFileAgeDays { get; set; }

public static int ClampMaxFileAgeDays (int value)
{
return Math.Clamp(value, 0, MAX_FILE_AGE_DAYS);
}

public string FormatPattern { get; set; } = "*$J(.)";

#endregion
Expand Down
3 changes: 3 additions & 0 deletions src/LogExpert.Persister.Tests/PersisterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ public void RoundTrip_WithApplicationStartupDir_PreservesAllData ()
MultiFile = true,
MultiFilePattern = "test*.log",
MultiFileMaxDays = 7,
MultiFileMaxFileAgeDays = 5,
LineCount = 1000,
CellSelectMode = true
};
Expand All @@ -377,6 +378,8 @@ public void RoundTrip_WithApplicationStartupDir_PreservesAllData ()
Assert.That(loadedData.MultiFile, Is.EqualTo(originalData.MultiFile), "MultiFile should match");
Assert.That(loadedData.MultiFilePattern, Is.EqualTo(originalData.MultiFilePattern), "MultiFilePattern should match");
Assert.That(loadedData.MultiFileMaxDays, Is.EqualTo(originalData.MultiFileMaxDays), "MultiFileMaxDays should match");
Assert.That(loadedData.MultiFileMaxFileAgeDays, Is.EqualTo(originalData.MultiFileMaxFileAgeDays),
"MultiFileMaxFileAgeDays should match");
Assert.That(loadedData.LineCount, Is.EqualTo(originalData.LineCount), "LineCount should match");
Assert.That(loadedData.CellSelectMode, Is.EqualTo(originalData.CellSelectMode), "CellSelectMode should match");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public void Load_FileElementWithoutOptions_ReturnsDefaults ()
Assert.That(data.MultiFile, Is.False);
Assert.That(data.MultiFilePattern, Is.Null);
Assert.That(data.MultiFileMaxDays, Is.Zero);
Assert.That(data.MultiFileMaxFileAgeDays, Is.Zero);
Assert.That(data.FilterVisible, Is.False);

// A missing <options> means "nothing was persisted", so every option keeps its
Expand Down
26 changes: 26 additions & 0 deletions src/LogExpert.Persister.Tests/SessionFileComposerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ private static SessionSnapshot CreateFullSnapshot (int filterTabDepth = 2)
FilterSaveListVisible = true,
MultiFile = true,
MultiFileMaxDays = 7,
MultiFileMaxFileAgeDays = 5,
MultiFilePattern = "app*.log",
TabName = "My Tab",
HighlightGroupName = "Errors",
Expand Down Expand Up @@ -247,6 +248,7 @@ private static PersistenceData CreateCarriedPersistenceData (int filterTabDepth)
FilterSaveListVisible = true,
MultiFile = true,
MultiFileMaxDays = 7,
MultiFileMaxFileAgeDays = 5,
MultiFilePattern = "app*.log",
TabName = "My Tab",
HighlightGroupName = "Errors",
Expand Down Expand Up @@ -422,6 +424,28 @@ public void DiskTrip_ComposePersistLoadDecompose_ReturnsEqualSnapshot ()
Assert.That(SerializeForComparison(roundTripped), Is.EqualTo(SerializeForComparison(snapshot)));
}

[Test]
public void Decompose_LegacyJsonWithoutMaxFileAgeDays_DefaultsToOff ()
{
var sessionFileName = Path.Join(_testDirectory, "legacy-json.lxp");
File.WriteAllText(sessionFileName, """
{
"MultiFile": true,
"MultiFileMaxDays": 7,
"MultiFilePattern": "*$J(.)"
}
""");

var loaded = Core.Classes.Persister.Persister.Load(sessionFileName);
var snapshot = SessionFileComposer.Decompose(loaded);

Assert.Multiple(() =>
{
Assert.That(loaded.MultiFileMaxFileAgeDays, Is.Zero);
Assert.That(snapshot.MultiFileMaxFileAgeDays, Is.Zero);
});
}

#endregion

#region Window filter (issue #666)
Expand Down Expand Up @@ -490,6 +514,8 @@ public void Decompose_LegacyXmlSessionFileOnDisk_TakesTheFirstFilterAsTheWindowF
Assert.That(snapshot.FilterParams.SearchText, Is.EqualTo("the first global filter"));
Assert.That(snapshot.FilterParams.SpreadBefore, Is.EqualTo(1),
"the restored entry must be the first one, whole — the second carries SpreadBefore 2");
Assert.That(snapshot.MultiFileMaxFileAgeDays, Is.Zero,
"legacy XML Session Files without the new field must keep age filtering disabled");
Assert.That(snapshot.FilterParams.Color, Is.EqualTo(Color.Black),
"Color is the field the legacy payload writes as \"Black\" — losing it means the entry was not really read");
}
Expand Down
Loading
Loading