ioHook + Electron + Webpack integration

For the variety of multi-platform applications, we are often faced with situations when we need to globally listen to user events outside of the scope of the main window. This task brings extra security risks that’s why mechanisms for its implementation are limited. But limited not means not possible. Despite the fact that we can’t listen for users interactions with a system from a pure browser, we can do it from node applications that are launched from an Electron shell.

The best (and maybe the only for the moment of writing this article) representative of such node applications is ioHook. ioHook is a Node.js global native keyboard and mouse listener. This module can handle keyboard and mouse events via native hooks inside and outside your JavaScript/TypeScript application.

Platform support

  • Versions >= 0.6.0 support only officially supported platforms versions (Electron, NodeJS).
  • Versions 0.5.X are the last to support Electron < 4.0.0
  • Versions 0.4.X are the last to support for Node < 8.0 and Electron < 2.0.0

Full package description: https://www.npmjs.com/package/iohook

ioHook integration issues

Despite all the cool features that ioHook provides, the integration of this really young package can be a challenging task. The amount of questions from struggling developers grows every week so this pushed me to create a small example project which answers some of these questions.

Let’s summarize the existing integration issues:

  • ioHook + Webpack bundle problem which leads to the error: Error: Cannot find module ../<path>/<to>/<binary>/.iohook.node; (When we try to import ioHook in renderer context)
  • Webpack externals issue: module.exports = iohook; ReferenceError: iohook is not defined ; (When we try to import ioHook in renderer context)
  • ioHook binaries uploading: can’t find binary iohook-vX.X.X-electron-vXX-darwin-xXX.tar.gz;
  • Running ioHook from Electron main vs Electon renderer process, app.allowRendererProcessReuse issue; (When we try to use ioHook in renderer context)

Let’s create a small test project which resolves all these questions.

ioHook integration example

We are going to create a small example that 1) uses ioHook to catch global user “mousemove” and “mouseclick” interactions with the system, and 2) visualizes the received information in Electron’s browser window.

Folders structure:

package.json

Here, it is important to know that (from the official website):

Before installing this module, you will need to set a runtime version in your package.json.

When developing with webpack, you will need the Node.js runtime. In production, your Electron app will need the Electron version.

Checkout your ABI for node.js (opens new window)or electron (opens new window). The example below uses Node.js v12.X and Electron v11.X.

In our case, the package.json ioHook part will be looking like this:

"iohook": {
"targets": [
"electron-82"
],
"platforms": [
"win32",
"darwin",
"linux"
],
"arches": [
"x64",
"ia32"
]
}

There is also another possible way of binaries definition (check: https://wilix-team.github.io/iohook/usage.html#usage-with-electron)

But what if you want to use another version of Electron/Node/IoHook ? Well, for this you need to change the versions in the code above (preliminary checking Node/Electron ABI’s) and, depending on your luck, you will successfully get new binaries, or, most probably receive an error: can’t find binary iohook-vX.X.X-electron-vXX-darwin-xXX.tar.gz

To solve this issue (number 3 in our list) you may follow two approaches: Carefully check the ioHook releases binaries: https://github.com/wilix-team/iohook/releases/tag/undefined0.9.1 and find a combination of ioHook/Electron/Node/MacOS/Windows/Linux binary version which fulfils your needs. For some reason developers include different combinations of binaries in different releases so you should be careful. Alternatively you may try to build the binary by yourself following the official documentation: https://wilix-team.github.io/iohook/manual-build.html#linux Take into account that doing it on MacOS is much easier than on a Windows machine.

webpack.config.js

module.exports = {
......
externals: {
iohook: "iohook"
}
......
},

The webpack config for the Electron renderer web page contains the most tricky and important part of our ioHook friendly bundle (code above).

Note that the problem and solution below are relevant ONLY for using ioHook in Electron’s renderer context. Although possible, this is not the best solution. I’ll explain later why.

WebPack is an amazing tool, but what happens if you try to bundle a backend prepared .node application? In our case you will receive the following error (number one on our issues list): Error: Cannot find module ../<path>/<to>/<binary>/.iohook.node; It will appear regardless of whether the module actually exists in the right directory or not. Why did this happen? Short explanation: backend designed application that are supposed to be running in runtime can’t be bundled by WebPack and run in Browser like environment. You can read more about it in this article: https://archive.jlongster.com/Backend-Apps-with-Webpack--Part-I Solution? Use a nice WebPack feature called “externals” which excludes ioHook with it’s predefined binaries from the bundle. You can read all details of this feature on the official documentation page: https://webpack-v3.jsx.app/configuration/externals/

electron/main.js

Electron main entry point. From here we can directly import our ioHook package:

// Add iohook functionality to main electron process
import ioHook from 'iohook';

Since we import the package in the Main process (and WebPack electron config target is set to: “electron-main”) We do not need externals from the previous step.

Now ioHook can be used:

ioHook.on('mousemove', event => {
//console.log(event); // { type: 'mousemove', x: 700, y: 400 }
mainWindow.webContents.send('mousemove', event);
});

ioHook.on('mouseclick', event => {
mainWindow.webContents.send('mouseclick', event);
});

// Register and start hook
ioHook.start(); or ioHook.start(true); to enable debugging

We now listening for global user events and passing that information into the Renderer process via “mainWindow.webContents” send interface.

Note that the problem and solution below (same as previous issue) are relevant ONLY for using ioHook in Electron’s renderer context. Although possible, this is not the best solution.

But what if we already imported and tried running ioHook directly from the renderer context? Well, most probably we will receive an error that tells us that app.allowRendererProcessReuse Electron window property flag should be set to false to make runtime node modules work (Our issue listed under number 4).

From the Electron documentation: allowRendererProcessReuse A Boolean which when true disables the overrides that Electron has in place to ensure renderer processes are restarted on every navigation. The current default value for this property is true.

The intention is for these overrides to become disabled by default and then at some point in the future, wait for this property to be removed. This property impacts which native modules you can use in the renderer process. For more information on the direction Electron is going with renderer process restarts and usage of native modules in the renderer process please check this Tracking Issue.

Based on the property description, and , especially on the tacking issue discussion Electron developers plan to fully eliminate support of loading native modules into electron renderer and make allowRendererProcessReuse variable allways true.

Timeline

- Land the app.allowRendererProcessReuse option in Electron 6

- Have the first deprecation warning land in Electron 7 for non-context aware native modules

- Have a deprecation warning land in Electron 8 for the default value of app.allowRendererProcessReuse to switch

- Switch the default value of app.allowRendererProcessReuse to true in Electron 9

- Deprecate the ability to change app.allowRendererProcessReuse in Electron 10

- Remove the ability to change app.allowRendererProcessReuse in Electron 14

So the solution for renderer process ioHook: set app.allowRendererProcessReuse = false but take into account that this approach won’t be working in newer Electron versions which can prevent your software from updates and increases security risks (and other risks) in the future.

src/index.js

Main entry point for application rendered in Electron browser window.

// Since ioHook added to WebPack externals (due to binaries .node backages)
// which can't be bundled it should be called from electron window global scope
const ioHook = window.require('iohook');

The code above resolves the last remaining issue from our scope (number 2). Since we include ioHook in our Browser build as an external module, it won’t be available any more via “import” or pure “require” structures but, instead it can be required from Electron window scope: const ioHook = window.require(‘iohook’); That’s basically it, after the proper require, ioHook functionality can be freely used from Renderer scope.

ioHook usage directly from Renderer scope:

ioHook.on('mousemove', event => {
//console.log(event); // { type: 'mousemove', x: 700, y: 400 }
rendererData.innerText = JSON.stringify(event);
});

ioHook.on('mouseclick', event => {
rendererData.innerText = JSON.stringify(event);
});

// Register and start hook
ioHook.start(true);

For the Main scope, as I described previously, we define “webContents.send” functionality which sends interactions data caught by ioHook to the Renderer process. The next lines of code provide proper data management and visualization on the Renderer side:

// init ipcRenderer to receive an ioHook events from Electron main process
const ipcRenderer = window.require('electron').ipcRenderer;

First we get the current ipcRenderer instance from Electron. (Notice that we require Electron in the same manner as an external ioHook).

// Listen ioHook events from main process
ipcRenderer.on('mousemove', function (event,store) {
mainData.innerText = JSON.stringify(store);
});

ipcRenderer.on('mouseclick', function (event,store) {
mainData.innerText = JSON.stringify(store);
});

Then we listen for ipcRenderer corresponding events it receives from the main process and do all the necessary job inside the listeners callbacks.

Conclusion

ioHook is a powerful tool to get out of the box and catch some extra information from the user’s interactions with the surrounding environment. The project is quite young and has some not documented integration issues (especially WebPack related). Hope that this article saves a lot of your time and helps to create cool multi-platform applications faster.

Link to the full example project: https://github.com/IevgenySp/ioHookElectronWebpack

Enjoy!

I turn data into visual stories that reveal their true value. Enthusiastic traveler, visited 34 countries.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store