Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ Contents:
- [`deleteKeyValue`](#deletekeyvalue)
- [`deleteValue`](#deletevalue)
- [`closeKey`](#closekey)
- [`watch`](#watch)
- [Format Helpers](#format-helpers)
- [`parseValue`](#parsevalue)
- [`parseString`](#parsestring)
Expand Down Expand Up @@ -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`
Expand Down
101 changes: 101 additions & 0 deletions reg.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#include <atomic>
#include <memory>
#include <vector>

#ifndef NAPI_CPP_EXCEPTIONS
Expand Down Expand Up @@ -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<bool> 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<Boolean>().Value();
auto notifyFilter = info[3].As<Number>().Uint32Value();
auto cb = info[4].As<Function>();

auto watcher = std::make_shared<Watcher>(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) { \
Expand Down Expand Up @@ -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;
Expand Down
92 changes: 92 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import EventEmitter = require('events');
const assert = require('assert');
const types = require('util').types || {
// approximate polyfill for Node.js < 10
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
20 changes: 20 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down