mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-12 23:32:50 -04:00
refactor(sysupdate): Streamline DMS Updater command handling
This commit is contained in:
@@ -100,29 +100,13 @@ func runSystemUpdateCheck() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stopSpin := startSpinner("Checking for updates… ")
|
stopSpin := startSpinner("Checking for updates… ")
|
||||||
|
allPkgs, firstErr := collectUpdates(ctx, backends)
|
||||||
type backendResult struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Display string `json:"displayName"`
|
|
||||||
Packages []sysupdate.Package `json:"packages"`
|
|
||||||
}
|
|
||||||
var results []backendResult
|
|
||||||
var allPkgs []sysupdate.Package
|
|
||||||
var firstErr error
|
|
||||||
|
|
||||||
for _, b := range backends {
|
|
||||||
pkgs, err := b.CheckUpdates(ctx)
|
|
||||||
if err != nil && firstErr == nil {
|
|
||||||
firstErr = fmt.Errorf("%s: %w", b.ID(), err)
|
|
||||||
}
|
|
||||||
results = append(results, backendResult{ID: b.ID(), Display: b.DisplayName(), Packages: pkgs})
|
|
||||||
allPkgs = append(allPkgs, pkgs...)
|
|
||||||
}
|
|
||||||
stopSpin()
|
stopSpin()
|
||||||
|
allPkgs = filterUpdateTargets(allPkgs)
|
||||||
|
|
||||||
if sysUpdateJSON {
|
if sysUpdateJSON {
|
||||||
out, _ := json.MarshalIndent(map[string]any{
|
out, _ := json.MarshalIndent(map[string]any{
|
||||||
"backends": results,
|
"backends": backendResults(backends, allPkgs),
|
||||||
"packages": allPkgs,
|
"packages": allPkgs,
|
||||||
"error": errOrEmpty(firstErr),
|
"error": errOrEmpty(firstErr),
|
||||||
"count": len(allPkgs),
|
"count": len(allPkgs),
|
||||||
@@ -145,6 +129,26 @@ func runSystemUpdateCheck() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type backendResult struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Display string `json:"displayName"`
|
||||||
|
Packages []sysupdate.Package `json:"packages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func backendResults(backends []sysupdate.Backend, pkgs []sysupdate.Package) []backendResult {
|
||||||
|
results := make([]backendResult, 0, len(backends))
|
||||||
|
for _, b := range backends {
|
||||||
|
var backendPkgs []sysupdate.Package
|
||||||
|
for _, p := range pkgs {
|
||||||
|
if sysupdate.BackendHasTargets(b, []sysupdate.Package{p}, true, true) {
|
||||||
|
backendPkgs = append(backendPkgs, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results = append(results, backendResult{ID: b.ID(), Display: b.DisplayName(), Packages: backendPkgs})
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
func runSystemUpdateApply() {
|
func runSystemUpdateApply() {
|
||||||
checkCtx, checkCancel := context.WithTimeout(context.Background(), sysUpdateListPmTime)
|
checkCtx, checkCancel := context.WithTimeout(context.Background(), sysUpdateListPmTime)
|
||||||
defer checkCancel()
|
defer checkCancel()
|
||||||
@@ -157,6 +161,7 @@ func runSystemUpdateApply() {
|
|||||||
stopSpin := startSpinner("Checking for updates…")
|
stopSpin := startSpinner("Checking for updates…")
|
||||||
pkgs, firstErr := collectUpdates(checkCtx, backends)
|
pkgs, firstErr := collectUpdates(checkCtx, backends)
|
||||||
stopSpin()
|
stopSpin()
|
||||||
|
pkgs = filterUpdateTargets(pkgs)
|
||||||
if firstErr != nil {
|
if firstErr != nil {
|
||||||
fmt.Printf("Warning: %v\n\n", firstErr)
|
fmt.Printf("Warning: %v\n\n", firstErr)
|
||||||
}
|
}
|
||||||
@@ -190,14 +195,24 @@ func runSystemUpdateApply() {
|
|||||||
DryRun: sysUpdateDry,
|
DryRun: sysUpdateDry,
|
||||||
UseSudo: true,
|
UseSudo: true,
|
||||||
}
|
}
|
||||||
|
opts.AttachStdio = sysupdate.UpgradeNeedsPrivilege(backends, pkgs, opts)
|
||||||
|
|
||||||
onLine := func(line string) { fmt.Println(line) }
|
onLine := func(line string) { fmt.Println(line) }
|
||||||
|
ran := false
|
||||||
for _, b := range backends {
|
for _, b := range backends {
|
||||||
|
if !sysupdate.BackendHasTargets(b, pkgs, opts.IncludeAUR, opts.IncludeFlatpak) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ran = true
|
||||||
fmt.Printf("\n== %s ==\n", b.DisplayName())
|
fmt.Printf("\n== %s ==\n", b.DisplayName())
|
||||||
if err := b.Upgrade(ctx, opts, onLine); err != nil {
|
if err := b.Upgrade(ctx, opts, onLine); err != nil {
|
||||||
log.Fatalf("%s upgrade failed: %v", b.ID(), err)
|
log.Fatalf("%s upgrade failed: %v", b.ID(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !ran {
|
||||||
|
fmt.Println("Nothing to upgrade.")
|
||||||
|
return
|
||||||
|
}
|
||||||
if sysUpdateDry {
|
if sysUpdateDry {
|
||||||
fmt.Println("\nDry run complete (no changes applied).")
|
fmt.Println("\nDry run complete (no changes applied).")
|
||||||
return
|
return
|
||||||
@@ -218,6 +233,20 @@ func collectUpdates(ctx context.Context, backends []sysupdate.Backend) ([]sysupd
|
|||||||
return all, firstErr
|
return all, firstErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func filterUpdateTargets(pkgs []sysupdate.Package) []sysupdate.Package {
|
||||||
|
if !sysUpdateNoAUR {
|
||||||
|
return pkgs
|
||||||
|
}
|
||||||
|
out := pkgs[:0]
|
||||||
|
for _, p := range pkgs {
|
||||||
|
if p.Repo == sysupdate.RepoAUR {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, p)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func runSystemUpdateSetInterval(seconds int) {
|
func runSystemUpdateSetInterval(seconds int) {
|
||||||
resp, err := sendServerRequest(models.Request{
|
resp, err := sendServerRequest(models.Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
|
|||||||
@@ -45,13 +45,14 @@ func (aptBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(
|
|||||||
OnLine: onLine,
|
OnLine: onLine,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
names := pickTargetNames(opts.Targets, "apt", true)
|
if !BackendHasTargets(aptBackend{}, opts.Targets, opts.IncludeAUR, opts.IncludeFlatpak) {
|
||||||
if len(names) == 0 {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
privesc := privescBin(opts.UseSudo)
|
return Run(ctx, aptUpgradeArgv(bin, opts), RunOptions{OnLine: onLine, AttachStdio: opts.AttachStdio})
|
||||||
argv := append([]string{privesc, "env", "DEBIAN_FRONTEND=noninteractive", "LC_ALL=C", bin, "install", "-y", "--only-upgrade"}, names...)
|
}
|
||||||
return Run(ctx, argv, RunOptions{OnLine: onLine})
|
|
||||||
|
func aptUpgradeArgv(bin string, opts UpgradeOptions) []string {
|
||||||
|
return privilegedArgv(opts, "env", "DEBIAN_FRONTEND=noninteractive", "LC_ALL=C", bin, "upgrade", "-y")
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseAptUpgradable(text string) []Package {
|
func parseAptUpgradable(text string) []Package {
|
||||||
|
|||||||
@@ -45,27 +45,37 @@ func (b dnfBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine fun
|
|||||||
if opts.DryRun {
|
if opts.DryRun {
|
||||||
return Run(ctx, []string{b.bin, "upgrade", "--assumeno"}, RunOptions{OnLine: onLine})
|
return Run(ctx, []string{b.bin, "upgrade", "--assumeno"}, RunOptions{OnLine: onLine})
|
||||||
}
|
}
|
||||||
names := pickTargetNames(opts.Targets, b.bin, true)
|
if !BackendHasTargets(b, opts.Targets, opts.IncludeAUR, opts.IncludeFlatpak) {
|
||||||
if len(names) == 0 {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
privesc := privescBin(opts.UseSudo)
|
return Run(ctx, dnfUpgradeArgv(b.bin, opts), RunOptions{OnLine: onLine, AttachStdio: opts.AttachStdio})
|
||||||
argv := append([]string{privesc, b.bin, "upgrade", "-y"}, names...)
|
}
|
||||||
return Run(ctx, argv, RunOptions{OnLine: onLine})
|
|
||||||
|
func dnfUpgradeArgv(bin string, opts UpgradeOptions) []string {
|
||||||
|
return privilegedArgv(opts, bin, "upgrade", "--refresh", "-y")
|
||||||
}
|
}
|
||||||
|
|
||||||
func dnfListUpgrades(ctx context.Context, bin string) (string, error) {
|
func dnfListUpgrades(ctx context.Context, bin string) (string, error) {
|
||||||
cmd := exec.CommandContext(ctx, bin, "list", "--upgrades", "--refresh", "--quiet")
|
argv := dnfCheckUpdatesArgv(bin)
|
||||||
out, err := cmd.Output()
|
cmd := exec.CommandContext(ctx, argv[0], argv[1:]...)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return string(out), nil
|
return string(out), nil
|
||||||
}
|
}
|
||||||
if exitErr, ok := errors.AsType[*exec.ExitError](err); ok && exitErr.ExitCode() == 1 {
|
if exitErr, ok := errors.AsType[*exec.ExitError](err); ok && exitErr.ExitCode() == 100 {
|
||||||
return "", nil
|
return string(out), nil
|
||||||
}
|
}
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dnfCheckUpdatesArgv(bin string) []string {
|
||||||
|
subcommand := "check-update"
|
||||||
|
if bin == "dnf5" {
|
||||||
|
subcommand = "check-upgrade"
|
||||||
|
}
|
||||||
|
return []string{bin, subcommand, "--refresh", "--quiet"}
|
||||||
|
}
|
||||||
|
|
||||||
func rpmInstalledVersions(ctx context.Context) map[string]string {
|
func rpmInstalledVersions(ctx context.Context) map[string]string {
|
||||||
out, err := exec.CommandContext(ctx, "rpm", "-qa", "--qf", `%{NAME}\t%{VERSION}-%{RELEASE}\n`).Output()
|
out, err := exec.CommandContext(ctx, "rpm", "-qa", "--qf", `%{NAME}\t%{VERSION}-%{RELEASE}\n`).Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -67,6 +67,21 @@ bash.x86_64 5.2.40-1.fc41 updates`,
|
|||||||
{Name: "bash.x86_64", Repo: RepoSystem, Backend: "dnf", FromVersion: "", ToVersion: "5.2.40-1.fc41"},
|
{Name: "bash.x86_64", Repo: RepoSystem, Backend: "dnf", FromVersion: "", ToVersion: "5.2.40-1.fc41"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "skips dnf warning lines while keeping package rows",
|
||||||
|
input: `Failed to expire repository cache in path "/home/user/.cache/libdnf5/updates": cannot open file
|
||||||
|
example-driver.x86_64 2:9.8.7-1.fc99 updates
|
||||||
|
example-tool.noarch 1.2.3^45.gitabcdef-1.fc99 copr`,
|
||||||
|
backendID: "dnf5",
|
||||||
|
installed: map[string]string{
|
||||||
|
"example-driver": "2:9.8.6-1.fc99",
|
||||||
|
"example-tool": "1.2.2^44.gitabcdef-1.fc99",
|
||||||
|
},
|
||||||
|
want: []Package{
|
||||||
|
{Name: "example-driver.x86_64", Repo: RepoSystem, Backend: "dnf5", FromVersion: "2:9.8.6-1.fc99", ToVersion: "2:9.8.7-1.fc99"},
|
||||||
|
{Name: "example-tool.noarch", Repo: RepoSystem, Backend: "dnf5", FromVersion: "1.2.2^44.gitabcdef-1.fc99", ToVersion: "1.2.3^45.gitabcdef-1.fc99"},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -78,3 +93,22 @@ bash.x86_64 5.2.40-1.fc41 updates`,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDnfCheckUpdatesArgv(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
bin string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{bin: "dnf5", want: []string{"dnf5", "check-upgrade", "--refresh", "--quiet"}},
|
||||||
|
{bin: "dnf", want: []string{"dnf", "check-update", "--refresh", "--quiet"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.bin, func(t *testing.T) {
|
||||||
|
got := dnfCheckUpdatesArgv(tt.bin)
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Fatalf("dnfCheckUpdatesArgv(%q) = %#v, want %#v", tt.bin, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -73,27 +73,14 @@ func (flatpakBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine f
|
|||||||
if opts.DryRun {
|
if opts.DryRun {
|
||||||
return Run(ctx, []string{"flatpak", "update", "--no-deploy", "-y"}, RunOptions{OnLine: onLine})
|
return Run(ctx, []string{"flatpak", "update", "--no-deploy", "-y"}, RunOptions{OnLine: onLine})
|
||||||
}
|
}
|
||||||
refs := flatpakTargetRefs(opts.Targets)
|
if !BackendHasTargets(flatpakBackend{}, opts.Targets, opts.IncludeAUR, opts.IncludeFlatpak) {
|
||||||
if len(refs) == 0 {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
argv := append([]string{"flatpak", "update", "-y", "--noninteractive"}, refs...)
|
return Run(ctx, flatpakUpgradeArgv(), RunOptions{OnLine: onLine})
|
||||||
return Run(ctx, argv, RunOptions{OnLine: onLine})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func flatpakTargetRefs(targets []Package) []string {
|
func flatpakUpgradeArgv() []string {
|
||||||
out := make([]string, 0, len(targets))
|
return []string{"flatpak", "update", "-y", "--noninteractive"}
|
||||||
for _, p := range targets {
|
|
||||||
if p.Backend != "flatpak" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ref := p.Ref
|
|
||||||
if ref == "" {
|
|
||||||
ref = p.Name
|
|
||||||
}
|
|
||||||
out = append(out, ref)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFlatpakUpdates(text string, installed map[string]flatpakInstalledEntry) []Package {
|
func parseFlatpakUpdates(text string, installed map[string]flatpakInstalledEntry) []Package {
|
||||||
|
|||||||
@@ -43,13 +43,14 @@ func (b pacmanBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine
|
|||||||
if opts.DryRun {
|
if opts.DryRun {
|
||||||
return Run(ctx, []string{"pacman", "-Sup"}, RunOptions{OnLine: onLine})
|
return Run(ctx, []string{"pacman", "-Sup"}, RunOptions{OnLine: onLine})
|
||||||
}
|
}
|
||||||
names := pickTargetNames(opts.Targets, b.ID(), opts.IncludeAUR)
|
if !BackendHasTargets(b, opts.Targets, opts.IncludeAUR, opts.IncludeFlatpak) {
|
||||||
if len(names) == 0 {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
privesc := privescBin(opts.UseSudo)
|
return Run(ctx, pacmanUpgradeArgv(opts), RunOptions{OnLine: onLine, AttachStdio: opts.AttachStdio})
|
||||||
argv := append([]string{privesc, "pacman", "-Sy", "--noconfirm", "--needed"}, names...)
|
}
|
||||||
return Run(ctx, argv, RunOptions{OnLine: onLine})
|
|
||||||
|
func pacmanUpgradeArgv(opts UpgradeOptions) []string {
|
||||||
|
return privilegedArgv(opts, "pacman", "-Syu", "--noconfirm", "--needed")
|
||||||
}
|
}
|
||||||
|
|
||||||
type archHelperBackend struct {
|
type archHelperBackend struct {
|
||||||
@@ -94,35 +95,28 @@ func (b archHelperBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onL
|
|||||||
if opts.DryRun {
|
if opts.DryRun {
|
||||||
return Run(ctx, []string{b.id, "-Sup"}, RunOptions{OnLine: onLine})
|
return Run(ctx, []string{b.id, "-Sup"}, RunOptions{OnLine: onLine})
|
||||||
}
|
}
|
||||||
names := pickTargetNames(opts.Targets, b.id, opts.IncludeAUR)
|
if !BackendHasTargets(b, opts.Targets, opts.IncludeAUR, opts.IncludeFlatpak) {
|
||||||
if len(names) == 0 {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if os.Getenv("DMS_FORCE_PKEXEC") == "1" {
|
if os.Getenv("DMS_FORCE_PKEXEC") == "1" {
|
||||||
argv := append([]string{"pkexec", b.id, "-Sy", "--noconfirm", "--needed"}, names...)
|
argv := append([]string{"pkexec"}, archHelperUpgradeArgv(b.id, opts.IncludeAUR)...)
|
||||||
return Run(ctx, argv, RunOptions{OnLine: onLine})
|
return Run(ctx, argv, RunOptions{OnLine: onLine, AttachStdio: opts.AttachStdio})
|
||||||
}
|
}
|
||||||
term := findTerminal(opts.Terminal)
|
term := findTerminal(opts.Terminal)
|
||||||
if term == "" {
|
if term == "" {
|
||||||
return fmt.Errorf("no terminal found (pick one in DMS settings, set $TERMINAL, or install kitty/ghostty/foot/alacritty)")
|
return fmt.Errorf("no terminal found (pick one in DMS settings, set $TERMINAL, or install kitty/ghostty/foot/alacritty)")
|
||||||
}
|
}
|
||||||
cmd := fmt.Sprintf("%s -Sy --noconfirm --needed %s", b.id, strings.Join(names, " "))
|
cmd := strings.Join(archHelperUpgradeArgv(b.id, opts.IncludeAUR), " ")
|
||||||
title := fmt.Sprintf("DMS — System Update (%s)", b.id)
|
title := fmt.Sprintf("DMS — System Update (%s)", b.id)
|
||||||
return Run(ctx, wrapInTerminal(term, title, cmd), RunOptions{OnLine: onLine})
|
return Run(ctx, wrapInTerminal(term, title, cmd), RunOptions{OnLine: onLine})
|
||||||
}
|
}
|
||||||
|
|
||||||
func pickTargetNames(targets []Package, backendID string, includeAUR bool) []string {
|
func archHelperUpgradeArgv(id string, includeAUR bool) []string {
|
||||||
out := make([]string, 0, len(targets))
|
argv := []string{id, "-Syu", "--noconfirm", "--needed"}
|
||||||
for _, p := range targets {
|
if !includeAUR {
|
||||||
if p.Backend != backendID {
|
argv = append(argv, "--repo")
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !includeAUR && p.Repo == RepoAUR {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, p.Name)
|
|
||||||
}
|
}
|
||||||
return out
|
return argv
|
||||||
}
|
}
|
||||||
|
|
||||||
func pacmanRepoUpdates(ctx context.Context) (string, error) {
|
func pacmanRepoUpdates(ctx context.Context) (string, error) {
|
||||||
|
|||||||
@@ -117,9 +117,16 @@ func bootedDeployment(deps []ostreeDeployment) *ostreeDeployment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rpmOstreeBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error {
|
func (rpmOstreeBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error {
|
||||||
|
if !BackendHasTargets(rpmOstreeBackend{}, opts.Targets, opts.IncludeAUR, opts.IncludeFlatpak) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return Run(ctx, rpmOstreeUpgradeArgv(opts), RunOptions{OnLine: onLine, AttachStdio: opts.AttachStdio})
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpmOstreeUpgradeArgv(opts UpgradeOptions) []string {
|
||||||
argv := []string{"rpm-ostree", "upgrade"}
|
argv := []string{"rpm-ostree", "upgrade"}
|
||||||
if opts.DryRun {
|
if opts.DryRun {
|
||||||
argv = append(argv, "--check")
|
argv = append(argv, "--check")
|
||||||
}
|
}
|
||||||
return Run(ctx, argv, RunOptions{OnLine: onLine})
|
return argv
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,11 +74,12 @@ func (zypperBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine fu
|
|||||||
if opts.DryRun {
|
if opts.DryRun {
|
||||||
return Run(ctx, []string{"zypper", "--non-interactive", "--dry-run", "update"}, RunOptions{OnLine: onLine})
|
return Run(ctx, []string{"zypper", "--non-interactive", "--dry-run", "update"}, RunOptions{OnLine: onLine})
|
||||||
}
|
}
|
||||||
names := pickTargetNames(opts.Targets, "zypper", true)
|
if !BackendHasTargets(zypperBackend{}, opts.Targets, opts.IncludeAUR, opts.IncludeFlatpak) {
|
||||||
if len(names) == 0 {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
privesc := privescBin(opts.UseSudo)
|
return Run(ctx, zypperUpgradeArgv(opts), RunOptions{OnLine: onLine, AttachStdio: opts.AttachStdio})
|
||||||
argv := append([]string{privesc, "zypper", "--non-interactive", "update"}, names...)
|
}
|
||||||
return Run(ctx, argv, RunOptions{OnLine: onLine})
|
|
||||||
|
func zypperUpgradeArgv(opts UpgradeOptions) []string {
|
||||||
|
return privilegedArgv(opts, "zypper", "--non-interactive", "update")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type RunOptions struct {
|
type RunOptions struct {
|
||||||
Env []string
|
Env []string
|
||||||
OnLine func(string)
|
OnLine func(string)
|
||||||
|
AttachStdio bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run(ctx context.Context, argv []string, opts RunOptions) error {
|
func Run(ctx context.Context, argv []string, opts RunOptions) error {
|
||||||
@@ -27,6 +28,19 @@ func Run(ctx context.Context, argv []string, opts RunOptions) error {
|
|||||||
if len(opts.Env) > 0 {
|
if len(opts.Env) > 0 {
|
||||||
cmd.Env = append(cmd.Environ(), opts.Env...)
|
cmd.Env = append(cmd.Environ(), opts.Env...)
|
||||||
}
|
}
|
||||||
|
if opts.AttachStdio {
|
||||||
|
cmd.Cancel = func() error {
|
||||||
|
if cmd.Process == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return cmd.Process.Kill()
|
||||||
|
}
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
cmd.Cancel = func() error {
|
cmd.Cancel = func() error {
|
||||||
if cmd.Process == nil {
|
if cmd.Process == nil {
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultIntervalSeconds = 30 * 60
|
defaultIntervalSeconds = 30 * 60
|
||||||
minIntervalSeconds = 5 * 60
|
minIntervalSeconds = 5 * 60
|
||||||
recentLogCapacity = 200
|
recentLogCapacity = 200
|
||||||
checkTimeout = 5 * time.Minute
|
checkTimeout = 5 * time.Minute
|
||||||
upgradeTimeout = 30 * time.Minute
|
upgradeTimeout = 30 * time.Minute
|
||||||
|
postUpgradeCompleteDelay = 3 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
@@ -310,18 +311,18 @@ func (m *Manager) runUpgrade(ctx context.Context, opts UpgradeOptions) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
backends := upgradeBackends(m.selection, opts)
|
|
||||||
if len(backends) == 0 {
|
|
||||||
m.setError(ErrCodeNoBackend, "no backend selected for upgrade")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(opts.Targets) == 0 {
|
if len(opts.Targets) == 0 {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
opts.Targets = append([]Package(nil), m.state.Packages...)
|
opts.Targets = append([]Package(nil), m.state.Packages...)
|
||||||
m.mu.RUnlock()
|
m.mu.RUnlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
backends := upgradeBackends(m.selection, opts)
|
||||||
|
if len(backends) == 0 {
|
||||||
|
m.setError(ErrCodeNoBackend, "no backend selected for upgrade")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
opID := fmt.Sprintf("op-%d", time.Now().UnixNano())
|
opID := fmt.Sprintf("op-%d", time.Now().UnixNano())
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
m.state.Phase = PhaseUpgrading
|
m.state.Phase = PhaseUpgrading
|
||||||
@@ -351,13 +352,7 @@ func (m *Manager) runUpgrade(ctx context.Context, opts UpgradeOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.mu.Lock()
|
m.finishSuccessfulUpgrade(true)
|
||||||
m.state.Phase = PhaseIdle
|
|
||||||
m.state.OperationID = ""
|
|
||||||
m.state.OperationStarted = 0
|
|
||||||
m.mu.Unlock()
|
|
||||||
m.markDirty()
|
|
||||||
go m.runRefresh(context.Background())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) runCustomUpgrade(ctx context.Context, command, terminalOverride string) {
|
func (m *Manager) runCustomUpgrade(ctx context.Context, command, terminalOverride string) {
|
||||||
@@ -395,10 +390,29 @@ func (m *Manager) runCustomUpgrade(ctx context.Context, command, terminalOverrid
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.finishSuccessfulUpgrade(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) finishSuccessfulUpgrade(clearPackages bool) {
|
||||||
|
m.appendLog("Upgrade complete.")
|
||||||
|
|
||||||
|
timer := time.NewTimer(postUpgradeCompleteDelay)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-m.stopChan:
|
||||||
|
return
|
||||||
|
case <-timer.C:
|
||||||
|
}
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
m.state.Phase = PhaseIdle
|
m.state.Phase = PhaseIdle
|
||||||
m.state.OperationID = ""
|
m.state.OperationID = ""
|
||||||
m.state.OperationStarted = 0
|
m.state.OperationStarted = 0
|
||||||
|
if clearPackages {
|
||||||
|
m.state.Packages = m.state.Packages[:0]
|
||||||
|
m.state.Count = 0
|
||||||
|
}
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
m.markDirty()
|
m.markDirty()
|
||||||
go m.runRefresh(context.Background())
|
go m.runRefresh(context.Background())
|
||||||
@@ -407,18 +421,25 @@ func (m *Manager) runCustomUpgrade(ctx context.Context, command, terminalOverrid
|
|||||||
func upgradeBackends(sel Selection, opts UpgradeOptions) []Backend {
|
func upgradeBackends(sel Selection, opts UpgradeOptions) []Backend {
|
||||||
var out []Backend
|
var out []Backend
|
||||||
if sel.System != nil {
|
if sel.System != nil {
|
||||||
out = append(out, sel.System)
|
out = appendUpgradeBackend(out, sel.System, opts)
|
||||||
}
|
}
|
||||||
for _, b := range sel.Overlay {
|
for _, b := range sel.Overlay {
|
||||||
switch {
|
switch {
|
||||||
case b.Repo() == RepoFlatpak && !opts.IncludeFlatpak:
|
case b.Repo() == RepoFlatpak && !opts.IncludeFlatpak:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out = append(out, b)
|
out = appendUpgradeBackend(out, b, opts)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func appendUpgradeBackend(out []Backend, b Backend, opts UpgradeOptions) []Backend {
|
||||||
|
if !BackendHasTargets(b, opts.Targets, opts.IncludeAUR, opts.IncludeFlatpak) {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
return append(out, b)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) appendLog(line string) {
|
func (m *Manager) appendLog(line string) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
if cap(m.state.RecentLog) == 0 {
|
if cap(m.state.RecentLog) == 0 {
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package sysupdate
|
||||||
|
|
||||||
|
func BackendHasTargets(b Backend, targets []Package, includeAUR, includeFlatpak bool) bool {
|
||||||
|
if b == nil || len(targets) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
id := b.ID()
|
||||||
|
repo := b.Repo()
|
||||||
|
for _, p := range targets {
|
||||||
|
switch p.Repo {
|
||||||
|
case RepoFlatpak:
|
||||||
|
if !includeFlatpak {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case RepoAUR:
|
||||||
|
if !includeAUR {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch repo {
|
||||||
|
case RepoFlatpak:
|
||||||
|
if p.Repo == RepoFlatpak || p.Backend == id {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case RepoOSTree:
|
||||||
|
if p.Repo == RepoOSTree || p.Backend == id {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if p.Backend == id {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpgradeNeedsPrivilege(backends []Backend, targets []Package, opts UpgradeOptions) bool {
|
||||||
|
if opts.DryRun {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, b := range backends {
|
||||||
|
if b == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if b.NeedsAuth() && BackendHasTargets(b, targets, opts.IncludeAUR, opts.IncludeFlatpak) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func privilegedArgv(opts UpgradeOptions, argv ...string) []string {
|
||||||
|
privesc := privescBin(opts.UseSudo)
|
||||||
|
out := make([]string, 0, len(argv)+1)
|
||||||
|
out = append(out, privesc)
|
||||||
|
out = append(out, argv...)
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -77,6 +77,7 @@ type UpgradeOptions struct {
|
|||||||
IncludeAUR bool
|
IncludeAUR bool
|
||||||
DryRun bool
|
DryRun bool
|
||||||
UseSudo bool
|
UseSudo bool
|
||||||
|
AttachStdio bool
|
||||||
CustomCommand string
|
CustomCommand string
|
||||||
Terminal string
|
Terminal string
|
||||||
Targets []Package
|
Targets []Package
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package sysupdate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUpgradeCommandBuilders(t *testing.T) {
|
||||||
|
pkexecOpts := UpgradeOptions{UseSudo: false}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
got []string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "dnf full upgrade",
|
||||||
|
got: dnfUpgradeArgv("dnf5", pkexecOpts),
|
||||||
|
want: []string{"pkexec", "dnf5", "upgrade", "--refresh", "-y"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "apt full upgrade",
|
||||||
|
got: aptUpgradeArgv("apt-get", pkexecOpts),
|
||||||
|
want: []string{"pkexec", "env", "DEBIAN_FRONTEND=noninteractive", "LC_ALL=C", "apt-get", "upgrade", "-y"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zypper full update",
|
||||||
|
got: zypperUpgradeArgv(pkexecOpts),
|
||||||
|
want: []string{"pkexec", "zypper", "--non-interactive", "update"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pacman full sync upgrade",
|
||||||
|
got: pacmanUpgradeArgv(pkexecOpts),
|
||||||
|
want: []string{"pkexec", "pacman", "-Syu", "--noconfirm", "--needed"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "aur helper full update with aur",
|
||||||
|
got: archHelperUpgradeArgv("paru", true),
|
||||||
|
want: []string{"paru", "-Syu", "--noconfirm", "--needed"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "aur helper repo-only full update",
|
||||||
|
got: archHelperUpgradeArgv("yay", false),
|
||||||
|
want: []string{"yay", "-Syu", "--noconfirm", "--needed", "--repo"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "flatpak full update",
|
||||||
|
got: flatpakUpgradeArgv(),
|
||||||
|
want: []string{"flatpak", "update", "-y", "--noninteractive"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rpm-ostree upgrade",
|
||||||
|
got: rpmOstreeUpgradeArgv(UpgradeOptions{}),
|
||||||
|
want: []string{"rpm-ostree", "upgrade"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rpm-ostree check",
|
||||||
|
got: rpmOstreeUpgradeArgv(UpgradeOptions{DryRun: true}),
|
||||||
|
want: []string{"rpm-ostree", "upgrade", "--check"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if !reflect.DeepEqual(tt.got, tt.want) {
|
||||||
|
t.Fatalf("argv = %#v, want %#v", tt.got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackendHasTargetsRespectsBackendAndOptions(t *testing.T) {
|
||||||
|
targets := []Package{
|
||||||
|
{Name: "bash.x86_64", Repo: RepoSystem, Backend: "dnf5"},
|
||||||
|
{Name: "google-chrome", Repo: RepoAUR, Backend: "paru"},
|
||||||
|
{Name: "Discord", Repo: RepoFlatpak, Backend: "flatpak"},
|
||||||
|
{Name: "silverblue", Repo: RepoOSTree, Backend: "rpm-ostree"},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !BackendHasTargets(dnfBackend{bin: "dnf5"}, targets, true, true) {
|
||||||
|
t.Fatal("dnf5 target was not detected")
|
||||||
|
}
|
||||||
|
if BackendHasTargets(dnfBackend{bin: "dnf"}, targets, true, true) {
|
||||||
|
t.Fatal("dnf target should not match dnf5 package targets")
|
||||||
|
}
|
||||||
|
if !BackendHasTargets(archHelperBackend{id: "paru"}, targets, true, true) {
|
||||||
|
t.Fatal("AUR helper target was not detected")
|
||||||
|
}
|
||||||
|
if BackendHasTargets(archHelperBackend{id: "paru"}, targets, false, true) {
|
||||||
|
t.Fatal("AUR helper should not match AUR-only target when AUR is disabled")
|
||||||
|
}
|
||||||
|
if !BackendHasTargets(flatpakBackend{}, targets, true, true) {
|
||||||
|
t.Fatal("Flatpak target was not detected")
|
||||||
|
}
|
||||||
|
if BackendHasTargets(flatpakBackend{}, targets, true, false) {
|
||||||
|
t.Fatal("Flatpak target should not match when Flatpak is disabled")
|
||||||
|
}
|
||||||
|
if !BackendHasTargets(rpmOstreeBackend{}, targets, true, true) {
|
||||||
|
t.Fatal("rpm-ostree target was not detected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpgradeNeedsPrivilegeSkipsFlatpakOnly(t *testing.T) {
|
||||||
|
backends := []Backend{dnfBackend{bin: "dnf5"}, flatpakBackend{}}
|
||||||
|
opts := UpgradeOptions{IncludeAUR: true, IncludeFlatpak: true}
|
||||||
|
|
||||||
|
flatpakOnly := []Package{{Name: "Discord", Repo: RepoFlatpak, Backend: "flatpak"}}
|
||||||
|
if UpgradeNeedsPrivilege(backends, flatpakOnly, opts) {
|
||||||
|
t.Fatal("Flatpak-only updates should not need privileged auth")
|
||||||
|
}
|
||||||
|
|
||||||
|
mixed := []Package{
|
||||||
|
{Name: "bash.x86_64", Repo: RepoSystem, Backend: "dnf5"},
|
||||||
|
{Name: "Discord", Repo: RepoFlatpak, Backend: "flatpak"},
|
||||||
|
}
|
||||||
|
if !UpgradeNeedsPrivilege(backends, mixed, opts) {
|
||||||
|
t.Fatal("mixed system updates should need privileged auth")
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.DryRun = true
|
||||||
|
if UpgradeNeedsPrivilege(backends, mixed, opts) {
|
||||||
|
t.Fatal("dry-run updates should not need privileged auth")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpgradeBackendsFiltersFlatpakOnly(t *testing.T) {
|
||||||
|
sel := Selection{
|
||||||
|
System: dnfBackend{bin: "dnf5"},
|
||||||
|
Overlay: []Backend{flatpakBackend{}},
|
||||||
|
}
|
||||||
|
opts := UpgradeOptions{
|
||||||
|
IncludeAUR: true,
|
||||||
|
IncludeFlatpak: true,
|
||||||
|
Targets: []Package{{Name: "Discord", Repo: RepoFlatpak, Backend: "flatpak"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := upgradeBackends(sel, opts)
|
||||||
|
if len(got) != 1 || got[0].ID() != "flatpak" {
|
||||||
|
t.Fatalf("upgradeBackends(flatpak-only) = %#v, want only flatpak", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.Targets = append(opts.Targets, Package{Name: "bash.x86_64", Repo: RepoSystem, Backend: "dnf5"})
|
||||||
|
got = upgradeBackends(sel, opts)
|
||||||
|
if len(got) != 2 || got[0].ID() != "dnf5" || got[1].ID() != "flatpak" {
|
||||||
|
t.Fatalf("upgradeBackends(mixed) = %#v, want dnf5 then flatpak", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user