using Microsoft.Win32;
using Microsoft.Win32.SafeHandles;
using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Threading;
using Vanara.PInvoke;
using static Vanara.PInvoke.AdvApi32;
namespace Vanara.Registry
{
/// Watches the Windows Registry for any changes.
[DefaultEvent(nameof(SubkeyChanged))]
public class RegistryEventMonitor : Component, ISupportInitialize
{
private const int eventCount = 4;
private static readonly RegNotifyChangeFilter[] events = { RegNotifyChangeFilter.REG_NOTIFY_CHANGE_LAST_SET, RegNotifyChangeFilter.REG_NOTIFY_CHANGE_NAME, RegNotifyChangeFilter.REG_NOTIFY_CHANGE_ATTRIBUTES, RegNotifyChangeFilter.REG_NOTIFY_CHANGE_SECURITY };
private readonly Action[] actions;
private readonly ManualResetEvent breakEvent = new ManualResetEvent(false);
private readonly object threadLock = new object();
private readonly Thread[] threads = new Thread[eventCount];
private readonly AutoResetEvent[] threadsEnded = new AutoResetEvent[eventCount];
private readonly AutoResetEvent[] threadsStarted = new AutoResetEvent[eventCount];
private bool enabled;
private SafeRegistryHandle hkey;
private bool includeSubKeys;
private bool initializing;
private string keyName;
private string remoteMachine;
private ISynchronizeInvoke synchObj;
/// Initializes a new instance of the class.
public RegistryEventMonitor()
{
actions = new Action[eventCount] { OnValueChanged, OnSubkeyChanged, OnAttributesChanged, OnSecurityChanged };
for (var i = 0; i < eventCount; i++)
{
threadsStarted[i] = new AutoResetEvent(false);
threadsEnded[i] = new AutoResetEvent(false);
}
}
/// Initializes a new instance of the class.
///
/// The root registry key. Events will be watched in this key and optionally below (set to ).
///
/// if set to true, include sub keys.
public RegistryEventMonitor(RegistryKey registryKey, bool includeSubKeys) : this()
{
RegistryKey = registryKey;
IncludeSubKeys = includeSubKeys;
}
/// Occurs when attributes have changed.
[Category("Behavior"), Description("An attribute has changed.")]
public event EventHandler AttributesChanged;
/// Occurs when key security has changed.
[Category("Behavior"), Description("Key security has changed.")]
public event EventHandler SecurityChanged;
/// Occurs when a subkey has changed.
[Category("Behavior"), Description("A subkey has changed.")]
public event EventHandler SubkeyChanged;
/// Occurs when a value has changed.
[Category("Behavior"), Description("A value has changed.")]
public event EventHandler ValueChanged;
/// Gets or sets a value indicating whether to enable raising events.
/// true if raising events is enabled; otherwise, false.
[DefaultValue(false), Category("Behavior"), Description("Whether to enable raising events.")]
public bool EnableRaisingEvents
{
get => enabled;
set
{
if (enabled == value) return;
enabled = value;
if (IsSuspended) return;
if (value)
StartRaisingEvents();
else
StopRaisingEvents();
}
}
/// Gets or sets a value indicating whether to monitor all subkeys.
/// true if [include sub keys]; otherwise, false.
[DefaultValue(false), Category("Behavior"), Description("Whether to monitor all subkeys.")]
public bool IncludeSubKeys
{
get => includeSubKeys;
set
{
if (includeSubKeys == value) return;
includeSubKeys = value;
Restart();
}
}
/// Gets or sets the machine name.
/// The machine name. Use or to represent the local machine.
[DefaultValue(null), Category("Behavior"), Description("The machine name.")]
public string MachineName
{
get => remoteMachine;
set
{
if (value == string.Empty) value = null;
if (value != null)
{
if (!value.StartsWith("\\")) value = "\\" + value;
if (value.LastIndexOf('\\') > 0) throw new ArgumentException("Machine name cannot have any path delimiters.");
}
if (string.Equals(remoteMachine, value, StringComparison.InvariantCultureIgnoreCase)) return;
remoteMachine = value;
}
}
///
/// Gets or sets the root registry key. Events will be watched in this key and optionally below (set to
/// ).
///
[Browsable(false)]
public RegistryKey RegistryKey
{
get => RegistryKeyFromName(RegistryKeyName, remoteMachine);
set => RegistryKeyName = value?.Name;
}
/// Gets or sets the name of the root registry key to monitor.
[DefaultValue(null), Category("Behavior"), Description("The name of the root registry key to monitor.")]
public string RegistryKeyName
{
get => keyName;
set
{
if (string.Equals(keyName, value, StringComparison.InvariantCultureIgnoreCase)) return;
keyName = value;
Restart();
}
}
/// Gets or sets the object used to marshal the event handler calls issued as a result of a registry change.
///
/// The that represents the object used to marshal the event handler calls issued as a result of a
/// registry change. The default is .
///
///
/// When SynchronizingObject is , methods handling the ,
/// , , and events are called on a thread from the
/// system thread pool. For more information on system thread pools, see ThreadPool.
///
/// When the , , , and
/// events are handled by a visual Windows Forms component, such as a Button, accessing the component through
/// the system thread pool might not work, or may result in an exception. Avoid this by setting SynchronizingObject to a Windows
/// Forms component, which causes the methods that handle the , ,
/// , and events to be called on the same thread on which the component was created.
///
///
/// If the is used inside Visual Studio in a Windows Forms designer, SynchronizingObject
/// automatically sets to the control that contains the . For example, if you place a
/// on a designer for Form1 (which inherits from Form) the SynchronizingObject property of
/// is set to the instance of Form1.
///
///
[Browsable(false), DefaultValue(null)]
public ISynchronizeInvoke SynchronizingObject
{
get
{
if (synchObj == null && DesignMode)
{
var service = (IDesignerHost)GetService(typeof(IDesignerHost));
if (service?.RootComponent is ISynchronizeInvoke root)
synchObj = root;
}
return synchObj;
}
set => synchObj = value;
}
private bool IsSuspended => initializing || DesignMode;
/// Signals the object that initialization is starting.
public void BeginInit()
{
var enabled = EnableRaisingEvents;
StopRaisingEvents();
EnableRaisingEvents = enabled;
initializing = true;
}
/// Signals the object that initialization is complete.
public void EndInit()
{
initializing = false;
if (keyName != null && EnableRaisingEvents)
StartRaisingEvents();
}
/// Releases unmanaged and - optionally - managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
///
protected override void Dispose(bool disposing)
{
try
{
if (disposing)
{
StopRaisingEvents();
hkey?.Dispose();
hkey = null;
}
else
keyName = null;
}
finally
{
base.Dispose(disposing);
}
}
/// Raises the event.
/// The instance containing the event data.
protected virtual void OnAttributesChanged(RegistryEventArgs e)
{
SynchedInvoke(AttributesChanged, e);
}
/// Raises the event.
/// The instance containing the event data.
protected virtual void OnSecurityChanged(RegistryEventArgs e)
{
SynchedInvoke(SecurityChanged, e);
}
/// Raises the event.
/// The instance containing the event data.
protected virtual void OnSubkeyChanged(RegistryEventArgs e)
{
SynchedInvoke(SubkeyChanged, e);
}
/// Raises the event.
/// The instance containing the event data.
protected virtual void OnValueChanged(RegistryEventArgs e)
{
SynchedInvoke(ValueChanged, e);
}
private static SafeRegistryHandle RegistryHandleFromName(string keyName, string serverName = null)
{
if (keyName == null)
return null;
var index = keyName.IndexOf('\\');
var str = index != -1 ? keyName.Substring(0, index).ToUpper(System.Globalization.CultureInfo.InvariantCulture) : keyName.ToUpper(System.Globalization.CultureInfo.InvariantCulture);
if (!(typeof(Vanara.PInvoke.AdvApi32).GetField(str)?.GetValue(null) is HKEY hive))
return null;
try
{
RegConnectRegistry(serverName, hive, out var mhive).ThrowIfFailed();
if ((index == -1) || (index == keyName.Length))
return mhive;
var subKeyName = keyName.Substring(index + 1, (keyName.Length - index) - 1);
RegOpenKeyEx(mhive, subKeyName, 0, REGSAM.KEY_NOTIFY, out var hkey).ThrowIfFailed();
return hkey;
}
catch
{
return null;
}
}
private static RegistryKey RegistryKeyFromName(string keyName, string serverName = null)
{
if (keyName == null)
return null;
var index = keyName.IndexOf('\\');
var str = index != -1 ? keyName.Substring(0, index).ToUpper(System.Globalization.CultureInfo.InvariantCulture) : keyName.ToUpper(System.Globalization.CultureInfo.InvariantCulture);
RegistryHive hive;
switch (str)
{
case "HKEY_CLASSES_ROOT":
hive = RegistryHive.ClassesRoot;
break;
case "HKEY_CURRENT_USER":
hive = RegistryHive.CurrentUser;
break;
case "HKEY_LOCAL_MACHINE":
hive = RegistryHive.LocalMachine;
break;
case "HKEY_USERS":
hive = RegistryHive.Users;
break;
case "HKEY_PERFORMANCE_DATA":
hive = RegistryHive.PerformanceData;
break;
case "HKEY_CURRENT_CONFIG":
hive = RegistryHive.CurrentConfig;
break;
#if (NET20 || NET35 || NET40 || NET45)
case "HKEY_DYN_DATA":
hive = RegistryHive.DynData;
break;
#endif
default:
return null;
}
try
{
var mhive = RegistryKey.OpenRemoteBaseKey(hive, serverName ?? string.Empty);
if ((index == -1) || (index == keyName.Length))
return mhive;
var subKeyName = keyName.Substring(index + 1, (keyName.Length - index) - 1);
return mhive.OpenSubKey(subKeyName, RegistryKeyPermissionCheck.Default, System.Security.AccessControl.RegistryRights.Notify);
}
catch
{
return null;
}
}
private void MonitorRegThreadProc(object i)
{
var idx = (int)i;
var filter = events[idx];
using (var autoEvent = new AutoResetEvent(false))
using (var hEvent = autoEvent.SafeWaitHandle)
{
var waitHandles = new WaitHandle[] { autoEvent, breakEvent };
while (!breakEvent.WaitOne(0, true))
{
Debug.WriteLine($"Calling RegNotify for {filter}");
RegNotifyChangeKeyValue(hkey, includeSubKeys, filter, autoEvent, true).ThrowIfFailed();
threadsStarted[idx].Set();
Debug.WriteLine($"Waiting for {filter}");
if (WaitHandle.WaitAny(waitHandles) == 0)
{
Debug.WriteLine($"Event called for {filter}");
actions[idx]?.Invoke(new RegistryEventArgs(RegistryKeyName, IncludeSubKeys));
}
else
{
Debug.WriteLine($"Canceled {filter}");
}
}
}
threadsEnded[idx].Set();
Debug.WriteLine($"Exiting {filter}");
}
private void Restart()
{
if (IsSuspended || !EnableRaisingEvents) return;
StopRaisingEvents();
StartRaisingEvents();
}
private void StartRaisingEvents()
{
if (Environment.OSVersion.Platform != PlatformID.Win32NT)
throw new PlatformNotSupportedException();
if (keyName == null) throw new ArgumentNullException(nameof(RegistryKeyName));
if (!IsSuspended && EnableRaisingEvents)
{
lock (threadLock)
{
breakEvent.Reset();
hkey = RegistryHandleFromName(keyName, remoteMachine);
if (hkey == null || hkey.IsClosed || hkey.IsInvalid) throw new InvalidOperationException($"Unable to connect to registry key specified in {nameof(RegistryKeyName)}");
for (var i = 0; i < eventCount; i++)
{
threads[i]?.Abort();
threads[i] = new Thread(MonitorRegThreadProc) { IsBackground = false };
threads[i].Start(i);
}
if (!WaitHandle.WaitAll(threadsStarted, 5000))
{
StopRaisingEvents();
throw new InvalidOperationException("Threads waiting for changes failed to start within reasonable time.");
}
}
}
}
private void StopRaisingEvents()
{
if (IsSuspended) return;
breakEvent.Set();
WaitHandle.WaitAll(threadsEnded);
enabled = false;
}
private void SynchedInvoke(EventHandler h, RegistryEventArgs e)
{
if (h == null) return;
if (SynchronizingObject != null && SynchronizingObject.InvokeRequired)
SynchronizingObject.BeginInvoke(h, new object[] { this, e });
else
h.Invoke(this, e);
}
/// Argument used in events.
///
public class RegistryEventArgs : EventArgs
{
internal RegistryEventArgs(string keyName, bool inclSubKeys)
{
RegistryKeyName = keyName;
IncludeSubKeys = inclSubKeys;
}
/// Gets a value indicating whether subkeys were monitored.
/// true if subkeys were monitored; otherwise, false.
public bool IncludeSubKeys { get; }
/// Gets the name of the root registry key being monitored.
public string RegistryKeyName { get; }
}
}
}