Skip to content
Merged
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
14 changes: 7 additions & 7 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
- Serilog.Formatting.Compact format support

### Technology Stack
- **Primary Language**: C# (.NET 8.0-windows target framework)
- **Primary Language**: C# (.NET 10.0-windows target framework)
- **UI Framework**: Windows Forms
- **Build System**: Nuke Build System with MSBuild
- **Target Platform**: Windows (requires Windows-specific dependencies)
Expand All @@ -39,9 +39,9 @@
**CRITICAL**: This project requires Windows development environment and .NET 9.0.301 SDK or compatible.

### Environment Setup
1. **Install .NET SDK**: Project requires .NET 9.0.301 SDK (specified in `global.json`)
2. **Windows Environment**: Build targets `net8.0-windows` and uses Windows Forms
3. **Visual Studio**: Recommended Visual Studio 2017+ or Visual Studio Code with C# extension
1. **Install .NET SDK**: Project requires .NET 10.0.100 SDK (specified in `global.json`)
2. **Windows Environment**: Build targets `net10.0-windows` and uses Windows Forms
3. **Visual Studio**: Recommended Visual Studio 2026+ or Visual Studio Code with C# extension
4. **Optional Dependencies**:
- Chocolatey (for packaging)
- Inno Setup 5 or 6 (for setup creation)
Expand Down Expand Up @@ -88,7 +88,7 @@ dotnet test --no-build --verbosity normal
- **Workaround**: Use Windows environment or Windows Subsystem for Linux with proper .NET Windows SDK

2. **.NET Version Mismatch**:
- Project requires .NET 9.0.301 but may encounter .NET 8.0 environments
- Project requires .NET 10.0.100 but may encounter .NET 8.0 environments
- **Workaround**: Nuke build system automatically downloads correct SDK version

3. **Build Timing**:
Expand Down Expand Up @@ -176,7 +176,7 @@ LogExpert/
2. **`.github/workflows/test_dotnet.yml`**:
- Runs on push to Development branch
- Executes unit tests
- Uses .NET 9.0.x SDK
- Uses .NET 10.0.x SDK

#### AppVeyor Integration
- **`appveyor.yml`**: Legacy CI configuration
Expand Down Expand Up @@ -224,7 +224,7 @@ Key external dependencies (managed via Directory.Packages.props):
#### Adding Dependencies
1. Update `src/Directory.Packages.props` for version management
2. Add `<PackageReference>` in specific project files
3. Ensure compatibility with .NET 8.0 target framework
3. Ensure compatibility with .NET 10.0 target framework

### Build Troubleshooting
- **Missing Windows SDK**: Ensure Windows development environment
Expand Down
61 changes: 52 additions & 9 deletions src/ColumnizerLib.UnitTests/ColumnTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,74 @@ namespace ColumnizerLib.UnitTests;
[TestFixture]
public class ColumnTests
{
[SetUp]
public void SetUp()
{
// Reset to default before each test
Column.SetMaxDisplayLength(20_000);
}

[Test]
public void Column_DisplayMaxLength_DefaultIs20000()
{
Assert.That(Column.GetMaxDisplayLength(), Is.EqualTo(20_000));
}

[Test]
public void Column_LineCutOff ()
public void Column_DisplayMaxLength_CanBeConfigured()
{
var expectedFullValue = new StringBuilder().Append('6', 4675).Append("1234").ToString();
var expectedDisplayValue = expectedFullValue[..4675] + "..."; // Using substring shorthand
Column.SetMaxDisplayLength(50_000);
Assert.That(Column.GetMaxDisplayLength(), Is.EqualTo(50_000));

// Reset for other tests
Column.SetMaxDisplayLength(20_000);
}

[Test]
public void Column_DisplayMaxLength_EnforcesMinimum()
{
Assert.Throws<ArgumentOutOfRangeException>(() => Column.SetMaxDisplayLength(500));
}

[Test]
public void Column_TruncatesAtConfiguredDisplayLength()
{
Column.SetMaxDisplayLength(10_000);

// Create a line longer than the display max length
var longValue = new StringBuilder().Append('X', 15_000).ToString();

Column column = new()
{
FullValue = expectedFullValue
FullValue = longValue
};

Assert.That(column.DisplayValue, Is.EqualTo(expectedDisplayValue));
Assert.That(column.FullValue, Is.EqualTo(expectedFullValue));
// FullValue should contain the full line
Assert.That(column.FullValue, Is.EqualTo(longValue));
Assert.That(column.FullValue.Length, Is.EqualTo(15_000));

// DisplayValue should be truncated at 10,000 with "..." appended
Assert.That(column.DisplayValue.Length, Is.EqualTo(10_003)); // 10000 + "..."
Assert.That(column.DisplayValue.EndsWith("..."), Is.True);
Assert.That(column.DisplayValue.StartsWith("XXX"), Is.True);

// Reset for other tests
Column.SetMaxDisplayLength(20_000);
}

[Test]
public void Column_NoLineCutOff ()
public void Column_NoTruncationWhenBelowLimit()
{
var expected = new StringBuilder().Append('6', 4675).ToString();
Column.SetMaxDisplayLength(20_000);

var normalValue = new StringBuilder().Append('Y', 5_000).ToString();
Column column = new()
{
FullValue = expected
FullValue = normalValue
};

Assert.That(column.DisplayValue, Is.EqualTo(column.FullValue));
Assert.That(column.DisplayValue.Length, Is.EqualTo(5_000));
}

[Test]
Expand Down
41 changes: 31 additions & 10 deletions src/ColumnizerLib/Column.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
using System;
using System.Collections.Generic;

using LogExpert;

namespace ColumnizerLib;
Expand All @@ -9,21 +6,22 @@ public class Column : IColumn
{
#region Fields

private const int MAXLENGTH = 4678 - 3;
private const string REPLACEMENT = "...";

// Display-level maximum line length (separate from reader-level limit)
// Can be configured via SetMaxDisplayLength()
private static int _maxDisplayLength = 20_000;

private static readonly List<Func<string, string>> _replacements = [
//replace tab with 3 spaces, from old coding. Needed???
input => input.Replace("\t", " ", StringComparison.Ordinal),

//shorten string if it exceeds maxLength
input => input.Length > MAXLENGTH
? string.Concat(input.AsSpan(0, MAXLENGTH), REPLACEMENT)
input => input.Length > _maxDisplayLength
? string.Concat(input.AsSpan(0, _maxDisplayLength), REPLACEMENT)
: input
];

private string _fullValue;

#endregion

#region cTor
Expand All @@ -40,6 +38,9 @@ static Column ()
{
//Everything below Win8 the installed fonts seems to not to support reliabel
//Replace null char with space
//.net 10 does no longer support windows lower then windows 10
//TODO: remove if with one of the next releases
//https://github.com/dotnet/core/blob/main/release-notes/10.0/supported-os.md
_replacements.Add(input => input.Replace("\0", " ", StringComparison.Ordinal));
}

Expand All @@ -56,10 +57,10 @@ static Column ()

public string FullValue
{
get => _fullValue;
get;
set
{
_fullValue = value;
field = value;

var temp = FullValue;

Expand All @@ -80,6 +81,26 @@ public string FullValue

#region Public methods

/// <summary>
/// Configures the maximum display length for all Column instances.
/// This is separate from the reader-level MaxLineLength.
/// </summary>
/// <param name="maxLength">Maximum length for displayed content. Must be at least 1000.</param>
public static void SetMaxDisplayLength (int maxLength)
{
if (maxLength < 1000)
{
throw new ArgumentOutOfRangeException(nameof(maxLength), "Maximum display length must be at least 1000 characters.");
}

_maxDisplayLength = maxLength;
}

/// <summary>
/// Gets the current maximum display length setting.
/// </summary>
public static int GetMaxDisplayLength () => _maxDisplayLength;

public static Column[] CreateColumns (int count, IColumnizedLogLine parent)
{
return CreateColumns(count, parent, string.Empty);
Expand Down
40 changes: 26 additions & 14 deletions src/LogExpert.Core/Classes/Log/LogfileReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,13 @@ public class LogfileReader : IAutoLogLineColumnizerCallback, IDisposable
private ReaderWriterLock _bufferListLock;
private bool _contentDeleted;
private int _currLineCount;

private readonly int _maximumLineLength;

private ReaderWriterLock _disposeLock;

private EncodingOptions _encodingOptions;

private long _fileLength;
private Task _garbageCollectorTask;
private Task _monitorTask;
Expand All @@ -50,27 +55,34 @@ public class LogfileReader : IAutoLogLineColumnizerCallback, IDisposable
#region cTor

/// Public constructor for single file.
public LogfileReader (string fileName, EncodingOptions encodingOptions, bool multiFile, int bufferCount, int linesPerBuffer, MultiFileOptions multiFileOptions, bool useNewReader, IPluginRegistry pluginRegistry)
: this([fileName], encodingOptions, multiFile, bufferCount, linesPerBuffer, multiFileOptions, useNewReader, pluginRegistry)
public LogfileReader (string fileName, EncodingOptions encodingOptions, bool multiFile, int bufferCount, int linesPerBuffer, MultiFileOptions multiFileOptions, bool useNewReader, IPluginRegistry pluginRegistry, int maximumLineLength)
: this([fileName], encodingOptions, multiFile, bufferCount, linesPerBuffer, multiFileOptions, useNewReader, pluginRegistry, maximumLineLength)
{
}

/// Public constructor for multiple files.
public LogfileReader (string[] fileNames, EncodingOptions encodingOptions, int bufferCount, int linesPerBuffer, MultiFileOptions multiFileOptions, bool useNewReader, IPluginRegistry pluginRegistry)
: this(fileNames, encodingOptions, true, bufferCount, linesPerBuffer, multiFileOptions, useNewReader, pluginRegistry)
public LogfileReader (string[] fileNames, EncodingOptions encodingOptions, int bufferCount, int linesPerBuffer, MultiFileOptions multiFileOptions, bool useNewReader, IPluginRegistry pluginRegistry, int maximumLineLength)
: this(fileNames, encodingOptions, true, bufferCount, linesPerBuffer, multiFileOptions, useNewReader, pluginRegistry, maximumLineLength)
{
// In this overload, we assume multiFile is always true.
}

// Single private constructor that contains the common initialization logic.
private LogfileReader (string[] fileNames, EncodingOptions encodingOptions, bool multiFile, int bufferCount, int linesPerBuffer, MultiFileOptions multiFileOptions, bool useNewReader, IPluginRegistry pluginRegistry)
private LogfileReader (string[] fileNames, EncodingOptions encodingOptions, bool multiFile, int bufferCount, int linesPerBuffer, MultiFileOptions multiFileOptions, bool useNewReader, IPluginRegistry pluginRegistry, int maximumLineLength)
{
// Validate input: at least one file must be provided.
if (fileNames == null || fileNames.Length < 1)
{
throw new ArgumentException("Must provide at least one file.", nameof(fileNames));
}

//Set default maximum line length if invalid value provided.
if (maximumLineLength <= 0)
{
maximumLineLength = 500;
}

_maximumLineLength = maximumLineLength;
_useNewReader = useNewReader;
EncodingOptions = encodingOptions;
_max_buffers = bufferCount;
Expand Down Expand Up @@ -1535,9 +1547,12 @@ private void FileChanged ()

private void FireChangeEvent ()
{
LogEventArgs args = new();
args.PrevFileSize = FileSize;
args.PrevLineCount = LineCount;
LogEventArgs args = new()
{
PrevFileSize = FileSize,
PrevLineCount = LineCount
};

var newSize = _fileLength;
if (newSize < FileSize || _isDeleted)
{
Expand Down Expand Up @@ -1604,12 +1619,9 @@ private ILogStreamReader GetLogStreamReader (Stream stream, EncodingOptions enco

private ILogStreamReader CreateLogStreamReader (Stream stream, EncodingOptions encodingOptions, bool useSystemReader)
{
if (useSystemReader)
{
return new PositionAwareStreamReaderSystem(stream, encodingOptions);
}

return new PositionAwareStreamReaderLegacy(stream, encodingOptions);
return useSystemReader
? new PositionAwareStreamReaderSystem(stream, encodingOptions, _maximumLineLength)
: new PositionAwareStreamReaderLegacy(stream, encodingOptions, _maximumLineLength);
}

private bool ReadLine (ILogStreamReader reader, int lineNum, int realLineNum, out string outLine)
Expand Down
36 changes: 16 additions & 20 deletions src/LogExpert.Core/Classes/Log/PositionAwareStreamReaderBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ public abstract class PositionAwareStreamReaderBase : LogStreamReaderBase
{
#region Fields

private const int MAX_LINE_LEN = 20000;

private static readonly Encoding[] _preambleEncodings = [Encoding.UTF8, Encoding.Unicode, Encoding.BigEndianUnicode, Encoding.UTF32];

private readonly BufferedStream _stream;
Expand All @@ -24,10 +22,12 @@ public abstract class PositionAwareStreamReaderBase : LogStreamReaderBase

#region cTor

protected PositionAwareStreamReaderBase (Stream stream, EncodingOptions encodingOptions)
protected PositionAwareStreamReaderBase (Stream stream, EncodingOptions encodingOptions, int maximumLineLength)
{
_stream = new BufferedStream(stream);

MaximumLineLength = maximumLineLength;

_preambleLength = DetectPreambleLengthAndEncoding(out var detectedEncoding);

var usedEncoding = GetUsedEncoding(encodingOptions, detectedEncoding);
Expand Down Expand Up @@ -61,7 +61,7 @@ public sealed override long Position
_position = value; // +Encoding.GetPreamble().Length; // 1
//stream.Seek(pos, SeekOrigin.Begin); // 2
//stream.Seek(pos + Encoding.GetPreamble().Length, SeekOrigin.Begin); // 3
_stream.Seek(_position + _preambleLength, SeekOrigin.Begin); // 4
_ = _stream.Seek(_position + _preambleLength, SeekOrigin.Begin); // 4

ResetReader();
}
Expand All @@ -71,8 +71,11 @@ public sealed override long Position

public sealed override bool IsBufferComplete => true;

//Refactor this needs to be given and should not be added like this
protected static int MaxLineLen => 500;//ConfigManager.Settings.Preferences.MaxLineLength;
protected static int MaximumLineLength
{
get => field;
private set => field = value;
}

#endregion

Expand All @@ -89,7 +92,7 @@ protected override void Dispose (bool disposing)
_stream.Dispose();
_reader.Dispose();
IsDisposed = true;
}
}
}

//TODO This is unsafe and should be refactored
Expand Down Expand Up @@ -188,21 +191,14 @@ private int DetectPreambleLengthAndEncoding (out Encoding detectedEncoding)
return 0;
}

private Encoding GetUsedEncoding (EncodingOptions encodingOptions, Encoding detectedEncoding)
private static Encoding GetUsedEncoding (EncodingOptions encodingOptions, Encoding detectedEncoding)
{
if (encodingOptions.Encoding != null)
{
return encodingOptions.Encoding;
}

if (detectedEncoding != null)
{
return detectedEncoding;
}

return encodingOptions.DefaultEncoding ?? Encoding.Default;
return encodingOptions.Encoding ??
detectedEncoding ??
encodingOptions.DefaultEncoding ??
Encoding.Default;
}
private int GetPosIncPrecomputed (Encoding usedEncoding)
private static int GetPosIncPrecomputed (Encoding usedEncoding)
{
switch (usedEncoding)
{
Expand Down
Loading