A single npm module that enables Node developers to update the Windows Registry, create file associations, and much more!
As cross-platform application developers, we often need the ability to update the Windows Registry when installing our applications on Windows. Common features requested by app developers include reading and writing keys to Windows registry, creating file associations to make Windows assign default programs to a file extension, elevating processes to run as an administrator, and last but not least doing all this within the same process as our application. This code story describes the development of a single npm module that enables Node developers to perform these commonly-requested operations when installing Node.js applications on Windows.
The Problem
Electron is a framework that enables developers to build cross platform desktop apps with web technologies. Electron is behind Microsoft’s Visual Studio Code, Slack’s Apps, GitHub’s Atom, Facebook’s Nucleide, Docker’s Kinematic, and a few others. Prior to our hackathon with GitHub and Visual Studio Code, developers had limited number of resources to work with in order to interact with the Windows Registry. More importantly, there was no library that allowed developers to update the Windows Registry without running a separate shell script. In order to provide a great app install experience for users on Windows, it was obvious that we needed to create an NPM module that can address all these issues. Since this is an NPM module, all Node.js applications can take advantage of this library.
Overview of the Solution
We partnered with GitHub, the maintainer of Electron, and Visual Studio Code, to enable registry operations and file associations for Node application on Windows. As a result of our hackathon, there is now a NPM module for Windows Registry you can include in your application to do just that.
Interact with Windows APIs
This library interacts with native Windows APIs. We leveraged the following node modules to enable Node.js application to communicate with Windows interfaces:
- node-ffi – A Node.js addon for loading and calling dynamic libraries using pure Javascript. It can be used to create bindings to native libraries without writing any C++ code. We used this module to load a list of Windows DLLs to call native Windows APIs.
- ref – A NPM module that turns buffer instances into pointers. We used this module to define our own data types to map to the Windows data types. It also conveniently allowed us to reference and dereference buffers.
- ref-struct – This module allows us to define and implement structures. In order to communicate with Windows APIs, we needed to implement structs that mirror the inputs and outputs of Windows interfaces.
We leveraged the following Windows DLLs to communicate with Windows APIs.
- Advapi32.dll – A Windows library that supports numerous APIs including many security and registry calls. We used this library to perform CRUD operations in Windows Registry.
- Shell32.dll – A Windows library that contains Windows Shell APIs, which are used to open processes on Windows. We used this library to elevate privilege of a process, similar to the experience of running an application as an administrator.
Let’s dive into the code!
Creating Node Wrappers for Windows APIs
We created a Node wrapper adv_api.js for Advapi32.dll
to support basic registry manipulation. Using the ffi
node module, we can load Advapi32.dll
. Then for each Windows API we wanted to call, we created a Javascript API with the expected inputs and outputs.
In C++, the follow example illustrates the use of RegOpenCurrentUser
, which is used to retrieve a handle to the HKEY_CURRENT_USER key for the user that the current thread is impersonating.
HKEY keyCurrentUser;
lResult = RegOpenCurrentUser(KEY_READ, &keyCurrentUser);
In our Node.js wrapper, we used the ffi
node module to load Advapi32.dll
. Then we defined the RegOpenCurrentUser
API and other Registry related APIs with their expected inputs and outputs.
var ffi = require('ffi');
var advApi = ffi.Library('Advapi32', {
RegOpenCurrentUser: ['uint64', [types.REGSAM, types.PHKEY]],
RegQueryValueExA: ['uint64', [types.HKEY, 'string', 'pointer', types.LPDWORD, types.LPBYTE, types.LPDWORD]],
...
});
Similarly, we created a Node wrapper shell32.js for Shell32.dll
, which is used to launch a process on Windows.
In C++, the following example illustrates the use of ShellExecuteEx
to launch an application:
BOOL result = ShellExecuteExA(&ShExecInfo);
In our Node.js wrapper, we used the ffi
node module to load Shell32.dll
, then we defined the ShellExecuteExA
API with its expected input and output.
var ffi = require('ffi');
var shell32 = ffi.Library('Shell32', {
ShellExecuteExA: ['bool', [SHELLEXECUTEINFOPtr]]
});
Create Windows Data Types in JavaScript
As you can see, in order to replicate the exact Windows API call in JavaScript, we needed to create the same function with the exact inputs and outputs of the same data type as the Windows native data types.
For example, let’s look at how we defined the data types for the ShellExecuteExA
function.
In C++, here is the definition of the ShellExecuteEx
function:
BOOL ShellExecuteExA(
_Inout_ SHELLEXECUTEINFO *pExecInfo
);
With the above definition, ShellExecuteEx
expects a parameter of the type SHELLEXECUTEINFO*
, which is a pointer to a SHELLEXECUTEINFO
structure that contains and receives information about the application being executed. It also has a return value of type BOOL
, which represents the result of executing this call.
In C++, this is the definition of the SHELLEXECUTEINFO
structure.
typedef struct _SHELLEXECUTEINFO {
DWORD cbSize;
ULONG fMask;
HWND hwnd;
LPCTSTR lpVerb;
LPCTSTR lpFile;
LPCTSTR lpParameters;
LPCTSTR lpDirectory;
int nShow;
HINSTANCE hInstApp;
LPVOID lpIDList;
LPCTSTR lpClass;
HKEY hkeyClass;
DWORD dwHotKey;
union {
HANDLE hIcon;
HANDLE hMonitor;
} DUMMYUNIONNAME;
HANDLE hProcess;
} SHELLEXECUTEINFO, *LPSHELLEXECUTEINFO;
In our Node.js wrapper windef.js, we needed to create the same SHELLEXECUTEINFO
struct. Luckily, we had ref
, ref struct
, and ref union
node modules to help us create the same struct in Javascript.
SHELLEXECUTEINFO: struct({
cbSize: types.DWORD,
fMask: types.ULONG,
hwnd: types.HWND,
lpVerb: types.STRING,
lpFile: types.STRING,
lpParameters: types.STRING,
lpDirectory: types.STRING,
nShow: types.INT,
hInstApp: types.HINSTANCE,
lpIDList: types.LPVOID,
lpClass: types.STRING,
hkeyClass: types.HKEY,
dwHotKey: types.DWORD,
DUMMYUNIONNAME: DUMMYUNIONNAME,
hProcess: types.HANDLE
})
For each struct member, we looked up its data type and its size from the Windows Data Types reference, then created the same data type with the same properties in types.js. We leveraged the ref
node module to create types.
var ref = require('ref');
var types = {
REGSAM: ref.types.uint64,
DWORD: ref.types.uint32,
ULONG: ref.types.uint32,
...
};
Now that we have the SHELLEXECUTEINFO
struct in Javascript, we can create an instance of it in utils.js with the runas
ShellExecuteEx verb for the lpVerb
member and a file path to the process we want to launch for the lpFile
member. Let’s call it shellexecuteinfoval
. This instance can then be used as a parameter of ShellExecuteExA
so that we can launch a process with elevated privilege.
var shellexecuteinfoval = new windef.SHELLEXECUTEINFO({
cbSize: windef.SHELLEXECUTEINFO.size,
fMask: 0x00000000,
hwnd: null,
lpVerb: lpVerb,
lpFile: filepath,
lpParameters: parameters,
lpDirectory: null,
nShow: SW_SHOWNORMAL,
hInstApp: hInstApp,
lpIDList: null,
lpCLass: null,
hkeyClass: null,
dwHotKey: null,
DUMMYUNIONNAME: {
hIcon: null,
hMonitor: null
},
hProcess: ref.alloc(types.HANDLE)
});
Let’s call ShellExecuteExA
with a reference of our new parameter. We implemented this as an asynchronous call so that when the UAC (User Account Control) prompt is launched, the current process can continue to run without waiting on the user to respond.
shell32.ShellExecuteExA.async(shellexecuteinfoval.ref(), callback);
To wrap everything together, we created an API elevate
in our module that takes in a filepath
of the process you want to launch, its parameters
if any, and a callback to get the user’s response to the UAC prompt.
module.exports = {
elevate: function (filepath, parameters, callback) {
...
shell32.ShellExecuteExA.async(shellexecuteinfoval.ref(), callback);
}};
The process you want to launch with admin access will only be launched after the callback is called and only if the user clicks Yes in the UAC prompt. Otherwise, the process will not be launched. If the user is already running as an admin, the UAC prompt will not be triggered and the process you provided will be launched as an administrator automatically.
Here is an example of this running:
utils.elevate('C:\Program Files\nodejs\node.exe', 'index.js', function (err, result){console.log('callback');});
Enable Your Application
To add the NPM module for Windows Registry to your Node application, install the package:
npm install windows-registry
Installation
To install node modules that require compilation on Windows, make sure you have installed the necessary build tools. Specifically, you need npm install -g node-gyp
, a cross-platform cli written in Node.js for native addon modules for Node.js.
To install node-gyp
, you need to have the following prerequisites installed and configured in your development environment.
- Install python v2.7.3, and add it to your
PATH
,npm config set python python2.7
- Install VC++ Build Tools Technical Preview. You do not need to install the full Visual Studio, only the build tools are required.
- [Windows 7 only] requires .NET Framework 4.5.1
- Launch cmd,
npm config set msvs_version 2015 --global
(this is instead ofnpm install [package name] --msvs_version=2015
every time.)
Once the prerequisites are installed, you should be able to do npm install -g node-gyp
.
Reading and Writing to the Windows Registry
This library implements only a few of the basic registry commands, which allow you to do basic CRUD operations for keys in the registry.
Opening a Registry Key
Registry keys can be opened by either opening a predefined registry key defined in the windef module:
var Key = require('windows-registry').Key;
var key = new Key(windef.HKEY.HKEY_CLASSES_ROOT, '.txt', windef.KEY_ACCESS.KEY_ALL_ACCESS);
Or you can open a sub key from an already opened key:
var Key = require('windows-registry').Key;
var key = new Key(windef.HKEY.HKEY_CLASSES_ROOT, '', windef.KEY_ACCESS.KEY_ALL_ACCESS);
var key2 = key.openSubKey('.txt', windef.KEY_ACCESS.KEY_ALL_ACCESS);
And don’t forget to close your key when you’re done. Otherwise, you will leak native resources:
key.close();
Creating a Key
Creating a key just requires that you have a Key object by either using the predefined keys within the windef.HKEY
or opening a subkey from an existing key.
var Key = require('windows-registry').Key;
// predefined key
var key = new Key(windef.HKEY.HKEY_CLASSES_ROOT, '', windef.KEY_ACCESS.KEY_ALL_ACCESS);
var createdKey = key.createSubKey('test_key_name', windef.KEY_ACCESS.KEY_ALL_ACCESS);
Deleting a Key
To delete a key just call the Key.deleteKey()
function.
createdKey.deleteKey();
Writing a Value to a Key
To write a value, you will again need a Key object and just need to call the Key.setValue
function:
var Key = require('windows-registry').Key,
windef = require('windows-registry').windef;
var key = new Key(windef.HKEY.HKEY_CLASSES_ROOT, '.txt', windef.KEY_ACCESS.KEY_ALL_ACCESS);
key.setValue('test_value_name', windef.REG_VALUE_TYPE.REG_SZ, 'test_value');
Getting a Value From a Key
To get a value from a key, just call Key.getValue
:
var value = key.getValue('test_value_name');
The return value depends on the type of the key (REG_SZ for example will give you a string).
Creating File Associations
To create a file association, you can call the fileAssociation.associateExeForFile
api, which will make windows assign a default program for an arbitrary file extension:
var utils = require('windows-registry').utils;
utils.associateExeForFile('myTestHandler', 'A test handler for unit tests', 'C:\path\to\icon', 'C:\Program Files\nodejs\node.exe %1', '.zzz');
After running the code above, you will see files with the extension of .zzz will be automatically associated with the Node program and their file icon will be changed to the Node file icon.
Launching Process as an Admin
To launch a process as an Administrator, you can call the utils.elevate
api, which will launch a process as an Administrator causing the UAC (User Account Control) elevation prompt to appear if required. This is similar to the Windows Explorer command “Run as administrator”. Pass in FILEPATH
to the process you want to elevate. Pass in any PARAMETERS
to run with the process. Since this is an asynchronous call, pass in a callback to handle user’s selection.
var utils = require('windows-registry').utils;
utils.elevate('C:\Program Files\nodejs\node.exe', 'index.js', function (err, result){console.log(result);});
Opportunities for Reuse
With NPM module for Windows Registry, developers can now read and write keys to Windows registry, create file associations to make Windows assign default programs to a file extension, and elevate processes to run as an administrator for any Node application running on Windows. With the source code of this NPM module in GitHub, our solution also serves as an example for how to interact with native Windows APIs in Node.js.
0 comments