From 3c1f39f529821bad0e03002509cce3d0a66309de Mon Sep 17 00:00:00 2001 From: Josh Dague Date: Thu, 30 Apr 2026 04:07:36 -0700 Subject: [PATCH 1/5] =?UTF-8?q?add=20watch(=E2=80=A6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- reg.cc | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 12 +++++++ 2 files changed, 103 insertions(+) diff --git a/reg.cc b/reg.cc index 2022f8e..5afddb8 100644 --- a/reg.cc +++ b/reg.cc @@ -2,6 +2,9 @@ #define WIN32_LEAN_AND_MEAN #define NOMINMAX #include +#include +#include +#include #include #ifndef NAPI_CPP_EXCEPTIONS @@ -550,6 +553,93 @@ void closeKey(const CallbackInfo& info) { } } +class Watcher { +private: + ThreadSafeFunction tsfn; + std::thread nativeThread; + HKEY hKey; + HANDLE hEvent; + +public: + Watcher() {} + Watcher(Napi::Env env, HKEY hkey, std::wstring subKey, Function cb) { + + this->hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + + auto status = RegOpenKeyExW( + hkey, + subKey.c_str(), + 0, + KEY_NOTIFY | KEY_WOW64_64KEY, + &this->hKey + ); + + if (status != ERROR_SUCCESS) + { + throw win32_error(env, status, "RegOpenKeyExW"); + } + + status = this->RegNotifyChange(); + + if (status != ERROR_SUCCESS) + { + throw win32_error(env, status, "RegNotifyChangeKeyValue"); + } + + this->tsfn = ThreadSafeFunction::New( + env, + cb, + "native-reg watcher", + 0, + 1, + [&](Napi::Env) { + nativeThread.join(); + }); + + this->nativeThread = std::thread([&] { + auto callback = []( Napi::Env env, Function jsCallback ) { + jsCallback.Call(0, NULL); + }; + + while (WaitForSingleObject(hEvent, INFINITE) != WAIT_FAILED) { + napi_status status = tsfn.BlockingCall(callback); + if (status != napi_ok || RegNotifyChange() != ERROR_SUCCESS) + break; + } + + }); + + } + + LSTATUS RegNotifyChange() { + const DWORD dwEventFilter = REG_NOTIFY_CHANGE_NAME | + REG_NOTIFY_CHANGE_ATTRIBUTES | + REG_NOTIFY_CHANGE_LAST_SET | + REG_NOTIFY_CHANGE_SECURITY; + + return RegNotifyChangeKeyValue( + this->hKey, + TRUE, + dwEventFilter, + this->hEvent, + TRUE + ); + } +}; + +std::optional w; +Value watch(const CallbackInfo &info) +{ + auto env = info.Env(); + + auto hkey = to_hkey(info[0]); + auto subKey = to_wstring(info[1]); + auto cb = info[2].As(); + + w.emplace(env, hkey, subKey, cb); + return env.Null(); +} + // 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 +710,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..8957046 100644 --- a/src/index.ts +++ b/src/index.ts @@ -391,3 +391,15 @@ export function getValue( export function queryValue(hkey: HKEY, valueName: string | null): ParsedValue | null { return parseValue(queryValueRaw(hkey, valueName)); } + +export function watch( + hkey: HKEY, + subKey: string, + cb: () => void, +): void { + assert(isWindows); + assert(isHKEY(hkey)); + assert(typeof subKey === 'string'); + assert(typeof cb === 'function'); + native.watch(hkey, subKey, cb); +} From 902c80e9e122a72eecff0e3622f42549c0197fca Mon Sep 17 00:00:00 2001 From: Josh Dague Date: Thu, 30 Apr 2026 04:39:31 -0700 Subject: [PATCH 2/5] watcher cleanup --- reg.cc | 104 +++++++++++++++++++++++++++------------------------ src/index.ts | 8 +++- 2 files changed, 62 insertions(+), 50 deletions(-) diff --git a/reg.cc b/reg.cc index 5afddb8..fecf5c6 100644 --- a/reg.cc +++ b/reg.cc @@ -4,7 +4,6 @@ #include #include #include -#include #include #ifndef NAPI_CPP_EXCEPTIONS @@ -557,87 +556,96 @@ class Watcher { private: ThreadSafeFunction tsfn; std::thread nativeThread; - HKEY hKey; - HANDLE hEvent; + HKEY hKey = nullptr; + HANDLE hEvent = nullptr; + HANDLE hStopEvent = nullptr; + std::atomic closed{false}; + + LSTATUS RegNotifyChange() { + const DWORD dwEventFilter = REG_NOTIFY_CHANGE_NAME | + REG_NOTIFY_CHANGE_ATTRIBUTES | + REG_NOTIFY_CHANGE_LAST_SET | + REG_NOTIFY_CHANGE_SECURITY; + return RegNotifyChangeKeyValue(hKey, TRUE, dwEventFilter, hEvent, TRUE); + } public: - Watcher() {} - Watcher(Napi::Env env, HKEY hkey, std::wstring subKey, Function cb) { + Watcher() = delete; + Watcher(const Watcher&) = delete; + Watcher& operator=(const Watcher&) = delete; - this->hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + Watcher(Napi::Env env, HKEY hkey, std::wstring subKey, Function cb) { + hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + hStopEvent = CreateEvent(NULL, TRUE, FALSE, NULL); auto status = RegOpenKeyExW( hkey, subKey.c_str(), 0, KEY_NOTIFY | KEY_WOW64_64KEY, - &this->hKey - ); + &hKey); - if (status != ERROR_SUCCESS) - { + if (status != ERROR_SUCCESS) { + CloseHandle(hEvent); + CloseHandle(hStopEvent); throw win32_error(env, status, "RegOpenKeyExW"); } - status = this->RegNotifyChange(); - - if (status != ERROR_SUCCESS) - { + status = RegNotifyChange(); + if (status != ERROR_SUCCESS) { + RegCloseKey(hKey); + CloseHandle(hEvent); + CloseHandle(hStopEvent); throw win32_error(env, status, "RegNotifyChangeKeyValue"); } - this->tsfn = ThreadSafeFunction::New( - env, - cb, - "native-reg watcher", - 0, - 1, - [&](Napi::Env) { - nativeThread.join(); - }); + tsfn = ThreadSafeFunction::New(env, cb, "native-reg watcher", 0, 1); - this->nativeThread = std::thread([&] { - auto callback = []( Napi::Env env, Function jsCallback ) { + nativeThread = std::thread([this] { + auto callback = [](Napi::Env, Function jsCallback) { jsCallback.Call(0, NULL); }; - while (WaitForSingleObject(hEvent, INFINITE) != WAIT_FAILED) { - napi_status status = tsfn.BlockingCall(callback); - if (status != napi_ok || RegNotifyChange() != ERROR_SUCCESS) - break; + 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; } - }); - } - LSTATUS RegNotifyChange() { - const DWORD dwEventFilter = REG_NOTIFY_CHANGE_NAME | - REG_NOTIFY_CHANGE_ATTRIBUTES | - REG_NOTIFY_CHANGE_LAST_SET | - REG_NOTIFY_CHANGE_SECURITY; + ~Watcher() { + close(); + } - return RegNotifyChangeKeyValue( - this->hKey, - TRUE, - dwEventFilter, - this->hEvent, - TRUE - ); + 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; } } }; -std::optional w; -Value watch(const CallbackInfo &info) -{ +Value watch(const CallbackInfo& info) { auto env = info.Env(); auto hkey = to_hkey(info[0]); auto subKey = to_wstring(info[1]); auto cb = info[2].As(); - w.emplace(env, hkey, subKey, cb); - return env.Null(); + auto watcher = std::make_shared(env, hkey, subKey, 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 diff --git a/src/index.ts b/src/index.ts index 8957046..aedf5e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -392,14 +392,18 @@ export function queryValue(hkey: HKEY, valueName: string | null): ParsedValue | return parseValue(queryValueRaw(hkey, valueName)); } +export interface Watcher { + close(): void; +} + export function watch( hkey: HKEY, subKey: string, cb: () => void, -): void { +): Watcher { assert(isWindows); assert(isHKEY(hkey)); assert(typeof subKey === 'string'); assert(typeof cb === 'function'); - native.watch(hkey, subKey, cb); + return native.watch(hkey, subKey, cb); } From 3e56b445db2effc870fd91b93e80190984908392 Mon Sep 17 00:00:00 2001 From: Josh Dague Date: Thu, 30 Apr 2026 22:40:23 -0700 Subject: [PATCH 3/5] EventEmitter + docs --- README.md | 18 ++++++++++++++++++ src/index.ts | 44 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 985a292..e91182e 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,23 @@ 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. + +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): RegistryWatcher; +``` + ### Format helpers #### `parseValue` diff --git a/src/index.ts b/src/index.ts index aedf5e8..ec11657 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 @@ -392,18 +393,47 @@ export function queryValue(hkey: HKEY, valueName: string | null): ParsedValue | return parseValue(queryValueRaw(hkey, valueName)); } -export interface Watcher { - close(): void; +class RegistryWatcher extends EventEmitter<{ change: [] }> { + private _native; + + constructor(public readonly hkey: HKEY, public readonly subKey: string) { + super(); + + this._native = native.watch(hkey, subKey, () => { + this.emit('change'); + }); + } + + /** + * Stops watching the registry path associated with this instance and frees native resources. + */ + close() { + this._native.close(); + } } +export type { RegistryWatcher }; + +/** + * 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. + * + * 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. + * @returns A `RegistryWatcher` instance. + */ export function watch( hkey: HKEY, - subKey: string, - cb: () => void, -): Watcher { + subKey: string +): RegistryWatcher { assert(isWindows); assert(isHKEY(hkey)); assert(typeof subKey === 'string'); - assert(typeof cb === 'function'); - return native.watch(hkey, subKey, cb); + return new RegistryWatcher(hkey, subKey); } From 5181affd89dc63c460d74ecfe21363be513fba8b Mon Sep 17 00:00:00 2001 From: Josh Dague Date: Thu, 30 Apr 2026 22:51:47 -0700 Subject: [PATCH 4/5] add test --- test/test.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) 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) { From c5e13a16cc82d5614a923aea16dd0f2926873949 Mon Sep 17 00:00:00 2001 From: Josh Dague Date: Sat, 2 May 2026 01:00:11 -0700 Subject: [PATCH 5/5] expose `watchSubtree` and `notifyFilter` options --- README.md | 9 +++++++-- reg.cc | 18 +++++++++-------- src/index.ts | 56 +++++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e91182e..c87cb35 100644 --- a/README.md +++ b/README.md @@ -562,7 +562,8 @@ 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. +`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, @@ -573,7 +574,11 @@ in the watched registry path, but it necessarily consumes some RAM (roughly 64 k Call `RegistryWatcher.close()` to stop watching the registry and release resources. ```ts -export function watch(hkey: HKEY, subKey: string): RegistryWatcher; +export function watch(hkey: HKEY, subKey: string, options?: WatchOptions): RegistryWatcher; +export type WatchOptions = { + watchSubtree?: boolean, // default true + notifyFilter?: NotifyFilterFlags // default all changes +}; ``` ### Format helpers diff --git a/reg.cc b/reg.cc index fecf5c6..b859300 100644 --- a/reg.cc +++ b/reg.cc @@ -560,13 +560,11 @@ class Watcher { HANDLE hEvent = nullptr; HANDLE hStopEvent = nullptr; std::atomic closed{false}; + BOOL bWatchSubtree = FALSE; + DWORD dwEventFilter = 0; LSTATUS RegNotifyChange() { - const DWORD dwEventFilter = REG_NOTIFY_CHANGE_NAME | - REG_NOTIFY_CHANGE_ATTRIBUTES | - REG_NOTIFY_CHANGE_LAST_SET | - REG_NOTIFY_CHANGE_SECURITY; - return RegNotifyChangeKeyValue(hKey, TRUE, dwEventFilter, hEvent, TRUE); + return RegNotifyChangeKeyValue(hKey, bWatchSubtree, dwEventFilter, hEvent, TRUE); } public: @@ -574,9 +572,11 @@ class Watcher { Watcher(const Watcher&) = delete; Watcher& operator=(const Watcher&) = delete; - Watcher(Napi::Env env, HKEY hkey, std::wstring subKey, Function cb) { + 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, @@ -636,9 +636,11 @@ Value watch(const CallbackInfo& info) { auto hkey = to_hkey(info[0]); auto subKey = to_wstring(info[1]); - auto cb = info[2].As(); + 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, cb); + 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&) { diff --git a/src/index.ts b/src/index.ts index ec11657..ae62a8d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -98,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; @@ -396,10 +412,15 @@ export function queryValue(hkey: HKEY, valueName: string | null): ParsedValue | class RegistryWatcher extends EventEmitter<{ change: [] }> { private _native; - constructor(public readonly hkey: HKEY, public readonly subKey: string) { + constructor( + public readonly hkey: HKEY, + public readonly subKey: string, + public readonly watchSubtree: boolean, + public readonly notifyFilter: NotifyFilterFlags + ) { super(); - this._native = native.watch(hkey, subKey, () => { + this._native = native.watch(hkey, subKey, watchSubtree, notifyFilter, () => { this.emit('change'); }); } @@ -414,9 +435,30 @@ class RegistryWatcher extends EventEmitter<{ change: [] }> { 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. + * `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. @@ -426,14 +468,18 @@ export type { RegistryWatcher }; * 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 + subKey: string, + options?: WatchOptions ): RegistryWatcher { assert(isWindows); assert(isHKEY(hkey)); assert(typeof subKey === 'string'); - return new RegistryWatcher(hkey, subKey); + 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); }