diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 1bc42591c..d33413c3b 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -87,7 +87,7 @@ jobs: uses: actions/checkout@v3.0.2 with: fetch-depth: 0 - + - name: Write env-file if: needs.init.outputs.requirements == 'true' run: | @@ -297,6 +297,12 @@ jobs: exit 1 fi + # Make sure its state is started + test="$(docker exec hassio_cli ha addons info core_ssh --no-progress --raw-json | jq -r '.data.state')" + if [ "$test" != "started" ]; then + exit 1 + fi + - name: Check the Supervisor code sign if: needs.init.outputs.publish == 'true' run: | @@ -369,6 +375,12 @@ jobs: exit 1 fi + # Make sure its state is started + test="$(docker exec hassio_cli ha addons info core_ssh --no-progress --raw-json | jq -r '.data.state')" + if [ "$test" != "started" ]; then + exit 1 + fi + - name: Restore SSL directory from backup run: | test=$(docker exec hassio_cli ha backups restore ${{ steps.backup.outputs.slug }} --folders ssl --no-progress --raw-json | jq -r '.result') diff --git a/supervisor/addons/__init__.py b/supervisor/addons/__init__.py index c7c7463cd..6efb4a52f 100644 --- a/supervisor/addons/__init__.py +++ b/supervisor/addons/__init__.py @@ -203,7 +203,7 @@ class AddonManager(CoreSysAttributes): else: addon.state = AddonState.UNKNOWN - await addon.remove_data() + await addon.unload() # Cleanup audio settings if addon.path_pulse.exists(): diff --git a/supervisor/addons/addon.py b/supervisor/addons/addon.py index ebf34b88a..62d64024a 100644 --- a/supervisor/addons/addon.py +++ b/supervisor/addons/addon.py @@ -18,6 +18,7 @@ from securetar import atomic_contents_add, secure_path import voluptuous as vol from voluptuous.humanize import humanize_error +from ..bus import EventListener from ..const import ( ATTR_ACCESS_TOKEN, ATTR_AUDIO_INPUT, @@ -115,6 +116,7 @@ class Addon(AddonModel): self._manual_stop: bool = ( self.sys_hardware.helper.last_boot == self.sys_config.last_boot ) + self._listeners: list[EventListener] = [] @Job( name=f"addon_{slug}_restart_after_problem", @@ -197,11 +199,15 @@ class Addon(AddonModel): async def load(self) -> None: """Async initialize of object.""" - self.sys_bus.register_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, self.container_state_changed + self._listeners.append( + self.sys_bus.register_event( + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, self.container_state_changed + ) ) - self.sys_bus.register_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, self.watchdog_container + self._listeners.append( + self.sys_bus.register_event( + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, self.watchdog_container + ) ) with suppress(DockerError): @@ -504,6 +510,11 @@ class Addon(AddonModel): return options_schema.pwned + @property + def loaded(self) -> bool: + """Is add-on loaded.""" + return bool(self._listeners) + def save_persist(self) -> None: """Save data of add-on.""" self.sys_addons.data.save_data() @@ -572,8 +583,11 @@ class Addon(AddonModel): raise AddonConfigurationError() - async def remove_data(self) -> None: - """Remove add-on data.""" + async def unload(self) -> None: + """Unload add-on and remove data.""" + for listener in self._listeners: + self.sys_bus.remove_listener(listener) + if not self.path_data.is_dir(): return @@ -931,6 +945,10 @@ class Addon(AddonModel): ) raise AddonsError() from err + # Is add-on loaded + if not self.loaded: + await self.load() + # Run add-on if data[ATTR_STATE] == AddonState.STARTED: return await self.start() diff --git a/tests/addons/test_addon.py b/tests/addons/test_addon.py index 9b7602e9c..68aba2245 100644 --- a/tests/addons/test_addon.py +++ b/tests/addons/test_addon.py @@ -279,3 +279,28 @@ async def test_install_update_fails_if_out_of_date( await coresys.addons.install(TEST_ADDON_SLUG) with pytest.raises(AddonsJobError): await install_addon_ssh.update() + + +async def test_listeners_removed_on_uninstall( + coresys: CoreSys, install_addon_ssh: Addon +) -> None: + """Test addon listeners are removed on uninstall.""" + with patch.object(DockerAddon, "attach"): + await install_addon_ssh.load() + + assert install_addon_ssh.loaded is True + # pylint: disable=protected-access + listeners = install_addon_ssh._listeners + for listener in listeners: + assert ( + listener in coresys.bus._listeners[BusEvent.DOCKER_CONTAINER_STATE_CHANGE] + ) + + with patch.object(Addon, "persist", new=PropertyMock(return_value=MagicMock())): + await coresys.addons.uninstall(TEST_ADDON_SLUG) + + for listener in listeners: + assert ( + listener + not in coresys.bus._listeners[BusEvent.DOCKER_CONTAINER_STATE_CHANGE] + )