diff --git a/aldermon/Makefile b/aldermon/Makefile
index 75738f1..3abad17 100644
--- a/aldermon/Makefile
+++ b/aldermon/Makefile
@@ -7,7 +7,7 @@ DATADIR ?= /usr/share
BIN := $(DESTDIR)$(PREFIX)/bin/aldermon
MSRBIN := $(DESTDIR)$(PREFIX)/bin/aldermon-msr
CONF := $(DESTDIR)$(SYSCONF)/aldermon/aldermon.conf
-SETUP := $(DESTDIR)$(DATADIR)/aldermon/aldermon-setup.sh
+SETUP := $(DESTDIR)$(DATADIR)/aldermon/aldermon-install.sh
.PHONY: all build check install uninstall clean
@@ -24,18 +24,22 @@ check:
install: build
install -Dm755 target/release/aldermon $(BIN)
install -Dm755 target/release/aldermon-msr $(MSRBIN)
- install -Dm644 aldermon.conf $(CONF)
- install -Dm755 aldermon-setup.sh $(SETUP)
+ # never clobber a hand-edited system conf; aldermon-install.sh owns
+ # force-replacing it (--clean)
+ [ -e $(CONF) ] || install -Dm644 aldermon.conf $(CONF)
+ install -Dm755 aldermon-install.sh $(SETUP)
ifeq ($(DESTDIR),)
# VID needs the cap half of a two-gate check (msr nodes also go 0440 adm
- # via aldermon-setup.sh); grant cap_sys_rawio to the read-only helper
- # only. Re-applied on every install since rebuilding drops file caps.
+ # via the rules installed by aldermon-install.sh); grant cap_sys_rawio
+ # to the read-only helper only. Re-applied on every install since
+ # rebuilding drops file caps.
setcap cap_sys_rawio+ep $(MSRBIN)
endif
uninstall:
rm -f $(BIN) $(MSRBIN) $(CONF) $(SETUP)
rm -f $(DESTDIR)$(DATADIR)/aldermon/powercap-pl-rules.sh
+ rm -f $(DESTDIR)$(DATADIR)/aldermon/aldermon-setup.sh
rmdir -p --ignore-fail-on-non-empty $(CONF) 2>/dev/null || true
rmdir -p --ignore-fail-on-non-empty $(SETUP) 2>/dev/null || true
diff --git a/aldermon/README.md b/aldermon/README.md
index f0ac840..49ddebc 100644
--- a/aldermon/README.md
+++ b/aldermon/README.md
@@ -11,6 +11,7 @@ aldermon --vid # VID spike tool (debug; needs MSR access, see below)
aldermon --log # append CSV samples to ./aldermon-vid.log
aldermon # one-shot sensor dump
aldermon --help # options
+aldermon --version # aldermon x.y.z
```
The TUI always shows the delivered vCore (SIO in0), the CPU's requested SVID
@@ -66,33 +67,49 @@ layout = auto # auto | single | dual
## Install
-```sh
-make # release build
-make check # fmt + clippy + tests
-doas make install # PREFIX=/usr/local by default
-```
-
-Installs `aldermon` + `aldermon-msr` (VID helper) to `$(PREFIX)/bin`, this
-repo's `aldermon.conf` to `/etc/aldermon/aldermon.conf` (system default —
-override per-user via `~/.config/aldermon/`), and the setup script to
-`/usr/share/aldermon/`. `make install` also setcaps the helper (skipped for
-staged `DESTDIR=` installs — `aldermon-setup.sh` applies it). `make
-uninstall` reverses it (leaves the system conf dir if non-empty).
-
-## Privileged setup — one-time (RAPL watts + VID)
+One command, from this directory:
```sh
-doas /usr/share/aldermon/aldermon-setup.sh
-doas usermod -aG adm $USER # then re-login
+./aldermon-install.sh # recon -> printed plan -> confirm -> ONE doas/sudo
+ # prompt -> install only what's needed -> verify
```
-1. RAPL: `energy_uj` is root-only, so the script installs a udev rule that
- chgrps it 0440 to group `adm` — watts then work unprivileged.
-2. VID: opening `/dev/cpu/*/msr` passes two gates — the file-mode check
- (node is 0600; `CAP_SYS_RAWIO` does NOT override DAC) and `msr_open()`'s
- capability check. The script installs a udev rule making the nodes
- `0440 adm` and sets `cap_sys_rawio+ep` on `aldermon-msr`, a read-only
- helper that touches exactly one MSR; the app never holds the capability.
+Stages: **[1/4]** binary+helper — version-gated (installs if missing or
+older, skips if current); **[2/4]** `/etc` config — only if missing (your
+system-conf edits survive; per-user `~/.config/aldermon/` never touched);
+**[3/4]** watts — powercap udev rule + chmod helper (group `adm`);
+**[4/4] VID is OPTIONAL**: after the root prompt the installer asks whether
+to add the msr udev rule + helper capability. Declining leaves vCore/temps/
+freqs untouched (already unprivileged) — the panel just keeps showing
+`VID n/a (no msr access)`.
+
+Modes: `--check` status report, changes nothing; `--setup` privileged parts
+only (for staged/package installs); `--clean`/`-c` force-reinstall binary,
+helper, permissions and STOCK config; `--vid`/`--no-vid` pre-answer stage 4;
+`--yes`/`-y` accept the plan without prompting; `--uninstall` removes the
+binaries, capability, udev rules and RUN helper and restores the device
+nodes' stock permissions (keeps your `/etc` config); `--purge` also deletes
+the config. On uninstall it asks whether to drop your user from group `adm`
+(`--del-adm`/`--keep-adm` pre-answer).
+
+Low-level: `make check`, `doas make install` (honors PREFIX/DESTDIR; keeps
+an existing system conf; setcaps when unstaged — run
+`aldermon-install.sh --setup` after staged installs), `make uninstall`
+reverses (leaves the system conf dir if non-empty).
+
+## Why the privileges look like this (RAPL watts + VID)
+
+- RAPL `energy_uj` is `0400` root-only; powercap has NO devnode, so udev's
+ `RUN+=` helper chgrp/chmods it to `0440 adm`.
+- `/dev/cpu/*/msr` (VID) passes TWO gates on open: the DAC file-mode check
+ first (nodes are `0600` — `CAP_SYS_RAWIO` is not `CAP_DAC_OVERRIDE`, so
+ setcap alone gets `EACCES`), then `msr_open()`'s `capable(CAP_SYS_RAWIO)`
+ (so chmod alone gets `EPERM`). The installer opens both: udev
+ `MODE/GROUP 0440 adm` for the nodes, `cap_sys_rawio+ep` on the read-only
+ `aldermon-msr` helper only — the app never holds the capability.
+- Once per user: the installer adds you to group `adm` if missing; activate
+ it with a re-login (or `newgrp adm` for the current shell). Uninstall asks
+ whether to remove you from `adm` (`adm` also grants log access).
## Status
diff --git a/aldermon/aldermon-install.sh b/aldermon/aldermon-install.sh
new file mode 100755
index 0000000..0f43899
--- /dev/null
+++ b/aldermon/aldermon-install.sh
@@ -0,0 +1,543 @@
+#!/bin/sh
+# aldermon-install.sh - one-stop installer / uninstaller for aldermon.
+#
+# Flow: RECON (checks, nothing changed) -> printed plan -> confirm ->
+# ONE root escalation (doas/sudo) -> per-component install with status
+# lines -> verify.
+#
+# Install components (each checked first; skipped when already right, except
+# watts + the optional VID answer):
+# [1/4] binary+helper install if missing or older than the built
+# version; identical/newer left alone (--clean forces).
+# The helper FILE is inert without its capability.
+# [2/4] /etc config installed only if missing (system-conf edits are
+# never clobbered unless --clean).
+# [3/4] watts powercap udev rule + RUN chmod helper (energy_uj
+# is 0400 root-only and powercap has no devnode).
+# Always done - the TUI power panel needs it.
+# [4/4] VID (optional) asked at the root stage (y/n): msr udev rule
+# (nodes 0440 adm) + cap_sys_rawio+ep on aldermon-msr
+# only. open /dev/cpu/N/msr takes TWO gates: DAC
+# file-mode first (setcap alone gets EACCES - the cap
+# is not CAP_DAC_OVERRIDE), then msr_open()'s
+# capable(CAP_SYS_RAWIO) (chmod alone gets EPERM).
+# Declining leaves VID showing "no msr access";
+# vCore/temps/freqs stay fully unprivileged either
+# way.
+# Install also removes pre-rename leftovers (60-adlermon-powercap.rules,
+# sbin/adlermon-powercap-chmod) and superseded installer scripts.
+#
+# Uninstall removes what install created (binaries, capability, udev rules,
+# RUN helper) and restores the device nodes' stock permissions, so recon
+# reports everything missing again. The /etc config is KEPT (your edits)
+# unless --purge.
+#
+# usage: aldermon-install.sh [MODE|OPTION]
+# (default) recon -> plan -> confirm -> install
+# --clean, -c force-reinstall every component (resets /etc config to stock)
+# --vid plan VID as ON; no prompt
+# --no-vid plan VID as SKIP; no prompt
+# --setup privileged parts only (rules, setcap) for staged/package
+# installs; still plan+confirm
+# --uninstall remove binaries, capability, rules, RUN helper; restore node
+# perms (keeps config)
+# --purge uninstall AND delete the /etc config
+# --del-adm on uninstall, also remove the original user from group adm
+# (default: ask at the root stage)
+# --keep-adm on uninstall, leave the adm group alone; don't ask
+# Install adds the original user to group adm if missing; activate it with a
+# re-login or `newgrp adm` (current shell only).
+# --check status report; changes nothing
+# --yes, -y accept the plan without prompting
+# --help, -h this text
+#
+# env: PREFIX (default /usr/local), SYSCONF (/etc), DATADIR (/usr/share)
+
+set -eu
+
+SRCDIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
+PREFIX=${PREFIX:-/usr/local}
+SYSCONF=${SYSCONF:-/etc}
+DATADIR=${DATADIR:-/usr/share}
+
+BIN=$PREFIX/bin/aldermon
+MSRBIN=$PREFIX/bin/aldermon-msr
+CONF=$SYSCONF/aldermon/aldermon.conf
+STOCK_CONF=$SRCDIR/aldermon.conf
+HELPER=$PREFIX/sbin/aldermon-powercap-chmod
+RULE=$SYSCONF/udev/rules.d/60-aldermon-powercap.rules
+MSRRULE=$SYSCONF/udev/rules.d/60-aldermon-msr.rules
+REL=$SRCDIR/target/release
+
+_seen_section=0
+section() { if [ "$_seen_section" = 1 ]; then printf '\n'; fi; printf '== %s ==\n' "$1"; _seen_section=1; }
+item() { printf ' %s\n' "$*"; }
+sub() { printf ' %s\n' "$*"; }
+say() { printf '%s\n' "$*"; }
+die() { printf 'aldermon-install: error: %s\n' "$*" >&2; exit 1; }
+
+usage() { sed -n '/^# usage:/,/^# env:/p' "$0" | sed 's/^# //;s/^#$//'; }
+
+ver_older() { # a b -> true if a is a strictly older dotted version
+ [ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)" = "$1" ]
+}
+
+installed_ver() { # echoes "" | version | 0.0.0 (pre-version build)
+ [ -x "$BIN" ] || return 0
+ _v=$("$BIN" --version 2>/dev/null | sed -n 's/^aldermon //p') || true
+ echo "${_v:-0.0.0}"
+}
+
+src_ver() { # version the tree builds (from binary if built, else Cargo.toml)
+ if [ -x "$REL/aldermon" ]; then
+ "$REL/aldermon" --version | sed -n 's/^aldermon //p'
+ else
+ sed -n 's/^version = "\([^"]*\)"/\1/p' "$SRCDIR/Cargo.toml" 2>/dev/null || echo ""
+ fi
+}
+
+helper_state() { # "missing" | "installed, no capability" | "installed + capable"
+ if [ ! -x "$MSRBIN" ]; then echo missing
+ elif getcap "$MSRBIN" 2>/dev/null | grep -q cap_sys_rawio; then echo "installed + capable"
+ else echo "installed, no capability"; fi
+}
+
+# echoes the [1/4] gate action for FORCE INST SRC
+bin_action() {
+ if [ "$1" = clean ]; then echo "install (forced)"
+ elif [ -z "$2" ]; then echo "install (missing)"
+ elif ver_older "$2" "$3"; then echo "upgrade $2 -> $3"
+ elif [ "$2" = "$3" ]; then echo "skip (up to date)"
+ else echo "keep (installed $2 newer than source $3)"
+ fi
+}
+
+write_if_changed() { # FILE MODE, content on stdin
+ _f=$1 _m=$2 _t="$_f.tmp-aldermon-install"
+ install -d "$(dirname "$_f")"
+ cat >"$_t"
+ if [ -f "$_f" ] && cmp -s "$_t" "$_f"; then
+ sub "kept (unchanged): $_f"
+ rm -f "$_t"
+ else
+ install -D -m "$_m" "$_t" "$_f"
+ sub "installed: $_f"
+ rm -f "$_t"
+ fi
+}
+
+write_powercap() { # watts: RUN-chmod helper + rule, applied now
+ install -d "$(dirname "$HELPER")"
+ cat <<EOF | write_if_changed "$HELPER" 755
+#!/bin/sh
+for d in /sys/class/powercap/intel-rapl*; do
+ [ -e "\$d/energy_uj" ] && chgrp adm "\$d/energy_uj" && chmod 0440 "\$d/energy_uj"
+done
+exit 0
+EOF
+ printf '%s\n' 'SUBSYSTEM=="powercap", ACTION=="add", RUN+="'$HELPER'"' |
+ write_if_changed "$RULE" 644
+ "$HELPER" || sub "warning: powercap chmod failed (watts may stay unreadable)"
+ udevadm control --reload-rules
+ udevadm trigger --subsystem-match=powercap
+ sub "udev reloaded + powercap triggered"
+ clean_legacy
+}
+
+write_msr_rule() { # VID gate 1: msr IS a devnode -> plain MODE/GROUP
+ printf '%s\n' 'KERNEL=="msr[0-9]*", SUBSYSTEM=="msr", MODE="0440", GROUP="adm"' |
+ write_if_changed "$MSRRULE" 644
+ udevadm control --reload-rules # required: write_powercap reloaded BEFORE this file existed
+ udevadm trigger --subsystem-match=msr
+ sub "udev msr nodes triggered (0440 adm)"
+}
+
+# adm group membership: watts (energy_uj 0440 adm) and VID msr nodes both
+# need it. Adds USER if missing so a fresh install works after one re-login.
+ensure_adm() { # USER
+ _user=${1:-}
+ [ -n "$_user" ] && [ "$_user" != root ] || return 0
+ if ! getent group adm >/dev/null 2>&1; then
+ sub "warning: no group adm on this system; grant node access to $_user's group instead"
+ elif id -nG "$_user" 2>/dev/null | tr ' ' '\n' | grep -qx adm; then
+ sub "user $_user: already in group adm"
+ elif usermod -aG adm "$_user"; then
+ sub "added $_user to group adm - re-login (or 'newgrp adm') to activate"
+ else
+ sub "warning: could not add $_user to adm; do it manually: doas usermod -aG adm $_user"
+ fi
+}
+
+# Pre-rename (adlermon) and superseded installer leftovers - the old rule
+# still RUNs its old chmod helper, so remove the pair once ours is in.
+clean_legacy() {
+ for _l in "$SYSCONF/udev/rules.d/60-adlermon-powercap.rules" \
+ "$PREFIX/sbin/adlermon-powercap-chmod" \
+ "$DATADIR/aldermon/powercap-pl-rules.sh" \
+ "$DATADIR/aldermon/aldermon-setup.sh"; do
+ if [ -e "$_l" ]; then
+ rm -f "$_l"
+ sub "removed legacy leftover: $_l"
+ fi
+ done
+}
+
+setcap_helper() { # FORCE - VID gate 2: capability on the minimal helper only
+ [ -x "$MSRBIN" ] || die "$MSRBIN not found - install the binary first (or use 'aldermon --vid' under root)"
+ if [ "$1" != clean ] && getcap "$MSRBIN" 2>/dev/null | grep -q 'cap_sys_rawio'; then
+ sub "kept (already capable): $MSRBIN"
+ else
+ setcap cap_sys_rawio+ep "$MSRBIN"
+ sub "setcap cap_sys_rawio+ep: $MSRBIN"
+ fi
+}
+
+vid_choice() { # root side: ask Y/n unless pre-answered
+ case "$VIDMODE" in
+ on) return 0 ;;
+ off) sub "skipped (declined): VID stays unreadable - the TUI shows 'VID n/a (no msr access)'. Re-run with --vid to add it."; return 1 ;;
+ esac
+ printf 'enable VID extras (msr rule + helper setcap)? [Y/n] '
+ read -r _ans || _ans=y
+ case "$_ans" in
+ [nN]*) sub "skipped (declined): VID stays unreadable - the TUI shows 'VID n/a (no msr access)'. Re-run with --vid to add it."; return 1 ;;
+ *) return 0 ;;
+ esac
+}
+
+verify() { # ORIGUSER VIDENABLED
+ _u=${1:-$(id -un)} _vid=${2:-yes}
+ section verify
+ if [ -x "$BIN" ]; then item "binary: $("$BIN" --version 2>/dev/null || echo 'installed, pre-version')"; fi
+ _c=$(getcap "$MSRBIN" 2>/dev/null || true)
+ if [ "$_vid" = yes ]; then
+ item "helper caps: ${_c:-NONE (VID stays unreadable - run --setup)}"
+ else
+ item "VID: not wanted this run - TUI shows 'VID n/a (no msr access)'"
+ if [ -f "$MSRRULE" ]; then
+ item "note: an msr rule from a previous install is still present (left alone; VID still off until the helper is setcap'd)"
+ fi
+ fi
+ if [ -e /dev/cpu/0/msr ]; then
+ item "msr nodes: $(ls -l /dev/cpu/0/msr | awk '{print $1, $3, $4}')"
+ else
+ item "warning: /dev/cpu/0/msr absent - msr module not loaded"
+ fi
+ _e=$(ls /sys/class/powercap/intel-rapl:0/energy_uj 2>/dev/null || true)
+ if [ -n "$_e" ]; then item "energy_uj: $(ls -l "$_e" | awk '{print $1, $3, $4}')"; fi
+ if [ -f "$CONF" ]; then item "config: $CONF present"; else item "config: $CONF MISSING"; fi
+ if id -nG "$_u" 2>/dev/null | tr ' ' '\n' | grep -qx adm; then
+ item "user $_u: in group adm (existing shells need re-login or 'newgrp adm')"
+ else
+ item "user $_u: NOT in group adm"
+ fi
+}
+
+recon() { # install/setup: status + planned actions, BEFORE any root action
+ _src=$(src_ver) _inst=$(installed_ver)
+ section recon
+ item "installed: ${_inst:-none} | source: ${_src:-unknown}"
+ item "helper: $(helper_state)"
+ item "rules: powercap $([ -f "$RULE" ] && echo present || echo missing), msr $([ -f "$MSRRULE" ] && echo present || echo missing)"
+ item "config: $CONF $([ -f "$CONF" ] && echo present || echo missing)"
+ _e=$(ls -l /sys/class/powercap/intel-rapl:0/energy_uj 2>/dev/null | awk '{print $1, $3, $4}')
+ item "watts source: energy_uj ${_e:-unreadable}"
+ item "planned actions:"
+ if [ "$MODE" != setup ]; then
+ _act=$(bin_action "$FORCE" "$_inst" "$_src")
+ if [ "$MODE" = install ] && [ ! -x "$REL/aldermon" ]; then _act="$_act (after release build)"; fi
+ sub "[1/4] binary+helper: $_act"
+ if [ "$FORCE" = clean ]; then sub "[2/4] config: reset $CONF to stock"
+ elif [ -f "$CONF" ]; then sub "[2/4] config: keep existing $CONF"
+ else sub "[2/4] config: install stock $CONF"; fi
+ fi
+ sub "[3/4] watts: ensure powercap rule + $HELPER (group adm; needs root)"
+ _u=$(id -un)
+ if id -nG "$_u" 2>/dev/null | tr ' ' '\n' | grep -qx adm; then
+ sub "groups: $_u already in adm"
+ else
+ sub "groups: add $_u to adm (needs root; re-login or 'newgrp adm' after)"
+ fi
+ case "$VIDMODE" in
+ off) sub "[4/4] VID: SKIP (--no-vid)" ;;
+ on) sub "[4/4] VID: msr rule + setcap helper (needs root)" ;;
+ *) sub "[4/4] VID: OPTIONAL - asked after the root prompt; msr rule 0440 adm + cap_sys_rawio+ep on aldermon-msr only. Decline keeps 'VID n/a'; vCore/temps/freqs need no privileges either way." ;;
+ esac
+}
+
+recon_uninstall() {
+ section recon
+ item "binary: $([ -x "$BIN" ] && echo "$BIN ($("$BIN" --version 2>/dev/null || echo 'pre-version'))" || echo "not installed")"
+ item "helper: $(helper_state)"
+ item "rules: powercap $([ -f "$RULE" ] && echo present || echo missing), msr $([ -f "$MSRRULE" ] && echo present || echo missing)"
+ item "config: $CONF $([ -f "$CONF" ] && echo present || echo missing)"
+ item "planned actions:"
+ if [ "$PURGE" = yes ]; then
+ sub "remove binaries, capability, both udev rules, RUN helper, installer copy"
+ sub "restore /dev/cpu/*/msr to 0600 root:root and energy_uj to 0400 root:root"
+ sub "remove config $CONF (--purge)"
+ else
+ sub "remove binaries, capability, both udev rules, RUN helper, installer copy"
+ sub "restore /dev/cpu/*/msr to 0600 root:root and energy_uj to 0400 root:root"
+ sub "keep config $CONF (add --purge to remove)"
+ fi
+ case "$ADMREMOVE" in
+ yes) sub "remove $(id -un) from group adm" ;;
+ no) sub "leave group adm alone" ;;
+ *) sub "ask at the root stage whether to remove $(id -un) from group adm" ;;
+ esac
+}
+
+confirm_or_go() { # plan acceptance before escalating
+ [ "$ASSUME" = yes ] && return 0
+ printf 'proceed? [Y/n] '
+ read -r _r || die "no tty for confirmation - use --yes"
+ case "$_r" in
+ [nN]*) say "aborted, nothing changed"; exit 0 ;;
+ esac
+}
+
+root_install() { # FORCE VIDMODE PURGE ORIGUSER PREFIX SYSCONF DATADIR
+ FORCE=$1; VIDMODE=$2; _orig=$4; PREFIX=$5; SYSCONF=$6; DATADIR=$7
+ BIN=$PREFIX/bin/aldermon; MSRBIN=$PREFIX/bin/aldermon-msr
+ CONF=$SYSCONF/aldermon/aldermon.conf; STOCK_CONF=$SRCDIR/aldermon.conf
+ HELPER=$PREFIX/sbin/aldermon-powercap-chmod
+ RULE=$SYSCONF/udev/rules.d/60-aldermon-powercap.rules
+ MSRRULE=$SYSCONF/udev/rules.d/60-aldermon-msr.rules
+
+ [ -x "$REL/aldermon" ] || die "no release build in $SRCDIR - run the script unprivileged (it builds) or cargo build --release first"
+ SRCVER=$(src_ver)
+ INSTVER=$(installed_ver)
+
+ section install
+ item "[1/4] binary (installed: ${INSTVER:-none}, source: $SRCVER)"
+ _act=$(bin_action "$FORCE" "$INSTVER" "$SRCVER")
+ case $_act in
+ install* | upgrade*)
+ install -Dm755 "$REL/aldermon" "$BIN"
+ install -Dm755 "$REL/aldermon-msr" "$MSRBIN"
+ sub "installed: $BIN + $MSRBIN" ;;
+ skip*)
+ if [ ! -x "$MSRBIN" ]; then
+ install -Dm755 "$REL/aldermon-msr" "$MSRBIN"
+ sub "skipped binary (up to date); installed missing helper"
+ else
+ sub "skipped: up to date (use --clean to force)"
+ fi ;;
+ keep*) sub "$_act" ;;
+ esac
+
+ item "[2/4] config"
+ if [ "$FORCE" = clean ] || [ ! -e "$CONF" ]; then
+ [ -f "$STOCK_CONF" ] || die "stock config $STOCK_CONF not found (installed copy? use --setup)"
+ install -Dm644 "$STOCK_CONF" "$CONF"
+ sub "installed stock: $CONF"
+ else
+ sub "kept existing: $CONF (--clean resets to stock)"
+ fi
+
+ item "[3/4] watts (powercap)"
+ write_powercap
+
+ item "[4/4] VID (optional)"
+ _vid=no
+ if vid_choice; then
+ _vid=yes
+ write_msr_rule
+ setcap_helper "$FORCE"
+ fi
+
+ item "group membership"
+ ensure_adm "$_orig"
+
+ verify "$_orig" "$_vid"
+}
+
+root_setup() { # FORCE VIDMODE PURGE ORIGUSER PREFIX SYSCONF DATADIR
+ FORCE=$1; VIDMODE=$2; _orig=$4; PREFIX=$5; SYSCONF=$6; DATADIR=$7
+ BIN=$PREFIX/bin/aldermon; MSRBIN=$PREFIX/bin/aldermon-msr
+ CONF=$SYSCONF/aldermon/aldermon.conf
+ HELPER=$PREFIX/sbin/aldermon-powercap-chmod
+ RULE=$SYSCONF/udev/rules.d/60-aldermon-powercap.rules
+ MSRRULE=$SYSCONF/udev/rules.d/60-aldermon-msr.rules
+ section install
+ item "setup: watts (powercap)"
+ write_powercap
+ item "setup: VID (optional)"
+ _vid=no
+ if vid_choice; then
+ _vid=yes
+ write_msr_rule
+ setcap_helper "$FORCE"
+ fi
+ item "group membership"
+ ensure_adm "$_orig"
+ verify "$_orig" "$_vid"
+}
+
+root_uninstall() { # FORCE VIDMODE PURGE ORIGUSER PREFIX SYSCONF DATADIR ADMREMOVE
+ PURGE=$3; _orig=$4; PREFIX=$5; SYSCONF=$6; DATADIR=$7; ADMREMOVE=$8
+ BIN=$PREFIX/bin/aldermon; MSRBIN=$PREFIX/bin/aldermon-msr
+ CONF=$SYSCONF/aldermon/aldermon.conf
+ HELPER=$PREFIX/sbin/aldermon-powercap-chmod
+ RULE=$SYSCONF/udev/rules.d/60-aldermon-powercap.rules
+ MSRRULE=$SYSCONF/udev/rules.d/60-aldermon-msr.rules
+
+ section uninstall
+ if [ -x "$MSRBIN" ]; then
+ setcap -r "$MSRBIN" 2>/dev/null && item "dropped capability: $MSRBIN" || item "capability already absent"
+ fi
+ for _f in "$BIN" "$MSRBIN" "$HELPER" "$RULE" "$MSRRULE" "$DATADIR/aldermon/aldermon-install.sh"; do
+ if [ -e "$_f" ]; then rm -f "$_f"; item "removed: $_f"; fi
+ done
+ clean_legacy
+ # Restore stock device perms so recon reports them unset again.
+ if [ -e /dev/cpu/0/msr ]; then
+ chgrp root /dev/cpu/*/msr 2>/dev/null || true
+ chmod 0600 /dev/cpu/*/msr 2>/dev/null || true
+ item "restored /dev/cpu/*/msr -> 0600 root:root"
+ fi
+ _e=$(ls /sys/class/powercap/intel-rapl:0/energy_uj 2>/dev/null || true)
+ if [ -n "$_e" ]; then
+ chgrp root "$_e" 2>/dev/null || true
+ chmod 0400 "$_e" 2>/dev/null || true
+ item "restored energy_uj -> 0400 root:root"
+ fi
+ udevadm control --reload-rules
+ item "udev rules reloaded"
+ if [ "$PURGE" = yes ]; then
+ if [ -e "$CONF" ]; then rm -f "$CONF"; item "removed config: $CONF"; fi
+ else
+ item "kept config: $CONF (--purge removes it)"
+ fi
+ rmdir -p --ignore-fail-on-non-empty "$DATADIR/aldermon" 2>/dev/null || true
+ verify_uninstall "$_orig" "$ADMREMOVE"
+}
+
+verify_uninstall() { # ORIGUSER ADMREMOVE
+ _u=${1:-$(id -un)} _adm=${2:-no}
+ section verify
+ item "binary: $([ -e "$BIN" ] && echo STILL PRESENT || echo gone)"
+ item "helper: $([ -e "$MSRBIN" ] && echo STILL PRESENT || echo gone)"
+ item "powercap rule: $([ -e "$RULE" ] && echo STILL PRESENT || echo gone)"
+ item "msr rule: $([ -e "$MSRRULE" ] && echo STILL PRESENT || echo gone)"
+ if [ -e /dev/cpu/0/msr ]; then
+ item "msr node: $(ls -l /dev/cpu/0/msr | awk '{print $1, $3, $4}') (stock 0600 root root)"
+ fi
+ _e=$(ls /sys/class/powercap/intel-rapl:0/energy_uj 2>/dev/null || true)
+ if [ -n "$_e" ]; then item "energy_uj: $(ls -l "$_e" | awk '{print $1, $3, $4}')"; fi
+ item "config: $CONF $([ -e "$CONF" ] && echo present || echo gone)"
+ if id -nG "$_u" 2>/dev/null | tr ' ' '\n' | grep -qx adm; then
+ _do_adm=no
+ case "$_adm" in
+ yes) _do_adm=yes ;;
+ no) item "user $_u: still in group adm (use --del-adm to remove; adm also grants log access)" ;;
+ *)
+ printf 'remove %s from group adm? [y/N] ' "$_u"
+ read -r _r || _r=n
+ case "$_r" in [yY]*) _do_adm=yes ;; esac
+ ;;
+ esac
+ if [ "$_do_adm" = yes ]; then
+ if gpasswd -d "$_u" adm >/dev/null 2>&1; then
+ item "removed $_u from group adm (re-login for it to take effect)"
+ else
+ item "could not remove $_u from adm; do it manually: doas gpasswd -d $_u adm"
+ fi
+ elif [ "$_adm" = ask ]; then
+ item "kept $_u in group adm (adm also grants log access)"
+ fi
+ fi
+}
+
+escalate() { # INTERNALMODE
+ _mode=$1 _u=$(id -un)
+ if [ "$(id -u)" -eq 0 ]; then
+ "$0" "$_mode" "$FORCE" "$VIDMODE" "$PURGE" "$_u" "$PREFIX" "$SYSCONF" "$DATADIR" "$ADMREMOVE"
+ else
+ PRIV=$(command -v doas 2>/dev/null || command -v sudo 2>/dev/null || true)
+ [ -n "$PRIV" ] || die "no doas or sudo found - run the privileged steps as root"
+ say "requesting root via $PRIV"
+ exec "$PRIV" "$0" "$_mode" "$FORCE" "$VIDMODE" "$PURGE" "$_u" "$PREFIX" "$SYSCONF" "$DATADIR" "$ADMREMOVE"
+ fi
+}
+
+check_status() {
+ section status
+ if [ -x "$BIN" ]; then
+ item "binary: $("$BIN" --version 2>/dev/null || echo 'installed, pre-version (<= 2026-09)')"
+ if [ -x "$REL/aldermon" ]; then
+ _s=$("$REL/aldermon" --version | sed -n 's/^aldermon //p')
+ _i=$("$BIN" --version 2>/dev/null | sed -n 's/^aldermon //p')
+ [ -n "$_i" ] || _i=0.0.0
+ if ver_older "$_i" "$_s"; then item "update available: source builds $_s"
+ else item "source build: $_s $([ "$_i" = "$_s" ] && echo '(match)' || echo '(source is older)')"; fi
+ fi
+ else
+ item "binary: NOT INSTALLED ($BIN)"
+ fi
+ item "helper: $(helper_state)"
+ item "powercap rule: $([ -f "$RULE" ] && echo present || echo MISSING)"
+ item "msr rule: $([ -f "$MSRRULE" ] && echo present || echo MISSING)"
+ if [ -e /dev/cpu/0/msr ]; then
+ item "msr node: $(ls -l /dev/cpu/0/msr | awk '{print $1, $3, $4}') (needs 0440 adm + helper cap)"
+ fi
+ item "config: $CONF $([ -f "$CONF" ] && echo present || echo MISSING)"
+ if id -nG "$(id -un)" | tr ' ' '\n' | grep -qx adm; then
+ item "user $(id -un): in adm"
+ else
+ item "user $(id -un): NOT in adm"
+ fi
+}
+
+MODE=install
+FORCE=plain
+VIDMODE=ask
+PURGE=no
+ADMREMOVE=ask
+ASSUME=no
+while [ $# -gt 0 ]; do
+ case $1 in
+ -h | --help) usage; exit 0 ;;
+ --check) MODE=check ;;
+ --setup) MODE=setup ;;
+ --uninstall) MODE=uninstall ;;
+ --purge) MODE=uninstall; PURGE=yes ;;
+ --clean | -c) FORCE=clean ;;
+ --yes | -y) ASSUME=yes ;;
+ --no-vid) VIDMODE=off ;;
+ --vid) VIDMODE=on ;;
+ --del-adm) ADMREMOVE=yes ;;
+ --keep-adm) ADMREMOVE=no ;;
+ _root) shift; root_install "$@"; exit 0 ;;
+ _setup) shift; root_setup "$@"; exit 0 ;;
+ _unroot) shift; root_uninstall "$@"; exit 0 ;;
+ *) die "unknown argument: $1 (see --help)" ;;
+ esac
+ shift
+done
+
+case $MODE in
+ check) check_status ;;
+ uninstall)
+ recon_uninstall
+ confirm_or_go
+ escalate _unroot
+ ;;
+ setup)
+ recon
+ confirm_or_go
+ escalate _setup
+ ;;
+ install)
+ [ -f "$SRCDIR/Cargo.toml" ] || die "$SRCDIR is not a source tree - run from the checkout, or use --setup/--check"
+ recon
+ confirm_or_go
+ if [ ! -x "$REL/aldermon" ]; then
+ command -v cargo >/dev/null || die "cargo not found and no release build present"
+ say "building release binaries (as $(id -un))"
+ (cd "$SRCDIR" && cargo build --release)
+ fi
+ escalate _root
+ ;;
+esac
diff --git a/aldermon/aldermon-setup.sh b/aldermon/aldermon-setup.sh
deleted file mode 100755
index 39bb1c5..0000000
--- a/aldermon/aldermon-setup.sh
+++ /dev/null
@@ -1,69 +0,0 @@
-#!/bin/sh
-# aldermon-setup.sh - one-shot privileged setup for aldermon (run once as
-# root; idempotent, re-running overwrites installed files):
-#
-# 1. RAPL watts: energy_uj is 0400 root-only. Installs a udev rule + RUN
-# helper chgrp'ing it 0440 to group `adm` so the TUI reads watts
-# unprivileged.
-# 2. VID: opening /dev/cpu/N/msr must pass TWO gates: the DAC file-mode
-# check (node is 0600 root:root — CAP_SYS_RAWIO does NOT override DAC),
-# then msr_open()'s capable(CAP_SYS_RAWIO). So: udev puts the nodes at
-# 0440 group `adm` (gate 1), and cap_sys_rawio+ep goes on the minimal
-# read-only helper aldermon-msr (gate 2, reads only 0x198). Neither
-# alone works; the app itself never holds the capability.
-#
-# doas ./aldermon-setup.sh (from the source tree)
-# doas /usr/share/aldermon/aldermon-setup.sh (after make install)
-
-set -eu
-
-PREFIX=${PREFIX:-/usr/local}
-MSRBIN=$PREFIX/bin/aldermon-msr
-HELPER=$PREFIX/sbin/aldermon-powercap-chmod
-RULE=/etc/udev/rules.d/60-aldermon-powercap.rules
-
-# Helper: relax energy_uj to 0440, group adm, on every powercap dir.
-cat > "$HELPER" <<'EOF'
-#!/bin/sh
-for d in /sys/class/powercap/intel-rapl*; do
- [ -e "$d/energy_uj" ] && chgrp adm "$d/energy_uj" && chmod 0440 "$d/energy_uj"
-done
-exit 0
-EOF
-chmod 0755 "$HELPER"
-
-# RUN+= (not MODE=/GROUP=) - powercap has no devnode, so udev's standard
-# MODE/GROUP assignment doesn't apply; we chmod in-process instead.
-cat > "$RULE" <<EOF
-SUBSYSTEM=="powercap", ACTION=="add", RUN+="$HELPER"
-EOF
-
-# Apply immediately so a reboot isn't required.
-"$HELPER"
-
-# VID gate 1: unlike powercap, msr IS a real devnode (SUBSYSTEM=="msr",
-# KERNEL=="msr0..N", DEVNAME=/dev/cpu/N/msr), so udev MODE/GROUP works
-# directly — put the nodes 0440 group adm.
-MSRRULE=/etc/udev/rules.d/60-aldermon-msr.rules
-cat > "$MSRRULE" <<'EOF'
-KERNEL=="msr[0-9]*", SUBSYSTEM=="msr", MODE="0440", GROUP="adm"
-EOF
-udevadm control --reload-rules
-udevadm trigger --subsystem-match=msr
-
-# VID gate 2: hand the capability to the helper only. Fail loudly if it's
-# missing — without the cap the helper is inert and the TUI shows "VID n/a".
-if [ -x "$MSRBIN" ]; then
- setcap cap_sys_rawio+ep "$MSRBIN"
- echo " setcap cap_sys_rawio+ep $MSRBIN"
-else
- echo "aldermon-setup: $MSRBIN not found - run 'doas make install' first," >&2
- echo " or use aldermon --vid under root." >&2
- exit 1
-fi
-
-echo "Installed:"
-echo " $HELPER"
-echo " $RULE"
-echo " $MSRRULE"
-echo "Add yourself to adm if not already: doas usermod -aG adm \$USER"
\ No newline at end of file
diff --git a/aldermon/src/main.rs b/aldermon/src/main.rs
index a2088b8..ed5df05 100644
--- a/aldermon/src/main.rs
+++ b/aldermon/src/main.rs
@@ -26,15 +26,16 @@ usage: aldermon [OPTION]
--vid VID vs SIO in0 cross-check spike (needs MSR access, see below)
--log append CSV samples to ./aldermon-vid.log
--help show this help
+ --version print version and exit
The TUI always shows the delivered vCore, the requested VID and the delta.
VID needs MSR access: run as root, or install the setcap'd aldermon-msr
-helper (doas make install + aldermon-setup.sh); otherwise it shows
+helper (doas make install + aldermon-install.sh); otherwise it shows
'VID n/a (no msr access)'.
--log appends one CSV row per poll; with --tui it logs live, with --vid it
logs continuously until interrupted (otherwise --vid runs a 5-sample spike).
-Short flags group: -lvt == --log --vid --tui.";
+Short flags group: -lvt == --log --vid --tui. -V == --version.";
#[derive(Default, Clone, Copy)]
struct Opts {
@@ -54,6 +55,10 @@ fn main() {
"tui" => opts.tui = true,
"vid" => opts.vid = true,
"log" => opts.log = true,
+ "version" => {
+ println!("aldermon {}", env!("CARGO_PKG_VERSION"));
+ return;
+ }
"help" => {
println!("{USAGE}");
return;
@@ -66,6 +71,10 @@ fn main() {
't' => opts.tui = true,
'v' => opts.vid = true,
'l' => opts.log = true,
+ 'V' => {
+ println!("aldermon {}", env!("CARGO_PKG_VERSION"));
+ return;
+ }
'h' => {
println!("{USAGE}");
return;
installer: one-stop aldermon-install.sh (check/plan/confirm/escalate) + uninstall
Replaces aldermon-setup.sh (git mv). From the source tree:
- recon (no changes) prints state + a per-stage plan, then confirm, then ONE
doas/sudo escalation, per-stage status, verify block. Sectioned output
('== recon/install/verify =='); no per-line prefix.
- [1/4] binary+helper version-gated (missing/older install, same skip,
newer keep; --clean forces); [2/4] /etc conf only-if-missing (--clean
resets to stock); [3/4] watts: powercap udev RUN-chmod helper (always,
write-if-changed); [4/4] VID OPTIONAL: y/n at the root stage -> msr udev
rule 0440 adm + cap_sys_rawio+ep on aldermon-msr only. Two-gate rationale
in header/README.
- manages group adm: install adds the user if missing (warning if no adm
group); uninstall asks whether to remove (--del-adm/--keep-adm).
- uninstall/--purge: drops binaries, capability, both udev rules, RUN
helper, installer copy; restores /dev/cpu/*/msr 0600 root:root and
energy_uj 0400 root:root; reloads udev; keeps /etc conf unless --purge.
- flags: --check/--setup/--clean(-c)/--uninstall/--purge/--del-adm/
--keep-adm/--yes(-y)/--vid/--no-vid/-h; PREFIX/SYSCONF/DATADIR env;
internal _root/_setup/_unroot get resolved dirs as args (doas resets env).
- fixes from UAT: udev reload BEFORE the msr trigger (write_powercap reloaded
before the msr rule file existed -> first-install msr nodes stayed 0600);
powercap helper says kept when byte-identical; --clean no longer forces a
cargo rebuild; clearer VID-decline wording.
- main.rs: --version/-V (CARGO_PKG_VERSION), needed by the version gate.
- Makefile: only-if-missing conf, installs aldermon-install.sh, legacy
uninstall paths.
make check (28 tests) green; full fake-root stub-doas matrix + live
install/clean/uninstall/check UAT on the real box.
d7832fa9ebbe5d90d271a0476963d120b63755b1
josie <josie@example.com> · 2026-10-08T20:43 ·
browse files at this commit
parents:
7b77caf