Hello,
I encountered a bug affecting all external integrations, not just mine: the Update button of an external integration consistently fails, and it fails partially, silently.
The Symptom
After clicking « Update », Gladys displays the new version as installed. But the container itself was never recreated:
$ docker inspect --format '{{.Config.Image}}' gladys-ext-<my-integration>
ghcr.io/<...>:1.0.7 <-- the OLD image, while the UI displays 2.0.0
In other words: the old code runs under the new manifest. If the new version declares features that the old one does not implement (widgets, scene triggers…), the core requests them from a container that does not know how to respond.
In the logs, at the time of the click:
<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)
The Cause
registerProxyService() builds the proxy with Object.freeze({ start, stop, device, ... }), and StateManager.setState() merges into the existing Store (Object.assign) instead of replacing it.
- 1st registration: the state of the
Storeis stillnull, the object is assigned → OK. - 2nd registration:
Object.assign(frozenProxy, newProxy)→ exception on its first key,start.
But init.js registers all installed external integrations at Gladys startup. So when you click « Update », the Store still exists, and the update can never succeed. Note that uninstall() does clean up (deleteState on both keys) — that’s why uninstalling/reinstalling works, while updating never does.
And the exception falls in the worst place: after stopping the old container and rewriting the line in the database, but before createIntegrationContainer(). Hence the base/container inconsistency described above, plus the old images never cleaned up.
Minimal reproduction of the underlying bug:
const StateManager = require('./server/lib/state');
const sm = new StateManager();
const proxy = () => Object.freeze({ start: async () => {}, stop: async () => {} });
sm.setState('service', 'my-service', proxy()); // ok
sm.setState('service', 'my-service', proxy()); // TypeError: Cannot assign to read only property 'start'
The Workaround for Now
docker rm -f gladys-ext-<my-integration>
Then start the integration from the UI: start.js no longer finds the container and recreates it from the docker_image in the database, so with the correct version. To be redone with each update until it is fixed.
The Fix
Two lines, in registerProxyService(), just before the two setState — exactly what uninstall() already does:
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);
I preferred this to modifying Store.setState(): changing « merge » to « replace » in a helper shared by the entire core is much riskier than these two lines local to the bug.
I prepared the patch with three non-regression tests: they fail on master with the exact TypeError and pass with the fix. The external-integration, state, and service suites pass in full (809 tests), prettier and eslint too. I’ll push the PR right away.
Version: Gladys 5.1.0, Docker Compose installation.
Thanks!