Split install_usage_monitoring.sh into an entry plus five libs

The 290-line installer was 40 over the 250-line cap. Its five banner-marked
steps move into a sibling lib/, leaving a 53-line entry that owns log()/die(),
the globals that cross between steps, and the call order.

Proven by execution, not inspection: meta/scripts/trace_shell_split.sh
--prefix was captured at HEAD in place (tracing from a detached worktree
changes REPO_DIR, which is interpolated into a generated file, and fakes a
diff), then re-run after the split. The traces are identical -- same exit
status, same ordered stubbed calls, and all five generated-file hashes
unchanged, including usage-report-catchup.sh at sha=9f4803ab157c5527.

REPO_DIR is computed once in the entry and read as a global rather than
recomputed inside a lib one directory deeper; recomputing it would have
pointed the generated catch-up script at the wrong repo while still exiting 0.

test_usage_monitoring_installer_efficiency.sh greps the installer's source
text for the pmon heredoc, so it follows that heredoc to lib/nvidia_pmon.sh.
Its extraction anchor also had to become whitespace-insensitive: the
pre-commit hook runs `shfmt -w` on staged shell files and rewrites
`cat > "x" << 'SCRIPT'` to `cat >"x" <<'SCRIPT'`, which silently broke
extraction. All five property assertions are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
Krzysztof Rudnicki 2026-08-17 07:26:56 +02:00
parent 3a97f0517b
commit 26965bad32
9 changed files with 412 additions and 267 deletions

View File

@ -0,0 +1,18 @@
{
"title": "Split install_usage_monitoring.sh under the 250-line cap",
"objective": "Bring linux_configuration/scripts/periodic_background/system-maintenance/bin/install_usage_monitoring.sh (290 lines) under the repo-wide 250-line file cap by moving its five banner-marked steps into a sibling lib/ directory, with byte-identical runtime behaviour proven by an executed before/after trace rather than by inspection.",
"acceptance_criteria": [
"install_usage_monitoring.sh and all five new lib/*.sh files are each at most 250 lines.",
"meta/scripts/trace_shell_split.sh --prefix produces an identical trace before and after the split: same exit status, same stubbed calls in the same order, and all five generated-file content hashes unchanged (notably usage-report-catchup.sh at sha=9f4803ab157c5527, which is where a wrong REPO_DIR would show up).",
"linux_configuration/tests/test_usage_monitoring_installer_efficiency.sh still passes with all five of its property assertions unchanged.",
"shellcheck clean and shfmt clean on all seven touched files, with no suppression added -- no # shellcheck disable, no per-file ignores.",
"The verification runs write nothing outside the trace prefix: ~/.config/systemd/user/ and ~/.local/bin/ mtimes still read May 2026."
],
"out_of_scope": [
"Any behaviour change, including 'tidying' the absolute path in the final log line into a relative one.",
"Weakening or rewriting the five property assertions in the efficiency test; only its INSTALLER path and the whitespace-tolerance of its extraction anchor may change.",
"Wiring the file-length pre-commit hook -- it must land last, once check_file_length.sh --all exits 0.",
"Fixing the pre-existing repo-wide jscpd overage caused by vendored .venv site-packages."
],
"verifier": "meta/scripts/trace_shell_split.sh install_usage_monitoring.sh --prefix <fresh-empty-dir> captured at HEAD in place and again after the split, then diff of the two traces (must be empty); bash linux_configuration/tests/test_usage_monitoring_installer_efficiency.sh; shellcheck and shfmt -d on all seven files; bash ~/utils/scripts/check_file_length.sh --all for the cap itself; ls -la on ~/.local/bin and ~/.config/systemd/user to confirm the live system was untouched."
}

View File

@ -0,0 +1,64 @@
{
"intent": "Bring install_usage_monitoring.sh under the 250-line cap by splitting its five steps into a sibling lib/, without changing what the installer does to a machine.",
"scope": [
"linux_configuration/scripts/periodic_background/system-maintenance/bin/install_usage_monitoring.sh (290 -> 53 lines)",
"New: bin/lib/{distro_detect,packages,system_services,nvidia_pmon,catchup_timer}.sh",
"linux_configuration/tests/test_usage_monitoring_installer_efficiency.sh (INSTALLER path + whitespace-tolerant anchor)",
"Non-goal: any behaviour change; the trace is the assertion that there is none",
"Non-goal: wiring the file-length hook, or the pre-existing jscpd venv overage"
],
"changes": [
"Moved the five banner-marked sections into lib/ files sourced by the entry script, each wrapped in a named function called in the original order.",
"REPO_DIR is computed once in the entry script from its own SCRIPT_DIR and read as a global by catchup_timer.sh, rather than recomputed at a lib's directory depth -- recomputing would have silently interpolated the wrong repo root into the generated catch-up script while still exiting 0.",
"Entry SCRIPT_DIR uses readlink -f on ${BASH_SOURCE[0]} so a symlinked entry point still resolves lib/; the final log line's usage_report.py path is built from that same value so it keeps pointing at bin/, not bin/lib/.",
"The PKG_* associative arrays stay at lib top level, since `declare -A` inside a function is local and would make every pkg_name lookup return empty.",
"resolve_and_install_packages ends its bare `[[ -n $clip ]] && pkgs+=(...)` conditional with `|| true` so a false tail cannot become the function's return value under set -e.",
"Repointed the efficiency test's INSTALLER at lib/nvidia_pmon.sh (which now holds the heredoc it greps) and made its awk extraction anchor whitespace-insensitive, because the repo's pre-commit hook runs `shfmt -w` on staged shell files and rewrites `cat > \"x\" << 'SCRIPT'` to `cat >\"x\" <<'SCRIPT'`. All five property assertions are unchanged."
],
"verification": [
{
"command": "meta/scripts/trace_shell_split.sh install_usage_monitoring.sh --prefix /tmp/pfx-before --out /tmp/before.txt (at HEAD, in place at the real repo path)",
"result": "pass",
"evidence": "exit 0; 11 stubbed calls; 5 files written matching the brief's recorded baseline byte for byte, including usage-report-catchup.sh size=757 sha=9f4803ab157c5527"
},
{
"command": "meta/scripts/trace_shell_split.sh install_usage_monitoring.sh --prefix /tmp/pfx-after2 --out /tmp/after2.txt; diff /tmp/before.txt /tmp/after2.txt",
"result": "pass",
"evidence": "Empty diff on the post-shfmt split: identical exit status, identical ordered stubbed calls, and all five content hashes unchanged. Each run used a fresh nonexistent prefix."
},
{
"command": "bash linux_configuration/tests/test_usage_monitoring_installer_efficiency.sh",
"result": "pass",
"evidence": "'Usage monitoring installer efficiency tests passed.' -- all five assertions (no read -t busy-loop, sleeps to day boundary, blocks on wait, printf date builtin, no external date) run against the relocated heredoc."
},
{
"command": "shellcheck + shfmt -d on all seven touched files",
"result": "pass",
"evidence": "No shellcheck findings and no shfmt diff on any of the seven, after applying shfmt -w so the committed state equals what the pre-commit hook produces."
},
{
"command": "bash ~/utils/scripts/check_file_length.sh --all",
"result": "pass",
"evidence": "None of the six files appear in the over-cap list. Line counts: entry 53, catchup_timer 82, nvidia_pmon 89, packages 73, distro_detect 36, system_services 20."
},
{
"command": "jscpd --pattern '**/*.sh' --min-lines 14 --threshold 2, run at HEAD in a clean worktree and again with the split applied",
"result": "pass",
"evidence": "27 clones / 1.47% in both cases -- the split adds zero duplication. The 2.51% seen from the main working tree is entirely vendored .venv and .ci-mirror-venv site-packages (tqdm/completion.sh, virtualenv activate.sh), pre-existing and unrelated."
},
{
"command": "ls -la ~/.local/bin/{nvidia-pmon-logger,usage-report-catchup}.sh ~/.config/systemd/user/nvidia-pmon.service",
"result": "pass",
"evidence": "mtimes still May 1 and May 6 2026 after every trace run -- no write escaped the prefix into the live system."
}
],
"risks": [
"The efficiency test greps a lib's source text, so moving the pmon heredoc again -- or reformatting it -- will break extraction rather than an assertion. The lib header says so and the anchor is now whitespace-tolerant, but a rename of lib/nvidia_pmon.sh still requires updating the test's INSTALLER in the same commit.",
"REPO_DIR is a global read across a seam. A future lib that shadows it with `local` would change the generated catch-up script's contents while the installer still exits 0; only the trace hash catches that.",
"The installer was verified under trace stubs, not by a real run against a live machine, so package-manager behaviour on non-Arch families remains as untested as it was before the split."
],
"rollback": [
"git revert the commit; the entry script returns to its 290-line form and lib/ disappears. The generated artifacts on disk are unaffected either way, since neither form was ever executed against the live system here.",
"After reverting, re-run bash linux_configuration/tests/test_usage_monitoring_installer_efficiency.sh -- it must still print the passed line, since the revert restores both the heredoc location and the original INSTALLER path together."
]
}

View File

@ -10,281 +10,44 @@
#
# Works on Arch, Debian/Ubuntu (and derivatives), Fedora/RHEL, openSUSE.
# Re-run safely; everything is idempotent.
#
# The steps live in lib/; this file owns the shared helpers, the globals that
# cross between steps (FAMILY, pkgs, clip, unit_dir, REPO_DIR) and the order.
set -euo pipefail
log() { printf '[install-usage] %s\n' "$*" >&2; }
die() {
printf '[install-usage] ERROR: %s\n' "$*" >&2
exit 1
printf '[install-usage] ERROR: %s\n' "$*" >&2
exit 1
}
# readlink -f so a symlinked entry point still finds lib/.
SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
# shellcheck source=lib/distro_detect.sh
. "$SCRIPT_DIR/lib/distro_detect.sh"
# shellcheck source=lib/packages.sh
. "$SCRIPT_DIR/lib/packages.sh"
# shellcheck source=lib/system_services.sh
. "$SCRIPT_DIR/lib/system_services.sh"
# shellcheck source=lib/nvidia_pmon.sh
. "$SCRIPT_DIR/lib/nvidia_pmon.sh"
# shellcheck source=lib/catchup_timer.sh
. "$SCRIPT_DIR/lib/catchup_timer.sh"
[[ $EUID -eq 0 ]] && die "run as your normal user; sudo is invoked where needed"
command -v sudo > /dev/null 2>&1 || die "sudo is required"
command -v sudo >/dev/null 2>&1 || die "sudo is required"
# --- Distro detection -------------------------------------------------------
. /etc/os-release 2> /dev/null || die "cannot read /etc/os-release"
detect_family
resolve_and_install_packages
enable_system_services
setup_nvidia_pmon
FAMILY=""
for id in ${ID:-} ${ID_LIKE:-}; do
case "$id" in
arch | manjaro | endeavouros)
FAMILY="arch"
break
;;
debian | ubuntu | linuxmint | pop)
FAMILY="debian"
break
;;
elementary)
FAMILY="debian"
break
;;
fedora | rhel | centos)
FAMILY="fedora"
break
;;
opensuse* | suse | sles)
FAMILY="suse"
break
;;
esac
done
[[ -n $FAMILY ]] || die "unsupported distro: ID=${ID:-?} ID_LIKE=${ID_LIKE:-?}"
log "detected distro family: $FAMILY (${PRETTY_NAME:-unknown})"
# --- Package names per family ----------------------------------------------
# Format: "<generic>=<package>"; empty package = skip on this distro.
declare -A PKG_ARCH=(
[atop]=atop [nvtop]=nvtop [netdata]=netdata
[wl_clipboard]=wl-clipboard [xclip]=xclip
)
declare -A PKG_DEBIAN=(
[atop]=atop [nvtop]=nvtop [netdata]=netdata
[wl_clipboard]=wl-clipboard [xclip]=xclip
)
declare -A PKG_FEDORA=(
[atop]=atop [nvtop]=nvtop [netdata]=netdata
[wl_clipboard]=wl-clipboard [xclip]=xclip
)
declare -A PKG_SUSE=(
[atop]=atop [nvtop]=nvtop [netdata]=netdata
[wl_clipboard]=wl-clipboard [xclip]=xclip
)
pkg_name() {
local key=$1
case "$FAMILY" in
arch) printf '%s' "${PKG_ARCH[$key]-}" ;;
debian) printf '%s' "${PKG_DEBIAN[$key]-}" ;;
fedora) printf '%s' "${PKG_FEDORA[$key]-}" ;;
suse) printf '%s' "${PKG_SUSE[$key]-}" ;;
esac
}
install_packages() {
local -a pkgs=("$@")
[[ ${#pkgs[@]} -eq 0 ]] && return 0
log "installing: ${pkgs[*]}"
case "$FAMILY" in
arch) sudo pacman -S --needed --noconfirm "${pkgs[@]}" ;;
debian)
sudo apt-get update -qq
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y "${pkgs[@]}"
;;
fedora) sudo dnf install -y "${pkgs[@]}" ;;
suse) sudo zypper --non-interactive install "${pkgs[@]}" ;;
esac
}
# --- Choose a clipboard tool matching the session --------------------------
clipboard_pkg() {
if [[ ${XDG_SESSION_TYPE:-} == "wayland" ]]; then
pkg_name wl_clipboard
else
pkg_name xclip
fi
}
# --- Resolve final package set ---------------------------------------------
want_keys=(atop nvtop netdata)
pkgs=()
for key in "${want_keys[@]}"; do
p=$(pkg_name "$key")
[[ -n $p ]] && pkgs+=("$p")
done
clip=$(clipboard_pkg)
[[ -n $clip ]] && pkgs+=("$clip")
install_packages "${pkgs[@]}"
# --- Enable system services -------------------------------------------------
enable_unit() {
local unit=$1
if systemctl list-unit-files "$unit" > /dev/null 2>&1; then
log "enabling $unit"
sudo systemctl enable --now "$unit" || log "warn: failed to enable $unit"
else
log "skip $unit (not present on this system)"
fi
}
enable_unit atop.service
# atop-rotate exists on Arch; Debian/Ubuntu rotate via cron instead.
enable_unit atop-rotate.timer
enable_unit netdata.service
# --- NVIDIA per-process GPU logger (optional) -------------------------------
if command -v nvidia-smi > /dev/null 2>&1; then
log "setting up nvidia-pmon user service"
mkdir -p "$HOME/.local/share/gpu-log"
mkdir -p "$HOME/.local/bin"
unit_dir="$HOME/.config/systemd/user"
mkdir -p "$unit_dir"
# Install the day-rolling wrapper script.
cat > "$HOME/.local/bin/nvidia-pmon-logger.sh" << 'SCRIPT'
#!/bin/bash
set -euo pipefail
LOG_DIR="$HOME/.local/share/gpu-log"
ERR_LOG="$LOG_DIR/pmon-errors.log"
mkdir -p "$LOG_DIR"
if ! command -v nvidia-smi >/dev/null 2>&1; then
echo "nvidia-pmon-logger: nvidia-smi not found" >&2
exit 1
fi
current_day() {
printf '%(%Y%m%d)T' -1
}
seconds_until_next_day() {
local hour minute second
printf -v hour '%(%H)T' -1
printf -v minute '%(%M)T' -1
printf -v second '%(%S)T' -1
printf '%s\n' $(((23 - 10#$hour) * 3600 + (59 - 10#$minute) * 60 + (60 - 10#$second)))
}
while true; do
day="$(current_day)"
out_file="$LOG_DIR/pmon-${day}.log"
rollover_pid=''
nvidia-smi pmon -d 10 -o DT >> "$out_file" 2>> "$ERR_LOG" &
pmon_pid=$!
(
sleep "$(seconds_until_next_day)"
kill "$pmon_pid" >/dev/null 2>&1 || true
) &
rollover_pid=$!
wait "$pmon_pid" || true
if [[ -n $rollover_pid ]]; then
kill "$rollover_pid" >/dev/null 2>&1 || true
wait "$rollover_pid" 2>/dev/null || true
fi
done
SCRIPT
chmod +x "$HOME/.local/bin/nvidia-pmon-logger.sh"
cat > "$unit_dir/nvidia-pmon.service" << 'UNIT'
[Unit]
Description=Per-day NVIDIA pmon logger
After=default.target
[Service]
Type=simple
ExecStart=%h/.local/bin/nvidia-pmon-logger.sh
Restart=always
RestartSec=5
[Install]
WantedBy=default.target
UNIT
systemctl --user daemon-reload
systemctl --user enable --now nvidia-pmon.service || log "warn: nvidia-pmon user service failed"
else
log "no nvidia-smi found; skipping GPU per-process logger"
fi
# --- Daily usage-report catch-up timer -------------------------------------
REPO_DIR="$(dirname "$(readlink -f "$0")")/../../../../.."
# Resolved here, once, from the entry script's own location: lib/ sits one
# level deeper, so a lib recomputing this would point at the wrong repo.
REPO_DIR="$SCRIPT_DIR/../../../../.."
REPO_DIR="$(readlink -f "$REPO_DIR")"
unit_dir="$HOME/.config/systemd/user"
mkdir -p "$unit_dir" "$HOME/.local/bin" "$HOME/.local/share/usage-reports"
cat > "$HOME/.local/bin/usage-report-catchup.sh" << SCRIPT
#!/bin/bash
set -euo pipefail
REPO="$REPO_DIR"
RUN_SCRIPT="\$REPO/run.sh"
OUT_DIR="\$HOME/.local/share/usage-reports"
ATOP_DIR="/var/log/atop"
mkdir -p "\$OUT_DIR"
if [[ ! -x "\$RUN_SCRIPT" ]]; then
echo "usage-report-catchup: missing executable \$RUN_SCRIPT" >&2
exit 1
fi
shopt -s nullglob
TODAY="\$(date +%Y%m%d)"
for atop_file in "\$ATOP_DIR"/atop_*; do
date_part="\${atop_file##*_}"
if [[ ! "\$date_part" =~ ^[0-9]{8}\$ ]]; then
continue
fi
out_file="\$OUT_DIR/usage-report-\${date_part}.md"
tmp_file="\$out_file.tmp"
if [[ "\$date_part" == "\$TODAY" || ! -s "\$out_file" ]]; then
if "\$RUN_SCRIPT" --date "\$date_part" > "\$tmp_file"; then
mv -f "\$tmp_file" "\$out_file"
else
rm -f "\$tmp_file"
fi
fi
done
SCRIPT
chmod +x "$HOME/.local/bin/usage-report-catchup.sh"
cat > "$unit_dir/usage-report-catchup.service" << 'UNIT'
[Unit]
Description=Generate usage reports for available atop days
After=default.target
[Service]
Type=oneshot
ExecStart=%h/.local/bin/usage-report-catchup.sh
UNIT
cat > "$unit_dir/usage-report-catchup.timer" << 'UNIT'
[Unit]
Description=Run usage report catch-up hourly
Requires=usage-report-catchup.service
[Timer]
OnBootSec=2min
OnCalendar=hourly
RandomizedDelaySec=2min
Persistent=true
[Install]
WantedBy=timers.target
UNIT
systemctl --user daemon-reload
systemctl --user enable --now usage-report-catchup.timer || log "warn: usage-report-catchup timer failed"
log "usage reports will be generated hourly in $HOME/.local/share/usage-reports/"
setup_catchup_timer
log "done. Wait for the first atop sample (default 10 min), then run:"
log " python $(dirname "$(readlink -f "$0")")/usage_report.py"
log " python $SCRIPT_DIR/usage_report.py"

View File

@ -0,0 +1,82 @@
#!/bin/bash
# Installs the hourly usage-report catch-up user timer.
# Sourced by install_usage_monitoring.sh; inherits the caller's strict mode.
#
# REPO_DIR is computed ONCE by the entry script and read here as a global. It
# is interpolated into the generated catch-up script at write time, so
# recomputing it from this file's own depth would silently point the generated
# script at the wrong repo while still exiting 0.
setup_catchup_timer() {
unit_dir="$HOME/.config/systemd/user"
mkdir -p "$unit_dir" "$HOME/.local/bin" "$HOME/.local/share/usage-reports"
# Unquoted delimiter: $HOME and $REPO_DIR interpolate at write time, while
# \$REPO and \$OUT_DIR stay literal. The escaping is load-bearing.
cat >"$HOME/.local/bin/usage-report-catchup.sh" <<SCRIPT
#!/bin/bash
set -euo pipefail
REPO="$REPO_DIR"
RUN_SCRIPT="\$REPO/run.sh"
OUT_DIR="\$HOME/.local/share/usage-reports"
ATOP_DIR="/var/log/atop"
mkdir -p "\$OUT_DIR"
if [[ ! -x "\$RUN_SCRIPT" ]]; then
echo "usage-report-catchup: missing executable \$RUN_SCRIPT" >&2
exit 1
fi
shopt -s nullglob
TODAY="\$(date +%Y%m%d)"
for atop_file in "\$ATOP_DIR"/atop_*; do
date_part="\${atop_file##*_}"
if [[ ! "\$date_part" =~ ^[0-9]{8}\$ ]]; then
continue
fi
out_file="\$OUT_DIR/usage-report-\${date_part}.md"
tmp_file="\$out_file.tmp"
if [[ "\$date_part" == "\$TODAY" || ! -s "\$out_file" ]]; then
if "\$RUN_SCRIPT" --date "\$date_part" > "\$tmp_file"; then
mv -f "\$tmp_file" "\$out_file"
else
rm -f "\$tmp_file"
fi
fi
done
SCRIPT
chmod +x "$HOME/.local/bin/usage-report-catchup.sh"
cat >"$unit_dir/usage-report-catchup.service" <<'UNIT'
[Unit]
Description=Generate usage reports for available atop days
After=default.target
[Service]
Type=oneshot
ExecStart=%h/.local/bin/usage-report-catchup.sh
UNIT
cat >"$unit_dir/usage-report-catchup.timer" <<'UNIT'
[Unit]
Description=Run usage report catch-up hourly
Requires=usage-report-catchup.service
[Timer]
OnBootSec=2min
OnCalendar=hourly
RandomizedDelaySec=2min
Persistent=true
[Install]
WantedBy=timers.target
UNIT
systemctl --user daemon-reload
systemctl --user enable --now usage-report-catchup.timer || log "warn: usage-report-catchup timer failed"
log "usage reports will be generated hourly in $HOME/.local/share/usage-reports/"
}

View File

@ -0,0 +1,36 @@
#!/bin/bash
# Maps /etc/os-release onto the FAMILY value the package layer switches on.
# Sourced by install_usage_monitoring.sh; inherits the caller's strict mode.
# Sets the global FAMILY. Uses log()/die() from the entry script.
detect_family() {
. /etc/os-release 2>/dev/null || die "cannot read /etc/os-release"
FAMILY=""
for id in ${ID:-} ${ID_LIKE:-}; do
case "$id" in
arch | manjaro | endeavouros)
FAMILY="arch"
break
;;
debian | ubuntu | linuxmint | pop)
FAMILY="debian"
break
;;
elementary)
FAMILY="debian"
break
;;
fedora | rhel | centos)
FAMILY="fedora"
break
;;
opensuse* | suse | sles)
FAMILY="suse"
break
;;
esac
done
[[ -n $FAMILY ]] || die "unsupported distro: ID=${ID:-?} ID_LIKE=${ID_LIKE:-?}"
log "detected distro family: $FAMILY (${PRETTY_NAME:-unknown})"
}

View File

@ -0,0 +1,89 @@
#!/bin/bash
# Installs the optional per-process NVIDIA GPU logger (user service).
# Sourced by install_usage_monitoring.sh; inherits the caller's strict mode.
#
# The nvidia-pmon-logger.sh heredoc below is asserted against by
# tests/test_usage_monitoring_installer_efficiency.sh, which greps THIS file's
# source text for the `cat > ... << 'SCRIPT'` anchor line. That match is
# whitespace-insensitive, so shfmt's `cat >"..." <<'SCRIPT'` form is fine, but
# the heredoc BODY is compared property-by-property -- keep it verbatim.
setup_nvidia_pmon() {
if command -v nvidia-smi >/dev/null 2>&1; then
log "setting up nvidia-pmon user service"
mkdir -p "$HOME/.local/share/gpu-log"
mkdir -p "$HOME/.local/bin"
unit_dir="$HOME/.config/systemd/user"
mkdir -p "$unit_dir"
# Install the day-rolling wrapper script.
cat >"$HOME/.local/bin/nvidia-pmon-logger.sh" <<'SCRIPT'
#!/bin/bash
set -euo pipefail
LOG_DIR="$HOME/.local/share/gpu-log"
ERR_LOG="$LOG_DIR/pmon-errors.log"
mkdir -p "$LOG_DIR"
if ! command -v nvidia-smi >/dev/null 2>&1; then
echo "nvidia-pmon-logger: nvidia-smi not found" >&2
exit 1
fi
current_day() {
printf '%(%Y%m%d)T' -1
}
seconds_until_next_day() {
local hour minute second
printf -v hour '%(%H)T' -1
printf -v minute '%(%M)T' -1
printf -v second '%(%S)T' -1
printf '%s\n' $(((23 - 10#$hour) * 3600 + (59 - 10#$minute) * 60 + (60 - 10#$second)))
}
while true; do
day="$(current_day)"
out_file="$LOG_DIR/pmon-${day}.log"
rollover_pid=''
nvidia-smi pmon -d 10 -o DT >> "$out_file" 2>> "$ERR_LOG" &
pmon_pid=$!
(
sleep "$(seconds_until_next_day)"
kill "$pmon_pid" >/dev/null 2>&1 || true
) &
rollover_pid=$!
wait "$pmon_pid" || true
if [[ -n $rollover_pid ]]; then
kill "$rollover_pid" >/dev/null 2>&1 || true
wait "$rollover_pid" 2>/dev/null || true
fi
done
SCRIPT
chmod +x "$HOME/.local/bin/nvidia-pmon-logger.sh"
cat >"$unit_dir/nvidia-pmon.service" <<'UNIT'
[Unit]
Description=Per-day NVIDIA pmon logger
After=default.target
[Service]
Type=simple
ExecStart=%h/.local/bin/nvidia-pmon-logger.sh
Restart=always
RestartSec=5
[Install]
WantedBy=default.target
UNIT
systemctl --user daemon-reload
systemctl --user enable --now nvidia-pmon.service || log "warn: nvidia-pmon user service failed"
else
log "no nvidia-smi found; skipping GPU per-process logger"
fi
}

View File

@ -0,0 +1,73 @@
#!/bin/bash
# Per-family package name tables and the installer front-end.
# Sourced by install_usage_monitoring.sh; inherits the caller's strict mode.
# Format: "<generic>=<package>"; empty package = skip on this distro.
# These stay at top level on purpose: `declare -A` inside a function is local,
# which would make every pkg_name lookup return empty.
declare -A PKG_ARCH=(
[atop]=atop [nvtop]=nvtop [netdata]=netdata
[wl_clipboard]=wl-clipboard [xclip]=xclip
)
declare -A PKG_DEBIAN=(
[atop]=atop [nvtop]=nvtop [netdata]=netdata
[wl_clipboard]=wl-clipboard [xclip]=xclip
)
declare -A PKG_FEDORA=(
[atop]=atop [nvtop]=nvtop [netdata]=netdata
[wl_clipboard]=wl-clipboard [xclip]=xclip
)
declare -A PKG_SUSE=(
[atop]=atop [nvtop]=nvtop [netdata]=netdata
[wl_clipboard]=wl-clipboard [xclip]=xclip
)
pkg_name() {
local key=$1
case "$FAMILY" in
arch) printf '%s' "${PKG_ARCH[$key]-}" ;;
debian) printf '%s' "${PKG_DEBIAN[$key]-}" ;;
fedora) printf '%s' "${PKG_FEDORA[$key]-}" ;;
suse) printf '%s' "${PKG_SUSE[$key]-}" ;;
esac
}
install_packages() {
local -a pkgs=("$@")
[[ ${#pkgs[@]} -eq 0 ]] && return 0
log "installing: ${pkgs[*]}"
case "$FAMILY" in
arch) sudo pacman -S --needed --noconfirm "${pkgs[@]}" ;;
debian)
sudo apt-get update -qq
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y "${pkgs[@]}"
;;
fedora) sudo dnf install -y "${pkgs[@]}" ;;
suse) sudo zypper --non-interactive install "${pkgs[@]}" ;;
esac
}
# Pick a clipboard tool matching the session type.
clipboard_pkg() {
if [[ ${XDG_SESSION_TYPE:-} == "wayland" ]]; then
pkg_name wl_clipboard
else
pkg_name xclip
fi
}
# Resolves the global `pkgs` array (and `clip`) from FAMILY, then installs.
# The trailing `|| true` guards the bare conditional from ending the function
# with a false return value under `set -e`.
resolve_and_install_packages() {
want_keys=(atop nvtop netdata)
pkgs=()
for key in "${want_keys[@]}"; do
p=$(pkg_name "$key")
[[ -n $p ]] && pkgs+=("$p")
done
clip=$(clipboard_pkg)
[[ -n $clip ]] && pkgs+=("$clip") || true
install_packages "${pkgs[@]}"
}

View File

@ -0,0 +1,20 @@
#!/bin/bash
# Enables the system-level units the monitoring stack needs.
# Sourced by install_usage_monitoring.sh; inherits the caller's strict mode.
enable_unit() {
local unit=$1
if systemctl list-unit-files "$unit" >/dev/null 2>&1; then
log "enabling $unit"
sudo systemctl enable --now "$unit" || log "warn: failed to enable $unit"
else
log "skip $unit (not present on this system)"
fi
}
enable_system_services() {
enable_unit atop.service
# atop-rotate exists on Arch; Debian/Ubuntu rotate via cron instead.
enable_unit atop-rotate.timer
enable_unit netdata.service
}

View File

@ -5,7 +5,7 @@ set -euo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd)
INSTALLER="$REPO_DIR/scripts/periodic_background/system-maintenance/bin/install_usage_monitoring.sh"
INSTALLER="$REPO_DIR/scripts/periodic_background/system-maintenance/bin/lib/nvidia_pmon.sh"
fail() {
printf 'FAIL: %s\n' "$1" >&2
@ -14,7 +14,7 @@ fail() {
logger_template=$(
awk '
/cat > "\$HOME\/.local\/bin\/nvidia-pmon-logger\.sh" << '\''SCRIPT'\''/ {capture=1; next}
/cat >[[:space:]]*"\$HOME\/.local\/bin\/nvidia-pmon-logger\.sh"[[:space:]]*<<[[:space:]]*'\''SCRIPT'\''/ {capture=1; next}
capture && /^SCRIPT$/ {capture=0; exit}
capture {print}
' "$INSTALLER"