URL: https://linuxfr.org/users/nlgranger/journaux/mini-projet-python-un-demon-systeme-pour-gerer-des-raccourcis-clavier Title: Mini-projet (python): un démon système pour gérer des raccourcis clavier Authors: nlgranger Date: 2019年08月12日T13:24:14+02:00 License: CC By-SA Tags: python3, python et programmation Score: 22 Voici un petit projet du dimanche qui pourra éventuellement servir à ceux d'entre vous qui ont un petit serveur audio ou une raspberry-pi pour de la domotique. Le titre n'est peut-être pas très clair, mais l'objectif est relativement simple: il faut exécuter des commandes lorsque certaines combinaisons de touches sont envoyés. Par exemple voici un extrait de ma configuration: ```ini [/dev/input/event0] KEY_STOP = mpc clear mpc random off mpc add / KEY_FORWARD = mpc next KEY_BACKWARD = mpc prev KEY_0 = mpc clear mpc load --range=0 Radios mpc play KEY_1 + KEY_0 = mpc clear mpc search album 'Truth Is a Beautiful Thing' | mpc add mpc play ``` Comme il n'y a pas d'environnement graphique sur la machine en question, j'ai décidé de récupérer les entrées au niveau de evdev. Il existe déjà un projet similaire du même nom (https://github.com/gandro/input-event-daemon) mais celui-ci n'est plus maintenu. D'autre part, j'ai ajouté le support pour des combinaisons de touches. Le résultat est ici pour ceux que ça intéresse: https://github.com/nlgranger/input-event-daemon J'ai aussi ajouté un fichier de service systemd et un PKGBUILD pour archilinux. Comme le résultat est assez simple et tient dans un seul fichier, je vais commenter certaines parties. # Lecture des combinaisons de touches Quand je détecte le début d'une possible combinaison de touches, le programme attend un peu pour qu'elle se complète puis la valide. Les fonctions asynchrones (async, await) sont pratiques pour gérer plusieurs boucles en parallèle. Ici, inutile d'utiliser des threads explicitement, lorsque le `await device.async_read_one()` bloque pour attendre une valeur, python saute automatiquement dans le corps d'une autre fonction asynchrone en cours. ```python async def event_handler(device, bindings, command_queue): while True: # lecture de la première touche event = await device.async_read_one() event = evdev.categorize(event) # on vérifie que c'est une frappe de touche if not isinstance(event, evdev.KeyEvent) or not event.keystate == evdev.KeyEvent.key_up: continue prefix = (event.keycode,) # tant que la série de touches coïncide avec plusieurs raccourcis, on attend d'autres entrée try: while is_ambiguous(prefix, bindings.keys()): event = await asyncio.wait_for(device.async_read_one(), timeout=1) event = evdev.categorize(event) if not isinstance(event, evdev.KeyEvent) or not event.keystate == evdev.KeyEvent.key_up: continue prefix += (event.keycode,) # mais si aucune entrée n'arrive, on valide la série de touches except asyncio.TimeoutError: pass # ... ``` # Lancement de commandes avec des droits restreints L'ouverture des périphériques /dev/input/event nécessite (à raison) les droits root, donc le démon tourne aussi en root. Par contre je n'étais pas enchanté à l'idée de lancer mes commandes de raccourcis en root aussi. J'ai essayé d'ouvrir les entrées puis de changer l'utilisateur avec setgid mais ça n'a pas fonctionné. La solution la plus élégante que j'ai finalement trouvée est de démarrer un processus indépendant sous un autre utilisateur et de lui communiquer les commandes à lancer via une queue. C'est facile à implémenter tout en assurant une bonne séparation des droits. Cette configuration a toutefois un petit défaut: il faut gérer proprement la fermeture du démon, de son enfant et de la queue. Déjà, il y a un risque que les signaux comme le sigint du ctrl-C dans le terminal soient interceptés par l'enfant. Pour éviter ça, il faut supprimer complètement la gestion des signaux avant de démarrer l'enfant (via un fork) puis les rétablir dans le parent. L'enfant hérite quant à lui de la 'non-gestion' des signaux au moment du fork: ```python command_worker = multiprocessing.Process( target=run_commands, args=[command_queue]) signal.signal(signal.SIGINT, signal.SIG_IGN) command_worker.start() ``` Ensuite il faut rétablir la gestion du signal SIGINT dans le démon, avec en plus notre code pour faire le ménage avant de quitter: ```python def cleanup(*kargs): # on notifie l'enfant de la fin command_queue.put_nowait(None) # puis on attend qu'il se termine command_worker.join() command_queue.close() # et on quitte logger.info("Exiting") sys.exit(0) signal.signal(signal.SIGINT, cleanup) ``` # Gestion d'une télécommande Ça n'a rien à voir, mais j'en profite pour rappeler que LIRC n'est plus la seule solution pour utiliser des télécommandes infrarouges sous linux. En effet, une partie de code a été transférée dans les pilotes côté noyau. Avec des pilotes comme sunxi-ir pour les cartes ARM allwinner, un périphérique evdev apparait pour le récepteur infrarouge. Il suffit ensuite communiquer au noyau la configuration de la commande avec la commande ir-keytable. Par exemple sur ma carte Cubietruck, j'ai une rêgle udev comme ceci: ``` #/etc/udev/rules.d/70-ir-remote.rules ACTION=="add", ATTRS{name}=="sunxi-ir", RUN+="/usr/bin/ir-keytable -c -w /lib/udev/rc_keymaps/avermedia_rm_ks.toml --sysdev rc0" ```

AltStyle によって変換されたページ (->オリジナル) /