Skip to content

Instantly share code, notes, and snippets.

@jnm2
Last active July 3, 2025 18:08
Show Gist options
  • Save jnm2/5dcd7c8f9392da62ddcc0b42aca48984 to your computer and use it in GitHub Desktop.
Save jnm2/5dcd7c8f9392da62ddcc0b42aca48984 to your computer and use it in GitHub Desktop.
using Microsoft.Win32;
using Microsoft.Win32.SafeHandles;
using System;
using System.ComponentModel;
using System.Threading;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Registry;
/// <summary>
/// <para>
/// Raises the <see cref="Changed"/> event on changes to the attributes or contents of a specified registry key.
/// </para>
/// <para>
/// See the documentation for the <see href="https://learn.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regnotifychangekeyvalue"><c>RegNotifyChangeKeyValue </c></see> Windows API for details.
/// </para>
/// </summary>
public sealed class RegistryWatcher : IDisposable
{
private readonly object lockObject = new();
private bool isDisposed;
private AutoResetEvent? changedEvent;
private RegisteredWaitHandle? registeredWaitHandle;
private EventHandler? changed;
private readonly SafeRegistryHandle keyHandle;
private readonly bool watchSubtree;
/// <param name="registryKey">
/// <para>
/// The registry key to watch for changes.
/// </para>
/// <para>
/// <paramref name="registryKey"/> may be disposed after the constructor returns. Its handle is duplicated for change subscription.
/// </para>
/// </param>
/// <param name="watchSubtree">
/// If <see langword="true"/>, the function reports changes in the specified key and its subkeys. If <see langword="false"/>, the function reports changes only in the specified key.
/// </param>
public RegistryWatcher(RegistryKey registryKey, bool watchSubtree)
{
keyHandle = DuplicateHandle(registryKey.Handle);
this.watchSubtree = watchSubtree;
}
private static SafeRegistryHandle DuplicateHandle(SafeRegistryHandle handle)
{
var currentProcessHandle = PInvoke.GetCurrentProcess();
HANDLE duplicatedHandle;
var addRef = false;
try
{
handle.DangerousAddRef(ref addRef);
unsafe
{
if (!PInvoke.DuplicateHandle(
hSourceProcessHandle: currentProcessHandle,
(HANDLE)handle.DangerousGetHandle(),
hTargetProcessHandle: currentProcessHandle,
&duplicatedHandle,
dwDesiredAccess: 0,
bInheritHandle: false,
DUPLICATE_HANDLE_OPTIONS.DUPLICATE_SAME_ACCESS))
{
throw new Win32Exception();
}
}
}
finally
{
if (addRef) handle.DangerousRelease();
}
return new SafeRegistryHandle(duplicatedHandle, ownsHandle: true);
}
public event EventHandler? Changed
{
add
{
lock (lockObject)
{
if (isDisposed) throw new ObjectDisposedException(nameof(RegistryWatcher));
changed += value;
if (registeredWaitHandle is null)
{
changedEvent = new AutoResetEvent(initialState: false);
registeredWaitHandle = ThreadPool.RegisterWaitForSingleObject(changedEvent, OnRegistryChange, state: null, Timeout.InfiniteTimeSpan, executeOnlyOnce: false);
SubscribeToNextRegistryChange();
}
}
}
remove
{
lock (lockObject)
{
changed -= value;
}
}
}
private void SubscribeToNextRegistryChange()
{
var error = PInvoke.RegNotifyChangeKeyValue(
keyHandle,
watchSubtree,
REG_NOTIFY_FILTER.REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_FILTER.REG_NOTIFY_CHANGE_LAST_SET | REG_NOTIFY_FILTER.REG_NOTIFY_THREAD_AGNOSTIC,
changedEvent!.SafeWaitHandle,
fAsynchronous: true);
if (error != 0)
throw new Win32Exception((int)error);
}
private void OnRegistryChange(object? state, bool timedOut)
{
SubscribeToNextRegistryChange();
changed?.Invoke(this, EventArgs.Empty);
}
public void Dispose()
{
lock (lockObject)
{
registeredWaitHandle?.Unregister(null);
changedEvent?.Dispose();
isDisposed = true;
}
// This cancels any pending wait.
keyHandle.Dispose();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment