Updating external integration still fails: new version appears but old image keeps running (5.1.0)

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 Store is still null, 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!

1 Like