// 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 }