From 26965bad32787a2195d4791cf0d273e6c02b0dca Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Mon, 17 Aug 2026 07:26:56 +0200 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY --- ...all-usage-monitoring-split-2026-08-17.json | 18 ++ ...all-usage-monitoring-split-2026-08-17.json | 64 ++++ .../bin/install_usage_monitoring.sh | 293 ++---------------- .../bin/lib/catchup_timer.sh | 82 +++++ .../bin/lib/distro_detect.sh | 36 +++ .../system-maintenance/bin/lib/nvidia_pmon.sh | 89 ++++++ .../system-maintenance/bin/lib/packages.sh | 73 +++++ .../bin/lib/system_services.sh | 20 ++ ...t_usage_monitoring_installer_efficiency.sh | 4 +- 9 files changed, 412 insertions(+), 267 deletions(-) create mode 100644 docs/superpowers/contracts/install-usage-monitoring-split-2026-08-17.json create mode 100644 docs/superpowers/evidence/install-usage-monitoring-split-2026-08-17.json create mode 100755 linux_configuration/scripts/periodic_background/system-maintenance/bin/lib/catchup_timer.sh create mode 100755 linux_configuration/scripts/periodic_background/system-maintenance/bin/lib/distro_detect.sh create mode 100755 linux_configuration/scripts/periodic_background/system-maintenance/bin/lib/nvidia_pmon.sh create mode 100755 linux_configuration/scripts/periodic_background/system-maintenance/bin/lib/packages.sh create mode 100755 linux_configuration/scripts/periodic_background/system-maintenance/bin/lib/system_services.sh diff --git a/docs/superpowers/contracts/install-usage-monitoring-split-2026-08-17.json b/docs/superpowers/contracts/install-usage-monitoring-split-2026-08-17.json new file mode 100644 index 00000000..0f9569fe --- /dev/null +++ b/docs/superpowers/contracts/install-usage-monitoring-split-2026-08-17.json @@ -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 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." +} diff --git a/docs/superpowers/evidence/install-usage-monitoring-split-2026-08-17.json b/docs/superpowers/evidence/install-usage-monitoring-split-2026-08-17.json new file mode 100644 index 00000000..f47f011b --- /dev/null +++ b/docs/superpowers/evidence/install-usage-monitoring-split-2026-08-17.json @@ -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." + ] +} diff --git a/linux_configuration/scripts/periodic_background/system-maintenance/bin/install_usage_monitoring.sh b/linux_configuration/scripts/periodic_background/system-maintenance/bin/install_usage_monitoring.sh index afadbb6c..fdfde53b 100755 --- a/linux_configuration/scripts/periodic_background/system-maintenance/bin/install_usage_monitoring.sh +++ b/linux_configuration/scripts/periodic_background/system-maintenance/bin/install_usage_monitoring.sh @@ -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: "="; 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" diff --git a/linux_configuration/scripts/periodic_background/system-maintenance/bin/lib/catchup_timer.sh b/linux_configuration/scripts/periodic_background/system-maintenance/bin/lib/catchup_timer.sh new file mode 100755 index 00000000..81d5d7e6 --- /dev/null +++ b/linux_configuration/scripts/periodic_background/system-maintenance/bin/lib/catchup_timer.sh @@ -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" <