added possibility to spawn commands with sudo, optimized some stuff

This commit is contained in:
Daniel Sommer 2022-03-10 12:29:41 +01:00
parent e2b7a71c8e
commit 3e40dc85e7
11 changed files with 205 additions and 205 deletions

1
.vscode/launch.json vendored
View file

@ -3,6 +3,7 @@
"configurations": [ "configurations": [
{ {
"type": "pwa-node", "type": "pwa-node",
"runtimeVersion": "17",
"request": "launch", "request": "launch",
"name": "ninwa", "name": "ninwa",
"skipFiles": [ "skipFiles": [

View file

@ -5,8 +5,9 @@
}, },
"watchers": [ "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, "restart": true,
"sudo": true,
"keys": [ "keys": [
{ {
"key": "key_f1", "key": "key_f1",
@ -21,7 +22,8 @@
"args": [ "args": [
"combo", "combo",
"{{ key }} {{ type }}" "{{ key }} {{ type }}"
] ],
"sudo": false
}, },
{ {
"key": "key_enter", "key": "key_enter",
@ -30,7 +32,8 @@
"args": [ "args": [
"{{ key }}", "{{ key }}",
"{{ type }}" "{{ type }}"
] ],
"sudo": false
}, },
{ {
"key": "key_esc", "key": "key_esc",
@ -39,7 +42,8 @@
"args": [ "args": [
"{{ key }}", "{{ key }}",
"{{ type }}" "{{ type }}"
] ],
"sudo": false
}, },
{ {
"key": "key_space", "key": "key_space",
@ -49,7 +53,8 @@
"args": [ "args": [
"{{ key }}", "{{ key }}",
"{{ type }}" "{{ type }}"
] ],
"sudo": false
} }
] ]
} }

1
libs/.nvmrc Normal file
View file

@ -0,0 +1 @@
17

View file

@ -1,58 +1,57 @@
const logger = require('./logger.js'); const logger = require('./logger.js');
const spawn = require('child_process').spawn; const spawn = require('child_process').spawn;
const sudo = require('sudo');
async function execute(command, args, returnOnClose) { async function execute(command, args, useSudo, returnOnClose) {
// return new Promise((resolve, reject) => { if (command === undefined || command.length === 0) {
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; 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(); if (resultError.length > 0) {
var resultData = ""; msg += " >>> error: " + resultError;
var resultError = ""; logger.error(msg)
command = command.trim(); return;
logger.debug('executing command \'' + command + '\' (args: \'' + args + '\') ...');
try {
var process = spawn(command, args);
} catch (err) {
logger.error(err);
} }
process.stdout.on('data', (data) => { logger.debug(msg);
resultData += data; if (returnOnClose) {
}); return;
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);
});
// });
} }
module.exports = { module.exports = {

View file

@ -13,13 +13,13 @@ class Keyfilter {
return; return;
} }
this.actions = new Map(); 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.setAction(keys[index]);
} }
this.currentCombo = undefined; this.currentCombo = undefined;
} }
setAction(config) { setAction(config) {
var type = ACTION_KEYDOWN; let type = ACTION_KEYDOWN;
switch (config.type.toLowerCase()) { switch (config.type.toLowerCase()) {
case ACTION_KEYUP.action: case ACTION_KEYUP.action:
type = ACTION_KEYUP; type = ACTION_KEYUP;
@ -34,6 +34,7 @@ class Keyfilter {
event: config.event, event: config.event,
command: config.command, command: config.command,
args: config.args, args: config.args,
sudo: config.sudo,
combo: config.combo, combo: config.combo,
delay: function () { delay: function () {
if (config.combo === undefined) { if (config.combo === undefined) {
@ -49,9 +50,9 @@ class Keyfilter {
return; return;
} }
input = input.toString(); input = input.toString();
var lines = input.split("\n"); let lines = input.split("\n");
for (var index = 0; index < lines.length; index++) { for (let index = 0; index < lines.length; index++) {
var line = lines[index]; let line = lines[index];
if (line.length === 0) { if (line.length === 0) {
continue; continue;
} }
@ -62,7 +63,7 @@ class Keyfilter {
if (parsedEvent === undefined) { if (parsedEvent === undefined) {
continue; continue;
} }
for (var [key, event] of this.actions) { for (let [key, event] of this.actions) {
if (this.currentCombo === undefined && !this.isParsedEventValid(key, event, parsedEvent)) { if (this.currentCombo === undefined && !this.isParsedEventValid(key, event, parsedEvent)) {
continue; continue;
} }
@ -103,6 +104,7 @@ class Keyfilter {
type: event.type.action, type: event.type.action,
command: event.command, command: event.command,
args: event.args, args: event.args,
sudo: event.sudo,
delay: event.delay delay: event.delay
} }
) )
@ -123,7 +125,7 @@ class Keyfilter {
} }
} }
replaceVariables(filtered) { 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_KEY, filtered.key);
filtered.args[index] = filtered.args[index].replace(VARIABLE_TYPE, filtered.type); filtered.args[index] = filtered.args[index].replace(VARIABLE_TYPE, filtered.type);
} }

View file

@ -10,49 +10,42 @@ const LOGLEVEL_INFO = 1;
const LOGLEVEL_WARNING = 2; const LOGLEVEL_WARNING = 2;
const LOGLEVEL_ERROR = 3; const LOGLEVEL_ERROR = 3;
var loglevel = getLogLevel(); let loglevel;
var timestamp = getTimestamp(); let timestamp;
function initialize() {
return new Promise((resolve, reject) => { function initialize(loglevel, timestamp) {
if (global.config == undefined) { setLogLevel(loglevel);
reject('could not initialize logger, config is undefined'); setTimestamp(timestamp);
}
loglevel = getLogLevel();
timestamp = getTimestamp();
resolve();
});
} }
// get the loglevel // set the loglevel
function getLogLevel() { function setLogLevel(value) {
if (global.config?.log?.level == undefined) { switch (value) {
return LOGLEVEL_INFO;
}
switch (global.config.log.level) {
case LOG_PREFIX_DEBUG: case LOG_PREFIX_DEBUG:
case LOGLEVEL_DEBUG: case LOGLEVEL_DEBUG:
return LOGLEVEL_DEBUG; loglevel = LOGLEVEL_DEBUG;
break;
case LOG_PREFIX_INFO: case LOG_PREFIX_INFO:
case LOGLEVEL_INFO: case LOGLEVEL_INFO:
return LOGLEVEL_INFO; loglevel = LOGLEVEL_INFO;
break;
case LOG_PREFIX_WARNING: case LOG_PREFIX_WARNING:
case LOGLEVEL_WARNING: case LOGLEVEL_WARNING:
return LOGLEVEL_WARNING; loglevel = LOGLEVEL_WARNING;
break;
case LOG_PREFIX_ERROR: case LOG_PREFIX_ERROR:
case LOGLEVEL_ERROR: case LOGLEVEL_ERROR:
return LOGLEVEL_ERROR; loglevel = LOGLEVEL_ERROR;
break;
default: default:
return LOGLEVEL_INFO; loglevel = LOGLEVEL_INFO;
} }
} }
// get the timestamp format // set the timestamp format
function getTimestamp() { function setTimestamp(value) {
if (global.config?.log?.format != undefined) { timestamp = value || 'DD.MM.YYYY HH:mm:ss:SS';
return global.config.log.timestamp;
}
return "DD.MM.YYYY HH:mm:ss:SS";
} }
// prefix log with 'info' // prefix log with 'info'
@ -84,12 +77,20 @@ function error(message) {
if (loglevel > LOGLEVEL_ERROR) { if (loglevel > LOGLEVEL_ERROR) {
return; return;
} }
if (message.errors != undefined) { if (message.stack) {
for (var index = 0; index < message.errors.length; index++) { trace(message.stack, 'error');
return;
}
if (message.errors !== undefined) {
for (let index = 0; index < message.errors.length; index++) {
trace(message.errors[index], 'error'); trace(message.errors[index], 'error');
} }
return; return;
} }
if (message.message) {
trace(message.message, 'error');
return;
}
trace(message, 'error'); trace(message, 'error');
} }

View file

@ -1,36 +1,23 @@
const realpath = require('fs/promises').realpath; const realpath = require('fs/promises').realpath;
const stat = require('fs/promises').stat; const stat = require('fs/promises').stat;
function fileExists(file) { async function getFileInfo(file) {
return new Promise((resolve, reject) => { if (file === undefined) {
if (file == undefined) { throw new Error('can not check the existence of an undefined file');
reject('can not check the existence of an undefined file'); }
} const path = await resolvePath(file);
resolvePath(file) const stats = await stat(path);
.then((path) => { return { path, stats };
stat(path)
.then((stats) => {
resolve({path, stats});
})
})
.catch(reject);
});
} }
function resolvePath(file) { async function resolvePath(file) {
return new Promise((resolve, reject) => { if (file === undefined) {
if (file == undefined) { throw new Error('can not resolve a path to an undefined file');
reject('can not resolve a path to an undefined file'); }
} return realpath(file);
realpath(file)
.then(resolve)
.catch((err) => {
reject('resolving path \'' + file + '\' encountered an error >>> ' + err);
});
});
} }
module.exports = { module.exports = {
fileExists, getFileInfo,
resolvePath resolvePath
} }

View file

@ -3,52 +3,61 @@ const util = require('./util.js');
const Keyfilter = require('./keyfilter.js'); const Keyfilter = require('./keyfilter.js');
const cli = require('./cli.js'); const cli = require('./cli.js');
const spawn = require('child_process').spawn; const spawn = require('child_process').spawn;
const sudo = require('sudo');
const inputDevices = '/dev/input/'; const inputDevices = '/dev/input/';
const inputDevicesById = '/dev/input/by-id/'; const inputDevicesById = '/dev/input/by-id/';
class Watcher { class Watcher {
constructor(config, callback) { constructor(config, callback) {
if (config == undefined || config.device == undefined) { if (config === undefined || config.device === undefined) {
return; return;
} }
for (var key in config) { for (let key in config) {
this[key] = config[key]; this[key] = config[key];
} }
this.keyfilter = new Keyfilter(config.keys, config.combos); this.keyfilter = new Keyfilter(config.keys, config.combos);
this.restart = config.restart; this.restart = config.restart;
this.callback = callback; this.callback = callback;
} }
start() { async start() {
return new Promise((resolve, reject) => { if (this.process !== undefined) {
if (this.process != undefined) { return;
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 + '\'...'); logger.debug('starting watcher \'' + this.device + '\'...');
this.process = spawn("evtest", [this.device]); this.process = spawn('evtest', [this.device]);
this.attachListeners(resolve, reject); }
}); try {
await this.attachListeners();
} catch (err) {
logger.error(err);
}
} }
stop() { stop() {
if (this.process == undefined) { if (this.process === undefined) {
return; return;
} }
logger.debug('stopping watcher \'' + this.device + '\'...'); logger.debug('stopping watcher \'' + this.device + '\'...');
this.process.kill(); this.process.kill();
logger.info('watcher \'' + this.device + '\' stopped'); logger.info('watcher \'' + this.device + '\' stopped');
} }
attachListeners(resolve, reject) { async attachListeners() {
if (this.process == undefined) { if (this.process === undefined) {
return; return;
} }
this.addSpawnListener(resolve);
this.addErrorListener(reject);
this.addCloseListener();
this.addStdOutListener(); this.addStdOutListener();
this.addStdErrListener(); this.addStdErrListener();
this.addErrorListener();
this.addCloseListener();
await this.addSpawnListener();
} }
addStdOutListener() { addStdOutListener() {
if (this.process == undefined) { if (this.process === undefined) {
return; return;
} }
logger.debug('adding stdout listener to watcher \'' + this.device + '\'...'); logger.debug('adding stdout listener to watcher \'' + this.device + '\'...');
@ -56,8 +65,8 @@ class Watcher {
if (this.keyfilter == undefined) { if (this.keyfilter == undefined) {
return; return;
} }
var filtered = this.keyfilter.filter(data); let filtered = this.keyfilter.filter(data);
if (filtered == undefined) { if (filtered === undefined) {
return; return;
} }
if (filtered.delayed) { if (filtered.delayed) {
@ -73,46 +82,55 @@ class Watcher {
} }
this.keyfilter.resetCurrentCombo(); 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 + '\''); 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) .then(logger.info)
.catch(logger.error); .catch(logger.error);
}); });
} }
addStdErrListener() { addStdErrListener() {
if (this.process == undefined) { if (this.process === undefined) {
return; return;
} }
logger.debug('adding stderr listener to watcher \'' + this.device + '\'...'); logger.debug('adding stderr listener to watcher \'' + this.device + '\'...');
this.process.stderr.on('data', (data) => { this.process.stderr.on('data', (data) => {
logger.error(data); this.error = data.toString().trim();
}); });
} }
addSpawnListener(resolve) { addSpawnListener() {
logger.debug('adding spawn listener to watcher \'' + this.device + '\'...'); return new Promise((resolve, reject) => {
this.process.on('spawn', () => { logger.debug('adding spawn listener to watcher \'' + this.device + '\'...');
logger.info('watcher \'' + this.device + '\' initialized and capturing configured events'); this.process.on('spawn', () => {
resolve(); logger.info('watcher \'' + this.device + '\' initialized and capturing configured events');
resolve();
});
}); });
} }
addCloseListener() { addCloseListener() {
if (this.process == undefined) { if (this.process === undefined) {
return; return;
} }
logger.debug('adding close listener to watcher \'' + this.device + '\'...'); logger.debug('adding close listener to watcher \'' + this.device + '\'...');
this.process.on('close', (code) => { this.process.on('close', (code) => {
if (code == undefined) { if (code === undefined) {
code = 0; 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.process = undefined;
this.code = code; this.code = code;
logger.info('watcher \'' + this.device + '\' finished with exit code ' + code); logger.info('watcher \'' + this.device + '\' finished with exit code ' + code);
if (this.callback != undefined) { if (this.callback !== undefined) {
this.callback(this); this.callback(this);
} }
}); });
} }
addErrorListener(reject) { addErrorListener(reject) {
if (this.process == undefined) { if (this.process === undefined) {
return; return;
} }
logger.debug('adding error listener to \'' + this.device + '\'...'); logger.debug('adding error listener to \'' + this.device + '\'...');
@ -125,9 +143,9 @@ class Watcher {
if (!this.keyfilter.isValid()) { if (!this.keyfilter.isValid()) {
reject('no key(s) defined for watcher \'' + this.device + '\''); 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) => { .then((result) => {
if (result.path != this.device) { if (result.path !== this.device) {
logger.info('resolved watcher for device \'' + this.device + '\' to \'' + result.path + '\'') logger.info('resolved watcher for device \'' + this.device + '\' to \'' + result.path + '\'')
} }
this.device = result.path; this.device = result.path;

View file

@ -3,44 +3,32 @@ const Watcher = require('./watcher.js');
const watchers = []; const watchers = [];
async function initialize() { async function initialize(config) {
if (global.config == undefined) { if (config == undefined) {
throw new Error('could not initialize watchers, no config defined'); 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'); throw new Error('no watchers in config \'' + global.config.path + '\' defined');
} }
for (var index = 0; index < global.config.watchers.length; index++) { for (var index = 0; index < config.length; index++) {
var watcher = new Watcher(global.config.watchers[index], watcherCallback); var watcher = new Watcher(config[index], watcherCallback);
try { await watcher.check();
await watcher.check(); watchers.push(watcher);
watchers.push(watcher); logger.debug('added watcher \'' + watcher.device + '\' to internal map');
logger.debug('added watcher \'' + watcher.device + '\' to internal map');
} catch(err) {
logger.error(err);
}
} }
} }
async function start() { async function start() {
logger.info('starting ' + watchers.length + ' watcher(s)...'); logger.info('starting ' + watchers.length + ' watcher(s)...');
for (var index = 0; index < watchers.length; index++) { for (var index = 0; index < watchers.length; index++) {
try { await watchers[index].start();
await watchers[index].start();
} catch (err) {
logger.error(err);
}
} }
} }
async function stop() { async function stop() {
logger.info('stopping ' + watchers.length + ' watcher(s)...'); logger.info('stopping ' + watchers.length + ' watcher(s)...');
for (var index = 0; index < watchers.length; index++) { for (var index = 0; index < watchers.length; index++) {
try { await watchers[index].stop();
await watchers[index].stop();
} catch (err) {
logger.error(err);
}
} }
} }
@ -49,12 +37,10 @@ async function watcherCallback(watcher) {
await watcher.start(); await watcher.start();
return; return;
} }
watchers.splice(watchers.findIndex((foundWatcher) => foundWatcher.device == watcher), 1); watchers.splice(watchers.findIndex((foundWatcher) => foundWatcher.device === watcher), 1);
logger.debug('removed watcher \'' + watcher + '\' from internal map'); logger.debug('removed watcher \'' + watcher.device + '\' from internal map');
if (watchers.length === 0) { if (watchers.length === 0) {
logger.info('no watchers are active any longer'); logger.warn('no watchers are active any longer');
logger.info(global.appName + ' ' + global.appVersion + ' exiting with code \'0\'');
process.exit(0);
} }
} }

View file

@ -5,45 +5,44 @@ const packageJSON = require('./package.json');
const INTERRUPTS = ['SIGINT', 'SIGTERM']; const INTERRUPTS = ['SIGINT', 'SIGTERM'];
global.appName = packageJSON.name; let config;
global.appVersion = packageJSON.version;
global.config = process.argv[2] || __dirname + '/config.json';
handleInterrupts(); main();
util.fileExists(config) async function main() {
.catch((err) => { handleInterrupts();
logger.error('given config file \'' + config + '\' does not exist'); 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); logger.error(err);
exit(1); 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() { function handleInterrupts() {
for (var index = 0; index < INTERRUPTS.length; index++) { for (var index = 0; index < INTERRUPTS.length; index++) {
process.once(INTERRUPTS[index], (code) => { process.on(INTERRUPTS[index], (code) => {
exit(code);
watchers.stop() watchers.stop()
.then(exit(code)) .then(exit(code))
.catch(exit(code)); .catch(exit(code));
}); });
} }
} }
function exit(code) {
code = code || 0;
logger.info(packageJSON.name + ' ' + packageJSON.version + ' exiting with code \'' + code + '\'...');
process.exit(code);
}

View file

@ -17,6 +17,7 @@
"author": "Daniel Sommer <daniel.sommer@velvettear.de>", "author": "Daniel Sommer <daniel.sommer@velvettear.de>",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"moment": "^2.29.1" "moment": "^2.29.1",
"sudo": "^1.0.3"
} }
} }