diff --git a/README.md b/README.md index 985a292..c87cb35 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,7 @@ Contents: - [`deleteKeyValue`](#deletekeyvalue) - [`deleteValue`](#deletevalue) - [`closeKey`](#closekey) + - [`watch`](#watch) - [Format Helpers](#format-helpers) - [`parseValue`](#parsevalue) - [`parseString`](#parsestring) @@ -558,6 +559,28 @@ For convenience, `null` or `undefined` values are allowed and ignored. export function closeKey(hkey: HKEY | null | undefined): void; ``` +#### `watch` + +Watches a given key and all of its descendants for changes. The returned `RegistryWatcher` is an +`EventEmitter` that emits `change` events when any data changes in the key or its children +(unless the `watchSubtree` option is `false`). + +Due to the way the Windows API ([`RegNotifyChangeKeyValue`](https://learn.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regnotifychangekeyvalue)) +works, we only receive a notification that *something* in the watched tree changed, +but not what specifically; therefore the `change` event has no arguments. + +Note that each watcher starts up a native background thread. The thread only wakes when changes occur +in the watched registry path, but it necessarily consumes some RAM (roughly 64 kB each). +Call `RegistryWatcher.close()` to stop watching the registry and release resources. + +```ts +export function watch(hkey: HKEY, subKey: string, options?: WatchOptions): RegistryWatcher; +export type WatchOptions = { + watchSubtree?: boolean, // default true + notifyFilter?: NotifyFilterFlags // default all changes +}; +``` + ### Format helpers #### `parseValue` diff --git a/reg.cc b/reg.cc index 2022f8e..b859300 100644 --- a/reg.cc +++ b/reg.cc @@ -2,6 +2,8 @@ #define WIN32_LEAN_AND_MEAN #define NOMINMAX #include +#include +#include #include #ifndef NAPI_CPP_EXCEPTIONS @@ -550,6 +552,104 @@ void closeKey(const CallbackInfo& info) { } } +class Watcher { +private: + ThreadSafeFunction tsfn; + std::thread nativeThread; + HKEY hKey = nullptr; + HANDLE hEvent = nullptr; + HANDLE hStopEvent = nullptr; + std::atomic closed{false}; + BOOL bWatchSubtree = FALSE; + DWORD dwEventFilter = 0; + + LSTATUS RegNotifyChange() { + return RegNotifyChangeKeyValue(hKey, bWatchSubtree, dwEventFilter, hEvent, TRUE); + } + +public: + Watcher() = delete; + Watcher(const Watcher&) = delete; + Watcher& operator=(const Watcher&) = delete; + + Watcher(Napi::Env env, HKEY hkey, std::wstring subKey, BOOL watchSubtree, DWORD eventFilter, Function cb) { + hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + hStopEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + bWatchSubtree = watchSubtree; + dwEventFilter = eventFilter & ~REG_NOTIFY_THREAD_AGNOSTIC; // do not allow JS to set this flag + + auto status = RegOpenKeyExW( + hkey, + subKey.c_str(), + 0, + KEY_NOTIFY | KEY_WOW64_64KEY, + &hKey); + + if (status != ERROR_SUCCESS) { + CloseHandle(hEvent); + CloseHandle(hStopEvent); + throw win32_error(env, status, "RegOpenKeyExW"); + } + + status = RegNotifyChange(); + if (status != ERROR_SUCCESS) { + RegCloseKey(hKey); + CloseHandle(hEvent); + CloseHandle(hStopEvent); + throw win32_error(env, status, "RegNotifyChangeKeyValue"); + } + + tsfn = ThreadSafeFunction::New(env, cb, "native-reg watcher", 0, 1); + + nativeThread = std::thread([this] { + auto callback = [](Napi::Env, Function jsCallback) { + jsCallback.Call(0, NULL); + }; + + HANDLE handles[2] = {hStopEvent, hEvent}; + while (WaitForMultipleObjects(2, handles, FALSE, INFINITE) == WAIT_OBJECT_0 + 1) { + if (tsfn.BlockingCall(callback) != napi_ok) break; + if (RegNotifyChange() != ERROR_SUCCESS) break; + } + }); + } + + ~Watcher() { + close(); + } + + void close() { + if (closed.exchange(true)) return; + SetEvent(hStopEvent); + if (nativeThread.joinable()) { + nativeThread.join(); + } + tsfn.Release(); + if (hKey) { RegCloseKey(hKey); hKey = nullptr; } + if (hEvent) { CloseHandle(hEvent); hEvent = nullptr; } + if (hStopEvent) { CloseHandle(hStopEvent); hStopEvent = nullptr; } + } +}; + +Value watch(const CallbackInfo& info) { + auto env = info.Env(); + + auto hkey = to_hkey(info[0]); + auto subKey = to_wstring(info[1]); + auto watchSubtree = info[2].As().Value(); + auto notifyFilter = info[3].As().Uint32Value(); + auto cb = info[4].As(); + + auto watcher = std::make_shared(env, hkey, subKey, watchSubtree, notifyFilter, cb); + + auto obj = Object::New(env); + obj.Set("close", Function::New(env, [watcher](const CallbackInfo&) { + watcher->close(); + })); + + return obj; +} + // error handling for functions not called through the C++ wrapper #define RAISE_IF_FAILED(env, expr) \ if (const napi_status status = (expr); status != napi_ok) { \ @@ -620,6 +720,7 @@ napi_value Init(napi_env env, napi_value exports) { NAPI_DESCRIPTOR_FUNCTION(deleteKeyValue), NAPI_DESCRIPTOR_FUNCTION(deleteValue), NAPI_DESCRIPTOR_FUNCTION(closeKey), + NAPI_DESCRIPTOR_FUNCTION(watch), }); return exports; diff --git a/src/index.ts b/src/index.ts index 70a0f6a..ae62a8d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import EventEmitter = require('events'); const assert = require('assert'); const types = require('util').types || { // approximate polyfill for Node.js < 10 @@ -97,6 +98,22 @@ export enum GetValueFlags { SUBKEY_WOW6432KEY = 0x00020000, } +/** + * Flags to indicate which changes should be reported when using `watch()`. + * @see https://learn.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regnotifychangekeyvalue + */ +export enum NotifyFilterFlags { + /** Notify the caller if a subkey is added or deleted. */ + REG_NOTIFY_CHANGE_NAME = 0x00000001, + /** Notify the caller of changes to the attributes of the key, such as the security descriptor information. */ + REG_NOTIFY_CHANGE_ATTRIBUTES = 0x00000002, + /** Notify the caller of changes to a value of the key. This can include adding or deleting a value, or changing an existing value. */ + REG_NOTIFY_CHANGE_LAST_SET = 0x00000004, + /** Notify the caller of changes to the security descriptor of the key. */ + REG_NOTIFY_CHANGE_SECURITY = 0x00000008 + // REG_NOTIFY_THREAD_AGNOSTIC intentionally omitted; not a flag JS should set. +} + export const HKCR = HKEY.CLASSES_ROOT; export const HKCU = HKEY.CURRENT_USER; export const HKLM = HKEY.LOCAL_MACHINE; @@ -391,3 +408,78 @@ export function getValue( export function queryValue(hkey: HKEY, valueName: string | null): ParsedValue | null { return parseValue(queryValueRaw(hkey, valueName)); } + +class RegistryWatcher extends EventEmitter<{ change: [] }> { + private _native; + + constructor( + public readonly hkey: HKEY, + public readonly subKey: string, + public readonly watchSubtree: boolean, + public readonly notifyFilter: NotifyFilterFlags + ) { + super(); + + this._native = native.watch(hkey, subKey, watchSubtree, notifyFilter, () => { + this.emit('change'); + }); + } + + /** + * Stops watching the registry path associated with this instance and frees native resources. + */ + close() { + this._native.close(); + } +} + +export type { RegistryWatcher }; + +export type WatchOptions = { + /** + * If this parameter is `true`, the function reports changes in the specified key and its subkeys. + * If the parameter is `false`, the function reports changes only in the specified key. + * @default true + */ + watchSubtree?: boolean, + /** + * A value that indicates the changes that should be reported. This parameter can be one or more of the values + * in the `NotifyFilterFlags` enum. + * @default REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_ATTRIBUTES | REG_NOTIFY_CHANGE_LAST_SET | REG_NOTIFY_CHANGE_SECURITY + */ + notifyFilter?: NotifyFilterFlags +} + +const DEFAULT_NOTIFY_FILTER = NotifyFilterFlags.REG_NOTIFY_CHANGE_NAME + | NotifyFilterFlags.REG_NOTIFY_CHANGE_ATTRIBUTES + | NotifyFilterFlags.REG_NOTIFY_CHANGE_LAST_SET + | NotifyFilterFlags.REG_NOTIFY_CHANGE_SECURITY; + +/** + * Watches a given key and all of its descendants for changes. The returned `RegistryWatcher` is an + * `EventEmitter` that emits `change` events when any data changes in the key or its children + * (unless the `watchSubtree` option is `false`). + * + * Due to the way the Windows API (`RegNotifyChangeKeyValue`) works, we only receive a notification that + * *something* in the watched tree changed, but not what specifically; therefore the `change` event has no arguments. + * + * Note that each watcher starts up a native background thread. The thread only wakes when changes occur + * in the watched registry path, but it necessarily consumes some RAM (roughly 64 kB each). + * Call `RegistryWatcher.close()` to stop watching the registry and release resources. + * @param hkey The registry root key. + * @param subKey The path to the key to be watched. + * @param options Optional `WatchOptions` object. + * @returns A `RegistryWatcher` instance. + */ +export function watch( + hkey: HKEY, + subKey: string, + options?: WatchOptions +): RegistryWatcher { + assert(isWindows); + assert(isHKEY(hkey)); + assert(typeof subKey === 'string'); + const notifyFilter = typeof options?.notifyFilter === 'number' ? options.notifyFilter : DEFAULT_NOTIFY_FILTER; + const watchSubtree = typeof options?.watchSubtree === 'boolean' ? options.watchSubtree : true; + return new RegistryWatcher(hkey, subKey, watchSubtree, notifyFilter); +} diff --git a/test/test.js b/test/test.js index 46ec2b5..b14a05b 100644 --- a/test/test.js +++ b/test/test.js @@ -231,6 +231,26 @@ suite('with temporary key', () => { }); }); +suite('watch', () => { + let testKey = null; + let watcher = null; + + setup(() => { + testKey = reg.createKey(reg.HKCU, testingSubKeyName, reg.Access.ALL_ACCESS); + }); + + teardown(() => { + reg.deleteKey(testKey, testingSubKeyName); + watcher.close(); + }); + + test('receives registry change events', done => { + watcher = reg.watch(reg.HKCU, testingSubKeyName); + watcher.on('change', done); + reg.setValueDWORD(testKey, "watched", 1); + }); +}); + function assertNameList(nameList) { assert(Array.isArray(nameList)); for (const name of nameList) {