testsAndMisc/scripts/check_polling_antipatterns.sh
Krzysztof kuhy Rudnicki 42a66a1419 refactor(linux_configuration/scripts): split all scripts into single_use/ and periodic_background/
- Move all linux_configuration scripts into two semantic categories:
  - single_use/: scripts run once manually (fresh install, fixes, setup)
  - periodic_background/: scripts run by systemd timers or daemons
- Preserve existing subdirectory structure within each category
- Fix lib/common.sh source paths for new directory depths
- Fix CONFIG_DIR depth in setup_periodic_system.sh and check_and_enable_services.sh
- Update all references in tests, fresh-install/main.sh, nix modules, and docs
- Fix check_polling_antipatterns.sh false positives (||, regex |, case patterns, jq strings)
- Fix pre-existing mypy exclusion path and type annotations for moved tools/ directory
- Rewrite check_polling_antipatterns.sh using awk (no bash regex loops); add require_serial: true
2026-05-15 00:32:35 +02:00

135 lines
4.4 KiB
Bash
Executable File
Raw 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)
# shellcheck disable=SC2034
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 "$@"