T7 Pièges du runtime
Du code qui paraît juste ne s'exécute jamais. Une erreur 500 ne nomme pas le node fautif. Un drapeau de désactivation est ignoré. Aucun de ces pièges ne se voit à la lecture.
Vue d'ensemble
| Piège | Ce qu'il a coûté | |
|---|---|---|
| 1 | Une sortie ne porte qu'un message | 48 h sans renouvellement de jetons |
| 2 | Pas d'analyseur JSON | Tout se fait par motifs, à la main |
| 3 | Drapeau de désactivation ignoré | Un node cru neutralisé s'exécute |
| 4 | Deux champs obligatoires | Erreur 500 anonyme |
| 5 | L'adresse vient du message | Déroutant à la lecture |
| 6 | Outils système réduits | Commandes qui répondent « rien » à tort |
| 7 | Fichiers appartenant à root | Détour obligatoire pour tout dépôt |
| 8 | Envoyer le flux redémarre tout | Passerelle hors service le 6 juillet |
| 9 | Le flux en service contient plus | Un capteur supprimé le 29 août |
| 10 | Configuration orpheline muette | Une file de rejeu qui n'existe pas |
1 · Une sortie ne porte qu'un seul message
Le retour est lu comme un message par sortie. Une liste de messages n'est pas comprise comme une série, mais comme un message unique — invalide.
-- NON : le moteur lit une liste, pas un message
return { msgs }
-- OUI : un message
return msg
-- OUI : un message par sortie
return { msg_sortie1, msg_sortie2 }
| Symptôme | not a valid message dans le journal, sans
indication du node |
| Coût | Renouvellement des jetons interrompu 48 heures, du 29 au 31 août 2026 |
| Le piège dans le piège | Deux autres nodes portaient le même motif sans planter — parce qu'ils n'étaient jamais déclenchés. Les observer menait à la conclusion inverse. |
| Contourner | Chercher la structure qui accepte plusieurs éléments dans un message. Pour le downlink : la file du moteur LoRa, indexée par équipement. Pour un déclencheur périodique : un déclencheur par cible. |
2 · Pas d'analyseur JSON garanti
Le bac à sable Lua n'expose pas de lecteur JSON fiable. Tout se fait par motifs, dont
%b{} qui capture une accolade équilibrée :
local section = corps:match('"device_list"%s*:%s*(%b{})')
for cle, val in section:gmatch('"([^"]+)"%s*:%s*"([^"]*)"') do t[cle] = val end
À l'écriture, le JSON est construit par concaténation : une accolade oubliée casse toute la réponse. Les tests vérifient donc l'équilibre du résultat, pas seulement son contenu.
3 · Le drapeau de désactivation est ignoré
Marquer un node désactivé n'a aucun effet : il continue de s'exécuter. Pour désactiver réellement, couper la liaison sortante qui y mène.
Un node qu'on croit neutralisé peut donc consommer un jeton ou écrire un fichier.
4 · Deux champs obligatoires sur tout nouveau node
Chaque node doit porter __package et __version. Sans eux, l'envoi du
flux échoue avec une erreur 500 qui ne nomme pas le node fautif.
Parade : ne jamais écrire un node de zéro. Copier un node existant du même type, changer identifiant, nom et liaisons.
5 · L'adresse d'une requête ne vient pas du node
Les nodes de requête HTTP ont leur champ d'adresse vide : elle est posée par la fonction en amont, dans le message.
C'est ce qui a permis de servir deux environnements sans dupliquer une requête. Mais en lisant le flux, la destination d'un appel est toujours dans la fonction qui précède.
6 · Un système ancien, aux outils réduits
| Attendu | Réalité |
|---|---|
bash | Absent. Passer les commandes en une seule chaîne. |
grep motif fichier |
grep -a obligatoire sur les journaux. Sans l'option :
« binary file matches » et rien d'affiché — on croit qu'il n'y a aucune occurrence. |
\s | Non reconnu. Zéro résultat en silence. Utiliser
[[:space:]]. |
| SSH standard | Refusé : réactiver explicitement les algorithmes anciens. |
| Ping sans réponse = panne | Faux : l'ICMP entrant est filtré. Tester en HTTP. |
7 · Les fichiers appartiennent à root
Tout ce que le flux écrit appartient à root, alors que le compte SSH ne l'est pas.
On ne peut pas déposer un fichier à son emplacement final.
Le chemin : déposer dans le répertoire temporaire, puis déclencher un node du flux — qui, lui, s'exécute en root — pour la copie.
Toute réparation d'état est faisable sans intervention d'un tiers. C'est ce que vérifie la répétition de restauration avant chaque déploiement.
8 · Envoyer le flux redémarre tout
Pas de rechargement partiel : toutes les chaînes redémarrent — radio, envois, authentification.
| Conséquence 1 | Tout redémarrage rejoue la restauration de l'authentification, donc peut déclencher un renouvellement. Ces jetons sont à usage unique : deux renouvellements rapprochés en perdent un. |
| Conséquence 2 | Un ajout de capteur passe par cette réécriture : ajouter un capteur redémarre tout. Un ajout lancé pendant un renouvellement a mis la passerelle hors service le 6 juillet 2026. |
9 · Le flux en service peut contenir plus que votre copie
Un capteur déclaré depuis le cockpit n'existe que dans le flux de la passerelle. Envoyer un instantané antérieur le supprime sans rien signaler.
Arrivé le 29 août 2026, failli se reproduire le 4 septembre. Le contrôle préalable compare désormais et refuse tout envoi qui retirerait un node.
10 · Une configuration orpheline ne se signale pas
Deux requêtes portent une option de file de rejeu pointant vers une configuration qui n'existe plus. Ni erreur, ni avertissement : la file ne fonctionne simplement pas.
Le moteur ne vérifie pas que les références d'un node existent — d'où le contrôle de câblage ajouté aux tests.
À retenir
| Le silence n'est pas une preuve | Un node qui ne plante pas peut n'avoir jamais été exécuté |
| Le flux en service fait autorité | Pas votre copie. Comparer avant d'envoyer |
| Tout redémarrage touche aux jetons | Ils sont à usage unique. La séquence de déploiement existe pour ça |