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; } } } }