316 lines
7.8 KiB
Go
316 lines
7.8 KiB
Go
package updates
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Controller struct {
|
|
app AppDescriptor
|
|
provider ReleaseProvider
|
|
downloader Downloader
|
|
store SnapshotStore
|
|
platform PlatformInstaller
|
|
logger *slog.Logger
|
|
|
|
mu sync.Mutex
|
|
snapshot Snapshot
|
|
busy bool
|
|
listeners map[int]chan Snapshot
|
|
nextListener int
|
|
}
|
|
|
|
func NewController(cfg Config) (*Controller, error) {
|
|
if cfg.Provider == nil || cfg.Downloader == nil || cfg.Store == nil || cfg.Platform == nil {
|
|
return nil, ErrInvalidConfig
|
|
}
|
|
if _, err := NormalizeVersion(cfg.App.CurrentVersion); err != nil {
|
|
return nil, err
|
|
}
|
|
if cfg.App.WorkingDirectory == "" {
|
|
workingDirectory, err := os.Getwd()
|
|
if err == nil {
|
|
cfg.App.WorkingDirectory = workingDirectory
|
|
}
|
|
}
|
|
|
|
logger := cfg.Logger
|
|
if logger == nil {
|
|
logger = slog.Default()
|
|
}
|
|
|
|
snapshot, err := cfg.Store.Load(context.Background())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
snapshot.CurrentVersion = cfg.App.CurrentVersion
|
|
snapshot.Channel = cfg.App.Channel
|
|
if snapshot.State == "" {
|
|
snapshot.State = StateIdle
|
|
}
|
|
|
|
controller := &Controller{
|
|
app: cfg.App,
|
|
provider: cfg.Provider,
|
|
downloader: cfg.Downloader,
|
|
store: cfg.Store,
|
|
platform: cfg.Platform,
|
|
logger: logger,
|
|
snapshot: snapshot,
|
|
listeners: make(map[int]chan Snapshot),
|
|
}
|
|
|
|
if snapshot.Staged != nil {
|
|
comparison, err := CompareVersions(snapshot.Staged.Release.Version, cfg.App.CurrentVersion)
|
|
if err == nil && comparison == 0 {
|
|
snapshot.State = StateUpToDate
|
|
snapshot.Staged = nil
|
|
snapshot.Candidate = nil
|
|
snapshot.LastError = nil
|
|
controller.snapshot = snapshot
|
|
if err := controller.store.Save(context.Background(), snapshot); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return controller, nil
|
|
}
|
|
|
|
func (controller *Controller) Snapshot() Snapshot {
|
|
controller.mu.Lock()
|
|
defer controller.mu.Unlock()
|
|
return controller.snapshot
|
|
}
|
|
|
|
func (controller *Controller) Subscribe(buffer int) (<-chan Snapshot, func()) {
|
|
if buffer < 1 {
|
|
buffer = 1
|
|
}
|
|
|
|
controller.mu.Lock()
|
|
defer controller.mu.Unlock()
|
|
|
|
controller.nextListener++
|
|
id := controller.nextListener
|
|
channel := make(chan Snapshot, buffer)
|
|
channel <- controller.snapshot
|
|
controller.listeners[id] = channel
|
|
|
|
return channel, func() {
|
|
controller.mu.Lock()
|
|
defer controller.mu.Unlock()
|
|
if _, ok := controller.listeners[id]; ok {
|
|
delete(controller.listeners, id)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (controller *Controller) Check(ctx context.Context, _ CheckRequest) (Snapshot, error) {
|
|
if !controller.beginOperation() {
|
|
return Snapshot{}, ErrBusy
|
|
}
|
|
defer controller.endOperation()
|
|
|
|
now := time.Now().UTC()
|
|
controller.transition(StateChecking, nil, nil)
|
|
|
|
release, err := controller.provider.Resolve(ctx, ResolveRequest{App: controller.app})
|
|
if err != nil {
|
|
return controller.fail("resolve_failed", err)
|
|
}
|
|
|
|
if release == nil {
|
|
snapshot := controller.currentSnapshot()
|
|
snapshot.State = StateUpToDate
|
|
snapshot.LastCheckedAt = &now
|
|
snapshot.Candidate = nil
|
|
snapshot.LastError = nil
|
|
if err := controller.save(snapshot); err != nil {
|
|
return Snapshot{}, err
|
|
}
|
|
return snapshot, nil
|
|
}
|
|
|
|
comparison, err := CompareVersions(release.Version, controller.app.CurrentVersion)
|
|
if err != nil {
|
|
return controller.fail("invalid_release_version", err)
|
|
}
|
|
if comparison <= 0 {
|
|
snapshot := controller.currentSnapshot()
|
|
snapshot.State = StateUpToDate
|
|
snapshot.LastCheckedAt = &now
|
|
snapshot.Candidate = nil
|
|
snapshot.LastError = nil
|
|
if err := controller.save(snapshot); err != nil {
|
|
return Snapshot{}, err
|
|
}
|
|
return snapshot, nil
|
|
}
|
|
|
|
snapshot := controller.currentSnapshot()
|
|
snapshot.State = StateUpdateAvailable
|
|
snapshot.LastCheckedAt = &now
|
|
snapshot.Candidate = release
|
|
snapshot.Staged = nil
|
|
snapshot.LastError = nil
|
|
if err := controller.save(snapshot); err != nil {
|
|
return Snapshot{}, err
|
|
}
|
|
|
|
return snapshot, nil
|
|
}
|
|
|
|
func (controller *Controller) Download(ctx context.Context) (Snapshot, error) {
|
|
if !controller.beginOperation() {
|
|
return Snapshot{}, ErrBusy
|
|
}
|
|
defer controller.endOperation()
|
|
|
|
snapshot := controller.currentSnapshot()
|
|
if snapshot.Candidate == nil {
|
|
return Snapshot{}, ErrNoUpdate
|
|
}
|
|
|
|
controller.transition(StateDownloading, snapshot.Candidate, nil)
|
|
|
|
downloadedFile, err := controller.downloader.Download(ctx, snapshot.Candidate.Artifact)
|
|
if err != nil {
|
|
return controller.fail("download_failed", err)
|
|
}
|
|
if downloadedFile.SHA256 != "" && snapshot.Candidate.Artifact.SHA256 != "" && downloadedFile.SHA256 != snapshot.Candidate.Artifact.SHA256 {
|
|
return controller.fail("checksum_mismatch", ErrChecksumMismatch)
|
|
}
|
|
|
|
controller.transition(StateVerifying, snapshot.Candidate, nil)
|
|
|
|
root, err := controller.platform.DetectInstallRoot(controller.app)
|
|
if err != nil {
|
|
return controller.fail("detect_root_failed", err)
|
|
}
|
|
|
|
stagedArtifact, err := controller.platform.Stage(ctx, root, downloadedFile, *snapshot.Candidate)
|
|
if err != nil {
|
|
return controller.fail("stage_failed", err)
|
|
}
|
|
|
|
nextSnapshot := controller.currentSnapshot()
|
|
nextSnapshot.State = StateReadyToApply
|
|
nextSnapshot.Staged = &stagedArtifact
|
|
nextSnapshot.Candidate = &stagedArtifact.Release
|
|
nextSnapshot.LastError = nil
|
|
if err := controller.save(nextSnapshot); err != nil {
|
|
return Snapshot{}, err
|
|
}
|
|
|
|
return nextSnapshot, nil
|
|
}
|
|
|
|
func (controller *Controller) ApplyAndRestart(ctx context.Context) error {
|
|
if !controller.beginOperation() {
|
|
return ErrBusy
|
|
}
|
|
defer controller.endOperation()
|
|
|
|
snapshot := controller.currentSnapshot()
|
|
if snapshot.Staged == nil {
|
|
return ErrNoStagedUpdate
|
|
}
|
|
|
|
controller.transition(StateApplying, snapshot.Candidate, snapshot.Staged)
|
|
|
|
if err := controller.platform.SpawnApplyAndRestart(ctx, ApplyRequest{
|
|
App: controller.app,
|
|
Root: snapshot.Staged.Root,
|
|
Staged: *snapshot.Staged,
|
|
}); err != nil {
|
|
_, failErr := controller.fail("apply_failed", err)
|
|
return failErr
|
|
}
|
|
|
|
nextSnapshot := controller.currentSnapshot()
|
|
nextSnapshot.State = StateRestarting
|
|
nextSnapshot.LastError = nil
|
|
return controller.save(nextSnapshot)
|
|
}
|
|
|
|
func (controller *Controller) beginOperation() bool {
|
|
controller.mu.Lock()
|
|
defer controller.mu.Unlock()
|
|
if controller.busy {
|
|
return false
|
|
}
|
|
controller.busy = true
|
|
return true
|
|
}
|
|
|
|
func (controller *Controller) endOperation() {
|
|
controller.mu.Lock()
|
|
defer controller.mu.Unlock()
|
|
controller.busy = false
|
|
}
|
|
|
|
func (controller *Controller) currentSnapshot() Snapshot {
|
|
controller.mu.Lock()
|
|
defer controller.mu.Unlock()
|
|
return controller.snapshot
|
|
}
|
|
|
|
func (controller *Controller) transition(state State, candidate *Release, staged *StagedArtifact) {
|
|
snapshot := controller.currentSnapshot()
|
|
snapshot.State = state
|
|
snapshot.Candidate = candidate
|
|
snapshot.Staged = staged
|
|
snapshot.LastError = nil
|
|
_ = controller.save(snapshot)
|
|
}
|
|
|
|
func (controller *Controller) save(snapshot Snapshot) error {
|
|
snapshot.CurrentVersion = controller.app.CurrentVersion
|
|
snapshot.Channel = controller.app.Channel
|
|
if err := controller.store.Save(context.Background(), snapshot); err != nil {
|
|
return err
|
|
}
|
|
|
|
controller.mu.Lock()
|
|
controller.snapshot = snapshot
|
|
listeners := make([]chan Snapshot, 0, len(controller.listeners))
|
|
for _, listener := range controller.listeners {
|
|
listeners = append(listeners, listener)
|
|
}
|
|
controller.mu.Unlock()
|
|
|
|
for _, listener := range listeners {
|
|
select {
|
|
case listener <- snapshot:
|
|
default:
|
|
select {
|
|
case <-listener:
|
|
default:
|
|
}
|
|
listener <- snapshot
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (controller *Controller) fail(code string, err error) (Snapshot, error) {
|
|
controller.logger.Error("update flow failed", "code", code, "error", err)
|
|
|
|
snapshot := controller.currentSnapshot()
|
|
snapshot.State = StateFailed
|
|
snapshot.LastError = &ErrorInfo{
|
|
Code: code,
|
|
Message: err.Error(),
|
|
}
|
|
if saveErr := controller.save(snapshot); saveErr != nil {
|
|
return Snapshot{}, fmt.Errorf("%w: %w", err, saveErr)
|
|
}
|
|
return snapshot, err
|
|
}
|