// Package system checks the host OS for pending package updates. It never // installs or upgrades anything - read-only status only, using the // existing apt package index (no "apt update" is run automatically, since // that touches network/package state on every page load). package system import ( "bufio" "context" "os" "os/exec" "strings" "time" ) // UpdateStatus summarizes pending OS package updates on a Debian/Ubuntu host. type UpdateStatus struct { Supported bool `json:"supported"` // false if apt isn't available (e.g. non-Debian host) UpgradableCount int `json:"upgradable_count"` Packages []string `json:"packages"` RebootRequired bool `json:"reboot_required"` CheckedAt time.Time `json:"checked_at"` Error string `json:"error,omitempty"` } // CheckAptUpdates lists upgradable packages from the current apt index // (`apt list --upgradable`) without refreshing it, and checks for the // standard Debian/Ubuntu reboot-required marker file. func CheckAptUpdates() UpdateStatus { status := UpdateStatus{CheckedAt: time.Now().UTC()} if _, err := exec.LookPath("apt"); err != nil { status.Supported = false return status } status.Supported = true ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "apt", "list", "--upgradable") out, err := cmd.Output() if err != nil { status.Error = "cannot list upgradable packages: " + err.Error() return status } scanner := bufio.NewScanner(strings.NewReader(string(out))) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "Listing...") { continue } status.Packages = append(status.Packages, line) } status.UpgradableCount = len(status.Packages) if _, err := os.Stat("/var/run/reboot-required"); err == nil { status.RebootRequired = true } return status }