testsAndMisc/meta/scripts/check_polling_antipatterns.sh
Krzysztof Rudnicki b83ce5d3c7
Some checks failed
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
billsplit CI / quality (push) Has been cancelled
reverse-survivors CI / check (push) Has been cancelled
Fail the build on any shellcheck finding; fix the 222 the old gate hid
The shellcheck hook ran with --severity=warning, so info- and style-level
findings never failed a commit. 222 had accumulated across 94 of 244 shell
files. They were not all style noise -- six were bugs:

  - Five scripts in single_use/misc/testsAndMisc-bash/ sourced
    $SCRIPT_DIR/../../lib/common.sh, one directory too shallow. Nothing exists
    at that path, so all five failed at runtime.
  - setup_personal_website.sh and setup_kcd2_dice_solver.sh called usage()
    (which ends in `exit 0`) and then `exit 1` for an unknown command. The
    exit 1 was unreachable, so a typo'd subcommand reported SUCCESS.
  - Six i3blocks scripts had a `# shellcheck source=` directive missing the
    scripts/periodic_background/ path segment. The stale path had silently
    disabled source-following for all of them.
  - diagnose_arch_performance.sh word-split a newline-joined file list; when
    the find came back empty, awk got no file operands, fell through to
    reading stdin, and the diagnostic hung.
  - lib/common.sh's handle_arg_help_or_unknown ended in an unreachable
    `return 0` while documenting "Returns: 0 if handled" -- a contract no code
    path could satisfy.
  - deploy.sh's SCRIPT_DIR is reassigned by config.sh, which it sources at top
    level, and config.sh can repoint it at the on-device path
    /data/local/tmp/focus_mode. Renamed to DEPLOY_DIR.

The SC1091 bulk (81 findings) was resolved rather than silenced: .shellcheckrc
sets external-sources + source-path=SCRIPTDIR so sourced files are actually
followed and analysed. That is what surfaced the i3blocks and deploy.sh bugs.

17 `# shellcheck disable=` directives remain, each with a written reason, and
only where the finding is provably wrong: injection-test payloads that must not
expand, literal source text being grepped for, POSIX sh scripts on the phone
where arrays do not exist, and source paths that only exist at runtime.
Everything else was fixed in code.

Verified in a CLEAN worktree, not just my own tree. config.sh sources the
gitignored config_secrets.sh, which exists locally but not in a fresh checkout;
a working-tree-only check passed while ci_mirror.sh -- which lints HEAD in a
clean worktree, exactly as CI does -- failed on it.

The gate is verified to bite: a file whose only finding is SC2016 (info) fails
the hook, while the old --severity=warning gate exits 0 on it.

third_party/ is excluded -- vendored upstream, matching the prettier hook.

Not fixed, reported: check_polling_antipatterns.sh carries a pre-existing
SC2034 disable that hides a real bug -- its `patterns` array is never used, so
the checker detects nothing. Confirmed unchanged by this commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 19:49:11 +02:00

137 lines
4.3 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# Pre-commit hook to detect polling script anti-patterns.
# Blocks commits of shell scripts that violate efficient polling best practices.
#
# Usage: check_polling_antipatterns.sh [files...]
# Exit codes: 0 = no issues, 1 = issues found
set -euo pipefail
errors=0
files_checked=0
# Patterns to detect (these are red flags for fork storms)
# These are regex patterns held literally -- the $( and ` inside them are
# part of what we search FOR, so they must not be expanded here.
# shellcheck disable=SC2034,SC2016
declare -A patterns=(
['while_true_with_sleep']='while\s+(true|:).*sleep'
['date_in_loop']='while.*date|for.*date'
['pgrep_in_loop']='while.*pgrep|for.*pgrep'
['xdotool_in_loop']='while.*xdotool|for.*xdotool'
['pipe_fork_chain']='|.*|.*|.*|'
['subshell_date']='$\(date\s+\+'
['backtick_date']='`date\s+\+'
['excessive_fork_chain']='(\$\(.*\|.*\|.*\)|`.*\|.*\|`)'
['sleep_less_than_one']='sleep\s+0\.[0-9]'
)
usage() {
echo "Usage: $(basename "$0") [files...]"
echo ""
echo "Checks shell scripts for polling anti-patterns that cause fork storms."
echo "Exit code 0 = no issues, 1 = issues found"
echo ""
echo "Patterns detected:"
echo " - while true + sleep (should use event-driven I/O)"
echo " - \$(date +...) in loops (forks process)"
echo " - pgrep/xdotool in loops (forks process)"
echo " - Multiple piped commands (each | forks)"
echo " - Aggressive polling (sleep < 1s)"
exit 0
}
check_file() {
local file="$1"
# Skip non-shell files
if ! grep -qE '^#!.*/(ba)?sh' "$file" 2>/dev/null; then
return 0
fi
files_checked=$((files_checked + 1))
# Single-pass awk analysis: C code, no bash loops, no per-line subshell forks.
# '\'' embeds a literal single quote inside a bash single-quoted string.
local findings
findings=$(awk '
/^[[:space:]]*([a-zA-Z0-9_]*_(loop|daemon)|poll[a-zA-Z0-9_]*)[[:space:]]*\(\)/ { in_poll=1 }
/^[[:space:]]*\}/ { in_poll=0 }
in_poll {
if (/while[[:space:]]+(true|:)/ && /sleep/)
print NR ": while true/: + sleep (use event-driven I/O)"
if (/\$\(date[[:space:]]/ || /`date[[:space:]]/)
print NR ": date fork in polling function"
if (/[^_a-zA-Z0-9]pgrep/ || /^[[:space:]]*pgrep/)
print NR ": pgrep in polling context"
if (/[^_a-zA-Z0-9]xdotool/ || /^[[:space:]]*xdotool/)
print NR ": xdotool in polling context"
if (/sleep[[:space:]]+0\.[0-9]/)
print NR ": Aggressive polling (sleep < 1s)"
}
!/^[[:space:]]*#/ && !/=/ && !/\)[[:space:]]*(#.*)?$/ && !/;;/ {
line = $0
gsub(/\|\|/, "", line)
while (match(line, /'\''[^'\'']*'\''/)) line = substr(line, 1, RSTART-1) substr(line, RSTART+RLENGTH)
while (match(line, /"[^"]*"/)) line = substr(line, 1, RSTART-1) substr(line, RSTART+RLENGTH)
n = gsub(/\|/, "", line)
if (n > 3) print NR ": Excessive pipes (" n " pipes = many forks)"
}
' "$file")
if [[ -n "$findings" ]]; then
echo " $file:"
while IFS= read -r finding; do
echo " Line $finding"
done <<<"$findings"
errors=$((errors + 1))
return 1
fi
return 0
}
provide_suggestions() {
echo ""
echo "📋 Optimization Tips:"
echo ""
echo "Replace polling loops with:"
echo " • inotifywait/fanotify for file system events"
echo " • timerfd for interval-based tasks"
echo " • select/poll for I/O multiplexing"
echo " • systemd timers for scheduled tasks"
echo " • dbus signals for system events"
echo ""
echo "Reduce fork overhead:"
echo " • Cache \$(date +%s) in variable, update periodically"
echo " • Use /proc filesystem instead of pgrep"
echo " • Consolidate commands with && instead of separate invocations"
echo ""
}
main() {
# Show usage if requested
[[ "$#" -eq 0 || "$1" == "-h" || "$1" == "--help" ]] && usage
# Check all provided files
for file in "$@"; do
check_file "$file" || true
done
# Report results
if [[ $files_checked -eq 0 ]]; then
echo "ℹ️ No shell scripts to check"
exit 0
fi
if [[ $errors -gt 0 ]]; then
echo ""
echo "❌ Found $errors issue(s) in shell scripts"
provide_suggestions
exit 1
else
echo "✓ No polling anti-patterns detected in $files_checked file(s)"
exit 0
fi
}
main "$@"