Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/ColumnizerLib/ColumnizerLib.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<RootNamespace>ColumnizerLib</RootNamespace>

<PackageId>LogExpert.ColumnizerLib</PackageId>
<PackageVersion>1.22.0</PackageVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
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
45 changes: 39 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,35 @@ 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;
}

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

var cutoffUtc = _timeProvider.GetUtcNow().UtcDateTime.AddDays(-_options.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
12 changes: 12 additions & 0 deletions src/LogExpert.Core/Classes/Persister/PersisterXML.cs
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,18 @@ FormatException or
persistenceData.MultiFileMaxDays = 0;
}

value = GetOptionsAttribute(optionsNode, "multifile", "maxFileAgeDays");
try
{
persistenceData.MultiFileMaxFileAgeDays = value != null ? int.Parse(value, CultureInfo.InvariantCulture) : 0;
}
catch (Exception ex) when (ex is ArgumentNullException or
FormatException or
OverflowException)
{
persistenceData.MultiFileMaxFileAgeDays = 0;
}

XmlNode multiFileNode = optionsNode.SelectSingleNode("multifile");
if (multiFileNode != null)
{
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
8 changes: 8 additions & 0 deletions src/LogExpert.Core/Entities/MultifileOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,16 @@ public class MultiFileOptions
{
#region Properties

/// <summary>
/// Gets or sets how many consecutive days without a file may be skipped when walking a date pattern.
/// </summary>
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.
/// </summary>
public int MaxFileAgeDays { get; set; }

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
66 changes: 60 additions & 6 deletions src/LogExpert.Resources/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading