Files
DankMaterialShell/core/internal/server/sysupdate/shell.go
T
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

189 lines
4.8 KiB
Go

package sysupdate
import (
"context"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
const ownerQueryTimeout = 10 * time.Second
var dmsPackageNames = map[string]bool{
"dms": true, "dms-git": true,
"dms-shell": true, "dms-shell-git": true, "dms-shell-bin": true,
}
// Arch/Debian/SUSE: 1.7.0+git4915.56234761; Fedora COPR: 0.0.git.4915.56234761
var gitBuildRe = regexp.MustCompile(`\+git(\d+)\.|\.git\.(\d+)\.`)
func GitBuildCount(version string) int {
m := gitBuildRe.FindStringSubmatch(version)
if m == nil {
return 0
}
for _, s := range m[1:] {
if s != "" {
n, _ := strconv.Atoi(s)
return n
}
}
return 0
}
// "/usr/bin/dms (deleted)" after a package upgrade.
func dmsBinaryPath() string {
path, err := os.Executable()
if err != nil {
path, err = exec.LookPath("dms")
if err != nil {
return ""
}
}
path = strings.TrimSuffix(path, " (deleted)")
if resolved, err := filepath.EvalSymlinks(path); err == nil {
return resolved
}
return path
}
// Package managers unlink+create, so a different inode than /proc/self/exe means a new build.
func binaryReplaced(path string) bool {
if path == "" {
return false
}
running, err := os.Stat("/proc/self/exe")
if err != nil {
return false
}
onDisk, err := os.Stat(path)
if err != nil {
return false
}
return !os.SameFile(running, onDisk)
}
func unprobedShell(running string) ShellInfo {
info := ShellInfo{InstallMethod: InstallUnknown, Running: running, GitBuild: GitBuildCount(running)}
info.Channel = detectChannel(info, running)
return info
}
// Unowned binaries (manual copy, dev checkout) stay unknown: DMS only updates through packages.
func ProbeShell(ctx context.Context, running string) ShellInfo {
info := unprobedShell(running)
path := dmsBinaryPath()
if strings.HasPrefix(path, "/nix/store/") {
info.InstallMethod = InstallNix
} else if method, name, version := packageOwner(ctx, path); method != "" {
info.InstallMethod = method
info.PackageName = name
info.Installed = version
}
info.Channel = detectChannel(info, running)
info.RestartPending = binaryReplaced(path)
return info
}
func detectChannel(info ShellInfo, running string) Channel {
switch {
case strings.HasSuffix(info.PackageName, "-git"), GitBuildCount(running) > 0:
return ChannelGit
case info.InstallMethod == InstallUnknown, running == "", running == "dev":
return ChannelUnknown
default:
return ChannelStable
}
}
// Only one query runs on a normal system, but rpm can take a moment to open its database.
func packageOwner(ctx context.Context, path string) (InstallMethod, string, string) {
if path == "" {
return "", "", ""
}
ctx, cancel := context.WithTimeout(ctx, ownerQueryTimeout)
defer cancel()
switch {
case commandExists("pacman"):
// "/usr/bin/dms is owned by dms-shell-git 1.7.0+git4915-1"
out := runOutput(ctx, "pacman", "-Qo", path)
fields := strings.Fields(out)
if len(fields) < 2 {
return "", "", ""
}
return InstallPacman, fields[len(fields)-2], fields[len(fields)-1]
case commandExists("rpm"):
out := runOutput(ctx, "rpm", "-qf", "--qf", `%{NAME}\t%{VERSION}-%{RELEASE}`, path)
name, version, _ := strings.Cut(strings.TrimSpace(out), "\t")
if name == "" || strings.Contains(name, "not owned") {
return "", "", ""
}
return InstallRPM, name, version
case commandExists("dpkg-query"):
// "dms: /usr/bin/dms"
out := runOutput(ctx, "dpkg-query", "-S", path)
name, _, ok := strings.Cut(out, ":")
if !ok || name == "" {
return "", "", ""
}
name = strings.TrimSpace(name)
version := strings.TrimSpace(runOutput(ctx, "dpkg-query", "-W", "-f=${Version}", name))
return InstallDpkg, name, version
case commandExists("xbps-query"):
// "dms-1.7_1: /usr/bin/dms (regular file)"
out := runOutput(ctx, "xbps-query", "-o", path)
pkgver, _, found := strings.Cut(out, ":")
name, version, ok := splitPkgver(pkgver)
if !found || !ok {
return "", "", ""
}
return InstallXbps, name, version
}
return "", "", ""
}
func runOutput(ctx context.Context, name string, args ...string) string {
out, err := exec.CommandContext(ctx, name, args...).Output()
if err != nil {
log.Debugf("[sysupdate] %s %v: %v", name, args, err)
return ""
}
return string(out)
}
// Returns a copy: the packages slice is reused in place after upgrades.
func findShellPackage(pkgs []Package, ownerName string) *Package {
var fallback *Package
for i := range pkgs {
p := pkgs[i]
if ownerName != "" && p.Name == ownerName {
return &p
}
if fallback == nil && dmsPackageNames[p.Name] {
fallback = &p
}
}
return fallback
}
func commitsBehind(shell ShellInfo, master *MasterInfo) *int {
if master == nil || shell.Channel != ChannelGit {
return nil
}
n := GitBuildCount(shell.Running)
if n == 0 {
return nil
}
behind := max(master.CommitCount-n, 0)
return &behind
}