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
16 changes: 16 additions & 0 deletions src/EventLogExpert.EventDbTool/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.CommandLine;
using System.Security.Principal;

namespace EventLogExpert.EventDbTool;

Expand All @@ -17,6 +18,13 @@ internal static ServiceProvider BuildServiceProvider(bool verbose) =>

private static async Task<int> Main(string[] args)
{
if (!IsElevated())
{
await Console.Error.WriteLineAsync(
"WARNING: Running without administrator privileges. " +
"Some provider registry operations may fail or return incomplete results.");
}

RootCommand rootCommand = new("Tool used to create and modify databases for use with EventLogExpert");

rootCommand.Subcommands.Add(ShowCommand.GetCommand());
Expand All @@ -27,4 +35,12 @@ private static async Task<int> Main(string[] args)

return await rootCommand.Parse(args).InvokeAsync();
}

private static bool IsElevated()
{
using var identity = WindowsIdentity.GetCurrent();

return new WindowsPrincipal(identity)
.IsInRole(WindowsBuiltInRole.Administrator);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ internal sealed class CurrentVersionProvider(
IPackageVersionProvider packageVersionProvider,
IWindowsIdentityProvider identityProvider) : ICurrentVersionProvider
{
private readonly IWindowsIdentityProvider _identityProvider = identityProvider;
private readonly Lazy<bool> _isAdmin = new(identityProvider.IsUserInAdministratorRole);
private readonly IPackageVersionProvider _packageVersionProvider = packageVersionProvider;

public Version CurrentVersion => _packageVersionProvider.GetPackageVersion();

public bool IsAdmin => _identityProvider.IsUserInAdministratorRole();
public bool IsAdmin => _isAdmin.Value;

public bool IsDevBuild => CurrentVersion.Major <= 1;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,22 @@
using EventLogExpert.Runtime.FilterCache;
using EventLogExpert.Runtime.FilterGroup;
using EventLogExpert.Runtime.FilterPane;
using EventLogExpert.Filtering.Services;
using EventLogExpert.Runtime.LogTable;
using EventLogExpert.Runtime.Menu;
using EventLogExpert.Runtime.Modal;
using EventLogExpert.Runtime.Settings;
using EventLogExpert.Runtime.Update;
using EventLogExpert.Runtime.Update.Deployment;
using Effects = EventLogExpert.Runtime.EventLog.Effects;

namespace Microsoft.Extensions.DependencyInjection;

public static class RuntimeServiceCollectionExtensions
{
/// <summary>
/// Registers the runtime tier's services. Callers MUST also call <see cref="FilteringServiceCollectionExtensions.AddEventLogFiltering" />
/// before resolving the resulting provider — the runtime tier's Fluxor-scanned <c>EventLog.Effects</c> class
/// depends on <c>IFilterService</c>, which is owned by the filtering tier. Omitting it produces a hard-to-diagnose
/// DI resolution failure at the first action dispatch.
/// Registers the runtime tier's services. Callers MUST also call AddEventLogFiltering before resolving the
/// resulting provider — the runtime tier's Fluxor-scanned effect classes depend on <c>IFilterService</c>, which is
/// owned by the filtering tier. Omitting it produces a hard-to-diagnose DI resolution failure at the first action
/// dispatch.
/// </summary>
public static IServiceCollection AddEventLogRuntime(this IServiceCollection services)
{
Expand All @@ -43,10 +41,15 @@ public static IServiceCollection AddEventLogRuntime(this IServiceCollection serv
services.AddSingleton<IFilterPaneCommands, FilterPaneCommands>();
services.AddSingleton<ILogTableCommands, LogTableCommands>();

// Shared coordination state for EventLog effects classes.
services.AddSingleton<LogCloseCoordinator>();
services.AddSingleton<EventLogConcurrencyState>();

// UI capabilities.
services.AddSingleton<IHighlightSelector, HighlightSelector>();
services.AddSingleton<ILogTableColumnDefaultsProvider, ColumnDefaults>();
services.AddSingleton<ILogReloadCoordinator>(static sp => sp.GetRequiredService<Effects>());
services.AddSingleton<DatabaseCoordinationEffects>();
services.AddSingleton<ILogReloadCoordinator>(static sp => sp.GetRequiredService<DatabaseCoordinationEffects>());

// Application services.
services.AddSingleton<IAppTitleService, AppTitleService>();
Expand Down
111 changes: 111 additions & 0 deletions src/EventLogExpert.Runtime/EventLog/DatabaseCoordinationEffects.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

using EventLogExpert.Eventing.Common.Channels;
using EventLogExpert.Eventing.Common.EventLogs;
using EventLogExpert.Eventing.Logging;
using Fluxor;
using IDispatcher = Fluxor.IDispatcher;

namespace EventLogExpert.Runtime.EventLog;

internal sealed class DatabaseCoordinationEffects(
IState<EventLogState> eventLogState,
ITraceLogger logger,
LogCloseCoordinator closeCoordinator,
IDispatcher dispatcher) : ILogReloadCoordinator
{
private readonly LogCloseCoordinator _closeCoordinator = closeCoordinator;
private readonly IDispatcher _dispatcher = dispatcher;
private readonly IState<EventLogState> _eventLogState = eventLogState;
private readonly ITraceLogger _logger = logger;

public async Task PrepareForDatabaseRemovalAsync(
LogReopenSnapshot snapshot,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(snapshot);

var activeLogs = _eventLogState.Value.ActiveLogs.Values.ToList();

if (activeLogs.Count == 0) { return; }

await _closeCoordinator.AcquireCoordinatorLockAsync(cancellationToken);

try
{
var reloadNames = activeLogs.Select(l => l.Name).ToHashSet(StringComparer.Ordinal);

var selectionByLog = _eventLogState.Value.SelectedEvents
.Where(e => e.RecordId.HasValue && reloadNames.Contains(e.OwningLog))
.GroupBy(e => e.OwningLog)
.ToDictionary(g => g.Key, g => (IReadOnlySet<long>)g.Select(e => e.RecordId!.Value).ToHashSet());

var selectedRecordId = _eventLogState.Value.SelectedEvent?.RecordId;
var selectedLogName = _eventLogState.Value.SelectedEvent?.OwningLog;

if (selectedRecordId.HasValue &&
!string.IsNullOrEmpty(selectedLogName) &&
reloadNames.Contains(selectedLogName) &&
!selectionByLog.ContainsKey(selectedLogName))
{
selectionByLog[selectedLogName] = new HashSet<long>();
}

var waiters = new List<(EventLogId Id, string Name, LogPathType Type, Task Task)>(activeLogs.Count);

foreach (var log in activeLogs)
{
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
_closeCoordinator.RegisterCloseCompletion(log.Id, tcs);
waiters.Add((log.Id, log.Name, log.Type, tcs.Task));
}

foreach (var (id, name, _, _) in waiters)
{
_dispatcher.Dispatch(new CloseLogAction(id, name));
}

foreach (var (id, name, type, task) in waiters)
{
try
{
await task.WaitAsync(LogCloseCoordinator.LogCloseTimeout, cancellationToken);

snapshot.Add(new LogReopenInfo(name, type));
}
catch (Exception ex) when (ex is TimeoutException or OperationCanceledException)
{
_closeCoordinator.RemoveStrandedCompletion(id);

_logger.Trace(
$"{nameof(PrepareForDatabaseRemovalAsync)}: close for log '{name}' did not complete: {ex.GetType().Name}");

throw;
}
}

foreach (var (name, ids) in selectionByLog)
{
long? selectedIdForLog = string.Equals(name, selectedLogName, StringComparison.Ordinal) ?
selectedRecordId : null;

_closeCoordinator.WritePendingRestore(name, new PendingSelectionRestore(ids, selectedIdForLog));
}
}
finally
{
_closeCoordinator.ReleaseCoordinatorLock();
}
}

public void ReopenAfterDatabaseRemoval(IReadOnlyList<LogReopenInfo> snapshot)
{
ArgumentNullException.ThrowIfNull(snapshot);

foreach (var entry in snapshot)
{
_dispatcher.Dispatch(new OpenLogAction(entry.Name, entry.Type));
}
}
}
Loading