Documentation
¶
Index ¶
- Constants
- Variables
- func Apply(ctx context.Context, opts Options) (string, error)
- func CheckAndNotify(ctx context.Context, cfg *config.Instance, opts Options, ...)
- func Confirm(ctx context.Context, dataDir, currentVersion string) (string, error)
- func EligibilityCanOfferUpdates(eligibility string) bool
- func PreviouslyRolledBack(result *Result) bool
- func RecordCleanShutdown(dataDir, currentVersion string) error
- func ReportLastUpdate(dataDir string, inboxSvc *inbox.Service)
- func RollBackFailedStart(ctx context.Context, dataDir, currentVersion string) error
- func RollbackTargetPath(err error) (string, bool)
- func RolloutEligible(deviceID, releaseTag string, rollout int) bool
- func RunStartupWatchdog(ctx context.Context, dataDir, currentVersion string) error
- type CheckFn
- type GateDecision
- type GateDeps
- type GateError
- type Mode
- type Options
- type OutcomeReport
- type Progress
- type ProgressFn
- type ProgressStage
- type Result
- type StageOptions
- type StagedUpdate
- type UpdateBackupper
Constants ¶
const ( ReasonMediaIndexing = "mediaIndexing" ReasonMediaOptimizing = "mediaOptimizing" ReasonMediaScraping = "mediaScraping" ReasonBackupActive = "backupActive" ReasonReaderWriting = "readerWriting" ReasonRestoreActive = "restoreActive" ReasonActiveMedia = "activeMedia" ReasonBackgroundMedia = "backgroundMedia" ReasonActivePlaylist = "activePlaylist" ReasonPowerLow = "powerLow" ReasonPowerUnknown = "powerUnknown" ReasonAPIBusy = "apiBusy" )
Reasons an update cannot be applied right now. These are the machine-readable half of a refusal; clients match on them to decide what to say.
const ( OutcomeSucceeded = "succeeded" OutcomeRolledBack = "rolledBack" OutcomeRecoveryRequired = "recoveryRequired" // OutcomeRollbackBlocked means the binary was rolled back but the user // database snapshot could not be restored, or the rollback could not be // completed at all, and the new binary was left installed instead. A device // running a suspect version beats one that will not boot. OutcomeRollbackBlocked = "rollbackBlocked" )
The outcomes as they appear in API responses and on disk. Exported because anything rendering an update's result has to match on them, and a client spelling one out by hand compiles perfectly and then never matches.
const ( // EligibilityEligible means OTA updates work here. EligibilityEligible = "eligible" // EligibilityDevelopment means this is a build from source, which has no // release to compare itself against. EligibilityDevelopment = "development" // EligibilityManaged means a package manager owns this install and should // be the one updating it. EligibilityManaged = "managed" // EligibilityUnsupported means this install cannot replace its own binary, // so the update has to come from wherever it was installed from. On Windows // that is an install directory this process cannot write to. EligibilityUnsupported = "unsupported" )
Eligibility says whether this device can take an OTA update at all, ahead of any question about whether one is available.
Variables ¶
var ( // ErrNotAnUpgrade means the release is not newer than what is running. It is // checked here rather than trusted from the check response because that is // what stops a stale or tampered manifest installing an older build. ErrNotAnUpgrade = errors.New("release is not newer than the running version") // ErrUpgradeFloor means the release declares a minimum version to come from // and this device is below it, so it has to take an intermediate build // first. ErrUpgradeFloor = errors.New("release cannot be installed directly from the running version") // ErrArchiveRejected covers everything about the archive that fails a rule: // a size the manifest will not vouch for, an unreadable or overlong member, // a missing binary, or two of them. ErrArchiveRejected = errors.New("release archive was rejected") // ErrChecksumMismatch means the bytes that arrived are not the bytes the // signed manifest describes. ErrChecksumMismatch = errors.New("release archive does not match the manifest checksum") // ErrDownloadStalled means the transfer stopped making progress, either // because the stall guard saw no bytes for the whole stall timeout or because // one of the transport's own deadlines ran out waiting for a connect, a // handshake or the response headers. It is a fault of the network, never of // the release. ErrDownloadStalled = errors.New("release archive download stalled") // ErrProbeFailed means the staged binary would not run here, or ran and // disagreed about what version it is. This is the check that keeps a bad // build from reaching a device with no supervisor to recover it. ErrProbeFailed = errors.New("staged binary failed its version probe") )
var ( ErrDevelopmentVersion = errors.New("update check skipped for development version") ErrUpdateInProgress = errors.New("update already in progress") )
var ( ErrRolledBack = errors.New("update rolled back, restart into the restored version") // ErrRollbackStateUncertain means the user database was restored but that // fact could not be made durable. Startup must stop before anything can write // the database; the next boot can then repeat the restore without losing // intervening user data. ErrRollbackStateUncertain = errors.New("restored user database was not recorded durably") )
ErrRolledBack means an update was rolled back and the previous version is now on disk. The process is still running the image that failed, so every caller of Start has to re-exec rather than exit: on the platforms this matters for there is no supervisor to start anything again.
var ErrGenerationRollback = errors.New("update manifest is older than one already seen")
ErrGenerationRollback means the CDN served metadata older than something this device already accepted. Signed metadata cannot be forged, but a stale or deliberately replayed copy can still hide a release that fixes a problem, so it is refused rather than used.
var ErrInsufficientSpace = errors.New("insufficient disk space for the update")
ErrInsufficientSpace is returned before anything is downloaded, so a full disk costs a manifest fetch rather than an archive.
var ErrPlatformUnsupported = errors.New("this platform cannot install updates in place")
ErrPlatformUnsupported is returned when this build cannot finish an install no matter how the release turns out.
Functions ¶
func CheckAndNotify ¶
func CheckAndNotify( ctx context.Context, cfg *config.Instance, opts Options, inboxSvc *inbox.Service, waitFn func(context.Context, int) bool, checkFn CheckFn, managedInstall bool, )
CheckAndNotify checks for updates and posts a version-deduplicated inbox message when one is available. The service scheduler calls it periodically.
func Confirm ¶ added in v2.17.0
Confirm commits an update that has stayed up long enough to be trusted. The terminal outcome is made durable before cleanup, so a crash at any cleanup boundary resumes cleanup rather than attempting rollback without its files.
func EligibilityCanOfferUpdates ¶ added in v2.17.0
EligibilityCanOfferUpdates reports whether looking for a release could change what this device is told. It is false for the states whose answer is fixed by what the install is rather than by what has been released, so a caller can skip a request whose result it would have to discard.
Shared because the answer decides whether a person's explicit "update now" contacts the network, and two copies of that rule drifting apart would mean one entry point silently reporting a stale answer.
func PreviouslyRolledBack ¶ added in v2.17.0
PreviouslyRolledBack reports the same refusal as the one Apply enforces, from a check result a caller already has. Apply is the authority; this lets a scheduler skip the work instead of arranging an install that will be refused.
func RecordCleanShutdown ¶ added in v2.17.0
RecordCleanShutdown resets a confirming marker so an orderly stop during the soak window restarts confirmation on the next boot instead of looking like a crash. A startup failure still rolls back through Start's deferred hook after cleanup completes.
func ReportLastUpdate ¶ added in v2.17.0
ReportLastUpdate posts the outcome of an update that finished before there was an inbox to post it to. Rollbacks are exactly that case: they run before the databases are open and then re-exec, so this is the only chance the user gets to hear that the version they installed did not work.
func RollBackFailedStart ¶ added in v2.17.0
RollBackFailedStart is the same recovery driven by a startup that got past the watchdog and then failed anyway. The watchdog only sees a process that never started; this sees one that started and could not finish, which on a device with no supervisor is just as fatal.
func RollbackTargetPath ¶ added in v2.17.0
RollbackTargetPath returns the restored executable path carried by a rollback result, allowing callers to re-exec that path rather than the failing image.
func RolloutEligible ¶ added in v2.17.0
RolloutEligible reports whether this device is inside a release's staged rollout yet.
A device with no ID only takes releases that have gone out to everyone: an unidentified device has no stable bucket, and treating it as bucket 0 would quietly make every such device part of the first wave.
func RunStartupWatchdog ¶ added in v2.17.0
RunStartupWatchdog resolves any update left pending by a previous boot. It runs before configuration, the databases or the network are available, because the failure it exists to catch is a binary that cannot get that far.
It returns ErrRolledBack when the previous version has been put back and the caller must re-exec into it. ErrRollbackStateUncertain must also stop startup so the restored database cannot be changed before recovery retries. Every other error is advisory: startup continues, because refusing to boot over a bookkeeping problem is the outcome this whole mechanism exists to avoid.
Types ¶
type GateDecision ¶ added in v2.17.0
type GateDecision struct {
// Release gives back whatever the gate took. It is never nil, so callers can
// defer it without checking, and it does nothing when the gate was never
// taken.
Release func()
// Reason is the machine-readable refusal, empty when OK.
Reason string
// Message says the same thing in words, for a client with nothing better
// to show.
Message string
// Forceable means a person may go ahead anyway. It is never true for
// automatic installs, and never true for anything that risks data rather
// than the user's session.
Forceable bool
// Expires means this is a soft signal that an automatic install may
// ignore once the version has waited out autoInstallDeadline.
Expires bool
// OK means the update may go ahead.
OK bool
}
GateDecision is the gate's answer.
func CanApplyUpdate ¶ added in v2.17.0
func CanApplyUpdate(ctx context.Context, deps *GateDeps, mode Mode, force bool) (GateDecision, error)
CanApplyUpdate reports whether an update may be installed right now.
A decision that is OK carries the gates the install needs held, so the caller must call Release once the install has finished or failed. force lets a person past the signals that are only about their own session; it never gets past a signal that risks their data, and mode auto ignores it entirely.
The error is separate from the decision on purpose: a refusal is an answer, but a cancelled request is not an answer at all.
func PowerReady ¶ added in v2.17.0
func PowerReady(deps *GateDeps, mode Mode, force bool) GateDecision
PowerReady re-runs only the power part of the gate. The install calls it once the download is finished, because a download long enough to matter is also long enough to outlive a charger being unplugged.
func (*GateDecision) Err ¶ added in v2.17.0
func (d *GateDecision) Err() error
Err turns a refusal into an error. It returns nil for a decision that is OK.
type GateDeps ¶ added in v2.17.0
type GateDeps struct {
// IndexingStatus, OptimizationStatus and ScrapingStatus each return a
// mediadb status string. An error is treated as "not running": a database
// that cannot answer is a problem for the caller to notice elsewhere, and
// refusing every update because of it would leave the device unfixable.
IndexingStatus func() (string, error)
OptimizationStatus func() (string, error)
ScrapingStatus func() (string, error)
// BackupActive reports whether a backup, restore or upload is running.
BackupActive func() bool
// ReaderWriteActive reports whether a reader is part-way through writing
// a token.
ReaderWriteActive func() bool
// AcquireRestore takes the restore gate, which the install then holds so
// a restore cannot start underneath it. The release function it returns
// is carried on the decision.
AcquireRestore func() (func(), error)
// AcquireMediaGate stops anything new launching and waits for what is
// already launching to settle, so the install's own look at what is
// playing cannot be overtaken by a launch that starts a moment later. The
// install holds it until the restart.
//
// The gate takes this after AcquireRestore, which is the order the rest of
// the service takes those two locks in. Taking them the other way round is
// a lock inversion, which is why neither is the caller's to take.
AcquireMediaGate func(context.Context) (func(), error)
// ActiveMedia, BackgroundMedia and ActivePlaylist report what the user
// would lose if the service restarted now.
ActiveMedia func() bool
BackgroundMedia func() bool
ActivePlaylist func() bool
// Power reports where the device's power is coming from.
Power func() power.Status
// WaitForIdle blocks until the API has been quiet for a while. Only
// automatic installs wait for it; a person pressing update is the request
// that would otherwise stop it ever being idle.
WaitForIdle func(context.Context) error
// DeferredSince is when this version was first put off by a soft signal,
// or the zero time if it has not been. Once that is more than
// autoInstallDeadline ago the soft signals stop counting.
DeferredSince func() time.Time
// Now reads the clock. Tests replace it.
Now func() time.Time
}
GateDeps is everything the gate needs to look at, as plain functions so the gate can be tested without a running service. A nil function means the caller has nothing to report for that signal and it is skipped.
type GateError ¶ added in v2.17.0
GateError is a refusal from the gate, as an error, so a check made deep inside an install can be recognised again by the caller that started it.
type Mode ¶ added in v2.17.0
type Mode string
Mode is who asked for the update.
const ( // ModeManual is a person pressing update in a client. They are at the // device, they can see what it is doing, and they can be asked about // anything short of a real risk to their data. ModeManual Mode = "manual" // ModeAuto is the device deciding for itself. Nobody is watching, so // anything that would surprise a user is a reason to wait. ModeAuto Mode = "auto" )
type Options ¶ added in v2.17.0
type Options struct {
UserDB UpdateBackupper
// Progress is called as the update moves through its stages, when the
// caller wants to follow along. Nil reports nothing.
Progress ProgressFn
// PreQuiesce runs at the last moment an install can still be called off.
// The second power check goes here.
PreQuiesce func(context.Context) error
// Gate is what the device is busy with. A check uses it to report what
// would stop an update going ahead right now; nil reports nothing.
Gate *GateDeps
Payload []updatepayload.File
PlatformID string
Channel string
DataDir string
// DeviceID is this device's identifier, used to work out whether a staged
// rollout has reached it yet. Empty means only fully released versions
// count as rolled out.
DeviceID string
// Mode is who asked. It decides how the install is recorded and, for
// automatic installs, that nothing may be forced.
Mode Mode
// Managed says a package manager owns this install, which the check
// reports as the reason OTA updates do not apply here.
Managed bool
}
Options describes the device an update is being resolved for.
type OutcomeReport ¶ added in v2.17.0
type OutcomeReport struct {
At time.Time `json:"at"`
Outcome string `json:"outcome"`
FromVersion string `json:"fromVersion,omitempty"`
ToVersion string `json:"toVersion,omitempty"`
Detail string `json:"detail,omitempty"`
}
OutcomeReport is how the last update finished, for a client that was not connected when it happened. The confirm and rollback stages run on the boot after the restart, before any client is back, so this is the only way they are ever seen.
type Progress ¶ added in v2.17.0
type Progress struct {
Stage ProgressStage `json:"stage"`
Version string `json:"version,omitempty"`
Trigger string `json:"trigger,omitempty"`
Error string `json:"error,omitempty"`
BytesDownloaded int64 `json:"bytesDownloaded,omitempty"`
BytesTotal int64 `json:"bytesTotal,omitempty"`
}
Progress is one update on how an update is going.
type ProgressFn ¶ added in v2.17.0
type ProgressFn func(Progress)
ProgressFn receives progress updates. It is called from whichever goroutine is doing the work, so it must not block.
type ProgressStage ¶ added in v2.17.0
type ProgressStage string
ProgressStage is where an update has got to.
const ( ProgressIdle ProgressStage = "idle" ProgressChecking ProgressStage = "checking" ProgressDownloading ProgressStage = "downloading" ProgressVerifying ProgressStage = "verifying" ProgressProbing ProgressStage = "probing" ProgressInstalling ProgressStage = "installing" ProgressRestarting ProgressStage = "restarting" // ProgressConfirming through ProgressRolledBack happen on the boot after // the restart, before any client has reconnected, so they reach clients // through the last result an update check reports rather than live. ProgressConfirming ProgressStage = "confirming" ProgressSucceeded ProgressStage = "succeeded" ProgressRolledBack ProgressStage = "rolledBack" ProgressFailed ProgressStage = "failed" )
type Result ¶
type Result struct {
CheckedAt time.Time
DeferredSince time.Time
LastResult *OutcomeReport
Eligibility string
ReleaseNotes string
Channel string
LatestVersion string
DeferredReason string
BlockedReason string
BlockedMessage string
CurrentVersion string
UpdateAvailable bool
RolloutHeld bool
BlockedForceable bool
}
func Status ¶ added in v2.17.0
Status answers from what this device already knows, without contacting the release server or writing anything.
A check is expensive in a way that matters on the platforms Core runs on: it fetches and verifies signed metadata and writes the result to the data directory, which on MiSTer is a write onto exfat. That cost is why unpaired clients are refused one. Anything that wants to show update state whenever a screen is drawn has to be able to ask without paying it.
The answer is as fresh as the last check, and CheckedAt says when that was, so a caller can be honest about how old it is rather than implying it just looked. Unlike a check, this reports normally on a development build instead of refusing: saying which version is running and that updates do not apply to it is exactly what someone looking at the screen wants to know.
type StageOptions ¶ added in v2.17.0
type StageOptions struct {
Release *otameta.Release
PlatformID string
Arch string
OS string
TargetPath string
StagingRoot string
CurrentVersion string
// contains filtered or unexported fields
}
StageOptions describes one staging attempt.
type StagedUpdate ¶ added in v2.17.0
type StagedUpdate struct {
// Dir is the staging directory holding the archive and the payload.
// Removing it undoes the whole staging attempt.
Dir string
// BinaryPath is the new executable. It came out of an archive whose bytes on
// disk were checked against the signed manifest immediately before it was
// read, and it has answered a version probe on this device.
BinaryPath string
// ArchivePath is the downloaded archive, kept so a later stage can report
// what it installed from.
ArchivePath string
// Version is the release version, without the tag's leading v.
Version string
// contains filtered or unexported fields
}
func Stage ¶ added in v2.17.0
func Stage(ctx context.Context, opts *StageOptions) (*StagedUpdate, error)
Stage downloads a release, checks it against the signed manifest, pulls the binary out of it and proves that binary runs, without touching the live install. Every failure leaves the device exactly as it was.
type UpdateBackupper ¶ added in v2.17.0
type UpdateBackupper interface {
BackupForUpdate(targetVersion string) (database.BackupInfo, func() error, error)
}
UpdateBackupper is the part of the live UserDB needed to arm rollback before an update replaces the running binary.