Bonjour,
Je suis tombé sur un bug qui touche toutes les intégrations externes, pas seulement la mienne : le bouton Mettre à jour d’une intégration externe échoue systématiquement, et surtout il échoue à moitié, en silence.
Le symptôme
Après le clic sur « Mettre à jour », Gladys affiche bien la nouvelle version comme installée. Mais le conteneur, lui, n’a jamais été recréé :
$ docker inspect --format '{{.Config.Image}}' gladys-ext-<mon-intégration>
ghcr.io/<...>:1.0.7 <-- l'ANCIENNE image, alors que l'UI affiche 2.0.0
Autrement dit : l’ancien code tourne sous le nouveau manifest. Si la nouvelle version déclare des fonctionnalités que l’ancienne n’implémente pas (widgets, déclencheurs de scène…), le cœur les demande à un conteneur qui ne sait pas y répondre.
Dans les logs, au moment du clic :
<warn> errorMiddleware.js:83 TypeError: Cannot assign to read only property 'start' of object '#<Object>'
at Object.assign (<anonymous>)
at Store.setState (/src/server/lib/state/Store.js:19:12)
at StateManager.setState (/src/server/lib/state/index.js:17:38)
at ExternalIntegration.registerProxyService (/src/server/lib/external-integration/externalIntegration.registerProxyService.js:238:21)
at ExternalIntegration.update (/src/server/lib/external-integration/externalIntegration.update.js:233:8)
at update (/src/server/api/controllers/externalIntegration.controller.js:306:25)
La cause
registerProxyService() construit le proxy avec Object.freeze({ start, stop, device, ... }), et StateManager.setState() fusionne dans le Store existant (Object.assign) au lieu de le remplacer.
- 1er enregistrement : l’état du
Storeest encorenull, l’objet est affecté → OK. - 2e enregistrement :
Object.assign(proxyGelé, nouveauProxy)→ exception sur sa première clé,start.
Or init.js enregistre toutes les intégrations externes installées au démarrage de Gladys. Donc au moment où on clique sur « Mettre à jour », le Store existe toujours, et la mise à jour ne peut jamais aboutir. À noter que uninstall() fait bien le ménage (deleteState sur les deux clés) — c’est pour ça que désinstaller/réinstaller fonctionne, alors que mettre à jour ne fonctionne jamais.
Et l’exception tombe au pire endroit : après l’arrêt de l’ancien conteneur et la réécriture de la ligne en base, mais avant createIntegrationContainer(). D’où l’incohérence base / conteneur décrite plus haut, plus les anciennes images jamais nettoyées.
Reproduction minimale du bug sous-jacent :
const StateManager = require('./server/lib/state');
const sm = new StateManager();
const proxy = () => Object.freeze({ start: async () => {}, stop: async () => {} });
sm.setState('service', 'mon-service', proxy()); // ok
sm.setState('service', 'mon-service', proxy()); // TypeError: Cannot assign to read only property 'start'
Le contournement en attendant
docker rm -f gladys-ext-<mon-intégration>
Puis démarrer l’intégration depuis l’UI : start.js ne retrouve plus le conteneur et le recrée à partir du docker_image en base, donc avec la bonne version. À refaire à chaque mise à jour tant que ce n’est pas corrigé.
Le correctif
Deux lignes, dans registerProxyService(), juste avant les deux setState — exactement ce que fait déjà uninstall() :
this.stateManager.deleteState('service', service.name);
this.stateManager.deleteState('serviceById', service.id);
this.stateManager.setState('service', service.name, proxyService);
this.stateManager.setState('serviceById', service.id, proxyService);
J’ai préféré ça à une modification de Store.setState() : changer « fusionner » en « remplacer » dans un helper partagé par tout le cœur est bien plus risqué que ces deux lignes locales au bug.
J’ai préparé le patch avec trois tests de non-régression : ils échouent sur master avec le TypeError exact et passent avec le correctif. Les suites external-integration, state et service passent intégralement (809 tests), prettier et eslint aussi. Je pousse la PR dans la foulée.
Version : Gladys 5.1.0, installation Docker Compose.
Merci !