| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | "sync" |
| 6 | "time" |
| 7 | ) |
| 8 | |
| 9 | // Instance-side update awareness. The instance never applies updates itself — |
| 10 | // it runs sandboxed and can't restart anything; the box updater (`lchat update |
| 11 | // run`, root) does the staging and swapping. The instance's two jobs here: |
| 12 | // |
| 13 | // - Tell the box updater what the admin chose: GET /api/update-prefs (read |
| 14 | // over the instance's unix socket, like the deploy health checks). Un-authed |
| 15 | // by design — it reveals less than the already-public /api/version build id, |
| 16 | // and an old binary 404ing means "defaults" (auto-update on, stable). |
| 17 | // - Know whether its channel has something newer, so the admin console can |
| 18 | // say "a new version is available" — a periodic fetch+verify of the signed |
| 19 | // release manifest (release.go), surfaced through GET /api/admin/settings. |
| 20 | // A fetch failure just leaves the last result stale; nothing acts on it. |
| 21 | |
| 22 | // updateState is the last verified manifest fetch, guarded by mu — written by |
| 23 | // the checker goroutine, read by getSettings. |
| 24 | type updateState struct { |
| 25 | mu sync.Mutex |
| 26 | manifest *releaseManifest |
| 27 | checkedAt time.Time |
| 28 | } |
| 29 | |
| 30 | // getUpdatePrefs reports the admin's update settings to the box updater. |
| 31 | func (s *Server) getUpdatePrefs(w http.ResponseWriter, r *http.Request) { |
| 32 | writeJSON(w, http.StatusOK, map[string]any{ |
| 33 | "auto_update": s.db.updateAuto(), |
| 34 | "auto_download": s.db.updateDownload(), |
| 35 | "channel": s.db.updateChannel(), |
| 36 | "pin": s.db.updatePin(), |
| 37 | "major": s.db.updateMajor(), |
| 38 | }) |
| 39 | } |
| 40 | |
| 41 | // runUpdateChecker refreshes the availability info: once shortly after boot |
| 42 | // (so a fresh admin console isn't blank for hours), then every 6h. The box |
| 43 | // updater runs its own checks; this one is display-only, so failures are |
| 44 | // logged by the caller of checkForUpdate and otherwise ignored. |
| 45 | func (s *Server) runUpdateChecker() { |
| 46 | time.Sleep(time.Minute) |
| 47 | for { |
| 48 | _ = s.checkForUpdate() |
| 49 | time.Sleep(6 * time.Hour) |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | // checkForUpdate fetches and verifies the release manifest (the same fetch |
| 54 | // the box updater runs — fetchReleaseManifest, updater.go), keeping the whole |
| 55 | // channel map — so an admin switching channels sees that channel's version |
| 56 | // immediately, without waiting for the next fetch. |
| 57 | func (s *Server) checkForUpdate() error { |
| 58 | m, err := fetchReleaseManifest(releaseManifestURL) |
| 59 | if err != nil { |
| 60 | return err |
| 61 | } |
| 62 | s.upd.mu.Lock() |
| 63 | s.upd.manifest, s.upd.checkedAt = m, time.Now() |
| 64 | s.upd.mu.Unlock() |
| 65 | // A changed channel head alerts the space's admins (releasealert.go) — |
| 66 | // the detection half of the supply-chain bake window. |
| 67 | s.noteReleaseSightings(m) |
| 68 | return nil |
| 69 | } |
| 70 | |
| 71 | // updateInfo is the availability block getSettings serves the admin consoles: |
| 72 | // what we run, what the tracked channel offers, and how fresh that knowledge is. |
| 73 | func (s *Server) updateInfo() map[string]any { |
| 74 | info := map[string]any{"build": buildVersion} |
| 75 | s.upd.mu.Lock() |
| 76 | m, checkedAt := s.upd.manifest, s.upd.checkedAt |
| 77 | s.upd.mu.Unlock() |
| 78 | if m == nil { |
| 79 | return info // no successful check yet — the console shows just the build |
| 80 | } |
| 81 | info["checked_at"] = checkedAt.Unix() |
| 82 | if e, ok := m.Channels[s.db.updateChannel()]; ok { |
| 83 | info["available_version"] = e.Version |
| 84 | info["available_notes"] = e.Notes |
| 85 | if e.Semver != "" { |
| 86 | info["available_semver"] = e.Semver |
| 87 | } |
| 88 | // A major pin holding this head back is worth explaining in the console |
| 89 | // — same reason bake_hours below is served. |
| 90 | if hold := majorHold(s.db.updateMajor(), e); hold != "" { |
| 91 | info["major_held"] = true |
| 92 | } |
| 93 | // The channel's supply-chain bake window (bake.go), so the console can |
| 94 | // explain why a fresh release hasn't auto-installed yet. The instance |
| 95 | // can't see the box's bake clock — only the policy — so this is an |
| 96 | // "up to N hours" statement, never a countdown. |
| 97 | if h := bakeHours(s.db.updateChannel()); h > 0 { |
| 98 | info["bake_hours"] = h |
| 99 | } |
| 100 | } |
| 101 | return info |
| 102 | } |
| 103 | |