Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display record count & improvements #64

Merged
merged 10 commits into from
Feb 27, 2024
2 changes: 1 addition & 1 deletion EventLook/EventLook.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</PropertyGroup>
<PropertyGroup>
<AssemblyTitle>A fast &amp; handy Event Viewer</AssemblyTitle>
<AssemblyVersion>1.5.0.0</AssemblyVersion>
<AssemblyVersion>1.5.1.0</AssemblyVersion>
<Description>$(AssemblyTitle)</Description>
<Copyright>Copyright (C) K. Maki</Copyright>
<Product>EventLook</Product>
Expand Down
79 changes: 67 additions & 12 deletions EventLook/Model/DataService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,53 +11,84 @@ namespace EventLook.Model;

public interface IDataService
{
Task<int> ReadEvents(LogSource eventSource, DateTime fromTime, DateTime toTime, bool readFromNew, IProgress<ProgressInfo> progress);
Task<int> ReadEvents(LogSource logSource, DateTime fromTime, DateTime toTime, bool readFromNew, IProgress<ProgressInfo> progress);
void Cancel();
/// <summary>
/// Subscribes to events in an event log channel. The caller will be reported whenever a new event comes in.
/// </summary>
/// <param name="eventSource"></param>
/// <param name="logSource"></param>
/// <param name="handler"></param>
/// <returns>True if success.</returns>
bool SubscribeEvents(LogSource eventSource, IProgress<ProgressInfo> progress);
bool SubscribeEvents(LogSource logSource, IProgress<ProgressInfo> progress);
void UnsubscribeEvents();
}
public class DataService : IDataService
{
private CancellationTokenSource cts;
public async Task<int> ReadEvents(LogSource eventSource, DateTime fromTime, DateTime toTime, bool readFromNew, IProgress<ProgressInfo> progress)
const int WIN32ERROR_RPC_S_INVALID_BOUND = unchecked((int)0x800706C6);
public async Task<int> ReadEvents(LogSource logSource, DateTime fromTime, DateTime toTime, bool readFromNew, IProgress<ProgressInfo> progress)
{
using (cts = new CancellationTokenSource())
{
// Event records to be sent to the ViewModel
var eventRecords = new List<EventRecord>();
EventLogReader reader = null;
EventRecord eventRecord = null;
string errMsg = "";
int count = 0;
int totalCount = 0;
bool isFirst = true;
try
{
string sQuery = string.Format(" *[System[TimeCreated[@SystemTime > '{0}' and @SystemTime <= '{1}']]]",
fromTime.ToUniversalTime().ToString("o"),
toTime.ToUniversalTime().ToString("o"));

var elQuery = new EventLogQuery(eventSource.Path, eventSource.PathType, sQuery)
var elQuery = new EventLogQuery(logSource.Path, logSource.PathType, sQuery)
{
ReverseDirection = readFromNew
};
var reader = new EventLogReader(elQuery);
var eventRecord = reader.ReadEvent();
Debug.WriteLine("Begin Reading");

// Asynchronously get the record count info.
// The count is valid only when "All time" is selected for a Event Log on the local machine.
_ = Task.Run(() => totalCount = GetRecordCount(logSource));

reader = new EventLogReader(elQuery);
await Task.Run(() =>
{
for (; eventRecord != null; eventRecord = reader.ReadEvent())
int retryCount = 0;
while (true)
{
try
{
eventRecord = reader.ReadEvent();
}
catch (EventLogException ex)
{
// Workaround: ReadEvent can somehow throw an exception "The array bounds are invalid."
if (ex.HResult == WIN32ERROR_RPC_S_INVALID_BOUND && retryCount < 3 && eventRecord?.Bookmark != null)
{
reader.Seek(eventRecord.Bookmark, 1); // Reset the position to last successful read + 1.
if (reader.BatchSize > 1)
reader.BatchSize /= 2; // Halve the 2nd param of EvtNext Win32 API to be called.
retryCount++;
Debug.WriteLine($"Retry #{retryCount}. Last successful-read event's RecordId: {eventRecord.RecordId}, Time: {eventRecord.TimeCreated?.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
continue;
}
else
throw;
}
if (eventRecord == null) // No more records.
break;

cts.Token.ThrowIfCancellationRequested();

eventRecords.Add(eventRecord);
++count;
count++;
if (count % 100 == 0)
{
var info = new ProgressInfo(eventRecords.ConvertAll(e => new EventItem(e)), isComplete: false, isFirst);
var info = new ProgressInfo(eventRecords.ConvertAll(e => new EventItem(e)), isComplete: false, isFirst, totalCount);
cts.Token.ThrowIfCancellationRequested();
progress.Report(info);
isFirst = false;
Expand All @@ -84,8 +115,10 @@ await Task.Run(() =>
}
finally
{
var info_comp = new ProgressInfo(eventRecords.ConvertAll(e => new EventItem(e)), isComplete: true, isFirst, errMsg);
var info_comp = new ProgressInfo(eventRecords.ConvertAll(e => new EventItem(e)), isComplete: true, isFirst, totalCount, errMsg);
progress.Report(info_comp);
eventRecord?.Dispose();
reader?.Dispose();
Debug.WriteLine("End Reading");
}
return count;
Expand Down Expand Up @@ -139,7 +172,7 @@ private void EventRecordWrittenHandler(object sender, EventRecordWrittenEventArg
}
catch (Exception ex)
{
progress?.Report(new ProgressInfo(new List<EventItem>(), isComplete: true, isFirst: true, ex.Message));
progress?.Report(new ProgressInfo(new List<EventItem>(), isComplete: true, isFirst: true, totalEventCount: 0, ex.Message));
}
}

Expand Down Expand Up @@ -198,4 +231,26 @@ public static bool IsValidEventLog(string path, PathType type)
}
}
}

/// <summary>
/// Gets the total number of event records in a channel.
/// This refers to the RecordCount property in EventLogInformation instead of iterating ReadEvent() to count.
/// So the count does not necessarily match the number of records to be received by the query.
/// </summary>
private static int GetRecordCount(LogSource source)
{
if (source.PathType == PathType.LogName)
{
try
{
using (var elSession = new EventLogSession())
{
EventLogInformation info = elSession.GetLogInformation(source.Path, PathType.LogName);
return Convert.ToInt32(info.RecordCount ?? 0); // Int should be large enough.
}
}
catch (Exception) { }
}
return 0;
}
}
20 changes: 20 additions & 0 deletions EventLook/Model/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EventLook.Model;
public static class EnumerableExtensions
{
/// <summary>
/// Disposes all items in the sequence, i.e., releases unmanaged resources associated with the items.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="seq"></param>
public static void DisposeAll<T>(this IEnumerable<T> seq) where T : IDisposable
{
foreach (T item in seq)
item?.Dispose();
}
}
4 changes: 3 additions & 1 deletion EventLook/Model/EventItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace EventLook.Model;
/// Represents a event to display.
/// Could not inherit EventLogRecord as it doesn't have a public constructor.
/// </summary>
public class EventItem
public class EventItem : IDisposable
{
public EventItem(EventRecord eventRecord)
{
Expand Down Expand Up @@ -64,4 +64,6 @@ public EventItem(EventRecord eventRecord)
/// Indicates if the event is newly loaded.
/// </summary>
public bool IsNewLoaded { get; set; }

public void Dispose() => Record.Dispose();
}
24 changes: 11 additions & 13 deletions EventLook/Model/Progress.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,17 @@

namespace EventLook.Model;

public class ProgressInfo
public class ProgressInfo(IReadOnlyList<EventItem> eventItems, bool isComplete, bool isFirst, int totalEventCount = 0, string message = "")
{
public ProgressInfo(IReadOnlyList<EventItem> eventItems, bool isComplete, bool isFirst, string message = "")
{
LoadedEvents = eventItems;
IsComplete = isComplete;
IsFirst = isFirst;
Message = message;
}
public IReadOnlyList<EventItem> LoadedEvents { get; } = eventItems;

public IReadOnlyList<EventItem> LoadedEvents { get; }

public bool IsComplete { get; }
public bool IsFirst { get; }
public string Message { get; set; }
public bool IsComplete { get; } = isComplete;
public bool IsFirst { get; } = isFirst;
/// <summary>
/// Record count information for the local Event Log.
/// Can be 0 if the count is not determined yet or not applicable.
/// For example, when auto refresh is on, or for a .evtx file, it's always 0.
/// </summary>
public int RecordCountInfo { get; } = totalEventCount;
public string Message { get; } = message;
}
10 changes: 6 additions & 4 deletions EventLook/View/Converter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,18 +176,20 @@ public object ConvertBack(object value, Type targetType, object parameter, Cultu

/// <summary>
/// Generates the status text based on the various indicators.
/// 0: IsUpdating, 1: IsAutoRefreshing, 2: IsAppend, 3: LoadedEventCount, 4: AppendCount, 5: LastElapsedTime, 6: ErrorMessage
/// 0: IsUpdating, 1: IsAutoRefreshing, 2: IsAppend, 3: LoadedEventCount, 4: TotalEventCount, 5: AppendCount, 6: LastElapsedTime, 7: ErrorMessage
/// </summary>
public class StatusTextConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values[0] is bool isUpdating && values[1] is bool isAutoRefreshing && values[2] is bool isAppend
&& values[3] is int loadedEventCount && values[4] is int appendCount
&& values[5] is TimeSpan lastEpasedTime && values[6] is string errorMessage)
&& values[3] is int loadedEventCount && values[4] is int totalEventCount && values[5] is int appendCount
&& values[6] is TimeSpan lastEpasedTime && values[7] is string errorMessage)
{
if (isUpdating)
return $"Loading {loadedEventCount} events... {errorMessage}";
return totalEventCount > 0
? $"Loading {loadedEventCount}/{totalEventCount} events... {errorMessage}"
: $"Loading {loadedEventCount} events... {errorMessage}";
else if (isAutoRefreshing)
return $"{loadedEventCount} events loaded. Waiting for new events... {errorMessage}";
else
Expand Down
1 change: 1 addition & 0 deletions EventLook/View/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
<Binding Path="IsAutoRefreshing"/>
<Binding Path="IsAppend"/>
<Binding Path="LoadedEventCount"/>
<Binding Path="TotalEventCount"/>
<Binding Path="AppendCount"/>
<Binding Path="LastElapsedTime"/>
<Binding Path="ErrorMessage"/>
Expand Down
1 change: 1 addition & 0 deletions EventLook/View/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ private void MenuItem_About_Click(object sender, RoutedEventArgs e)
vm.HyperlinkText = "https://github.com/kmaki565/EventLook";

vm.Window.Content = about;
vm.Window.Owner = this; // To make the child window always on top of this window.
vm.Window.ShowDialog();
}
}
52 changes: 36 additions & 16 deletions EventLook/ViewModel/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ public Model.Range SelectedRange

SetProperty(ref selectedRange, value);

if (readyToRefresh && !selectedRange.IsCustom)
if (selectedRange.IsCustom)
TurnOffAutoRefresh(); // We do not support auto refresh for custom range.
else if (readyToRefresh)
Refresh(reset: false);
}
}
Expand All @@ -147,6 +149,9 @@ public bool IsAutoRefreshing
private int loadedEventCount = 0;
public int LoadedEventCount { get => loadedEventCount; set => SetProperty(ref loadedEventCount, value); }

private int totalEventCount = 0;
public int TotalEventCount { get => totalEventCount; set => SetProperty(ref totalEventCount, value); }

private bool isAppend = false;
public bool IsAppend { get => isAppend; set => SetProperty(ref isAppend, value); }

Expand Down Expand Up @@ -193,14 +198,13 @@ public bool IsAutoRefreshEnabled
SetProperty(ref isAutoRefreshEnabled, value);
if (value)
{
if (SelectedLogSource?.PathType == PathType.LogName)
// We do not support auto refresh for custom range.
if (!SelectedRange.IsCustom && SelectedLogSource?.PathType == PathType.LogName)
Refresh(reset: false, append: true); // Fast refresh will kick auto refresh
}
else
{
DataService.UnsubscribeEvents();
IsAutoRefreshing = false;
IsAppend = false;
TurnOffAutoRefresh();
}
}
}
Expand Down Expand Up @@ -389,10 +393,6 @@ private void UpdateDateTimeInUi()
if (!IsAppend)
FromDateTime = ToDateTime - TimeSpan.FromDays(SelectedRange.DaysFromNow);
}

// These seem necessary to ensure DateTimePicker be updated
OnPropertyChanged(nameof(FromDateTime));
OnPropertyChanged(nameof(ToDateTime));
}
private async Task LoadEvents()
{
Expand All @@ -411,6 +411,7 @@ private async Task Update(Task task)
{
ongoingTask = task;
stopwatch.Restart();
TotalEventCount = 0;
if (!IsAppend)
LoadedEventCount = 0;
IsUpdating = true;
Expand All @@ -436,7 +437,10 @@ private void ProgressCallback(ProgressInfo progressInfo)
else
{
if (progressInfo.IsFirst)
{
Events.DisposeAll();
Events.Clear();
}

foreach (var evt in progressInfo.LoadedEvents)
{
Expand All @@ -445,19 +449,16 @@ private void ProgressCallback(ProgressInfo progressInfo)
}

LoadedEventCount = Events.Count;
// Disregard unless the range is "All time".
TotalEventCount = (SelectedRange.DaysFromNow == 0 && !SelectedRange.IsCustom) ? progressInfo.RecordCountInfo : 0;
ErrorMessage = progressInfo.Message;

if (progressInfo.IsComplete)
{
isLastReadSuccess = Events.Any() && progressInfo.Message == "";
if (isLastReadSuccess && IsAutoRefreshEnabled && SelectedLogSource?.PathType == PathType.LogName)
if (isLastReadSuccess && IsAutoRefreshEnabled && !SelectedRange.IsCustom && SelectedLogSource?.PathType == PathType.LogName)
{
// Fast refresh should be done once before enabling auto refresh.
// Otherwise we'll miss events that came during loading the entire logs.
if (!IsAppend)
Refresh(reset: false, append: true);
DataService.SubscribeEvents(SelectedLogSource, progressAutoRefresh);
IsAutoRefreshing = true;
TurnOnAutoRefresh();
}
}
}
Expand All @@ -469,8 +470,27 @@ private void AutoRefreshCallback(ProgressInfo progressInfo)
InsertEvents(progressInfo.LoadedEvents); // Single event should be loaded at a time.
LoadedEventCount = Events.Count;
filters.ForEach(f => f.Refresh(Events, reset: false));
// If the range is like "Last x days", just adjust appearance of the date time picker.
if (!SelectedRange.IsCustom && SelectedRange.DaysFromNow != 0)
ToDateTime = DateTime.Now;
}
}
private void TurnOnAutoRefresh()
{
// Fast refresh should be done once before enabling auto refresh.
// Otherwise we'll miss events that came during loading the entire logs.
if (!IsAppend)
Refresh(reset: false, append: true);
DataService.SubscribeEvents(SelectedLogSource, progressAutoRefresh);
TotalEventCount = 0;
IsAutoRefreshing = true;
}
private void TurnOffAutoRefresh()
{
DataService.UnsubscribeEvents();
IsAutoRefreshing = false;
IsAppend = false;
}
private int InsertEvents(IEnumerable<EventItem> events)
{
int count = 0;
Expand Down
2 changes: 1 addition & 1 deletion EventLookPackage/Package.appxmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<Identity
Name="64247kmaki565.323654BB1C7D"
Publisher="CN=B5234934-E68F-4911-8E10-60FECC338A02"
Version="1.5.0.0" />
Version="1.5.1.0" />

<Properties>
<DisplayName>EventLook</DisplayName>
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The inbox Windows Event Viewer is a great app that provides comprehensive functi
- Overview events with Event Log messages
- Asynchronous event fetching for quick glance
- Provides quicker sort, specifying time range, and filters
- Supports auto refresh with new events highlighted
- Provides access to all Event Logs in local machine, including Applications and Services Logs
- Supports .evtx file (open from Explorer or drag & drop .evtx file)
- Double click to view event details in XML format
Expand Down
Loading