From 40a58e07eb17698f70aed0f00e155bf49c89e8cd Mon Sep 17 00:00:00 2001 From: Travis Collins Date: Mon, 15 Jan 2024 22:41:21 -0800 Subject: [PATCH] Implement writing and reading values. --- index.d.ts | 55 +++++++++ index.js | 48 ++++++++ native.js | 9 ++ native/Cargo.lock | 80 +++++++++++-- native/Cargo.toml | 6 +- native/src/lib.rs | 279 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 10 +- pnpm-lock.yaml | 9 ++ test/index.js | 125 +++++++++++++++++++++ 9 files changed, 603 insertions(+), 18 deletions(-) create mode 100644 index.d.ts create mode 100644 index.js create mode 100644 native.js create mode 100644 test/index.js diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..da89fea --- /dev/null +++ b/index.d.ts @@ -0,0 +1,55 @@ +export class WindowsRegistry { + /** + * Constructs a new `WindowsRegistry` instance. This will start a background thread to perform + * operations that will live as long as this instance does. + */ + constructor() + /** + * Closes the registry, ending its background thread immediately. This is not necessary to call, + * as the background thread will be automatically ended upon garbage collection, but can be used + * if you would like to exit the process immediately. + */ + close(): void + + read(hive: Hkey, key: string, name: string): Promise + write( + hive: Hkey, + key: string, + name: string, + type: R, + value: RegistryDataTypeJs[R], + ): Promise +} + +export const HKCU = 'HKEY_CURRENT_USER' +export const HKLM = 'HKEY_LOCAL_MACHINE' +export const HKCR = 'HKEY_CLASSES_ROOT' +export const HKU = 'HKEY_USERS' +export const HKCC = 'HKEY_CURRENT_CONFIG' +export type Hkey = typeof HKCU | typeof HKLM | typeof HKCR | typeof HKU | typeof HKCC + +export const REG_NONE = 'REG_NONE' +export const REG_SZ = 'REG_SZ' +export const REG_MULTI_SZ = 'REG_MULTI_SZ' +export const REG_EXPAND_SZ = 'REG_EXPAND_SZ' +export const REG_DWORD = 'REG_DWORD' +export const REG_QWORD = 'REG_QWORD' +export const REG_BINARY = 'REG_BINARY' +export type RegistryDataType = + | typeof REG_NONE + | typeof REG_SZ + | typeof REG_MULTI_SZ + | typeof REG_EXPAND_SZ + | typeof REG_DWORD + | typeof REG_QWORD + | typeof REG_BINARY + +interface RegistryDataTypeJs extends Record { + [REG_NONE]: undefined | null + [REG_SZ]: string + [REG_MULTI_SZ]: string[] + [REG_EXPAND_SZ]: string + [REG_DWORD]: number + [REG_QWORD]: number + [REG_BINARY]: Uint8Array +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..298eccb --- /dev/null +++ b/index.js @@ -0,0 +1,48 @@ +const { registryNew, registryClose, registryRead, registryWrite } = require('./native.js') + +class WindowsRegistry { + /** + * Constructs a new `WindowsRegistry` instance. This will start a background thread to perform + * operations that will live as long as this instance does. + */ + constructor() { + this.registry = registryNew() + } + + /** + * Closes the registry, ending its background thread immediately. This is not necessary to call, + * as the background thread will be automatically ended upon garbage collection, but can be used + * if you would like to exit the process immediately. + */ + close() { + registryClose.call(this.registry) + } + + read(hive, key, value) { + return registryRead.call(this.registry, hive, key, value) + } + + write(hive, key, value, type, data) { + return registryWrite.call(this.registry, hive, key, value, type, data) + } +} + +module.exports = { + WindowsRegistry, + + HKCU: 'HKEY_CURRENT_USER', + HKLM: 'HKEY_LOCAL_MACHINE', + HKCR: 'HKEY_CLASSES_ROOT', + HKU: 'HKEY_USERS', + HKCC: 'HKEY_CURRENT_CONFIG', + + REG_NONE: 'REG_NONE', + REG_SZ: 'REG_SZ', + REG_MULTI_SZ: 'REG_MULTI_SZ', + REG_EXPAND_SZ: 'REG_EXPAND_SZ', + REG_DWORD: 'REG_DWORD', + REG_QWORD: 'REG_QWORD', + REG_BINARY: 'REG_BINARY', + + DEFAULT_VALUE: '', +} diff --git a/native.js b/native.js new file mode 100644 index 0000000..1ea299b --- /dev/null +++ b/native.js @@ -0,0 +1,9 @@ +// TODO(tec27): Pick between 32 and 64-bit, as well as whether to use a locally built binary? +let native +try { + native = require('./prebuilds/x64.node') +} catch (e) { + native = require('./native/index.node') +} + +module.exports = native diff --git a/native/Cargo.lock b/native/Cargo.lock index c399fa8..6c1a840 100644 --- a/native/Cargo.lock +++ b/native/Cargo.lock @@ -8,6 +8,12 @@ version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "cfg-if" version = "1.0.0" @@ -25,12 +31,10 @@ dependencies = [ ] [[package]] -name = "native" -version = "0.1.0" -dependencies = [ - "anyhow", - "neon", -] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "neon" @@ -58,7 +62,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7288eac8b54af7913c60e0eb0e2a7683020dffa342ab3fd15e28f035ba897cf" dependencies = [ "quote", - "syn", + "syn 1.0.109", "syn-mid", ] @@ -91,6 +95,19 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "registry" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3b6d580d46b9d6d6c291f90c5d762e8b93a268a96e429556419fcd7e349f94" +dependencies = [ + "bitflags", + "log", + "thiserror", + "utfx", + "winapi", +] + [[package]] name = "semver" version = "0.9.0" @@ -123,6 +140,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn-mid" version = "0.5.4" @@ -131,7 +159,27 @@ checksum = "fea305d57546cc8cd04feb14b62ec84bf17f50e3f7b12560d7bfa9265f39d9ed" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", +] + +[[package]] +name = "thiserror" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", ] [[package]] @@ -140,6 +188,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "utfx" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133bf74f01486773317ddfcde8e2e20d2933cc3b68ab797e5d718bef996a81de" + [[package]] name = "winapi" version = "0.3.9" @@ -161,3 +215,13 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-registry" +version = "0.1.0" +dependencies = [ + "anyhow", + "neon", + "registry", + "utfx", +] diff --git a/native/Cargo.toml b/native/Cargo.toml index 4585165..4de37fd 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "native" +name = "windows-registry" version = "0.1.0" description = "A Rust/neon-based node.js native module for accessing and modifying the Windows registry." license = "MIT" @@ -11,8 +11,10 @@ crate-type = ["cdylib"] [dependencies] anyhow = "1.0" +registry = "1.2" +utfx = "0.1" [dependencies.neon] version = "0.10" default-features = false -features = ["napi-6", "promise-api", "channel-api"] +features = ["napi-6", "channel-api", "promise-api", "try-catch-api"] diff --git a/native/src/lib.rs b/native/src/lib.rs index 9fd52d6..fd0b0d5 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -1,11 +1,282 @@ -use neon::prelude::*; +use std::sync::mpsc; +use std::thread; -fn hello(mut cx: FunctionContext) -> JsResult { - Ok(cx.string("hello node")) +use neon::{ + prelude::*, + types::{buffer::TypedArray, Deferred}, +}; +use registry::{Data, Hive, Security}; +use utfx::U16CString; + +type RegistryCallback = Box; + +struct RegistryThread { + tx: mpsc::Sender, +} + +enum RegistryMessage { + Callback(Deferred, RegistryCallback), + Close, +} + +impl Finalize for RegistryThread {} + +impl RegistryThread { + fn new<'a, C>(cx: &mut C) -> anyhow::Result + where + C: Context<'a>, + { + let (tx, rx) = mpsc::channel::(); + + // FIXME: open registry? + + let channel = cx.channel(); + + thread::spawn(move || { + while let Ok(message) = rx.recv() { + match message { + RegistryMessage::Callback(deferred, f) => { + f(&channel, deferred); + } + RegistryMessage::Close => break, + } + } + }); + + Ok(Self { tx }) + } + + fn close(&self) -> Result<(), mpsc::SendError> { + self.tx.send(RegistryMessage::Close) + } + + fn send( + &self, + deferred: Deferred, + callback: impl FnOnce(&Channel, Deferred) + Send + 'static, + ) -> Result<(), mpsc::SendError> { + self.tx + .send(RegistryMessage::Callback(deferred, Box::new(callback))) + } + + fn js_new(mut cx: FunctionContext) -> JsResult> { + let reg = RegistryThread::new(&mut cx).or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.boxed(reg)) + } + + /// Closes the associated RegistryThread immediately. This is not required, as the thread will + /// close itself upon garbage collection, but this can be used to immediately end the thread to + /// allow the process to shut down. + fn js_close(mut cx: FunctionContext) -> JsResult { + cx.this() + .downcast_or_throw::, _>(&mut cx)? + .close() + .or_else(|err| cx.throw_error(err.to_string()))?; + + Ok(cx.undefined()) + } + + /// Reads a value from the registry. + /// Arguments are: + /// - `hive` (string): The hive to read from + /// - `key` (string): The key inside the hive to read + /// - `value` (string): The value inside the key to read + fn js_read(mut cx: FunctionContext) -> JsResult { + let hive = cx.argument::(0)?.value(&mut cx); + let key = cx.argument::(1)?.value(&mut cx); + let value = cx.argument::(2)?.value(&mut cx); + + let hive = hkey_str_to_hive(&hive).or_else(|e| cx.throw_error(e.to_string()))?; + + let reg = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + let (deferred, promise) = cx.promise(); + + reg.send(deferred, move |channel, deferred| { + let reg_key = match hive.open(key, Security::Read) { + Ok(r) => r, + Err(e) => { + match e { + registry::key::Error::NotFound(_, _) => { + deferred.settle_with(channel, move |mut cx| Ok(cx.undefined())); + } + _ => { + deferred.settle_with(channel, move |mut cx| { + cx.throw_error::>( + e.to_string(), + ) + }); + } + } + return; + } + }; + + let data = reg_key.value(value); + deferred.settle_with(channel, move |mut cx| match data { + Ok(data) => match data { + Data::None => Ok(cx.null().upcast::()), + Data::String(s) => Ok(cx.string(s.to_string_lossy()).upcast()), + Data::MultiString(s) => { + let js_array = JsArray::new(&mut cx, s.len() as u32); + for (i, s) in s.iter().enumerate() { + let s = cx.string(s.to_string_lossy()); + js_array.set(&mut cx, i as u32, s)?; + } + Ok(js_array.upcast()) + } + Data::ExpandString(s) => Ok(cx.string(s.to_string_lossy()).upcast()), + Data::U32(n) => Ok(cx.number(n as f64).upcast()), + Data::U64(n) => Ok(cx.number(n as f64).upcast()), + Data::Binary(b) => { + let typed_array = JsBuffer::external(&mut cx, b); + Ok(typed_array.upcast()) + } + t => cx.throw_error(format!("Unsupported registry type: {}", t)), + }, + Err(e) => match e { + registry::value::Error::NotFound(_, _) => Ok(cx.undefined().upcast()), + _ => cx.throw_error::>(e.to_string()), + }, + }); + }) + .into_rejection(&mut cx)?; + + Ok(promise) + } + + /// Writes a value from the registry. + /// Arguments are: + /// - `hive` (string): The hive to read from + /// - `key` (string): The key inside the hive to read + /// - `value` (string): The value inside the key to read + /// - `type` (string): The type of `data` + /// - `data` (depends on `type`): The data be written to the value + fn js_write(mut cx: FunctionContext) -> JsResult { + let hive = cx.argument::(0)?.value(&mut cx); + let key = cx.argument::(1)?.value(&mut cx); + let value = cx.argument::(2)?.value(&mut cx); + let type_name = cx.argument::(3)?.value(&mut cx); + + let hive = hkey_str_to_hive(&hive).or_else(|e| cx.throw_error(e.to_string()))?; + let data = match type_name.as_str() { + "REG_NONE" => Ok(Data::None), + "REG_SZ" => Ok(Data::String( + cx.argument::(4)? + .value(&mut cx) + .try_into() + .or_else(|e| cx.throw_error(format!("Invalid string: {}", e)))?, + )), + "REG_MULTI_SZ" => Ok(Data::MultiString( + cx.argument::(4)? + .to_vec(&mut cx)? + .iter() + .map(|s| { + s.downcast_or_throw::(&mut cx)? + .value(&mut cx) + .try_into() + .or_else(|e| { + cx.throw_error::(format!( + "Invalid string: {}", + e + )) + }) + }) + .collect::, _>>()?, + )), + "REG_EXPAND_SZ" => Ok(Data::ExpandString( + cx.argument::(4)? + .value(&mut cx) + .try_into() + .or_else(|e| cx.throw_error(format!("Invalid expand string: {}", e)))?, + )), + "REG_DWORD" => Ok(Data::U32(cx.argument::(4)?.value(&mut cx) as u32)), + "REG_QWORD" => Ok(Data::U64(cx.argument::(4)?.value(&mut cx) as u64)), + "REG_BINARY" => Ok(Data::Binary( + cx.argument::>(4)? + .as_slice(&mut cx) + .to_vec(), + )), + _ => Err(cx.throw_error(format!("Invalid registry type: {}", type_name))?), + }?; + + let reg = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + let (deferred, promise) = cx.promise(); + + reg.send(deferred, move |channel, deferred| { + let reg_key = match hive.create(key, Security::Write) { + Ok(r) => r, + Err(e) => { + deferred.settle_with(channel, move |mut cx| { + cx.throw_error::>( + e.to_string(), + ) + }); + return; + } + }; + + match reg_key.set_value(value, &data) { + Ok(_) => { + deferred.settle_with(channel, move |mut cx| Ok(cx.undefined())); + } + Err(e) => { + deferred.settle_with(channel, move |mut cx| { + cx.throw_error::>( + e.to_string(), + ) + }); + } + } + }) + .into_rejection(&mut cx)?; + + Ok(promise) + } +} +trait SendResultExt { + // Sending a closure to execute may fail if the channel has been closed. + // This method converts the failure into a promise rejection. + fn into_rejection<'a, C: Context<'a>>(self, cx: &mut C) -> NeonResult<()>; +} + +impl SendResultExt for Result<(), mpsc::SendError> { + fn into_rejection<'a, C: Context<'a>>(self, cx: &mut C) -> NeonResult<()> { + self.or_else(|err| { + let msg = err.to_string(); + + match err.0 { + RegistryMessage::Callback(deferred, _) => { + let err = cx.error(msg)?; + deferred.reject(cx, err); + Ok(()) + } + RegistryMessage::Close => cx.throw_error("Expected RegistryMessage::Callback"), + } + }) + } +} + +fn hkey_str_to_hive(hkey: &str) -> anyhow::Result { + match hkey { + "HKEY_CLASSES_ROOT" => Ok(Hive::ClassesRoot), + "HKEY_CURRENT_CONFIG" => Ok(Hive::CurrentConfig), + "HKEY_CURRENT_USER" => Ok(Hive::CurrentUser), + "HKEY_LOCAL_MACHINE" => Ok(Hive::LocalMachine), + "HKEY_USERS" => Ok(Hive::Users), + _ => Err(anyhow::anyhow!("Invalid hive: {}", hkey)), + } } #[neon::main] fn main(mut cx: ModuleContext) -> NeonResult<()> { - cx.export_function("hello", hello)?; + cx.export_function("registryNew", RegistryThread::js_new)?; + cx.export_function("registryClose", RegistryThread::js_close)?; + cx.export_function("registryRead", RegistryThread::js_read)?; + cx.export_function("registryWrite", RegistryThread::js_write)?; Ok(()) } diff --git a/package.json b/package.json index 7f6ed6e..ccdb324 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,13 @@ "version": "1.0.0", "description": "A Rust/neon-based node.js native module for accessing and modifying the Windows registry.", "main": "index.js", + "types": "index.d.ts", "scripts": { "build": "cd native && cargo-cp-artifact -nc index.node -- cargo build --target=x86_64-pc-windows-msvc --message-format=json-render-diagnostics", - "build-debug": "npm run build --", - "build-release": "npm run build -- --release", + "build-debug": "pnpm run build --", + "build-release": "pnpm run build -- --release", "tag-prebuild": "node ./tag-prebuild.js", - "test": "cd native && cargo test" + "test": "cd native && cargo test && cd .. && pnpm run build && node --test" }, "keywords": [ "windows", @@ -26,6 +27,7 @@ "homepage": "https://github.com/ShieldBattery/windows-registry#readme", "devDependencies": { "cargo-cp-artifact": "^0.1.8", - "neon-tag-prebuild": "^1.1.1" + "neon-tag-prebuild": "^1.1.1", + "prettier": "^3.2.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50b613f..375981d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ devDependencies: neon-tag-prebuild: specifier: ^1.1.1 version: 1.1.1 + prettier: + specifier: ^3.2.2 + version: 3.2.2 packages: @@ -39,6 +42,12 @@ packages: semver: 5.7.2 dev: true + /prettier@3.2.2: + resolution: {integrity: sha512-HTByuKZzw7utPiDO523Tt2pLtEyK7OibUD9suEJQrPUCYQqrHr74GGX6VidMrovbf/I50mPqr8j/II6oBAuc5A==} + engines: {node: '>=14'} + hasBin: true + dev: true + /semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..0991ee3 --- /dev/null +++ b/test/index.js @@ -0,0 +1,125 @@ +const test = require('node:test') +const assert = require('node:assert/strict') + +const { + WindowsRegistry, + HKCU, + REG_SZ, + REG_DWORD, + REG_QWORD, + REG_NONE, + REG_MULTI_SZ, + REG_EXPAND_SZ, + REG_BINARY, +} = require('../index.js') + +const TEST_KEY = 'SOFTWARE\\windows-registry\\test' + +test('roundtrips string values', async () => { + const registry = new WindowsRegistry() + try { + const randomValue = String(Math.round(Math.random() * 9999999)) + await registry.write(HKCU, TEST_KEY, 'RoundTripString', REG_SZ, randomValue) + // NOTE(tec27): We write a different value to the same key to ensure that creating the key does + // not clear its values + await registry.write(HKCU, TEST_KEY, 'RoundTripString2', REG_SZ, randomValue) + + const readValue = await registry.read( + HKCU, + 'SOFTWARE\\windows-registry\\test', + 'RoundTripString', + ) + assert.equal(readValue, randomValue) + } finally { + registry.close() + } +}) + +test('roundtrips dword values', async () => { + const registry = new WindowsRegistry() + try { + const randomValue = Math.round(Math.random() * 9999999) + await registry.write(HKCU, TEST_KEY, 'RoundTripDword', REG_DWORD, randomValue) + + const readValue = await registry.read(HKCU, TEST_KEY, 'RoundTripDword') + assert.equal(readValue, randomValue) + } finally { + registry.close() + } +}) + +test('roundtrips qword values', async () => { + const registry = new WindowsRegistry() + try { + const randomValue = Math.round(Math.random() * 999999999) + await registry.write(HKCU, TEST_KEY, 'RoundTripQword', REG_QWORD, randomValue) + + const readValue = await registry.read(HKCU, TEST_KEY, 'RoundTripQword') + assert.equal(readValue, randomValue) + } finally { + registry.close() + } +}) + +test('roundtrips none values', async () => { + const registry = new WindowsRegistry() + try { + await registry.write(HKCU, TEST_KEY, 'RoundTripNone', REG_NONE) + const readValue = await registry.read(HKCU, TEST_KEY, 'RoundTripNone') + assert.equal(readValue, null) + } finally { + registry.close() + } +}) + +test('roundtrips binary values', async () => { + const registry = new WindowsRegistry() + try { + const randomValue = Buffer.from([ + Math.round(Math.random() * 255), + Math.round(Math.random() * 255), + ]) + await registry.write(HKCU, TEST_KEY, 'RoundTripBinary', REG_BINARY, randomValue) + + const readValue = await registry.read(HKCU, TEST_KEY, 'RoundTripBinary') + assert.deepEqual(readValue, randomValue) + } finally { + registry.close() + } +}) + +test('roundtrips multi-string values', async () => { + const registry = new WindowsRegistry() + try { + const randomValue = [ + String(Math.round(Math.random() * 9999999)), + String(Math.round(Math.random() * 9999999)), + ] + await registry.write(HKCU, TEST_KEY, 'RoundTripMultiString', REG_MULTI_SZ, randomValue) + + const readValue = await registry.read(HKCU, TEST_KEY, 'RoundTripMultiString') + assert.deepEqual(readValue, randomValue) + } finally { + registry.close() + } +}) + +test('roundtrips expand-string values', async () => { + const registry = new WindowsRegistry() + try { + const randomValue = String(Math.round(Math.random() * 9999999)) + await registry.write(HKCU, TEST_KEY, 'RoundTripExpandString', REG_EXPAND_SZ, randomValue) + + const readValue = await registry.read(HKCU, TEST_KEY, 'RoundTripExpandString') + assert.deepEqual(readValue, randomValue) + } finally { + registry.close() + } +}) + +test('rejects after close', async () => { + const registry = new WindowsRegistry() + registry.close() + + await assert.rejects(() => registry.read(HKCU, TEST_KEY, 'SomeValue'), /closed/) +})