mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 19:07:04 +01:00
Two levels of pure taxonomy carrying no information, and a 2-directory
cap leaves no room for them. Categories now sit directly under
linux_configuration/: features, fixes, utils, misc, fresh-install,
installers, diagnostics, gaming, boot_recovery, periodic_background, lib,
meta, tests. A script's own lib/ and tests/ are exempt from the cap, so
those stay put.
Depth violations 248 -> 139; roughly 40 of the remainder are inside
digital_wellbeing and hosts, deferred until the guard-lib repoint.
The moves are the easy half. What breaks is every path that counted
levels, and three classes of it had to be found by resolving paths rather
than reading diffs:
- 52 relative `source` lines for lib/common.sh and lib/android.sh. The
correct prefix differs per file: 40 were ../../ and 12 were ../../../,
so a blanket replace would have fixed one set and broken the other.
- REPO_ROOT in 7 fixes/lib/tests harnesses and setup_syncyomi_guard.sh,
each walking up four levels from a directory now two levels shallower.
This caused a real regression -- the fixes suite went red with
"log_warn: command not found" -- caught by running it and comparing
against the pre-move baseline worktree.
- conftest.py's sys.path entries, which took the pytest gate down with
14 collection errors, and the `# shellcheck source=` directives, which
took shellcheck down with SC1091.
16 symlinks in $HOME broke, including the entire i3blocks bar. All
repointed, and cpu_monitor/memory/disk/time/volume were run to confirm
they emit real output rather than merely resolving.
Verified: 1798 tests pass at 100% coverage, 8 of 9 lib/tests suites pass
(the ninth needs the coverage jail, identically at baseline), pre-commit
is clean, and the two untracked secret configs are still ignored after
their gitignore paths changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
250 lines
7.1 KiB
Bash
Executable File
250 lines
7.1 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# ============================================================================
|
|
# diagnose_pacman_hook_stall.sh
|
|
#
|
|
# Reproduces and instruments the intermittent pacman freeze that happens at the
|
|
# FIRST PreTransaction hook of a transaction:
|
|
#
|
|
# :: Running pre-transaction hooks...
|
|
# (1/2) guard-lib: unlocking protected files before pacman transaction
|
|
# <hangs for minutes>
|
|
#
|
|
# /var/log/pacman.log shows the hook's "running '...hook'..." line and then
|
|
# nothing at all - the next entry is the user's retry. Five such transactions
|
|
# were abandoned between 2026-07-29 and 2026-08-03. The hook script itself is
|
|
# provably a no-op on this machine (/etc/guard-lib/targets is empty, so its
|
|
# loop body never executes), which means the stall is pacman forking/exec'ing
|
|
# the hook, not the hook doing work.
|
|
#
|
|
# This script drives real transactions in a loop and, when one stalls, captures
|
|
# the kernel-side evidence needed to tell "blocked in a syscall" from "the box
|
|
# could not spawn a process": /proc/<pid>/stack, wchan, syscall, the process
|
|
# tree, and memory/PSI counters.
|
|
#
|
|
# The repro target is a 0-file meta-package (base-devel), reinstalled from the
|
|
# local cache: a real transaction that fires every hook but writes no files and
|
|
# needs no network.
|
|
# ============================================================================
|
|
|
|
set -euo pipefail
|
|
|
|
# shellcheck source=lib/pacman_hook_stall_summary.sh
|
|
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/pacman_hook_stall_summary.sh"
|
|
|
|
# shellcheck source=lib/pacman_hook_stall_usage.sh
|
|
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/pacman_hook_stall_usage.sh"
|
|
|
|
# shellcheck source=lib/pacman_hook_stall_watch.sh
|
|
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/pacman_hook_stall_watch.sh"
|
|
|
|
# shellcheck source=lib/pacman_hook_stall_capture.sh
|
|
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/pacman_hook_stall_capture.sh"
|
|
|
|
# shellcheck source=lib/pacman_hook_stall_load.sh
|
|
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/pacman_hook_stall_load.sh"
|
|
|
|
# shellcheck source=lib/pacman_hook_stall_setup.sh
|
|
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/pacman_hook_stall_setup.sh"
|
|
|
|
SCRIPT_NAME="$(basename "$0")"
|
|
readonly SCRIPT_NAME
|
|
# All four below are env-overridable (not hardcoded readonly) so the test
|
|
# harness can point them at fakes instead of the real machine; production
|
|
# gets the same defaults as before since nothing else sets these.
|
|
: "${PACMAN_BIN:=/usr/bin/pacman.orig}"
|
|
readonly PACMAN_BIN
|
|
: "${PACMAN_LOG:=/var/log/pacman.log}"
|
|
readonly PACMAN_LOG
|
|
: "${CACHE_DIR:=/var/cache/pacman/pkg}"
|
|
readonly CACHE_DIR
|
|
: "${PACMAN_LOCK:=/var/lib/pacman/db.lck}"
|
|
readonly PACMAN_LOCK
|
|
|
|
# Defaults
|
|
RUNS=40
|
|
PACKAGE="base-devel"
|
|
STALL_TIMEOUT=20 # seconds with no new pacman.log line => declare a stall
|
|
HARD_TIMEOUT=120 # seconds before we give up and kill the stalled transaction
|
|
OUT_DIR="/var/log/pacman-hook-stall"
|
|
WITH_LOAD=0
|
|
WATCH_MODE=0
|
|
# Env-overridable like PACMAN_BIN etc above, so a test can force a tiny allocation.
|
|
: "${LOAD_FLOOR_MB:=800}" # never push MemAvailable below this
|
|
: "${LOAD_MIN_FREE_MB:=1500}" # refuse to start --with-load below this much available
|
|
|
|
HOG_FILE=""
|
|
LOAD_PID=""
|
|
PACMAN_PID=""
|
|
STALLS=0
|
|
RUN_INDEX=0
|
|
LAST_ELAPSED=0
|
|
|
|
cleanup() {
|
|
local rc=$?
|
|
stop_load
|
|
if [[ -n "$PACMAN_PID" ]] && kill -0 "$PACMAN_PID" 2>/dev/null; then
|
|
echo "Cleaning up: killing in-flight pacman ($PACMAN_PID)" >&2
|
|
kill_tree "$PACMAN_PID"
|
|
fi
|
|
exit "$rc"
|
|
}
|
|
|
|
trap cleanup EXIT INT TERM
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Preconditions
|
|
# ----------------------------------------------------------------------------
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Load generator (tmpfs allocation = real anonymous-ish memory, no extra deps)
|
|
# ----------------------------------------------------------------------------
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Diagnostics capture
|
|
# ----------------------------------------------------------------------------
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# The repro loop
|
|
# ----------------------------------------------------------------------------
|
|
|
|
log_size() { stat -c %s "$PACMAN_LOG" 2>/dev/null || echo 0; }
|
|
|
|
# Run one transaction, watching pacman.log for silence. Sets LAST_ELAPSED and,
|
|
# when it hangs, bumps STALLS via capture_stall. Deliberately NOT called in a
|
|
# command substitution: both counters and PACMAN_PID (which the EXIT trap needs)
|
|
# must live in this shell, not a subshell.
|
|
run_one() {
|
|
local pkg_file="$1"
|
|
local run="$2"
|
|
|
|
local started
|
|
printf -v started '%(%s)T' -1
|
|
|
|
"$PACMAN_BIN" -U --noconfirm "$pkg_file" >/dev/null 2>&1 &
|
|
PACMAN_PID=$!
|
|
|
|
local last_size last_change now captured=0
|
|
last_size="$(log_size)"
|
|
printf -v last_change '%(%s)T' -1
|
|
|
|
while kill -0 "$PACMAN_PID" 2>/dev/null; do
|
|
sleep 1
|
|
local size
|
|
size="$(log_size)"
|
|
printf -v now '%(%s)T' -1
|
|
|
|
if [[ $size != "$last_size" ]]; then
|
|
last_size="$size"
|
|
last_change="$now"
|
|
elif ((captured == 0 && now - last_change >= STALL_TIMEOUT)); then
|
|
capture_stall "$PACMAN_PID" "$run"
|
|
captured=1
|
|
fi
|
|
|
|
if ((now - started >= HARD_TIMEOUT)); then
|
|
echo " !! hard timeout (${HARD_TIMEOUT}s) - killing run $run"
|
|
((captured == 0)) && capture_stall "$PACMAN_PID" "$run"
|
|
kill_tree "$PACMAN_PID"
|
|
break
|
|
fi
|
|
done
|
|
|
|
wait "$PACMAN_PID" 2>/dev/null || true
|
|
PACMAN_PID=""
|
|
|
|
local ended
|
|
printf -v ended '%(%s)T' -1
|
|
LAST_ELAPSED=$((ended - started))
|
|
}
|
|
|
|
main() {
|
|
validate_requirements
|
|
|
|
if ((WATCH_MODE == 1)); then
|
|
mkdir -p "$OUT_DIR"
|
|
watch_forever
|
|
else
|
|
local pkg_file
|
|
pkg_file="$(resolve_package_file)"
|
|
|
|
mkdir -p "$OUT_DIR"
|
|
|
|
echo "============================================================================"
|
|
echo "pacman hook-stall diagnosis"
|
|
echo "============================================================================"
|
|
echo " package : $PACKAGE ($pkg_file)"
|
|
echo " runs : $RUNS"
|
|
echo " stall after : ${STALL_TIMEOUT}s of pacman.log silence"
|
|
echo " hard timeout: ${HARD_TIMEOUT}s"
|
|
local load_desc="idle"
|
|
((WITH_LOAD == 1)) && load_desc="memory pressure ON"
|
|
echo " load : $load_desc"
|
|
echo " dumps : $OUT_DIR"
|
|
echo
|
|
|
|
start_load
|
|
|
|
local durations=()
|
|
while ((RUN_INDEX < RUNS)); do
|
|
RUN_INDEX=$((RUN_INDEX + 1))
|
|
run_one "$pkg_file" "$RUN_INDEX"
|
|
durations+=("$LAST_ELAPSED")
|
|
local marker=""
|
|
((LAST_ELAPSED >= STALL_TIMEOUT)) && marker=" <-- slow"
|
|
printf ' run %2d/%d: %3ds%s\n' "$RUN_INDEX" "$RUNS" "$LAST_ELAPSED" "$marker"
|
|
done
|
|
|
|
stop_load
|
|
|
|
print_summary durations
|
|
fi
|
|
}
|
|
|
|
# Escalate BEFORE parsing: after the parse loop "$@" has been shifted away, so
|
|
# re-exec'ing under sudo there would silently drop every flag.
|
|
require_root "$@"
|
|
|
|
# Parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
-n | --runs)
|
|
RUNS="$2"
|
|
shift 2
|
|
;;
|
|
-p | --package)
|
|
PACKAGE="$2"
|
|
shift 2
|
|
;;
|
|
-t | --timeout)
|
|
STALL_TIMEOUT="$2"
|
|
shift 2
|
|
;;
|
|
--hard-timeout)
|
|
HARD_TIMEOUT="$2"
|
|
shift 2
|
|
;;
|
|
--with-load)
|
|
WITH_LOAD=1
|
|
shift
|
|
;;
|
|
--watch)
|
|
WATCH_MODE=1
|
|
shift
|
|
;;
|
|
-o | --out)
|
|
OUT_DIR="$2"
|
|
shift 2
|
|
;;
|
|
-h | --help)
|
|
usage
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
main
|