Skip to content

Commit

Permalink
➕ [#25] human/machine-readable output options (#29)
Browse files Browse the repository at this point in the history
➕ "list" program now supports different output formats
➕ JSON output format display

🧹 code cleanup
  • Loading branch information
nop77svk authored Sep 12, 2024
1 parent 20c3bd8 commit 483b366
Show file tree
Hide file tree
Showing 12 changed files with 373 additions and 242 deletions.
18 changes: 8 additions & 10 deletions wtwd.Model/PcSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,22 @@ public class PcSession

public TimeSpan IdleStartSpan { get => SessionLastStart.When.Subtract(SessionFirstStart.When); }
public IEnumerable<PcStateChangeEvent> StartEventsOrdered
{
get => _sessionStart
=> _sessionStart
.DistinctBy(x => x.Value.Event)
.OrderBy(x => x.Value.When)
.Select(x => x.Value.Event);
}

public bool IsStillRunning { get => !_sessionEnd.Any(); }
public TimeSpan? ShortSessionSpan { get => SessionFirstEnd?.When.Subtract(SessionLastStart.When); }
public TimeSpan? FullSessionSpan { get => SessionLastEnd?.When.Subtract(SessionFirstStart.When); }
public TimeSpan IdleEndSpan { get => SessionLastEnd?.When.Subtract(SessionFirstEnd?.When ?? DateTime.Now) ?? TimeSpan.Zero; }
public IEnumerable<PcStateChangeEvent> EndEventsOrdered
{
get => _sessionEnd
.DistinctBy(x => x.Value.Event)
.OrderBy(x => x.Value.When)
.Select(x => x.Value.Event);
}
public IEnumerable<PcStateChangeEvent>? EndEventsOrdered
=> _sessionEnd.Any()
? _sessionEnd
.DistinctBy(x => x.Value.Event)
.OrderBy(x => x.Value.When)
.Select(x => x.Value.Event)
: null;

public PcSession()
{
Expand Down
26 changes: 13 additions & 13 deletions wtwd.Model/PcStateChangeEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ namespace NoP77svk.wtwd.Model;

public record PcStateChangeEvent(PcStateChangeHow How, PcStateChangeWhat What)
{
public string? AsString
{
get => (How, What) switch
public PcStateChangeEventName AsEventName
=> (How, What) switch
{
(PcStateChangeHow.ShutdownOrStartup, PcStateChangeWhat.On) => "startup",
(PcStateChangeHow.ShutdownOrStartup, PcStateChangeWhat.Off) => "shutdown",
(PcStateChangeHow.Hibernate, PcStateChangeWhat.On) => "kickstart",
(PcStateChangeHow.Hibernate, PcStateChangeWhat.Off) => "hibernate",
(PcStateChangeHow.SleepOrWakeUp, PcStateChangeWhat.On) => "wakeup",
(PcStateChangeHow.SleepOrWakeUp, PcStateChangeWhat.Off) => "sleep",
(PcStateChangeHow.LockOrUnlock, PcStateChangeWhat.On) => "unlock",
(PcStateChangeHow.LockOrUnlock, PcStateChangeWhat.Off) => "lock",
_ => null
(PcStateChangeHow.ShutdownOrStartup, PcStateChangeWhat.On) => PcStateChangeEventName.Startup,
(PcStateChangeHow.ShutdownOrStartup, PcStateChangeWhat.Off) => PcStateChangeEventName.Shutdown,
(PcStateChangeHow.Hibernate, PcStateChangeWhat.On) => PcStateChangeEventName.KickStart,
(PcStateChangeHow.Hibernate, PcStateChangeWhat.Off) => PcStateChangeEventName.Hibernate,
(PcStateChangeHow.SleepOrWakeUp, PcStateChangeWhat.On) => PcStateChangeEventName.WakeUp,
(PcStateChangeHow.SleepOrWakeUp, PcStateChangeWhat.Off) => PcStateChangeEventName.Sleep,
(PcStateChangeHow.LockOrUnlock, PcStateChangeWhat.On) => PcStateChangeEventName.Unknown,
(PcStateChangeHow.LockOrUnlock, PcStateChangeWhat.Off) => PcStateChangeEventName.Lock,
_ => PcStateChangeEventName.Unknown
};
}

public string AsString => this.AsEventName.ToString();
}
14 changes: 14 additions & 0 deletions wtwd.Model/PcStateChangeEventName.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace NoP77svk.wtwd.Model;

public enum PcStateChangeEventName
{
Unknown,
Startup,
Shutdown,
KickStart,
Hibernate,
WakeUp,
Sleep,
Unlock,
Lock
}
107 changes: 107 additions & 0 deletions wtwd.cli.List/EventLogProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#pragma warning disable CA1416
namespace NoP77svk.wtwd.cli.List;

using System.Collections.Generic;
using System.Diagnostics.Eventing.Reader;
using System.Linq;
using System.Xml.Linq;

using NoP77svk.wtwd.Model;
using NoP77svk.wtwd.Model.Xform;
using NoP77svk.wtwd.Utilities;

internal static class EventLogProcessor
{
internal static IEnumerable<EventRecord> GetEventLogsSince(DateTime since)
{
string sinceAsStr = since.ToUniversalTime().ToString("O");

IEnumerable<EventRecord> result = Enumerable.Empty<EventRecord>();

string queryKernelBootStr = $"Event[System[Provider/@Name = 'Microsoft-Windows-Kernel-Boot' and {EventIdsOrExpanded(20, 25, 27)} and TimeCreated/@SystemTime >= '{sinceAsStr}']]";
EventLogQuery queryKernelBoot = new EventLogQuery("System", PathType.LogName, queryKernelBootStr);
result = result.Concat(queryKernelBoot.AsEnumerable());

string queryKernelGeneralStr = $"Event[System[Provider/@Name = 'Microsoft-Windows-Kernel-General' and {EventIdsOrExpanded(12, 13)} and TimeCreated/@SystemTime >= '{sinceAsStr}']]";
EventLogQuery queryKernelGeneral = new EventLogQuery("System", PathType.LogName, queryKernelGeneralStr);
result = result.Concat(queryKernelGeneral.AsEnumerable());

string queryKernelPowerStr = $"Event[System[Provider/@Name = 'Microsoft-Windows-Kernel-Power' and {EventIdsOrExpanded(109, 42, 107, 506, 507)} and TimeCreated/@SystemTime >= '{sinceAsStr}']]";
EventLogQuery queryKernelPower = new EventLogQuery("System", PathType.LogName, queryKernelPowerStr);
result = result.Concat(queryKernelPower.AsEnumerable());

string querySynTpEnhServiceForLockUnlockStr = $"Event[System[Provider/@Name = 'SynTPEnhService' and {EventIdsOrExpanded(0)}] and EventData/Data]";
EventLogQuery querySynTpEnhServiceForLockUnlock = new EventLogQuery("Application", PathType.LogName, querySynTpEnhServiceForLockUnlockStr);
result = result
.Concat(querySynTpEnhServiceForLockUnlock.AsEnumerable()
.Where(evnt => evnt.TimeCreated >= since)
);

string queryExplicitWtwdLockUnlockStr = @$"Event[System[Provider/@Name = '{LockUnlockEventLog.SourceName}' and Task = {LockUnlockEventLog.LockUnlockCategory} and TimeCreated/@SystemTime >= '{sinceAsStr}']]";
EventLogQuery queryExplicitWtwdLockUnlock = new EventLogQuery(LockUnlockEventLog.LogName, PathType.LogName, queryExplicitWtwdLockUnlockStr);

WindowsUser osUser = WindowsUser.Current();
var queryExplicitWtwdLockUnlockTimeFiltered = queryExplicitWtwdLockUnlock.AsEnumerable()
.Where(evnt => evnt.TimeCreated >= since)
.Select(evnt => new ValueTuple<EventRecord, ValueTuple<string?, string?, string?>>(
evnt,
XDocument.Parse(evnt.ToXml())
.Descendants(EventLogConst.XmlNS + "Event")
.Descendants(EventLogConst.XmlNS + "EventData")
.Descendants(EventLogConst.XmlNS + "Data")
.Where(node => node.Value.StartsWith(LockUnlockEventLog.EventDataUserDomainPrefix)
|| node.Value.StartsWith(LockUnlockEventLog.EventDataUserNamePrefix)
|| node.Value.StartsWith(LockUnlockEventLog.EventDataUserSIDPrefix)
)
.Select(node => new ValueTuple<string?, string?, string?>(
node.Value.StartsWith(LockUnlockEventLog.EventDataUserDomainPrefix) ? node.Value.Substring(LockUnlockEventLog.EventDataUserDomainPrefix.Length).Trim() : null,
node.Value.StartsWith(LockUnlockEventLog.EventDataUserNamePrefix) ? node.Value.Substring(LockUnlockEventLog.EventDataUserNamePrefix.Length).Trim() : null,
node.Value.StartsWith(LockUnlockEventLog.EventDataUserSIDPrefix) ? node.Value.Substring(LockUnlockEventLog.EventDataUserSIDPrefix.Length).Trim() : null
))
.Aggregate(
seed: new ValueTuple<string?, string?, string?>(null, null, null),
func: (accumulator, value) => (accumulator.Item1 ?? value.Item1, accumulator.Item2 ?? value.Item2, accumulator.Item3 ?? value.Item3)
)
))
.Where(evntPlus => evntPlus.Item2.Item1 == osUser.Domain && evntPlus.Item2.Item2 == osUser.Name
|| evntPlus.Item2.Item3 == osUser.SID
)
.Select(evntPlus => evntPlus.Item1);

result = result.Concat(queryExplicitWtwdLockUnlockTimeFiltered);

return result.Where(evnt => evnt.TimeCreated >= since);
}

internal static IEnumerable<PcSession> ToPcSessions(this IEnumerable<EventRecord> events, TimeSpan? trimSessionsUnder, bool allowMachineOnlySessions)
{
return events
.Select(evnt => evnt.AsPcStateChange())
.Where(stch => stch.Event.How != PcStateChangeHow.Unknown && stch.Event.What != PcStateChangeWhat.Unknown)
.StateChangesToSessions()
.Where(session => session.IsStillRunning || session.FullSessionSpan != TimeSpan.Zero)
.Where(session => trimSessionsUnder == null
|| session.ShortSessionSpan >= trimSessionsUnder
|| session.IsStillRunning
)
.Where(session => allowMachineOnlySessions
|| session.SessionLastStart.Event.How == PcStateChangeHow.LockOrUnlock
|| session.SessionFirstEnd?.Event.How == PcStateChangeHow.LockOrUnlock
|| session.IsStillRunning
);
}

private static string EventIdsOrExpanded(params int[] ids)
{
IEnumerable<string> idPredicates = ids.Select(id => $"EventID = {id}");

string result = string.Join(" or ", idPredicates);

if (ids.Length > 1)
{
result = $"({result})";
}

return result;
}
}
12 changes: 12 additions & 0 deletions wtwd.cli.List/JsonDisplayPcSessionDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#pragma warning disable SA1313
namespace NoP77svk.wtwd.cli.List;

using System;

internal record JsonDisplayPcSessionDto(DateTime Start, DateTime End)
{
public DateTime? FirstStart { get; init; }
public IEnumerable<string>? StartEventsOrdered { get; init; }
public DateTime? LastEnd { get; init; }
public IEnumerable<string>? EndEventsOrdered { get; init; }
}
3 changes: 3 additions & 0 deletions wtwd.cli.List/ListCLI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ public class ListCli

[Option("ignore-machine-only-sessions", Required = false, HelpText = "Sessions without starting unlock event may be considered purely technical/maintenance. When supplied, supposed \"machine-only\" sessions are not displayed.")]
public bool IgnoreMachineOnlySessions { get; set; } = false;

[Option("format", Required = false, HelpText = "Output format")]
public string? Format { get; set; }
}
65 changes: 40 additions & 25 deletions wtwd.cli.List/ListConfig.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,40 @@
namespace NoP77svk.wtwd.cli.List;
using NoP77svk.wtwd.Utilities;

internal class ListConfig
{
internal static ListConfig FromRawCLI(ListCli cli)
{
return new ListConfig()
{
TrimSessionsUnder = TimeSpanExt.Parse(cli.TrimSessionsUnder),
TrimBreaksUnder = TimeSpanExt.Parse(cli.TrimBreaksUnder),
AllowMachineOnlySessions = !cli.IgnoreMachineOnlySessions
};
}

internal TimeSpan? TrimSessionsUnder { get; init; }

internal TimeSpan? TrimBreaksUnder { get; init; }

internal bool AllowMachineOnlySessions { get; init; }

private ListConfig()
{
}
}
namespace NoP77svk.wtwd.cli.List;
using NoP77svk.wtwd.Utilities;

public class ListConfig
{
internal static ListConfig FromRawCLI(ListCli cli)
{
return new ListConfig()
{
TrimSessionsUnder = TimeSpanExt.Parse(cli.TrimSessionsUnder),
TrimBreaksUnder = TimeSpanExt.Parse(cli.TrimBreaksUnder),
AllowMachineOnlySessions = !cli.IgnoreMachineOnlySessions,
OutputFormat = RecognizeOutputFormat(cli.Format)
};
}

internal TimeSpan? TrimSessionsUnder { get; init; }

internal TimeSpan? TrimBreaksUnder { get; init; }

internal bool AllowMachineOnlySessions { get; init; }

internal ListOutputFormat OutputFormat { get; init; }

private ListConfig()
{
}

private static ListOutputFormat RecognizeOutputFormat(string? outputFormatCli)
{
ListOutputFormat result = ListOutputFormat.PrettyPrint;

if (Enum.TryParse(outputFormatCli?.Replace("-", string.Empty), true, out ListOutputFormat outputFormatParsed))
{
result = outputFormatParsed;
}

return result;
}
}
7 changes: 7 additions & 0 deletions wtwd.cli.List/ListOutputFormat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace NoP77svk.wtwd.cli.List;

internal enum ListOutputFormat
{
PrettyPrint,
JSON
}
Loading

0 comments on commit 483b366

Please sign in to comment.