mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-26 05:29:43 +00:00
Changed RegistryWatcher to Thread agnostic
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AutopilotMonitor.Agent.Core.Monitoring.Interop;
|
||||
using Microsoft.Win32;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
@@ -10,72 +9,83 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Watches a registry key for changes using RegNotifyChangeKeyValue.
|
||||
/// Fires the Changed event on each detected change.
|
||||
///
|
||||
/// Usage:
|
||||
/// var watcher = new RegistryWatcher(RegistryHive.LocalMachine, @"SOFTWARE\...", watchSubtree: true);
|
||||
/// watcher.Changed += (s, e) => { /* read values */ };
|
||||
/// watcher.Start();
|
||||
/// // later:
|
||||
/// await watcher.StopAsync();
|
||||
/// watcher.Dispose();
|
||||
///
|
||||
/// Important: one change per registration — the watcher re-registers automatically after each notification.
|
||||
/// The watched scope is a key (including values); to track a specific value, compare in the Changed handler.
|
||||
/// Raises Changed whenever the watched key/subtree changes.
|
||||
///
|
||||
/// Notes:
|
||||
/// - Notification is for the key scope, not one individual value.
|
||||
/// - To track a specific value, read/compare that value in the Changed handler.
|
||||
/// - Uses a dedicated background thread for robustness.
|
||||
/// </summary>
|
||||
internal sealed class RegistryWatcher : IDisposable
|
||||
{
|
||||
private readonly UIntPtr _hive;
|
||||
private readonly string _subKey;
|
||||
private readonly bool _watchSubtree;
|
||||
private readonly RegistryView _view;
|
||||
private readonly RegistryNativeMethods.RegChangeNotifyFilter _filter;
|
||||
|
||||
private CancellationTokenSource _cts;
|
||||
private Task _watchTask;
|
||||
private readonly object _sync = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the watched key (or subtree) changes. Handler runs on a thread pool thread.
|
||||
/// </summary>
|
||||
public event EventHandler Changed;
|
||||
private CancellationTokenSource _cts;
|
||||
private Thread _thread;
|
||||
private Exception _backgroundException;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the Changed handler throws. Allows the consumer to log without crashing the watcher.
|
||||
/// </summary>
|
||||
public event EventHandler Changed;
|
||||
public event EventHandler<Exception> Error;
|
||||
|
||||
public RegistryWatcher(
|
||||
RegistryHive hive,
|
||||
string subKey,
|
||||
bool watchSubtree = false,
|
||||
RegistryView view = RegistryView.Default,
|
||||
RegistryNativeMethods.RegChangeNotifyFilter filter =
|
||||
RegistryNativeMethods.RegChangeNotifyFilter.LastSet)
|
||||
RegistryNativeMethods.RegChangeNotifyFilter.LastSet |
|
||||
RegistryNativeMethods.RegChangeNotifyFilter.ThreadAgnostic)
|
||||
{
|
||||
_hive = HiveToPointer(hive);
|
||||
_subKey = subKey ?? throw new ArgumentNullException(nameof(subKey));
|
||||
_watchSubtree = watchSubtree;
|
||||
_view = view;
|
||||
_filter = filter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts watching on a background thread pool thread.
|
||||
/// Throws Win32Exception if the registry key cannot be opened.
|
||||
/// </summary>
|
||||
public bool IsRunning
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return _thread != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (_watchTask != null)
|
||||
if (_thread != null)
|
||||
throw new InvalidOperationException("Watcher is already running.");
|
||||
|
||||
_backgroundException = null;
|
||||
_cts = new CancellationTokenSource();
|
||||
_watchTask = Task.Run(() => WatchLoopAsync(_cts.Token));
|
||||
|
||||
_thread = new Thread(WatchThreadMain)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = $"RegistryWatcher: {_subKey}"
|
||||
};
|
||||
|
||||
_thread.Start(_cts.Token);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Non-blocking stop request. Safe to call from within the Changed handler
|
||||
/// (does not wait for the loop to exit, so no deadlock).
|
||||
/// Requests stop without blocking.
|
||||
/// Safe to call from Changed handlers.
|
||||
/// </summary>
|
||||
public void RequestStop()
|
||||
{
|
||||
@@ -86,59 +96,107 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the watcher and waits for the background loop to exit.
|
||||
/// Do NOT call from within the Changed handler — use RequestStop() instead.
|
||||
/// Stops and waits for the watcher thread to exit.
|
||||
/// Avoid calling from inside Changed; use RequestStop there.
|
||||
/// </summary>
|
||||
public async Task StopAsync()
|
||||
public void Stop()
|
||||
{
|
||||
Task taskToWait;
|
||||
Thread threadToJoin = null;
|
||||
CancellationTokenSource ctsToDispose = null;
|
||||
Exception backgroundException = null;
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (_cts == null)
|
||||
if (_thread == null)
|
||||
return;
|
||||
|
||||
_cts.Cancel();
|
||||
taskToWait = _watchTask;
|
||||
threadToJoin = _thread;
|
||||
}
|
||||
|
||||
if (taskToWait != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await taskToWait.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Win32Exception) { }
|
||||
}
|
||||
threadToJoin.Join();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
_cts?.Dispose();
|
||||
backgroundException = _backgroundException;
|
||||
|
||||
ctsToDispose = _cts;
|
||||
_cts = null;
|
||||
_watchTask = null;
|
||||
_thread = null;
|
||||
_backgroundException = null;
|
||||
}
|
||||
|
||||
ctsToDispose?.Dispose();
|
||||
|
||||
if (backgroundException != null &&
|
||||
!(backgroundException is OperationCanceledException))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Registry watcher stopped because the background thread failed.",
|
||||
backgroundException);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WatchLoopAsync(CancellationToken cancellationToken)
|
||||
private void WatchThreadMain(object state)
|
||||
{
|
||||
var cancellationToken = (CancellationToken)state;
|
||||
|
||||
try
|
||||
{
|
||||
WatchLoop(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_backgroundException = ex;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_backgroundException = ex;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Error?.Invoke(this, ex);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Never let handler exceptions escape the background thread.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WatchLoop(CancellationToken cancellationToken)
|
||||
{
|
||||
int samDesired = RegistryNativeMethods.GetSamDesired(_view);
|
||||
|
||||
int openResult = RegistryNativeMethods.RegOpenKeyEx(
|
||||
_hive, _subKey, 0, RegistryNativeMethods.KEY_NOTIFY,
|
||||
_hive,
|
||||
_subKey,
|
||||
0,
|
||||
samDesired,
|
||||
out SafeRegistryHandle keyHandle);
|
||||
|
||||
if (openResult != 0)
|
||||
throw new Win32Exception(openResult, $"RegOpenKeyEx failed for '{_subKey}'");
|
||||
{
|
||||
throw new Win32Exception(
|
||||
openResult,
|
||||
$"RegOpenKeyEx failed for '{_subKey}' (view: {_view}).");
|
||||
}
|
||||
|
||||
using (keyHandle)
|
||||
using (var changedEvent = new AutoResetEvent(false))
|
||||
using (var cancelEvent = new ManualResetEvent(false))
|
||||
using (cancellationToken.Register(() => cancelEvent.Set()))
|
||||
{
|
||||
var waitHandles = new WaitHandle[] { changedEvent, cancelEvent };
|
||||
WaitHandle[] waitHandles = { changedEvent, cancelEvent };
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Register for exactly ONE change notification
|
||||
int notifyResult = RegistryNativeMethods.RegNotifyChangeKeyValue(
|
||||
keyHandle,
|
||||
_watchSubtree,
|
||||
@@ -147,25 +205,35 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
true);
|
||||
|
||||
if (notifyResult != 0)
|
||||
throw new Win32Exception(notifyResult, "RegNotifyChangeKeyValue failed");
|
||||
{
|
||||
throw new Win32Exception(
|
||||
notifyResult,
|
||||
$"RegNotifyChangeKeyValue failed for '{_subKey}'.");
|
||||
}
|
||||
|
||||
// Wait for change or cancellation
|
||||
int signaled = WaitHandle.WaitAny(waitHandles);
|
||||
|
||||
if (signaled == 1)
|
||||
break; // cancel requested
|
||||
if (signaled == WaitHandle.WaitTimeout)
|
||||
continue;
|
||||
|
||||
if (signaled == 1)
|
||||
break;
|
||||
|
||||
// Notify consumer
|
||||
try
|
||||
{
|
||||
Changed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Error?.Invoke(this, ex);
|
||||
try
|
||||
{
|
||||
Error?.Invoke(this, ex);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore secondary error-handler failures.
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Yield();
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -174,29 +242,44 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
StopAsync().GetAwaiter().GetResult();
|
||||
Stop();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Dispose must not throw
|
||||
// Dispose must not throw.
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
// ===== Helpers =====
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(RegistryWatcher));
|
||||
}
|
||||
|
||||
private static UIntPtr HiveToPointer(RegistryHive hive)
|
||||
{
|
||||
switch (hive)
|
||||
{
|
||||
case RegistryHive.ClassesRoot: return (UIntPtr)0x80000000u;
|
||||
case RegistryHive.CurrentUser: return (UIntPtr)0x80000001u;
|
||||
case RegistryHive.LocalMachine: return (UIntPtr)0x80000002u;
|
||||
case RegistryHive.Users: return (UIntPtr)0x80000003u;
|
||||
case RegistryHive.CurrentConfig: return (UIntPtr)0x80000005u;
|
||||
default: throw new ArgumentOutOfRangeException(nameof(hive));
|
||||
case RegistryHive.ClassesRoot:
|
||||
return (UIntPtr)0x80000000u;
|
||||
case RegistryHive.CurrentUser:
|
||||
return (UIntPtr)0x80000001u;
|
||||
case RegistryHive.LocalMachine:
|
||||
return (UIntPtr)0x80000002u;
|
||||
case RegistryHive.Users:
|
||||
return (UIntPtr)0x80000003u;
|
||||
case RegistryHive.CurrentConfig:
|
||||
return (UIntPtr)0x80000005u;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(hive));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,60 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace AutopilotMonitor.Agent.Core.Monitoring.Interop
|
||||
{
|
||||
/// <summary>
|
||||
/// P/Invoke declarations for registry change notification via RegNotifyChangeKeyValue.
|
||||
/// Uses SafeRegistryHandle and SafeWaitHandle for proper handle lifecycle management.
|
||||
/// Used by RegistryWatcher to detect registry changes instantly.
|
||||
/// </summary>
|
||||
internal static class RegistryNativeMethods
|
||||
{
|
||||
public const int KEY_NOTIFY = 0x0010;
|
||||
public const int KEY_WOW64_64KEY = 0x0100;
|
||||
public const int KEY_WOW64_32KEY = 0x0200;
|
||||
|
||||
[Flags]
|
||||
public enum RegChangeNotifyFilter : uint
|
||||
{
|
||||
Name = 1,
|
||||
Attributes = 2,
|
||||
LastSet = 4,
|
||||
Security = 8
|
||||
Name = 0x00000001,
|
||||
Attributes = 0x00000002,
|
||||
LastSet = 0x00000004,
|
||||
Security = 0x00000008,
|
||||
|
||||
// Supported on Windows 8+
|
||||
ThreadAgnostic = 0x10000000
|
||||
}
|
||||
|
||||
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
public static extern int RegOpenKeyEx(
|
||||
UIntPtr hKey, string lpSubKey, uint ulOptions,
|
||||
int samDesired, out SafeRegistryHandle phkResult);
|
||||
UIntPtr hKey,
|
||||
string lpSubKey,
|
||||
uint ulOptions,
|
||||
int samDesired,
|
||||
out SafeRegistryHandle phkResult);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
public static extern int RegNotifyChangeKeyValue(
|
||||
SafeRegistryHandle hKey, bool bWatchSubtree,
|
||||
SafeRegistryHandle hKey,
|
||||
bool bWatchSubtree,
|
||||
RegChangeNotifyFilter dwNotifyFilter,
|
||||
SafeWaitHandle hEvent, bool fAsynchronous);
|
||||
SafeWaitHandle hEvent,
|
||||
bool fAsynchronous);
|
||||
|
||||
public static int GetSamDesired(RegistryView view)
|
||||
{
|
||||
switch (view)
|
||||
{
|
||||
case RegistryView.Registry64:
|
||||
return KEY_NOTIFY | KEY_WOW64_64KEY;
|
||||
|
||||
case RegistryView.Registry32:
|
||||
return KEY_NOTIFY | KEY_WOW64_32KEY;
|
||||
|
||||
case RegistryView.Default:
|
||||
return KEY_NOTIFY;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(view));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
424
|
||||
426
|
||||
|
||||
Reference in New Issue
Block a user