From 3e40dc85e7e431bdfff91762e955a8191b33c3fc Mon Sep 17 00:00:00 2001 From: velvettear Date: Thu, 10 Mar 2022 12:29:41 +0100 Subject: [PATCH] added possibility to spawn commands with sudo, optimized some stuff --- .vscode/launch.json | 1 + config.json | 15 ++++--- libs/.nvmrc | 1 + libs/cli.js | 95 ++++++++++++++++++++++----------------------- libs/keyfilter.js | 16 ++++---- libs/logger.js | 61 +++++++++++++++-------------- libs/util.js | 39 +++++++------------ libs/watcher.js | 84 +++++++++++++++++++++++---------------- libs/watchers.js | 40 +++++++------------ ninwa.js | 55 +++++++++++++------------- package.json | 3 +- 11 files changed, 205 insertions(+), 205 deletions(-) create mode 100644 libs/.nvmrc diff --git a/.vscode/launch.json b/.vscode/launch.json index 5f9654c..df55f29 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -3,6 +3,7 @@ "configurations": [ { "type": "pwa-node", + "runtimeVersion": "17", "request": "launch", "name": "ninwa", "skipFiles": [ diff --git a/config.json b/config.json index ce36057..f0613a3 100644 --- a/config.json +++ b/config.json @@ -5,8 +5,9 @@ }, "watchers": [ { - "device": "usb-Razer_Razer_Blade_Stealth-if01-event-kbd", + "device": "/dev/input/by-id/usb-Chicony_HP_Elite_USB_Keyboard-event-kbd", "restart": true, + "sudo": true, "keys": [ { "key": "key_f1", @@ -21,7 +22,8 @@ "args": [ "combo", "{{ key }} {{ type }}" - ] + ], + "sudo": false }, { "key": "key_enter", @@ -30,7 +32,8 @@ "args": [ "{{ key }}", "{{ type }}" - ] + ], + "sudo": false }, { "key": "key_esc", @@ -39,7 +42,8 @@ "args": [ "{{ key }}", "{{ type }}" - ] + ], + "sudo": false }, { "key": "key_space", @@ -49,7 +53,8 @@ "args": [ "{{ key }}", "{{ type }}" - ] + ], + "sudo": false } ] } diff --git a/libs/.nvmrc b/libs/.nvmrc new file mode 100644 index 0000000..8e2afd3 --- /dev/null +++ b/libs/.nvmrc @@ -0,0 +1 @@ +17 \ No newline at end of file diff --git a/libs/cli.js b/libs/cli.js index c3c63da..ed96aec 100644 --- a/libs/cli.js +++ b/libs/cli.js @@ -1,58 +1,57 @@ const logger = require('./logger.js'); const spawn = require('child_process').spawn; +const sudo = require('sudo'); -async function execute(command, args, returnOnClose) { - // return new Promise((resolve, reject) => { - if (command === undefined || command.length === 0) { +async function execute(command, args, useSudo, returnOnClose) { + if (command === undefined || command.length === 0) { + return; + } + if (returnOnClose === undefined) { + returnOnClose = false; + } + let startTime = new Date().getTime(); + let resultData = ""; + let resultError = ""; + command = command.trim(); + let process; + if (useSudo) { + logger.debug('executing sudo command \'' + command + '\' (args: \'' + args + '\')...'); + args.unshift(command); + process = sudo(args, { cachePassword: true, prompt: 'sudo password:' }); + } else { + logger.debug('executing command \'' + command + '\' (args: \'' + args + '\')...'); + process = spawn(command, args); + } + process.stdout.on('data', (data) => { + resultData += data; + }); + process.stderr.on('data', (data) => { + resultError += data; + }); + process.on('spawn', () => { + logger.info('spawned command \'' + command + '\' (args: \'' + args + '\')'); + if (!returnOnClose) { return; - // reject(); } - if (returnOnClose === undefined) { - returnOnClose = false; + }); + process.on('error', (err) => { + throw new Error(err); + }); + process.on('close', (code) => { + let msg = 'command \'' + command + '\' (args: \'' + args + '\') finished with exit code ' + code + ' after ' + (new Date().getTime() - startTime) + 'ms'; + if (resultData.length > 0) { + msg += " > data: " + resultData; } - 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); + if (resultError.length > 0) { + msg += " >>> error: " + resultError; + logger.error(msg) + return; } - process.stdout.on('data', (data) => { - resultData += data; - }); - process.stderr.on('data', (data) => { - resultError += data; - }); - process.on('spawn', () => { - logger.info('spawned command \'' + command + '\' (args: \'' + args + '\')'); - if (!returnOnClose) { - return; - } - }); - process.on('error', (err) => { - throw new Error(err); - // resultError += err; - }); - 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; - throw new Error(msg); - // reject(msg); - } - if (returnOnClose) { - return; - } - // resolve(msg); - }); - - // }); + logger.debug(msg); + if (returnOnClose) { + return; + } + }); } module.exports = { diff --git a/libs/keyfilter.js b/libs/keyfilter.js index 02e2dcd..904ed6c 100644 --- a/libs/keyfilter.js +++ b/libs/keyfilter.js @@ -13,13 +13,13 @@ class Keyfilter { return; } this.actions = new Map(); - for (var index = 0; index < keys.length; index++) { + for (let index = 0; index < keys.length; index++) { this.setAction(keys[index]); } this.currentCombo = undefined; } setAction(config) { - var type = ACTION_KEYDOWN; + let type = ACTION_KEYDOWN; switch (config.type.toLowerCase()) { case ACTION_KEYUP.action: type = ACTION_KEYUP; @@ -34,6 +34,7 @@ class Keyfilter { event: config.event, command: config.command, args: config.args, + sudo: config.sudo, combo: config.combo, delay: function () { if (config.combo === undefined) { @@ -49,9 +50,9 @@ class Keyfilter { return; } input = input.toString(); - var lines = input.split("\n"); - for (var index = 0; index < lines.length; index++) { - var line = lines[index]; + let lines = input.split("\n"); + for (let index = 0; index < lines.length; index++) { + let line = lines[index]; if (line.length === 0) { continue; } @@ -62,7 +63,7 @@ class Keyfilter { if (parsedEvent === undefined) { continue; } - for (var [key, event] of this.actions) { + for (let [key, event] of this.actions) { if (this.currentCombo === undefined && !this.isParsedEventValid(key, event, parsedEvent)) { continue; } @@ -103,6 +104,7 @@ class Keyfilter { type: event.type.action, command: event.command, args: event.args, + sudo: event.sudo, delay: event.delay } ) @@ -123,7 +125,7 @@ class Keyfilter { } } replaceVariables(filtered) { - for (var index = 0; index < filtered.args.length; index++) { + for (let index = 0; index < filtered.args.length; index++) { filtered.args[index] = filtered.args[index].replace(VARIABLE_KEY, filtered.key); filtered.args[index] = filtered.args[index].replace(VARIABLE_TYPE, filtered.type); } diff --git a/libs/logger.js b/libs/logger.js index 10d16d2..91c8160 100644 --- a/libs/logger.js +++ b/libs/logger.js @@ -10,49 +10,42 @@ const LOGLEVEL_INFO = 1; const LOGLEVEL_WARNING = 2; const LOGLEVEL_ERROR = 3; -var loglevel = getLogLevel(); -var timestamp = getTimestamp(); +let loglevel; +let timestamp; -function initialize() { - return new Promise((resolve, reject) => { - if (global.config == undefined) { - reject('could not initialize logger, config is undefined'); - } - loglevel = getLogLevel(); - timestamp = getTimestamp(); - resolve(); - }); + +function initialize(loglevel, timestamp) { + setLogLevel(loglevel); + setTimestamp(timestamp); } -// get the loglevel -function getLogLevel() { - if (global.config?.log?.level == undefined) { - return LOGLEVEL_INFO; - } - switch (global.config.log.level) { +// set the loglevel +function setLogLevel(value) { + switch (value) { case LOG_PREFIX_DEBUG: case LOGLEVEL_DEBUG: - return LOGLEVEL_DEBUG; + loglevel = LOGLEVEL_DEBUG; + break; case LOG_PREFIX_INFO: case LOGLEVEL_INFO: - return LOGLEVEL_INFO; + loglevel = LOGLEVEL_INFO; + break; case LOG_PREFIX_WARNING: case LOGLEVEL_WARNING: - return LOGLEVEL_WARNING; + loglevel = LOGLEVEL_WARNING; + break; case LOG_PREFIX_ERROR: case LOGLEVEL_ERROR: - return LOGLEVEL_ERROR; + loglevel = LOGLEVEL_ERROR; + break; default: - return LOGLEVEL_INFO; + loglevel = LOGLEVEL_INFO; } } -// get the timestamp format -function getTimestamp() { - if (global.config?.log?.format != undefined) { - return global.config.log.timestamp; - } - return "DD.MM.YYYY HH:mm:ss:SS"; +// set the timestamp format +function setTimestamp(value) { + timestamp = value || 'DD.MM.YYYY HH:mm:ss:SS'; } // prefix log with 'info' @@ -84,12 +77,20 @@ function error(message) { if (loglevel > LOGLEVEL_ERROR) { return; } - if (message.errors != undefined) { - for (var index = 0; index < message.errors.length; index++) { + if (message.stack) { + trace(message.stack, 'error'); + return; + } + if (message.errors !== undefined) { + for (let index = 0; index < message.errors.length; index++) { trace(message.errors[index], 'error'); } return; } + if (message.message) { + trace(message.message, 'error'); + return; + } trace(message, 'error'); } diff --git a/libs/util.js b/libs/util.js index 01788f4..4645991 100644 --- a/libs/util.js +++ b/libs/util.js @@ -1,36 +1,23 @@ const realpath = require('fs/promises').realpath; const stat = require('fs/promises').stat; -function fileExists(file) { - return new Promise((resolve, reject) => { - if (file == undefined) { - reject('can not check the existence of an undefined file'); - } - resolvePath(file) - .then((path) => { - stat(path) - .then((stats) => { - resolve({path, stats}); - }) - }) - .catch(reject); - }); +async function getFileInfo(file) { + if (file === undefined) { + throw new Error('can not check the existence of an undefined file'); + } + const path = await resolvePath(file); + const stats = await stat(path); + return { path, stats }; } -function resolvePath(file) { - return new Promise((resolve, reject) => { - if (file == undefined) { - reject('can not resolve a path to an undefined file'); - } - realpath(file) - .then(resolve) - .catch((err) => { - reject('resolving path \'' + file + '\' encountered an error >>> ' + err); - }); - }); +async function resolvePath(file) { + if (file === undefined) { + throw new Error('can not resolve a path to an undefined file'); + } + return realpath(file); } module.exports = { - fileExists, + getFileInfo, resolvePath } \ No newline at end of file diff --git a/libs/watcher.js b/libs/watcher.js index c8c9353..bb9c12e 100644 --- a/libs/watcher.js +++ b/libs/watcher.js @@ -3,52 +3,61 @@ const util = require('./util.js'); const Keyfilter = require('./keyfilter.js'); const cli = require('./cli.js'); const spawn = require('child_process').spawn; +const sudo = require('sudo'); const inputDevices = '/dev/input/'; const inputDevicesById = '/dev/input/by-id/'; class Watcher { constructor(config, callback) { - if (config == undefined || config.device == undefined) { + if (config === undefined || config.device === undefined) { return; } - for (var key in config) { + for (let key in config) { this[key] = config[key]; } this.keyfilter = new Keyfilter(config.keys, config.combos); this.restart = config.restart; this.callback = callback; } - start() { - return new Promise((resolve, reject) => { - if (this.process != undefined) { - return; - } + async start() { + if (this.process !== undefined) { + return; + } + if (this.sudo) { + logger.debug('starting sudo watcher \'' + this.device + '\'...'); + this.process = sudo(['evtest', this.device], { cachePassword: true, prompt: 'sudo password:' }); + } else { logger.debug('starting watcher \'' + this.device + '\'...'); - this.process = spawn("evtest", [this.device]); - this.attachListeners(resolve, reject); - }); + this.process = spawn('evtest', [this.device]); + } + try { + await this.attachListeners(); + } catch (err) { + logger.error(err); + } + } stop() { - if (this.process == undefined) { + if (this.process === undefined) { return; } logger.debug('stopping watcher \'' + this.device + '\'...'); this.process.kill(); logger.info('watcher \'' + this.device + '\' stopped'); } - attachListeners(resolve, reject) { - if (this.process == undefined) { + async attachListeners() { + if (this.process === undefined) { return; } - this.addSpawnListener(resolve); - this.addErrorListener(reject); - this.addCloseListener(); this.addStdOutListener(); this.addStdErrListener(); + this.addErrorListener(); + this.addCloseListener(); + await this.addSpawnListener(); } addStdOutListener() { - if (this.process == undefined) { + if (this.process === undefined) { return; } logger.debug('adding stdout listener to watcher \'' + this.device + '\'...'); @@ -56,8 +65,8 @@ class Watcher { if (this.keyfilter == undefined) { return; } - var filtered = this.keyfilter.filter(data); - if (filtered == undefined) { + let filtered = this.keyfilter.filter(data); + if (filtered === undefined) { return; } if (filtered.delayed) { @@ -73,46 +82,55 @@ class Watcher { } this.keyfilter.resetCurrentCombo(); 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) + cli.execute(filtered.command, filtered.args, filtered.sudo) .then(logger.info) .catch(logger.error); }); } addStdErrListener() { - if (this.process == undefined) { + if (this.process === undefined) { return; } logger.debug('adding stderr listener to watcher \'' + this.device + '\'...'); this.process.stderr.on('data', (data) => { - logger.error(data); + this.error = data.toString().trim(); }); } - addSpawnListener(resolve) { - logger.debug('adding spawn listener to watcher \'' + this.device + '\'...'); - this.process.on('spawn', () => { - logger.info('watcher \'' + this.device + '\' initialized and capturing configured events'); - resolve(); + addSpawnListener() { + return new Promise((resolve, reject) => { + logger.debug('adding spawn listener to watcher \'' + this.device + '\'...'); + this.process.on('spawn', () => { + logger.info('watcher \'' + this.device + '\' initialized and capturing configured events'); + resolve(); + }); }); } addCloseListener() { - if (this.process == undefined) { + if (this.process === undefined) { return; } logger.debug('adding close listener to watcher \'' + this.device + '\'...'); this.process.on('close', (code) => { - if (code == undefined) { + if (code === undefined) { code = 0; + if (this.error !== undefined) { + code = 1; + } + } + if (this.error !== undefined) { + logger.error('watcher \'' + this.device + '\' encountered an error > ' + this.error); + this.restart = false; } this.process = undefined; this.code = code; logger.info('watcher \'' + this.device + '\' finished with exit code ' + code); - if (this.callback != undefined) { + if (this.callback !== undefined) { this.callback(this); } }); } addErrorListener(reject) { - if (this.process == undefined) { + if (this.process === undefined) { return; } logger.debug('adding error listener to \'' + this.device + '\'...'); @@ -125,9 +143,9 @@ class Watcher { if (!this.keyfilter.isValid()) { reject('no key(s) defined for watcher \'' + this.device + '\''); } - Promise.any([this.device, inputDevices + this.device, inputDevicesById + this.device].map(util.fileExists)) + Promise.any([this.device, inputDevices + this.device, inputDevicesById + this.device].map(util.getFileInfo)) .then((result) => { - if (result.path != this.device) { + if (result.path !== this.device) { logger.info('resolved watcher for device \'' + this.device + '\' to \'' + result.path + '\'') } this.device = result.path; diff --git a/libs/watchers.js b/libs/watchers.js index 5d00561..759583e 100644 --- a/libs/watchers.js +++ b/libs/watchers.js @@ -3,44 +3,32 @@ const Watcher = require('./watcher.js'); const watchers = []; -async function initialize() { - if (global.config == undefined) { +async function initialize(config) { + if (config == undefined) { throw new Error('could not initialize watchers, no config defined'); } - if (global.config.watchers == undefined || global.config.watchers.length == 0) { + if (config.length == 0) { throw new Error('no watchers in config \'' + global.config.path + '\' defined'); } - for (var index = 0; index < global.config.watchers.length; index++) { - var watcher = new Watcher(global.config.watchers[index], watcherCallback); - try { - await watcher.check(); - watchers.push(watcher); - logger.debug('added watcher \'' + watcher.device + '\' to internal map'); - } catch(err) { - logger.error(err); - } + for (var index = 0; index < config.length; index++) { + var watcher = new Watcher(config[index], watcherCallback); + await watcher.check(); + watchers.push(watcher); + logger.debug('added watcher \'' + watcher.device + '\' to internal map'); } } async function start() { logger.info('starting ' + watchers.length + ' watcher(s)...'); for (var index = 0; index < watchers.length; index++) { - try { - await watchers[index].start(); - } catch (err) { - logger.error(err); - } + await watchers[index].start(); } } async function stop() { logger.info('stopping ' + watchers.length + ' watcher(s)...'); for (var index = 0; index < watchers.length; index++) { - try { - await watchers[index].stop(); - } catch (err) { - logger.error(err); - } + await watchers[index].stop(); } } @@ -49,12 +37,10 @@ async function watcherCallback(watcher) { await watcher.start(); return; } - watchers.splice(watchers.findIndex((foundWatcher) => foundWatcher.device == watcher), 1); - logger.debug('removed watcher \'' + watcher + '\' from internal map'); + watchers.splice(watchers.findIndex((foundWatcher) => foundWatcher.device === watcher), 1); + logger.debug('removed watcher \'' + watcher.device + '\' from internal map'); if (watchers.length === 0) { - logger.info('no watchers are active any longer'); - logger.info(global.appName + ' ' + global.appVersion + ' exiting with code \'0\''); - process.exit(0); + logger.warn('no watchers are active any longer'); } } diff --git a/ninwa.js b/ninwa.js index 247df16..95ed5ff 100644 --- a/ninwa.js +++ b/ninwa.js @@ -5,45 +5,44 @@ const packageJSON = require('./package.json'); const INTERRUPTS = ['SIGINT', 'SIGTERM']; -global.appName = packageJSON.name; -global.appVersion = packageJSON.version; -global.config = process.argv[2] || __dirname + '/config.json'; +let config; -handleInterrupts(); +main(); -util.fileExists(config) - .catch((err) => { - logger.error('given config file \'' + config + '\' does not exist'); +async function main() { + handleInterrupts(); + try { + let configFile = await util.getFileInfo(process.argv[2] || __dirname + '/config.json'); + config = require(configFile.path); + config.path = config.path; + } catch (err) { + console.error(err); + process.exit(1); + } + try { + logger.initialize(config.log.level, config.log.timestamp); + logger.info(packageJSON.name + ' ' + packageJSON.version + ' starting...'); + await watchers.initialize(config.watchers); + await watchers.start(); + } catch (err) { logger.error(err); exit(1); - }) - .then((result) => { - global.config = require(result.path); - global.config.path = result.path; - }) - .then(logger.initialize) - .then(() => { - logger.info(appName + ' ' + appVersion + ' starting...'); - }) - .then(watchers.initialize) - .then(watchers.start) - .catch((err) => { - logger.error(err); - exit(1); - }); - -function exit(code) { - code = code || 0; - logger.info(appName + ' ' + appVersion + ' exiting with code \'' + code + '\'...'); - process.exit(code); + } } function handleInterrupts() { for (var index = 0; index < INTERRUPTS.length; index++) { - process.once(INTERRUPTS[index], (code) => { + process.on(INTERRUPTS[index], (code) => { + exit(code); watchers.stop() .then(exit(code)) .catch(exit(code)); }); } } + +function exit(code) { + code = code || 0; + logger.info(packageJSON.name + ' ' + packageJSON.version + ' exiting with code \'' + code + '\'...'); + process.exit(code); +} diff --git a/package.json b/package.json index 3938fb8..cae32d4 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "author": "Daniel Sommer ", "license": "MIT", "dependencies": { - "moment": "^2.29.1" + "moment": "^2.29.1", + "sudo": "^1.0.3" } }