Files
Archduino/internal/privilege/sudo.go
MDietermann 8b43a0f273 fix: address final-review findings before closing Plan 1
- privilege: wire sudo -v to real stdin/stdout/stderr so it can prompt
- cmd: validate --only/--skip IDs against registry (prevents silent
  run-all on typo); gate install to Arch-only (spec requirement);
  mark --only and --skip mutually exclusive
- platform: Pacman.Update now runs -Syu --noconfirm, not -Sy (avoids
  Arch partial-upgrade anti-pattern)
- test/integration: trap-clean the named docker volume on any exit
- go.mod: tidy — yaml.v3 is a direct dep
2026-04-13 21:35:14 +02:00

83 lines
2.1 KiB
Go

// Package privilege manages sudo timestamp acquisition and keepalive so
// a long-running installer only prompts once.
package privilege
import (
"context"
"fmt"
"os"
"os/exec"
"time"
)
// Keepalive holds the lifetime of a background sudo-refresh goroutine.
type Keepalive struct {
cancel context.CancelFunc
done chan struct{}
}
// Stop terminates the keepalive goroutine. Safe to call multiple times.
func (k *Keepalive) Stop() {
if k == nil || k.cancel == nil {
return
}
k.cancel()
<-k.done
k.cancel = nil
}
// Acquirer abstracts the `sudo` binary so tests can inject a fake.
type Acquirer interface {
Validate(ctx context.Context) error // sudo -v (may prompt)
ValidateNonInteractive(ctx context.Context) error // sudo -n -v
}
// ExecSudo is the production Acquirer.
type ExecSudo struct{}
func (ExecSudo) Validate(ctx context.Context) error {
cmd := exec.CommandContext(ctx, "sudo", "-v")
// Connect to the real terminal so sudo can prompt for the password.
// exec.Cmd.Stdin defaults to /dev/null, not the parent process's stdin.
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func (ExecSudo) ValidateNonInteractive(ctx context.Context) error {
return exec.CommandContext(ctx, "sudo", "-n", "-v").Run()
}
// Acquire runs `sudo -v` once (prompting if needed) and returns a Keepalive
// that refreshes the timestamp every `interval` using ValidateNonInteractive.
// If the initial Validate fails, no goroutine is started and the error is returned.
func Acquire(ctx context.Context, a Acquirer, interval time.Duration) (*Keepalive, error) {
if a == nil {
a = ExecSudo{}
}
if interval <= 0 {
interval = 50 * time.Second
}
if err := a.Validate(ctx); err != nil {
return nil, fmt.Errorf("sudo -v failed: %w", err)
}
kCtx, cancel := context.WithCancel(ctx)
k := &Keepalive{cancel: cancel, done: make(chan struct{})}
go func() {
defer close(k.done)
t := time.NewTicker(interval)
defer t.Stop()
for {
select {
case <-kCtx.Done():
return
case <-t.C:
_ = a.ValidateNonInteractive(kCtx) // best-effort
}
}
}()
return k, nil
}