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
107 changes: 107 additions & 0 deletions src/LogExpert.Core/Classes/FileDrop/DroppedFileDiscovery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System.Security;

namespace LogExpert.Core.Classes.FileDrop;

/// <summary>Prepares a snapshot of dropped files without opening logs or touching the UI.</summary>
public sealed class DroppedFileDiscovery (
Func<string, FileAttributes>? getAttributes = null,
Func<string, IEnumerable<string>>? enumerateEntries = null)
{
private readonly Func<string, FileAttributes> _getAttributes = getAttributes ?? File.GetAttributes;
private readonly Func<string, IEnumerable<string>> _enumerateEntries = enumerateEntries ?? Directory.EnumerateFileSystemEntries;

public Task<DroppedFileDiscoveryResult> DiscoverAsync (IEnumerable<string> paths, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(paths);
var snapshot = paths.ToArray();
return Task.Run(() => Discover(snapshot, cancellationToken), cancellationToken);
}

private DroppedFileDiscoveryResult Discover (string[] paths, CancellationToken cancellationToken)
{
var files = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var directories = new Stack<string>();
var skipped = new Dictionary<string, SkippedDropPath>(StringComparer.OrdinalIgnoreCase);
var includesFolders = false;

void addPath (string path, bool explicitlyDropped)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
path = Path.GetFullPath(path);
var attributes = _getAttributes(path);
if ((attributes & FileAttributes.Directory) != 0)
{
includesFolders = true;
directories.Push(Path.TrimEndingDirectorySeparator(path));
}
else if (explicitlyDropped || !IsSessionPath(path))
{
files.Add(path);
}
}
catch (Exception ex) when (IsPathError(ex))
{
skipped[path] = new SkippedDropPath(path, ex.Message);
}
}

foreach (var path in paths)
{
addPath(path, true);
}

while (directories.TryPop(out var directory))
{
cancellationToken.ThrowIfCancellationRequested();
if (!visited.Add(directory))
{
continue;
}

try
{
// Recheck immediately before enumeration: a directory may have changed since it was queued.
if ((_getAttributes(directory) & FileAttributes.ReparsePoint) != 0)
{
skipped[directory] = new SkippedDropPath(directory, null);
continue;
}

using var entries = _enumerateEntries(directory).GetEnumerator();
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
if (!entries.MoveNext())
{
break;
}
addPath(entries.Current, false);
}
}
catch (Exception ex) when (IsPathError(ex))
{
skipped[directory] = new SkippedDropPath(directory, ex.Message);
}
}

cancellationToken.ThrowIfCancellationRequested();
return new DroppedFileDiscoveryResult(includesFolders,
files.Order(StringComparer.OrdinalIgnoreCase).ToArray(),
skipped.Values.OrderBy(skip => skip.Path, StringComparer.OrdinalIgnoreCase).ToArray());
}

private static bool IsSessionPath (string path)
{
var extension = Path.GetExtension(path);
return extension.Equals(".lxp", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".lxj", StringComparison.OrdinalIgnoreCase);
}

private static bool IsPathError (Exception exception)
{
return exception is IOException or UnauthorizedAccessException or SecurityException or ArgumentException or NotSupportedException;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace LogExpert.Core.Classes.FileDrop;

public sealed record DroppedFileDiscoveryResult (bool IncludesFolders, string[] Files, SkippedDropPath[] Skipped);

/// <param name="Reason">The filesystem error, or null for a deliberately skipped directory reparse point.</param>
public sealed record SkippedDropPath (string Path, string? Reason);
83 changes: 63 additions & 20 deletions src/LogExpert.Core/Classes/Log/LogfileReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public partial class LogfileReader : ILogfileReader, IMultiFileNavigation, ILogf
private bool _isDeleted;

private IList<ILogFileInfo> _logFileInfoList = [];
private readonly bool _hasExplicitFileList;
private bool _shouldStop;
private bool _disposed;
private ILogFileInfo _watchedILogFileInfo;
Expand Down Expand Up @@ -94,7 +95,8 @@ public LogfileReader (
ReaderType readerType,
IPluginRegistry pluginRegistry,
int maximumLineLength,
ILoadProgressReporter? progressReporter = null)
ILoadProgressReporter? progressReporter = null,
bool useExplicitFileList = false)
: this(
fileNames,
encodingOptions,
Expand All @@ -105,7 +107,8 @@ public LogfileReader (
readerType,
pluginRegistry,
maximumLineLength,
progressReporter)
progressReporter,
useExplicitFileList)
{
// In this overload, we assume multiFile is always true.
}
Expand All @@ -121,7 +124,8 @@ private LogfileReader (
ReaderType readerType,
IPluginRegistry pluginRegistry,
int maximumLineLength,
ILoadProgressReporter? progressReporter = null)
ILoadProgressReporter? progressReporter = null,
bool useExplicitFileList = false)
{
// Validate input: at least one file must be provided.
if (fileNames == null || fileNames.Length < 1)
Expand Down Expand Up @@ -151,9 +155,10 @@ private LogfileReader (
ILogFileInfo fileInfo = null;

IsMultiFile = multiFile;
_hasExplicitFileList = useExplicitFileList || fileNames.Length > 1;
_fileName = fileNames[0];

IEnumerable<string> names = IsMultiFile
IEnumerable<string> names = _hasExplicitFileList ? fileNames : IsMultiFile
// For multi-file rollover mode: get rollover names.
? new RolloverFilenameHandler(GetLogFileInfo(_fileName), _multiFileOptions).GetNameList(_pluginRegistry)
: [_fileName];
Expand All @@ -173,16 +178,23 @@ private LogfileReader (

foreach (var name in names)
{
fileInfo = AddFile(name);
try
{
fileInfo = AddFile(name);
}
catch (Exception ex) when (_hasExplicitFileList && ex is IOException or UnauthorizedAccessException or LogFileException)
{
_logger.Warn(ex, "Skipping unavailable selected file: {0}", name);
}
}

if (IsMultiFile)
{
// Use the full name of the last file as _fileName.
_fileName = fileInfo.FullName;
_fileName = fileInfo?.FullName ?? fileNames[^1];
}

_watchedILogFileInfo = fileInfo;
_watchedILogFileInfo = fileInfo ?? GetLogFileInfo(fileNames[^1]);

StartGCThread();
}
Expand Down Expand Up @@ -294,6 +306,11 @@ private EncodingOptions EncodingOptions
/// </remarks>
//TODO: Make this private
public void ReadFiles ()
{
ReadFiles(notifyChanges: true);
}

private void ReadFiles (bool notifyChanges)
{
FileSize = 0;

Expand All @@ -304,16 +321,23 @@ public void ReadFiles ()
using var _ = BufferIndex.AcquireWriteLock();
BufferIndex.ClearLru(_bufferPool);

ILogFileInfo lastReadableFile = null;
foreach (var info in _logFileInfoList)
{
ReadToBufferList(info, 0, LineCount);
if (ReadToBufferList(info, 0, LineCount))
{
lastReadableFile = info;
}
}

if (_logFileInfoList.Count > 0)
{
var info = _logFileInfoList[_logFileInfoList.Count - 1];
_fileLength = info.Length;
_watchedILogFileInfo = info;
var info = _hasExplicitFileList ? lastReadableFile : _logFileInfoList[^1];
if (info != null)
{
_fileLength = info.Length;
_watchedILogFileInfo = info;
}
}
}
catch (IOException e)
Expand All @@ -324,15 +348,18 @@ public void ReadFiles ()
LineCount = 0;
}

LogEventArgs args = new()
if (notifyChanges)
{
PrevFileSize = 0,
PrevLineCount = 0,
LineCount = LineCount,
FileSize = FileSize
};
LogEventArgs args = new()
{
PrevFileSize = 0,
PrevLineCount = 0,
LineCount = LineCount,
FileSize = FileSize
};

OnFileSizeChanged(args);
OnFileSizeChanged(args);
}
}

/// <summary>
Expand All @@ -351,6 +378,14 @@ public void ReadFiles ()
//TODO: Make this private
public int ShiftBuffers ()
{
if (_hasExplicitFileList)
{
// A selected batch keeps its paths after truncation; rollover discovery could
// discard selected files and introduce unselected siblings.
ReadFiles(notifyChanges: false);
return 0;
}

_logger.Info(CultureInfo.InvariantCulture, "ShiftBuffers() begin for {0}{1}", _fileName, IsMultiFile ? " (MultiFile)" : "");

using var writeLock = BufferIndex.AcquireWriteLock();
Expand Down Expand Up @@ -1139,7 +1174,7 @@ private void ReplaceBufferInfos (ILogFileInfo oldLogFileInfo, ILogFileInfo newLo
/// The line number corresponding to the starting position in the file. Used to assign line numbers to buffered log
/// lines.
/// </param>
private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int startLine)
private bool ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int startLine)
{
try
{
Expand Down Expand Up @@ -1214,7 +1249,7 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start
{
if (_shouldStop)
{
return;
return false;
}

if (wasDropped)
Expand Down Expand Up @@ -1292,6 +1327,13 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start
{
_progressReporter.ReportComplete(logFileInfo.FullName, filePos, _fileLength);
}
return true;
}
catch (Exception ex) when (_hasExplicitFileList && ex is IOException or UnauthorizedAccessException)
{
// A selected batch may change while the preview is open. Keep the other files readable.
_logger.Warn(ex, "Skipping unavailable selected file: {0}", logFileInfo.FullName);
return false;
}
catch (IOException ioex)
{
Expand All @@ -1300,6 +1342,7 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start
LineCount = 0;
FileSize = 0;
OnFileNotFound(); // notify LogWindow
return false;
}
}

Expand Down
Loading
Loading