mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:27:02 +01:00
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:
parent
3a97f0517b
commit
26965bad32
@ -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."
|
||||
}
|
||||
@ -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."
|
||||
]
|
||||
}
|
||||
@ -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"
|
||||
|
||||
@ -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/"
|
||||
}
|
||||
@ -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})"
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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[@]}"
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user