Files
bbedward f482642880 sysupdate: check only on demand, run at low priority, fix the settings page
scrollbar: interface style toggle and no scrollbars in the dash
dash: never allow scrollbars
cc: widget sheet cards, header close button and content-sized dialogs
2026-09-29 16:30:04 -04:00

152 lines
3.8 KiB
Go

package sysupdate
import (
"context"
"encoding/json"
"os"
"path/filepath"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/netfetch"
"github.com/AvengeMedia/DankMaterialShell/core/internal/site"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
const (
releasesURL = site.API + "/dms/releases"
releasesTimeout = 10 * time.Second
releasesMinAge = time.Hour
releasesMaxBytes = 1 << 20
)
var releasesFetch = func(ctx context.Context, etag string) (netfetch.Conditional, error) {
return netfetch.BytesConditional(ctx, releasesURL, etag, netfetch.Options{Timeout: releasesTimeout, MaxBytes: releasesMaxBytes})
}
func cacheDir() string {
return filepath.Join(utils.XDGCacheHome(), "dms")
}
func releasesCachePath() string {
return filepath.Join(cacheDir(), "releases.json")
}
// The feed never lives in Manager memory; every reader goes through the cache file.
func LoadReleases() (ReleasesFeed, bool) {
data, err := os.ReadFile(releasesCachePath())
if err != nil {
return ReleasesFeed{}, false
}
var feed ReleasesFeed
if err := json.Unmarshal(data, &feed); err != nil {
return ReleasesFeed{}, false
}
if feed.Releases == nil {
feed.Releases = []Release{}
}
return feed, true
}
func saveReleases(feed ReleasesFeed) error {
data, err := json.Marshal(feed)
if err != nil {
return err
}
return writeFileAtomic(releasesCachePath(), data)
}
func writeFileAtomic(path string, data []byte) error {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
tmp := path + ".tmp"
if err := os.WriteFile(tmp, data, 0o644); err != nil {
return err
}
return os.Rename(tmp, path)
}
// Serialized so overlapping requests don't fetch twice.
func (m *Manager) refreshReleases(ctx context.Context, force bool) (ReleasesFeed, error) {
m.releasesMu.Lock()
defer m.releasesMu.Unlock()
cached, ok := LoadReleases()
if ok && !force && time.Since(time.Unix(cached.FetchedAt, 0)) < releasesMinAge {
return cached, nil
}
res, err := releasesFetch(ctx, cached.ETag)
if err != nil {
if ok {
return cached, err
}
return ReleasesFeed{Releases: []Release{}}, err
}
now := time.Now().Unix()
feed := cached
if !res.NotModified {
feed = ReleasesFeed{}
if err := json.Unmarshal(res.Body, &feed); err != nil {
if ok {
return cached, err
}
return ReleasesFeed{Releases: []Release{}}, err
}
if feed.Releases == nil {
feed.Releases = []Release{}
}
}
feed.FetchedAt = now
feed.ETag = res.ETag
if err := saveReleases(feed); err != nil {
log.Warnf("[sysupdate] save releases cache: %v", err)
}
m.applyReleases(feed)
return feed, nil
}
func (m *Manager) applyReleases(feed ReleasesFeed) {
m.mu.Lock()
m.state.Shell.CommitsBehind = commitsBehind(m.state.Shell, feed.Master)
m.mu.Unlock()
m.markDirty()
}
// Packages ride along so a restart doesn't show an empty list until the next daily check.
type persistedState struct {
LastCheckUnix int64 `json:"lastCheckUnix"`
LastSuccessUnix int64 `json:"lastSuccessUnix"`
Packages []Package `json:"packages,omitempty"`
// Survives shell restarts, not the reboot itself.
RebootBootID string `json:"rebootBootId,omitempty"`
RebootPackages []string `json:"rebootPackages,omitempty"`
}
func statePath() string {
return filepath.Join(cacheDir(), "sysupdate.json")
}
func loadPersisted() persistedState {
var s persistedState
data, err := os.ReadFile(statePath())
if err != nil {
return s
}
if err := json.Unmarshal(data, &s); err != nil {
return persistedState{}
}
return s
}
func savePersisted(s persistedState) {
data, err := json.Marshal(s)
if err != nil {
return
}
if err := writeFileAtomic(statePath(), data); err != nil {
log.Debugf("[sysupdate] persist state: %v", err)
}
}