mirror of
https://github.com/home-assistant/supervisor.git
synced 2026-07-04 04:15:07 +01:00
a973d22e353bd950ad330c09ef95ca436069a0df
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a973d22e35 |
Derive App state from container state (#6890)
* Derive App state from container state
The App.state setter mixed two responsibilities: it both mutated a
private `_state` field and dispatched side effects (WebSocket events,
issue dismissal, startup_event signaling). On top of that, an installed
but never-started app stayed in AppState.UNKNOWN forever, because the
attach() image-only fallback never fires a container state-change event
and the AppState therefore kept its constructor default. Conceptually,
ContainerState.UNKNOWN ("container does not exist") and AppState.UNKNOWN
("nothing observed yet") happened to share a name but meant different
things, which made the distinction easy to lose.
Make App.state a pure derived property. The source of truth is the last
observed ContainerState (cached on the App), plus a sticky operation-
error flag for start/stop failures that the docker event stream cannot
reflect. When no container has been observed yet, the derivation falls
back to install signals: an attached instance (image present) is
STOPPED, otherwise UNKNOWN. As a side effect, an installed-but-never-
started app now correctly reports STOPPED instead of UNKNOWN.
container_state_changed updates the cached container state and routes
all side effects through a single _emit_state_change helper that diffs
old vs new derived state. The two start/stop failure paths route
through _set_operation_error. Uninstall resets the cached signals so
the derivation naturally returns UNKNOWN.
Tests use a new tests/common.force_app_state helper that pokes the
underlying signals directly; the production class no longer carries
test-only setters.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix App state drive to AppState.UNKNOWN
* Unify state mutation through _update_state
Previously, state-driving signal changes were spread across two helpers
(_set_operation_error, _emit_state_change(old_state)) and required each
caller to capture self.state before mutating a private field — leaking
implementation details to call sites and raising the "why am I emitting
the old state?" question pointed out in code review.
Replace both helpers with a single _update_state(*, container_state=,
operation_error=) entry point. Callers describe what changed via
keyword arguments (None leaves a signal untouched); the helper captures
the previous state, applies the updates, recomputes the derived state
and emits side effects if anything changed.
Diff against a tracked _last_state instead of a freshly derived
"current" state, so that an out-of-band mutation between updates does
not silently shift the comparison baseline. The concrete case is
App.uninstall: instance.remove() clears the docker meta mid-flow, which
would otherwise reshape the derivation (RUNNING with no healthcheck
becomes STARTED instead of STARTUP) and suppress the STARTUP transition
that resolves the start-wait task. As a side effect, the initial
UNKNOWN -> STOPPED transition on attach is also now reliably emitted.
Switch the uninstall path to ContainerState.UNKNOWN ("we know there is
no container") rather than the constructor sentinel None.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Cache app state instead of deriving on every read
Building on the previous commit, make App.state a plain read of a
cached _state field rather than re-deriving on every property access.
The derivation moves to _derive_state(), and _update_state() is the
sole place that recomputes and assigns _state, so the value consumers
read always matches what was last emitted to listeners.
This removes the _last_state bookkeeping introduced previously: with a
single cached value there is no longer a separate "derived now" vs
"last emitted" distinction to reconcile, and out-of-band mutations
(e.g. instance.remove() clearing _meta during uninstall) can no longer
silently shift what state returns between updates.
Call _update_state() at the end of load() so the cached state settles
once attach() has run. Image-only attaches do not fire a docker event,
so without this an installed app would stay in the constructor-default
UNKNOWN until first start; this also makes the initial transition on
attach observable to listeners.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Pass operation error to _derive_state instead of storing it
The two state-driving signals were not symmetric. _container_state is
genuinely persisted state ("the last thing docker told us") that
re-derivation legitimately reads across calls. _operation_error, on the
other hand, is a momentary "force ERROR for this transition" signal; the
persistence of an error condition already lives in the cached _state.
Storing it as an instance attribute implied a sticky cross-call behavior
that no call path actually exercised: every caller either set it
explicitly right before deriving (start/stop failures, container events)
or ran argless only at load time, where no failure has occurred.
Drop the _operation_error field and pass operation_error as a parameter
to _derive_state(), defaulting to False in _update_state(). A container
observation now supersedes a prior error implicitly via the default,
which lets the container-event and uninstall call sites drop their
explicit operation_error=False.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Settle load state synchronously from current_state
The argless _update_state() settle at the end of load() raced attach()'s
container-state event. attach() fires DOCKER_CONTAINER_STATE_CHANGE via
the bus, which schedules the container_state_changed listener as a task
rather than running it inline. In the deprecated-arch early-return path
there is no await between attach() and the settle, so the listener had
not run yet: _container_state was still None and the settle derived
STOPPED (instance attached) — emitting a transient UNKNOWN->STOPPED even
for a running container before the listener corrected it. The main path
only avoided this incidentally, by having awaits (check_image,
save_persist) in between for the listener to run.
Derive the load-time state synchronously from instance.current_state()
instead of relying on the asynchronously delivered event. current_state()
returns the real container state, or UNKNOWN when only an image is
present (which derives to STOPPED), so both paths settle correctly
without racing the event.
Add a regression test that loading a running container settles to
STARTED, and mock current_state() in the state-listener test which
relies on a clean UNKNOWN baseline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0f881d69fe |
Model client-state Apps* errors as APIError (#6856)
* apps: model client-state Apps* errors as APIError Follow-up on #6739: with HassioError now logged and captured by Sentry in api_process, a handful of Apps* exceptions raised from AppManager.install/update/rebuild and AppModel._validate_availability surfaced as "unexpected" 400s with a noisy log entry and a Sentry event, even though they are all user/client-state errors (clicked install on an already-installed add-on, "no update available", local and store versions diverged, system architecture/machine/HA version incompatible, etc.). The dominant offender is SUPERVISOR-1JVV ("No update available for app core_mosquitto", ~19k events / ~12k users), but several siblings show the same shape. Map these through properly so the API returns clean, structured 400s: - Add modeled APIError subclasses in exceptions.py for the previously raw raises in apps/manager.py: AppAlreadyInstalledError, AppNotFoundError, AppNotInstalledError, AppNotInStoreError, AppNoUpdateAvailableError, AppRebuildVersionChangedError, AppRebuildImageBasedError. Each gets a stable error_key, a message_template, and an "addon" extra_field. - Add APIError to AppNotSupportedArchitectureError, AppNotSupportedMachineTypeError and AppNotSupportedHomeAssistantVersionError so they behave the same as the other Apps* APIErrors instead of being treated as unexpected. - Pass the app's display name (app.name from the add-on config) instead of the slug to extra_fields wherever an App or AppModel is available at the raise site, so users see "Mosquitto broker" rather than "core_mosquitto" in error messages. Slug is only used as a fallback when no app object exists (install of an unknown slug, update/rebuild of a slug that is not installed). - Update raise sites in apps/manager.py and apps/model.py to use the new typed exceptions and the addon= keyword. These are all runtime states users hit during normal interaction with the apps UI, not Supervisor bugs worth paging on. * apps: include slug alongside name in Apps* APIError extra_fields Address review feedback from #6856: clients still need the slug to look up additional add-on information (the name is for display only), and we should be consistent about it across the Apps* errors touched by this PR. Every Apps* APIError raised with an App/AppModel available now carries both `addon` (display name, used by the message_template) and `slug` in extra_fields. Raise sites in apps/manager.py and apps/model.py pass both. The two errors raised before an app object exists keep slug-only extra_fields and use {slug} in their message: - AppNotFoundError (install of an unknown slug) - AppNotInstalledError (update/rebuild of a slug not in self.local) Pre-existing Apps* APIErrors outside the scope of this PR (AppUnknownError, AppConfigurationInvalidError, AppBootConfigCannot ChangeError, AppNotRunningError, AppPortConflict, AppNotSupportedWrite StdinError, AppBuild*) will be migrated in a follow-up. * apps: introduce AppAPIError base for uniform addon/slug extra_fields Address review follow-up on #6856: the addon/slug convention was enforced only by hand-rolled __init__s, easy to drift on (forget slug, use a different key, etc.). Promote it to a base class that owns the shape of extra_fields for all App-related API errors. - Add AppAPIError(AppsError, APIError). Its __init__ takes `app: AppModel | App | AppStore | str` and uniformly populates extra_fields with `addon` (display name) and `slug`. Pass a string when no app object exists; only `slug` is set in that case. Extra per-error fields flow through **extra_fields and merge with the defaults. - Convert the new exceptions added in this PR (AppAlreadyInstalledError, AppNotFoundError, AppNotInstalledError, AppNotInStoreError, AppNoUpdateAvailableError, AppRebuildVersionChangedError) into thin subclasses that only declare error_key and message_template -- the __init__ is inherited. - Migrate the AppNotSupported* errors (architecture, machine type, HA version) and AppRebuildImageBasedError to use AppAPIError too; their bespoke per-error fields go through **extra_fields. They keep inheriting AppNotSupportedError so `except AppNotSupportedError` callers (e.g., AppModel._available) still work; MRO routes __init__ through AppAPIError. - Update raise sites in apps/manager.py and apps/model.py to pass `app=<obj-or-slug>` instead of repeating `addon=...` and `slug=...`. Pre-existing App* APIErrors outside this PR's scope (AppUnknownError, AppConfigurationInvalidError, AppBootConfigCannotChangeError, AppNotRunningError, AppPortConflict, AppNotSupportedWriteStdinError, AppBuild*) will be migrated to AppAPIError in a follow-up; the base class is in place for them. * apps: tighten AppAPIError model per review Address mdegat01's two follow-ups on #6856 (review approved as-is, this is the cleanup): - AppNotFoundError and AppNotInstalledError are raised before any App/AppModel object exists (unknown slug; not-installed slug). Pull them out of AppAPIError and inherit (AppsError, APIError) directly with a slug-only __init__ + {slug} message_template. Removes the conceptually-wrong str branch from AppAPIError.__init__: it now strictly requires an app-like object with .name and .slug. - Restore per-class typed __init__ on AppNotSupportedArchitectureError, AppNotSupportedMachineTypeError and AppNotSupportedHomeAssistantVersionError so callers get an explicit signature for the bespoke architectures/machine_types/version params instead of dumping them through **extra_fields. Each override just delegates to AppAPIError.__init__, which keeps ownership of the addon/slug shape. The list-joining for architectures/machine_types moves back into the override (raise sites pass the raw list again). AppRebuildImageBasedError takes no bespoke fields and stays a plain AppAPIError subclass. |
||
|
|
f8880a72be |
Rename addon/addons to app/apps in filenames and imports (#6837)
* Rename addon/addons to app/apps in filenames and imports Continues the addon→app terminology migration (#6786). Renames all source files, test files, fixture files, and directories that contained 'addon'/'addons' in their names, and updates all imports accordingly. Resolution check files in supervisor/resolution/checks/ that were renamed override the slug property to preserve the existing API contract (slugs are exposed via the resolution info API and used to run checks by name). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Rename add-on.json fixture --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |