Skip to content

feat(updater): gate update installs on device state and power - #1282

Open
wizzomafizzo wants to merge 4 commits into
mainfrom
feat/ota-apply-gate
Open

feat(updater): gate update installs on device state and power#1282
wizzomafizzo wants to merge 4 commits into
mainfrom
feat/ota-apply-gate

Conversation

@wizzomafizzo

Copy link
Copy Markdown
Member

An update replaces the running binary and restarts the service. Until now
the only thing that could stop one was another update already running, so
applying an update mid-index, mid-backup, mid-token-write or mid-game took
all of it away without warning, and nothing checked whether the device was
about to run out of battery.

Gate

updater.CanApplyUpdate reads what the device is doing and either refuses
with a named reason or holds the device still until the restart.

Hard refusals: media indexing, optimizing, scraping, an active backup or
restore, and a reader part-way through writing a token. Forceable
refusals: active media, background media, an active playlist — the user's
own session, where only the user knows whether it matters.

Power is refused below 20% on battery for a manual install and below 40%
for an automatic one. An unknown power source is refused unless a person
forces it. The reading is taken again in PreQuiesce, at the last point
the install can still be called off, because a download can outlast a
charger being unplugged.

The gate takes the restore gate and then the media gate, matching the
order the rest of the service takes them, and holds both until the restart
so nothing starts underneath the install.

Power

New pkg/helpers/power reports no battery, external power, battery with a
charge percentage, or unknown. Linux reads /sys/class/power_supply
through afero, Windows calls GetSystemPowerStatus, darwin parses
pmset -g batt. Unknown means "could lose power at any moment", not "no
battery". platforms.PowerStatus prefers a platform's own provider when
it has one.

API

update.apply is now its own capability, and one that requires an
authenticated connection: a request from the device itself or from a
paired client. Unpaired remote clients resolve to admin for backward
compatibility, which is not enough to replace the running binary.
update.check is closed to them as well, since a check makes the device
fetch signed metadata and write to disk.

Both methods report the gate's reading, so a client can show what is in
the way and offer force only where forcing is allowed. Progress during an
install arrives on the new update.state notification.

Rollout

RolloutEligible buckets a device from its device ID and the release tag,
so widening a rollout keeps the devices that already have the release
while a new release draws an unrelated set. A device with no ID waits for
a full rollout rather than landing in the first wave.

Breaking change

The top-level auto_update, auto_update_install and update_channel
config keys are replaced by an [updates] table with channel, check
and install. There is no migration: the old keys were only ever set on
internal builds.

updateInstall is recorded and reported but nothing acts on it yet; the
automatic install scheduler is a follow-up.

An update rewrites the running binary, so losing power partway through is
the failure the whole install pipeline is arranged around. Nothing in Core
could tell whether the device was on mains or running down a battery.

pkg/helpers/power reports one of four answers: no battery, external power,
running on battery with a charge percentage, or unknown. Linux reads
/sys/class/power_supply through afero so the parsing is testable without a
real device, Windows calls GetSystemPowerStatus, and darwin shells out to
pmset because there is no syscall for it. The pmset parser lives in an
untagged file so Linux CI exercises it; only the exec wrapper is
darwin-tagged.

Unknown is the answer on a platform with no reader, and callers are
expected to treat it as "this device could lose power at any moment"
rather than as "no battery". Reporting no battery there would hand every
such build a green light no reading supports.

platforms.PowerStatus prefers a platform's own PowerStatusProvider when it
has one and falls back to the host reader otherwise, so a platform that
knows better than /sys can say so.
An update restarts the service. Until now the only thing that could stop
one was the update already running, so applying an update while a media
scan was half done, a backup was uploading, a token was part-way through
being written or a game was running would take all of it away without
warning.

CanApplyUpdate reads the device's current work through GateDeps and either
returns a refusal naming the reason or holds the device still until the
restart. Indexing, optimizing, scraping, an active backup and a token
write are hard refusals: each is either data being written or work that
would be lost. Running media, background media and an active playlist are
refusals a person can override, because they are the user's own session
and only the user knows whether it matters. Power is refused below 20%
on battery for a manual install and below 40% for an automatic one, and
an unknown power source is refused outright unless a person forces it.

The gate takes the restore gate and then the media gate, in that order,
because that is the order the rest of the service takes them and the other
way round is a lock inversion. It holds both until the restart so nothing
can start underneath the install. The power reading is taken again in
PreQuiesce, at the last point the install can still be called off, because
a download can easily outlast a charger being unplugged.

Reader writes are recorded against the reader doing them, so the gate asks
State.AnyReaderWriteActive rather than about a single reader ID.

Alongside the gate: RolloutEligible places a device in a stable bucket
from its device ID and the release tag, so widening a rollout keeps the
devices that already have the release while a new release draws an
unrelated set, and a device with no ID waits for a full rollout rather
than silently landing in the first wave. progressReporter turns stage
changes and downloaded byte counts into the progress values the API
forwards to clients, throttled to twice a second.
Applying an update decides what code the device runs from then on and
stops whatever is playing to do it, so it is now its own capability rather
than something any admin-resolved request can do. update.apply is also in
authenticatedCapabilities, which means it needs a request from the device
itself or from a paired client: an unpaired remote request resolves to
admin for backward compatibility, and handing that a binary replacement is
too much. update.check is closed to unpaired remote clients too, because a
check makes the device fetch signed metadata and write the result to disk.

update.apply reports its refusals from the updater's gate, so a client can
tell a low battery from a running game from a backup in progress, and can
offer force only where the gate says forcing is allowed. update.check
returns the same gate reading, so a client knows what is in the way before
it shows an update button rather than finding out from a failed apply. An
update in progress reports its stage and downloaded bytes through the new
update.state notification.

The three top-level update keys are replaced by an [updates] table with
channel, check and install. This is a breaking config change with no
migration: the old keys were only ever set on internal builds, and
carrying them forward would be permanent cruft.
Covers the update.apply capability and its authenticated-connection
requirement, the refusal reasons update.check and update.apply report and
which of them force can override, the update.state notification, and the
updateChannel, updateCheck and updateInstall settings replacing the old
top-level config keys.
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your current included review allowance is based on your included PR review attempts over the past 7 days.

Next review available in: 25 minutes

Limit details: You’ve used the included review currently available. Your 90 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 17849fc8-ce78-4441-a7d5-1649c970f050

📥 Commits

Reviewing files that changed from the base of the PR and between 4e10fe3 and cea953c.

📒 Files selected for processing (53)
  • docs/api/index.md
  • docs/api/methods.md
  • docs/api/notifications.md
  • pkg/api/methods/clients_test.go
  • pkg/api/methods/permissions.go
  • pkg/api/methods/settings.go
  • pkg/api/methods/settings_test.go
  • pkg/api/methods/update.go
  • pkg/api/methods/update_test.go
  • pkg/api/models/models.go
  • pkg/api/models/params.go
  • pkg/api/models/responses.go
  • pkg/api/notifications/notifications.go
  • pkg/api/notifications/notifications_test.go
  • pkg/api/pairing_test.go
  • pkg/api/permissions/permissions.go
  • pkg/api/permissions/permissions_test.go
  • pkg/api/request_priority.go
  • pkg/api/request_priority_test.go
  • pkg/api/server_logging_test.go
  • pkg/api/transport_timing_test.go
  • pkg/config/config.go
  • pkg/config/config_autoupdate_test.go
  • pkg/config/config_updatechannel_test.go
  • pkg/config/config_updates_test.go
  • pkg/helpers/power/pmset.go
  • pkg/helpers/power/pmset_test.go
  • pkg/helpers/power/power.go
  • pkg/helpers/power/power_darwin.go
  • pkg/helpers/power/power_linux.go
  • pkg/helpers/power/power_linux_test.go
  • pkg/helpers/power/power_other.go
  • pkg/helpers/power/power_windows.go
  • pkg/helpers/power/power_windows_test.go
  • pkg/platforms/power.go
  • pkg/platforms/power_test.go
  • pkg/service/mediadb_schema_reset_test.go
  • pkg/service/service_test.go
  • pkg/service/state/state.go
  • pkg/service/state/state_test.go
  • pkg/service/updater/gate.go
  • pkg/service/updater/gate_test.go
  • pkg/service/updater/install.go
  • pkg/service/updater/progress.go
  • pkg/service/updater/progress_test.go
  • pkg/service/updater/rollout.go
  • pkg/service/updater/rollout_test.go
  • pkg/service/updater/stage.go
  • pkg/service/updater/state.go
  • pkg/service/updater/state_test.go
  • pkg/service/updater/updater.go
  • pkg/service/updater/updater_test.go
  • pkg/testing/mocks/platform.go

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant