From bf1242f3502bbc7c625f5745640561e987a2b7f2 Mon Sep 17 00:00:00 2001 From: velvettear Date: Tue, 15 Feb 2022 04:33:19 +0100 Subject: [PATCH] initial commit --- .gitignore | 2 + .vscode/launch.json | 14 ++++++ README.md | 3 ++ config.json | 42 ++++++++++++++++ libs/cli.js | 44 ++++++++++++++++ libs/keyfilter.js | 84 +++++++++++++++++++++++++++++++ libs/logger.js | 98 ++++++++++++++++++++++++++++++++++++ libs/util.js | 43 ++++++++++++++++ libs/watcher.js | 120 ++++++++++++++++++++++++++++++++++++++++++++ libs/watchers.js | 62 +++++++++++++++++++++++ ninwa.js | 12 +++++ package.json | 22 ++++++++ 12 files changed, 546 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 README.md create mode 100644 config.json create mode 100644 libs/cli.js create mode 100644 libs/keyfilter.js create mode 100644 libs/logger.js create mode 100644 libs/util.js create mode 100644 libs/watcher.js create mode 100644 libs/watchers.js create mode 100644 ninwa.js create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..167ab9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +yarn.lock \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..68c6e51 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "pwa-node", + "request": "launch", + "name": "ninwa", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/ninwa.js" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..180381a --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# ninwa + +node input watcher \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..853a458 --- /dev/null +++ b/config.json @@ -0,0 +1,42 @@ +{ + "log": { + "level": "debug", + "timestamp": "DD.MM.YYYY HH:mm:ss:SS" + }, + "watchers": [ + { + "device": "usb-Razer_Razer_Blade_Stealth-if01-event-kbd", + "keys": [ + { + "key": "f9", + "type": "keydown", + "command": "notify-send", + "args": [ + "F9 DOWN", + "$(date)" + ] + }, + { + "key": "f8", + "type": "keyup", + "delay": 2000, + "command": "notify-send", + "args": [ + "F8 UP", + "$(date)" + ] + }, + { + "key": "f7", + "type": "keyhold", + "delay": 1000, + "command": "notify-send", + "args": [ + "F7 HOLD", + "$(date)" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/libs/cli.js b/libs/cli.js new file mode 100644 index 0000000..9c4f106 --- /dev/null +++ b/libs/cli.js @@ -0,0 +1,44 @@ +const logger = require('./logger.js'); +const spawn = require('child_process').spawn; + +function execute(command, args) { + return new Promise((resolve, reject) => { + if (command == undefined || command.length == 0) { + reject(); + } + var startTime = new Date().getTime(); + var resultData = ""; + var resultError = ""; + command = command.trim(); + logger.debug('executing command \'' + command + '\' (args: \'' + args + '\') ...'); + try { + var process = spawn(command, args); + } catch (err) { + logger.error(err); + } + process.stdout.on('data', (data) => { + resultData += data; + }); + process.stderr.on('data', (data) => { + resultError += data; + }); + process.on('close', (code) => { + var msg = 'command \'' + command + '\' (args: \'' + args + '\') finished with exit code ' + code + ' after ' + (new Date().getTime() - startTime) + 'ms'; + if (resultData.length > 0) { + msg += " > data: " + resultData; + } + if (resultError.length > 0) { + msg += " >>> error: " + resultError; + reject(msg); + } + resolve(msg); + }); + process.on('error', (err) => { + resultError += err; + }); + }); +} + +module.exports = { + execute +} \ No newline at end of file diff --git a/libs/keyfilter.js b/libs/keyfilter.js new file mode 100644 index 0000000..24c1f11 --- /dev/null +++ b/libs/keyfilter.js @@ -0,0 +1,84 @@ +const logger = require('./logger.js'); + +const FILTER_START = 'Testing ... (interrupt to exit)'; + +const KEY_PREFIX = 'KEY_'; + +const ACTION_KEYUP = { id: 0, action: 'keyup' }; +const ACTION_KEYDOWN = { id: 1, action: 'keydown' }; +const ACTION_KEYHOLD = { id: 2, action: 'keyhold' }; + +class Keyfilter { + constructor(config) { + if (config == undefined || config.length == 0) { + return; + } + this.actions = new Map(); + for (var index = 0; index < config.length; index++) { + var type = ACTION_KEYDOWN; + switch (config[index].type.toLowerCase()) { + case ACTION_KEYUP.action: + type = ACTION_KEYUP; + break; + case ACTION_KEYHOLD.action: + type = ACTION_KEYHOLD; + break; + } + var command = config[index].command; + var args = config[index].args; + var delay = config[index].delay; + this.actions.set(KEY_PREFIX + config[index].key.toUpperCase(), { type, command, args, delay }); + } + } + filterActive(input) { + if (this.active) { + return true; + } + input = input.toString(); + var index = input.indexOf(FILTER_START); + if (index == -1) { + return; + } + input = input.substring(index + FILTER_START.length).trim(); + this.active = true; + return true; + } + filter(input) { + if (input == undefined || input.length == 0) { + return; + } + input = input.toString(); + if (!this.filterActive(input)) { + return; + } + var lines = input.split("\n"); + for (var index = 0; index < lines.length; index++) { + var line = lines[index]; + if (line.length == 0) { + continue; + } + for (var [key, value] of this.actions) { + if (!line.includes(key)) { + continue; + } + if (!line.endsWith('value ' + value.type.id)) { + continue; + } + var action = this.actions.get(key); + if (this.shouldBeDelayed(action)) { + return { key: key, type: action.type.action, delayed: true }; + } + action.executed = new Date().getTime(); + return { key: key, type: action.type.action, command: action.command, args: action.args }; + } + } + } + shouldBeDelayed(action) { + if (action.delay == undefined || action.delay == 0 || action.executed == undefined) { + return false; + } + return new Date().getTime() - action.executed < action.delay; + } +} + +module.exports = Keyfilter; \ No newline at end of file diff --git a/libs/logger.js b/libs/logger.js new file mode 100644 index 0000000..f1980ad --- /dev/null +++ b/libs/logger.js @@ -0,0 +1,98 @@ +const config = require('../config.json'); +const moment = require('moment'); + +// constants +const LOG_PREFIX_DEBUG = 'debug'; +const LOG_PREFIX_INFO = 'info'; +const LOG_PREFIX_WARNING = 'warning'; +const LOG_PREFIX_ERROR = 'error'; +const LOGLEVEL_DEBUG = 0; +const LOGLEVEL_INFO = 1; +const LOGLEVEL_WARNING = 2; +const LOGLEVEL_ERROR = 3; + +// set loglevel on 'require' +const loglevel = function () { + switch (config.log.level) { + case LOG_PREFIX_DEBUG: + case LOGLEVEL_DEBUG: + return LOGLEVEL_DEBUG; + case LOG_PREFIX_INFO: + case LOGLEVEL_INFO: + return LOGLEVEL_INFO; + case LOG_PREFIX_WARNING: + case LOGLEVEL_WARNING: + return LOGLEVEL_WARNING; + case LOG_PREFIX_ERROR: + case LOGLEVEL_ERROR: + return LOGLEVEL_ERROR; + default: + return LOGLEVEL_INFO; + } +}(); + +// prefix log with 'info' +function info(message) { + if (loglevel > LOGLEVEL_INFO) { + return; + } + trace(message); +} + +// prefix log with 'info' +function warn(message) { + if (loglevel > LOGLEVEL_WARNING) { + return; + } + trace(message, 'warning'); +} + +// prefix log with 'debug' +function debug(message) { + if (loglevel > LOGLEVEL_DEBUG) { + return; + } + trace(message, 'debug'); +} + +// prefix log with 'error' +function error(message) { + if (loglevel > LOGLEVEL_ERROR) { + return; + } + trace(message, 'error'); +} + +// default logging function +function trace(message, prefix) { + if (message === undefined || message === null || message.length === 0) { + return; + } + if (prefix === undefined || prefix === null || prefix.length === 0) { + prefix = 'info'; + } + let print; + switch (prefix) { + case 'error': + print = console.error; + break; + case 'debug': + print = console.debug; + break; + case 'warning': + print = console.warn; + break; + default: + print = console.log; + } + message = moment().format(config.log.timestamp) + ' | ' + prefix + ' > ' + message; + print(message); +} + +// exports +module.exports = { + info, + warn, + debug, + error +}; \ No newline at end of file diff --git a/libs/util.js b/libs/util.js new file mode 100644 index 0000000..7489e2c --- /dev/null +++ b/libs/util.js @@ -0,0 +1,43 @@ +const realpath = require('fs/promises').realpath; +const stat = require('fs/promises').stat; + +function fileExists(file) { + return new Promise((resolve, reject) => { + if (file == undefined) { + reject('error: no file given'); + } + resolvePath(file) + .then((path) => { + stat(path) + .then((stats) => { + resolve({path, stats}); + }) + }) + .catch(reject); + }); +} + +function resolvePath(file) { + return new Promise((resolve, reject) => { + if (file == undefined) { + reject('error: no file given'); + } + realpath(file) + .then(resolve) + .catch((err) => { + reject('error: resolving path \'' + file + '\' encountered an error >>> ' + err); + }); + }); +} + +function executeCommand(command) { + if (command == undefined) { + return; + } + command = command.trim(); +} + +module.exports = { + fileExists, + resolvePath +} \ No newline at end of file diff --git a/libs/watcher.js b/libs/watcher.js new file mode 100644 index 0000000..a7608e0 --- /dev/null +++ b/libs/watcher.js @@ -0,0 +1,120 @@ +const logger = require('./logger.js'); +const util = require('./util.js'); +const Keyfilter = require('./keyfilter.js'); +const cli = require('./cli.js'); +const spawn = require('child_process').spawn; + +const inputDevices = '/dev/input/'; +const inputDevicesById = '/dev/input/by-id/'; + +class Watcher { + constructor(config, callback) { + if (config == undefined || config.device == undefined) { + return; + } + for (var key in config) { + this[key] = config[key]; + } + // if (!this.device.startsWith(inputDevices)) { + // this.device = inputDevices + this.device; + // } + this.keyfilter = new Keyfilter(config.keys); + this.callback = callback; + } + start() { + if (this.process != undefined) { + return; + } + logger.debug('starting watcher \'' + this.device + '\'...'); + this.process = spawn("evtest", [this.device]); + this.attachListeners(); + } + stop() { + if (this.process == undefined) { + return; + } + logger.debug('stopping watcher \'' + this.device + '\'...'); + this.process.kill(); + if (this.callback != undefined) { + this.callback(); + } + } + attachListeners() { + if (this.process == undefined) { + return; + } + this.addStdOutListener(); + this.addStdErrListener(); + this.addCloseListener(); + this.addErrorListener(); + } + addStdOutListener() { + if (this.process == undefined) { + return; + } + logger.debug('adding stdout listener to watcher \'' + this.device + '\'...'); + this.process.stdout.on('data', (data) => { + if (this.keyfilter == undefined) { + return; + } + var filtered = this.keyfilter.filter(data); + if (filtered == undefined) { + return; + } + var msg = 'watcher \'' + this.device + '\' captured event'; + if (filtered.delayed) { + logger.debug('delaying captured \'' + filtered.type + '\' event for \'' + filtered.key + '\' from watcher \'' + this.device + '\''); + return; + } + logger.info('executing command \'' + filtered.command + '\' (args: \'' + filtered.args + '\') registered for captured \'' + filtered.type + '\' event for \'' + filtered.key + '\' from watcher \'' + this.device + '\''); + cli.execute(filtered.command, filtered.args) + .then(logger.info) + .catch(logger.error); + }); + } + addStdErrListener() { + if (this.process == undefined) { + return; + } + logger.debug('adding stderr listener to watcher \'' + this.device + '\'...'); + this.process.stderr.on('data', (data) => { + logger.error(data); + }); + } + addCloseListener() { + if (this.process == undefined) { + return; + } + logger.debug('adding close listener to watcher \'' + this.device + '\'...'); + this.process.on('close', (code) => { + logger.info('watcher \'' + this.device + '\' finished with exit code ' + code); + if (this.callback != undefined) { + this.callback(null, this.device, code); + } + }); + } + addErrorListener() { + if (this.process == undefined) { + return; + } + logger.debug('adding error listener to \'' + this.device + '\'...'); + this.process.on('error', (err) => { + logger.error('error: watcher \'' + this.device + '\' encountered an error >>> ' + err); + }); + } + check() { + return new Promise((resolve, reject) => { + Promise.any([inputDevices + this.device, inputDevicesById + this.device].map(util.fileExists)) + .then((result) => { + if (result.path != this.device) { + logger.info('resolved watcher for device \'' + this.device + '\' to \'' + result.path + '\'') + } + logger.info('resolved ') + this.device = result.path; + resolve(); + }) + .catch(reject); + }); + } +} +module.exports = Watcher; \ No newline at end of file diff --git a/libs/watchers.js b/libs/watchers.js new file mode 100644 index 0000000..4e250c1 --- /dev/null +++ b/libs/watchers.js @@ -0,0 +1,62 @@ +const configJSON = require('../config.json'); +const logger = require('./logger.js'); +const Watcher = require('./watcher.js'); + +const watchers = []; + +function initialize() { + return new Promise(function (resolve, reject) { + if (configJSON == undefined || configJSON.watchers == undefined || configJSON.watchers.length == 0) { + reject('error: no input devices defined'); + } + var tmp = [] + for (var index = 0; index < configJSON.watchers.length; index++) { + tmp.push(new Watcher(configJSON.watchers[index])); + } + Promise.all(tmp.map(check)).then(resolve); + }); +} +function check(watcher) { + return new Promise(function (resolve, reject) { + if (watcher == undefined) { + reject(); + } + watcher.check() + .then(() => { + logger.info('watcher \'' + watcher.device) + watcher.start(); + watchers.push(watcher); + }) + .catch((err) => { + logger.error(err);d + }); + }); +} + +function start() { + logger.debug('starting ' + watchers.size + ' watcher(s)...'); + for (const watcher of watchers.entries()) { + watcher[1].start(); + } +} + +function stop() { + const count = watchers.size; + logger.info('stopping all ' + count + ' watchers...'); + for (var index = 0; index < count; index++) { + var watcher = watchers[index]; + logger.debug('stopping watcher ' + watcher.id + '...'); + watcher.kill(); + } +} + +function callback(err, id, code) { + this.watchers.delete(id); + logger.debug('removed watcher \'' + id + '\''); +} + +module.exports = { + initialize, + start, + stop +} \ No newline at end of file diff --git a/ninwa.js b/ninwa.js new file mode 100644 index 0000000..a2c48db --- /dev/null +++ b/ninwa.js @@ -0,0 +1,12 @@ +const watchers = require('./libs/watchers.js'); +const logger = require('./libs/logger.js'); +const packageJSON = require('./package.json'); + +logger.info(packageJSON.name + ' ' + packageJSON.version); + +watchers.initialize() + .then(watchers.start) + .catch((err) => { + logger.error(err); + process.exit(1); + }); diff --git a/package.json b/package.json new file mode 100644 index 0000000..b8bbbd9 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "ninwa", + "version": "0.0.1", + "description": "node input watcher", + "main": "ninwa.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://git.velvettear.de/velvettear/ninwa.git" + }, + "keywords": [ + "input", + "keyboard" + ], + "author": "Daniel Sommer ", + "license": "MIT", + "dependencies": { + "moment": "^2.29.1" + } +}