updater

package
v2.17.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 31, 2026 License: GPL-3.0 Imports: 42 Imported by: 0

Documentation

Index

Constants

View Source
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.

View Source
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.

View Source
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

View Source
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")
)
View Source
var (
	ErrDevelopmentVersion = errors.New("update check skipped for development version")
	ErrUpdateInProgress   = errors.New("update already in progress")
)
View Source
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.

View Source
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.

View Source
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.

View Source
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 Apply

func Apply(ctx context.Context, opts Options) (string, error)

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

func Confirm(ctx context.Context, dataDir, currentVersion string) (string, error)

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

func EligibilityCanOfferUpdates(eligibility string) bool

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

func PreviouslyRolledBack(result *Result) bool

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

func RecordCleanShutdown(dataDir, currentVersion string) error

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

func ReportLastUpdate(dataDir string, inboxSvc *inbox.Service)

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

func RollBackFailedStart(ctx context.Context, dataDir, currentVersion string) error

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

func RollbackTargetPath(err error) (string, bool)

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

func RolloutEligible(deviceID, releaseTag string, rollout int) bool

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

func RunStartupWatchdog(ctx context.Context, dataDir, currentVersion string) error

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 CheckFn

type CheckFn func(ctx context.Context, opts Options) (*Result, error)

CheckFn is the signature for a function that checks for updates.

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

type GateError struct {
	Reason    string
	Message   string
	Forceable bool
}

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.

func (*GateError) Error added in v2.17.0

func (e *GateError) Error() string

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 Check

func Check(ctx context.Context, opts Options) (*Result, error)

func Status added in v2.17.0

func Status(ctx context.Context, opts Options) *Result

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.

Directories

Path Synopsis
Package otameta reads the signed update manifest and decides which release archive a device is allowed to install from it.
Package otameta reads the signed update manifest and decides which release archive a device is allowed to install from it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL