Drives on a headless Jellyfin box ran hot 24/7: NAS-class drives (WD Red, IronWolf) ship with no idle timer at all. v0.1.2 stopped tuistream from keeping them awake; this makes them actually go to sleep. Spin-down is a DEFAULT, not a feature: when the inventory shows spinning media drives, the TUI silently installs/syncs the mechanism and flashes what it did. [s] on Setup is the opt-OUT, remembered via /etc/tuistream/spindown-off so the default never fights the user. The mechanism is a tiny systemd service (tuistream --spindown-watch) that samples /proc/diskstats and issues `hdparm -y` to any target drive idle past 3 minutes. We deliberately do NOT use the drive's own standby timer (hdparm -S): the 10TB helium WD Reds advertise it and then ignore it — verified on real hardware (fresh -S 36, zero I/O, four minutes, still active/idle). Forcing standby from outside works on everything. An rc shipped the udev+hdparm -S approach; the plans clean its rule up. Safety: targets are spinning (sysfs rotational), top-level, real disks, never a system disk — same classifier the rest of Setup trusts. The watcher's own probes can't disturb drives: /proc/diskstats and sysfs are kernel memory, and hdparm -C (CHECK POWER MODE) doesn't wake or reset anything. The unit's ExecStart points at the running binary and the auto-sync rewrites it when the content goes stale (e.g. binary moved from a dev path to /usr/local/bin). hdparm joins the dependency pre-flight. Verified end-to-end on moviebox (2x WD Red 10TB SATA + IronWolf 10TB USB): auto-applied on launch, all three drives reached standby after 3 idle minutes, Monitor shows them as "asleep" without waking them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
74 lines
2.6 KiB
Go
74 lines
2.6 KiB
Go
// Package system handles host-level concerns that don't fit drives /
|
|
// jellyfin / firewall — most notably the one-time install of every Arch
|
|
// package tuistream's flows actually invoke.
|
|
//
|
|
// The TUI's individual flows used to install their own missing tools on
|
|
// the fly, which interrupted the user mid-action and broke the "one
|
|
// confirm screen, then runs to completion" promise. Doing it once at
|
|
// launch keeps the flows themselves dependency-free.
|
|
package system
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
)
|
|
|
|
// Dep is one Arch package + the binary tuistream invokes from it. The
|
|
// binary check lets us decide whether the package is missing without
|
|
// shelling out to `pacman -Qi`, which is much slower.
|
|
type Dep struct {
|
|
Pkg string
|
|
Binary string
|
|
}
|
|
|
|
// Required is the closed set of userspace tools tuistream needs. Anything
|
|
// in Arch's `base` group (util-linux, e2fsprogs, coreutils) is assumed
|
|
// present and intentionally NOT listed.
|
|
var Required = []Dep{
|
|
{Pkg: "rsync", Binary: "rsync"}, // Manage > Copy
|
|
{Pkg: "acl", Binary: "setfacl"}, // Setup > add-drive ACL grant, Copy ACL re-grant
|
|
{Pkg: "btrfs-progs", Binary: "mkfs.btrfs"}, // Setup > format btrfs / pool
|
|
{Pkg: "xfsprogs", Binary: "mkfs.xfs"}, // Setup > format xfs
|
|
{Pkg: "gptfdisk", Binary: "sgdisk"}, // Setup > wipe-whole-disk
|
|
{Pkg: "parted", Binary: "partprobe"}, // Setup > partprobe after wipe
|
|
{Pkg: "smartmontools", Binary: "smartctl"}, // Monitor > SMART health per disk
|
|
{Pkg: "hdparm", Binary: "hdparm"}, // Setup > drive spin-down timer
|
|
}
|
|
|
|
// Missing returns the subset of Required whose binary isn't on $PATH.
|
|
func Missing() []Dep {
|
|
var out []Dep
|
|
for _, d := range Required {
|
|
if _, err := exec.LookPath(d.Binary); err != nil {
|
|
out = append(out, d)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// EnsureInstalled checks for missing deps and runs `pacman -S --needed
|
|
// --noconfirm` for them. Output is streamed to stderr so the user can see
|
|
// what's happening before the TUI takes over the terminal.
|
|
//
|
|
// Returns nil if everything's already present or the install succeeded.
|
|
// On pacman failure, returns the error but doesn't exit the process —
|
|
// the caller decides whether to abort or warn-and-continue.
|
|
func EnsureInstalled() error {
|
|
miss := Missing()
|
|
if len(miss) == 0 {
|
|
return nil
|
|
}
|
|
pkgs := make([]string, len(miss))
|
|
for i, d := range miss {
|
|
pkgs[i] = d.Pkg
|
|
}
|
|
fmt.Fprintf(os.Stderr,
|
|
"tuistream: installing missing tools: %s\n", strings.Join(pkgs, " "))
|
|
args := append([]string{"-S", "--needed", "--noconfirm"}, pkgs...)
|
|
cmd := exec.Command("pacman", args...)
|
|
cmd.Stdout = os.Stderr
|
|
cmd.Stderr = os.Stderr
|
|
return cmd.Run()
|
|
}
|