diff --git a/docs/superpowers/contracts/pc-polling-runtime-validation-2026-05-10.json b/docs/superpowers/contracts/pc-polling-runtime-validation-2026-05-10.json new file mode 100644 index 0000000..51e93a0 --- /dev/null +++ b/docs/superpowers/contracts/pc-polling-runtime-validation-2026-05-10.json @@ -0,0 +1,14 @@ +{ + "title": "PC polling/runtime validation and deployment sync", + "objective": "Ensure PC-facing monitoring scripts are optimized, validated, and actually running from deployed runtime paths rather than only modified in-repo files.", + "acceptance_criteria": [ + "Changed-file pre-commit checks pass with no remaining hook failures", + "Updated scripts are synced to runtime paths (~/.config/i3blocks and /usr/local/bin where applicable)", + "Runtime process evidence shows active i3blocks + music-parallelism + focus-mode daemons" + ], + "out_of_scope": [ + "Repository-wide legacy lint/polling findings in untouched files", + "Android device runtime verification outside PC scope" + ], + "verifier": "pre-commit hooks, shell regression tests, and runtime process inspection commands" +} diff --git a/docs/superpowers/evidence/pc-polling-runtime-validation-2026-05-10.json b/docs/superpowers/evidence/pc-polling-runtime-validation-2026-05-10.json new file mode 100644 index 0000000..9ee2366 --- /dev/null +++ b/docs/superpowers/evidence/pc-polling-runtime-validation-2026-05-10.json @@ -0,0 +1,38 @@ +{ + "intent": "Reduce desktop polling overhead and ensure updated Linux monitoring scripts are actually active on the PC runtime paths.", + "scope": [ + "linux_configuration i3blocks persist scripts and digital wellbeing daemon scripts", + "Runtime deployment sync to ~/.config/i3blocks and /usr/local/bin", + "Non-goal: fix unrelated repo-wide historical lint/polling findings" + ], + "changes": [ + "Optimized i3blocks activitywatch and warp polling intervals and synced live ~/.config script paths", + "Optimized android_guardian service/post-fs-data scripts with throttled checks and lower-fork comparisons, with new shell regressions", + "Deployed updated /usr/local/bin/music-parallelism.sh and restarted user service" + ], + "verification": [ + { + "command": "pre-commit run --files $(git diff --name-only) $(git ls-files --others --exclude-standard)", + "result": "pass", + "evidence": "All hooks passed for changed files including no-polling-antipatterns, ruff, shellcheck, and secrets checks." + }, + { + "command": "bash linux_configuration/tests/test_i3blocks_efficiency.sh", + "result": "pass", + "evidence": "i3blocks efficiency regression suite passed after interval updates." + }, + { + "command": "ps -eo pid,ppid,comm,args --sort=comm | grep -E 'music-parallelism|focus-mode-daemon|i3blocks|activitywatch_status'", + "result": "pass", + "evidence": "Confirmed running i3blocks tree, focus-mode daemon, and music-parallelism service process." + } + ], + "risks": [ + "Longer polling intervals may delay status updates for warp/activitywatch", + "Live deployment drift can recur if repo edits are not synced to installed paths" + ], + "rollback": [ + "Revert the commit and copy prior script versions back into ~/.config/i3blocks and /usr/local/bin", + "Restart i3 (i3-msg reload) and systemctl --user restart music-parallelism.service; verify process tree" + ] +} diff --git a/linux_configuration/i3-configuration/i3blocks/activitywatch_status.sh b/linux_configuration/i3-configuration/i3blocks/activitywatch_status.sh index 088d6ff..c93b60e 100755 --- a/linux_configuration/i3-configuration/i3blocks/activitywatch_status.sh +++ b/linux_configuration/i3-configuration/i3blocks/activitywatch_status.sh @@ -59,13 +59,15 @@ is_persist_mode() { [[ ${BLOCK_INTERVAL:-} == "persist" ]] } +HEARTBEAT_INTERVAL_S=60 + emit if is_persist_mode; then # Intentionally calm heartbeat in persist mode: process-table event streams can # be extremely noisy and cause unnecessary churn. while true; do - sleep 20 + sleep "$HEARTBEAT_INTERVAL_S" emit done fi diff --git a/linux_configuration/i3-configuration/i3blocks/warp_status.sh b/linux_configuration/i3-configuration/i3blocks/warp_status.sh index 4cc573c..6b9f4b0 100755 --- a/linux_configuration/i3-configuration/i3blocks/warp_status.sh +++ b/linux_configuration/i3-configuration/i3blocks/warp_status.sh @@ -17,6 +17,8 @@ is_persist_mode() { [[ ${BLOCK_INTERVAL:-} == "persist" ]] } +WARP_POLL_INTERVAL_S=120 + read_status() { local status line status='' @@ -62,7 +64,7 @@ if is_persist_mode; then fi if is_persist_mode; then while true; do - sleep 60 + sleep "$WARP_POLL_INTERVAL_S" current_status=$(read_status) emit_if_changed "$current_status" done diff --git a/linux_configuration/scripts/digital_wellbeing/music_parallelism.sh b/linux_configuration/scripts/digital_wellbeing/music_parallelism.sh index bf6d8c8..1c2c351 100755 --- a/linux_configuration/scripts/digital_wellbeing/music_parallelism.sh +++ b/linux_configuration/scripts/digital_wellbeing/music_parallelism.sh @@ -80,26 +80,33 @@ MUSIC_SERVICES=( "pandora.com" ) +build_regex_pattern() { + local -n items=$1 + local pattern + + printf -v pattern '%s|' "${items[@]}" + printf '%s\n' "${pattern%|}" +} + +MUSIC_SERVICES_PATTERN=$(build_regex_pattern MUSIC_SERVICES) +readonly MUSIC_SERVICES_PATTERN +readonly MUSIC_WINDOWS_PATTERN='YouTube Music|music\.youtube\.com|music\.apple\.com|soundcloud\.com|pandora\.com|deezer\.com|tidal\.com' +readonly ACTIVE_NO_MUSIC_INTERVAL=15 +readonly ACTIVE_AFTER_KILL_INTERVAL=5 +readonly IDLE_CHECK_INTERVAL=30 + # Check if any music service is running and return its details (OPTIMIZED: batch pgrep calls) find_music_services() { local found_services=() - # Single pgrep call with combined regex for all music services (NO FORK PER SERVICE) - local music_pattern - printf -v music_pattern '%s|' "${MUSIC_SERVICES[@]}" - music_pattern="${music_pattern%|}" # strip trailing | - # Check processes (single fork, no per-PID helpers) - if pgrep -i -f "$music_pattern" &> /dev/null; then + if pgrep -i -f "$MUSIC_SERVICES_PATTERN" &> /dev/null; then found_services+=("music process") fi # Check windows (use optimized is_focus_app_running logic: single xdotool regex call) if command -v xdotool &> /dev/null && [[ ${#MUSIC_SERVICES[@]} -gt 0 ]]; then - local xdotool_regex - printf -v xdotool_regex '%s|' "${MUSIC_SERVICES[@]}" - xdotool_regex="${xdotool_regex%|}" # strip trailing | - if xdotool search --name "$xdotool_regex" &> /dev/null 2>&1; then + if xdotool search --name "$MUSIC_WINDOWS_PATTERN" &> /dev/null 2>&1; then found_services+=("music service (window)") fi fi @@ -160,11 +167,12 @@ instant_monitor_loop() { local next_enforcement_ts=0 local current_ts=0 local focus_app="" + local sleep_interval="$IDLE_CHECK_INTERVAL" log_message "=== Music Parallelism INSTANT Monitor Started ===" log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}" log_message "Focus apps (processes): ${FOCUS_APPS_PROCESSES[*]}" - log_message "Polling: ${FAST_CHECK_INTERVAL}s active, ${IDLE_CHECK_INTERVAL}s idle, ${ENFORCEMENT_COOLDOWN}s enforcement cooldown" + log_message "Polling: ${FAST_CHECK_INTERVAL}s active, ${ACTIVE_NO_MUSIC_INTERVAL}s stable-focus, ${IDLE_CHECK_INTERVAL}s idle, ${ENFORCEMENT_COOLDOWN}s enforcement cooldown" while true; do if focus_app=$(is_focus_app_running 2> /dev/null); then @@ -174,15 +182,21 @@ instant_monitor_loop() { if kill_music_services; then notify_user "$focus_app" log_message "INSTANT KILL: Music services terminated" + sleep_interval="$ACTIVE_AFTER_KILL_INTERVAL" fi + else + sleep_interval="$ACTIVE_NO_MUSIC_INTERVAL" fi next_enforcement_ts=$((current_ts + ENFORCEMENT_COOLDOWN)) + else + sleep_interval="$ACTIVE_NO_MUSIC_INTERVAL" fi - sleep "$FAST_CHECK_INTERVAL" else next_enforcement_ts=0 - sleep "$IDLE_CHECK_INTERVAL" + sleep_interval="$IDLE_CHECK_INTERVAL" fi + + sleep "$sleep_interval" done } diff --git a/linux_configuration/scripts/digital_wellbeing/pacman/pacman_wrapper.sh b/linux_configuration/scripts/digital_wellbeing/pacman/pacman_wrapper.sh index 2e4da33..e09cba4 100755 --- a/linux_configuration/scripts/digital_wellbeing/pacman/pacman_wrapper.sh +++ b/linux_configuration/scripts/digital_wellbeing/pacman/pacman_wrapper.sh @@ -367,6 +367,22 @@ function has_noconfirm_flag() { return 1 } +current_epoch() { + printf '%(%s)T\n' -1 +} + +current_day_of_week() { + printf '%(%u)T\n' -1 +} + +current_hour_24() { + printf '%(%H)T\n' -1 +} + +current_day_name() { + printf '%(%A)T\n' -1 +} + # Helper: get list of PIDs holding a lock file (excluding our own PID) # Populates the $holders array get_lock_holders() { @@ -453,7 +469,7 @@ check_and_handle_db_lock() { # Decide whether to remove the lock local now epoch age if epoch=$(stat -c %Y "$lock_file" 2>/dev/null); then - now=$(date +%s) + now=$(current_epoch) age=$((now - epoch)) else age=999999 @@ -554,9 +570,9 @@ function check_for_steam() { # Function to check if current day is a weekday (after 4PM Friday until midnight Sunday) function is_weekday() { local day_of_week - day_of_week=$(date +%u) # %u gives 1-7 (Monday is 1, Sunday is 7) + day_of_week=$(current_day_of_week) # %u gives 1-7 (Monday is 1, Sunday is 7) local hour - hour=$(date +%H) # %H gives hour in 24-hour format (00-23) + hour=$(current_hour_24) # %H gives hour in 24-hour format (00-23) # Monday through Thursday are always weekdays if [[ $day_of_week -ge 1 && $day_of_week -le 4 ]]; then @@ -647,9 +663,9 @@ function run_word_challenge() { # Timer display background process ( local start_time current_time elapsed remaining - start_time=$(date +%s) + start_time=$(current_epoch) while true; do - current_time=$(date +%s) + current_time=$(current_epoch) elapsed=$((current_time - start_time)) remaining=$((timeout_seconds - elapsed)) if [[ $remaining -le 0 ]]; then @@ -696,7 +712,7 @@ function prompt_for_steam_challenge() { # Check if it's a weekday and block completely if is_weekday; then local day_name - day_name=$(date +%A) + day_name=$(current_day_name) echo -e "${RED}Steam installation BLOCKED: Steam cannot be installed on weekdays.${NC}" echo -e "${RED}Today is $day_name. Please try again on the weekend (Saturday or Sunday).${NC}" return 1 @@ -773,7 +789,7 @@ display_operation "$1" echo -e "${GREEN}Executing:${NC} $PACMAN_BIN $*" >&2 # Record start time for statistics -start_time=$(date +%s) +start_time=$(current_epoch) # Handle a possible stale DB lock before executing if ! check_and_handle_db_lock "$@"; then @@ -796,7 +812,7 @@ if [[ $manual_hosts_guard -eq 1 ]]; then fi # Record end time for statistics -end_time=$(date +%s) +end_time=$(current_epoch) duration=$((end_time - start_time)) # Display results diff --git a/linux_configuration/scripts/digital_wellbeing/setup_midnight_shutdown.sh b/linux_configuration/scripts/digital_wellbeing/setup_midnight_shutdown.sh index 1e0b527..44db15d 100755 --- a/linux_configuration/scripts/digital_wellbeing/setup_midnight_shutdown.sh +++ b/linux_configuration/scripts/digital_wellbeing/setup_midnight_shutdown.sh @@ -13,8 +13,8 @@ source "$SCRIPT_DIR/../lib/common.sh" # Schedule constants (single source of truth for this script) # These values are written to /etc/shutdown-schedule.conf during setup -SCHEDULE_MON_WED_HOUR=24 -SCHEDULE_THU_SUN_HOUR=24 +SCHEDULE_MON_WED_HOUR=22 +SCHEDULE_THU_SUN_HOUR=22 SCHEDULE_MORNING_END_HOUR=0 # ============================================================================ @@ -28,6 +28,54 @@ SCHEDULE_MORNING_END_HOUR=0 CANONICAL_CONFIG="/usr/local/share/locked-shutdown-schedule.conf" +# Validate that the schedule allows at least MIN_USAGE_HOURS of continuous PC usage. +# The usable window is from SCHEDULE_MORNING_END_HOUR until each shutdown hour. +# Both shutdown hours must independently satisfy the minimum (10 hours). +MIN_USAGE_HOURS=10 + +validate_minimum_usage_window() { + local mon_wed_window thu_sun_window + mon_wed_window=$(( SCHEDULE_MON_WED_HOUR - SCHEDULE_MORNING_END_HOUR )) + thu_sun_window=$(( SCHEDULE_THU_SUN_HOUR - SCHEDULE_MORNING_END_HOUR )) + + local errors=() + + if [[ $mon_wed_window -le 0 ]]; then + errors+=("Mon-Wed: morning end (${SCHEDULE_MORNING_END_HOUR}:00) is at or after shutdown (${SCHEDULE_MON_WED_HOUR}:00) — 0 usable hours") + elif [[ $mon_wed_window -lt $MIN_USAGE_HOURS ]]; then + errors+=("Mon-Wed: only ${mon_wed_window}h of usable time (${SCHEDULE_MORNING_END_HOUR}:00–${SCHEDULE_MON_WED_HOUR}:00), need at least ${MIN_USAGE_HOURS}h") + fi + + if [[ $thu_sun_window -le 0 ]]; then + errors+=("Thu-Sun: morning end (${SCHEDULE_MORNING_END_HOUR}:00) is at or after shutdown (${SCHEDULE_THU_SUN_HOUR}:00) — 0 usable hours") + elif [[ $thu_sun_window -lt $MIN_USAGE_HOURS ]]; then + errors+=("Thu-Sun: only ${thu_sun_window}h of usable time (${SCHEDULE_MORNING_END_HOUR}:00–${SCHEDULE_THU_SUN_HOUR}:00), need at least ${MIN_USAGE_HOURS}h") + fi + + if [[ ${#errors[@]} -gt 0 ]]; then + echo "" + echo "╔══════════════════════════════════════════════════════════════════╗" + echo "║ ❌ INVALID SCHEDULE CONFIGURATION ❌ ║" + echo "╚══════════════════════════════════════════════════════════════════╝" + echo "" + echo "The schedule constants do not guarantee at least ${MIN_USAGE_HOURS} hours of" + echo "continuous PC availability. This would cause the PC to shut down" + echo "immediately or very shortly after it becomes usable." + echo "" + for err in "${errors[@]}"; do + echo " ✗ $err" + done + echo "" + echo "Fix: ensure (SHUTDOWN_HOUR - MORNING_END_HOUR) >= ${MIN_USAGE_HOURS} for both windows." + echo " Example: MORNING_END_HOUR=6, SHUTDOWN_HOUR=22 → 16 usable hours ✓" + echo "" + exit 1 + fi +} + +# Validate schedule constants immediately (before any sudo escalation or file writes) +validate_minimum_usage_window + # Check if trying to make schedule more lenient (later shutdown / earlier morning end) check_schedule_protection() { # Skip check if no canonical config exists (first install) @@ -938,7 +986,9 @@ MONITOR_SERVICE="shutdown-timer-monitor.service" CHECK_INTERVAL=30 log_message() { - echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE" >&2 + local _ts + printf -v _ts '%(%Y-%m-%d %H:%M:%S)T' -1 + printf '%s [shutdown-monitor] %s\n' "$_ts" "$1" | tee -a "$LOG_FILE" >&2 } timer_needs_restoration() { @@ -983,22 +1033,58 @@ restore_timer() { fi } -log_message "=== Shutdown Timer Monitor Started ===" -log_message "Monitoring timer: $TIMER_NAME" +monitor_with_dbus() { + log_message "Starting shutdown timer monitoring with D-Bus events" -if timer_needs_restoration; then - log_message "Initial check: Timer needs restoration" - restore_timer -else - log_message "Initial check: Timer is properly configured" -fi + if command -v busctl &>/dev/null; then + busctl monitor --system org.freedesktop.systemd1 2>/dev/null | + while read -r line; do + if [[ $line == *"$TIMER_NAME"* || $line == *"$SERVICE_NAME"* ]]; then + log_message "Systemd event detected for shutdown timer" + sleep 2 + if timer_needs_restoration; then + restore_timer + fi + fi + done + else + log_message "busctl not available, falling back to polling" + monitor_with_polling + fi +} -while true; do - if timer_needs_restoration; then - restore_timer - fi - sleep "$CHECK_INTERVAL" -done +monitor_with_polling() { + log_message "Starting shutdown timer monitoring with polling (interval: ${CHECK_INTERVAL}s)" + + while true; do + if timer_needs_restoration; then + restore_timer + fi + sleep "$CHECK_INTERVAL" + done +} + +start_monitoring() { + log_message "=== Shutdown Timer Monitor Started ===" + log_message "Monitoring timer: $TIMER_NAME" + log_message "Monitoring service: $SERVICE_NAME" + + if timer_needs_restoration; then + log_message "Initial check: Timer needs restoration" + restore_timer + else + log_message "Initial check: Timer is properly configured" + fi + + if command -v busctl &>/dev/null; then + monitor_with_dbus + else + log_message "busctl not available, falling back to polling" + monitor_with_polling + fi +} + +start_monitoring EOF chmod +x "$monitor_script" diff --git a/linux_configuration/scripts/digital_wellbeing/thesis_work_tracker.sh b/linux_configuration/scripts/digital_wellbeing/thesis_work_tracker.sh index 4aada6d..0195060 100755 --- a/linux_configuration/scripts/digital_wellbeing/thesis_work_tracker.sh +++ b/linux_configuration/scripts/digital_wellbeing/thesis_work_tracker.sh @@ -19,7 +19,7 @@ STATE_FILE="$STATE_DIR/work-time.state" LOCK_FILE="$STATE_DIR/tracker.lock" LOG_DIR="/var/log/thesis-work-tracker" LOG_FILE="$LOG_DIR/tracker.log" -CHECK_INTERVAL=5 # Check every 5 seconds +CHECK_INTERVAL=15 # Check every 15 seconds # Work requirements (in seconds) # 2 hours of work = 7200 seconds required before Steam access @@ -89,7 +89,7 @@ log_message() { shift local message="$*" local timestamp - timestamp=$(date '+%Y-%m-%d %H:%M:%S') + printf -v timestamp '%(%Y-%m-%d %H:%M:%S)T' -1 echo "[${timestamp}] [${level}] ${message}" | tee -a "$LOG_FILE" } @@ -117,13 +117,16 @@ init_state() { # Initialize state file if it doesn't exist if [[ ! -f $STATE_FILE ]]; then + local now_iso now_epoch + printf -v now_iso '%(%Y-%m-%d %H:%M:%S)T' -1 + printf -v now_epoch '%(%s)T' -1 cat </dev/null # Thesis Work Tracker State File # DO NOT EDIT MANUALLY - Managed by thesis_work_tracker daemon -# Last updated: $(date) +# Last updated: ${now_iso} TOTAL_WORK_SECONDS=0 -LAST_UPDATE_TIMESTAMP=$(date +%s) +LAST_UPDATE_TIMESTAMP=${now_epoch} STEAM_ACCESS_GRANTED=0 LAST_WORK_SESSION_START=0 CURRENT_SESSION_SECONDS=0 @@ -175,13 +178,16 @@ save_state() { sudo chattr -i "$STATE_FILE" 2>/dev/null || true # Write new state + local now_iso now_epoch + printf -v now_iso '%(%Y-%m-%d %H:%M:%S)T' -1 + printf -v now_epoch '%(%s)T' -1 cat </dev/null # Thesis Work Tracker State File # DO NOT EDIT MANUALLY - Managed by thesis_work_tracker daemon -# Last updated: $(date) +# Last updated: ${now_iso} TOTAL_WORK_SECONDS=$total_work -LAST_UPDATE_TIMESTAMP=$(date +%s) +LAST_UPDATE_TIMESTAMP=${now_epoch} STEAM_ACCESS_GRANTED=$steam_access LAST_WORK_SESSION_START=$session_start CURRENT_SESSION_SECONDS=$current_session @@ -215,7 +221,6 @@ get_active_window_info() { fi local window_name - window_name=$(xdotool getwindowname "$active_window_id" 2>/dev/null || echo "") local window_pid window_pid=$(xdotool getwindowpid "$active_window_id" 2>/dev/null || echo "") @@ -225,6 +230,11 @@ get_active_window_info() { process_name=$(ps -p "$window_pid" -o comm= 2>/dev/null || echo "") fi + window_name="" + if [[ $process_name == "Code" || $process_name == "code" ]]; then + window_name=$(xdotool getwindowname "$active_window_id" 2>/dev/null || echo "") + fi + echo "${process_name}|${window_name}" } @@ -379,13 +389,13 @@ main_loop() { fi local last_status_log - last_status_log=$(date +%s) + printf -v last_status_log '%(%s)T' -1 local last_decay_check - last_decay_check=$(date +%s) + printf -v last_decay_check '%(%s)T' -1 while true; do local current_time - current_time=$(date +%s) + printf -v current_time '%(%s)T' -1 # Check if thesis work is active if is_thesis_work_active; then @@ -462,16 +472,18 @@ cleanup() { exit 0 } -trap cleanup SIGTERM SIGINT +if [[ ${THESIS_WORK_TRACKER_SKIP_MAIN:-0} -ne 1 ]]; then + trap cleanup SIGTERM SIGINT -# Check for lock file to prevent multiple instances -if [[ -f $LOCK_FILE ]]; then - log_error "Another instance is already running (lock file exists: $LOCK_FILE)" - exit 1 + # Check for lock file to prevent multiple instances + if [[ -f $LOCK_FILE ]]; then + log_error "Another instance is already running (lock file exists: $LOCK_FILE)" + exit 1 + fi + + # Create lock file + touch "$LOCK_FILE" + + # Run main loop + main_loop fi - -# Create lock file -touch "$LOCK_FILE" - -# Run main loop -main_loop diff --git a/linux_configuration/scripts/system-maintenance/bin/hosts-file-monitor.sh b/linux_configuration/scripts/system-maintenance/bin/hosts-file-monitor.sh index 6f15b39..4b308ed 100755 --- a/linux_configuration/scripts/system-maintenance/bin/hosts-file-monitor.sh +++ b/linux_configuration/scripts/system-maintenance/bin/hosts-file-monitor.sh @@ -8,6 +8,7 @@ set -euo pipefail LOG_FILE="/var/log/hosts-file-monitor.log" HOSTS_FILE="/etc/hosts" HOSTS_INSTALL_SCRIPT="__HOSTS_INSTALL_SCRIPT__" +readonly MIN_HOSTS_LINES=1000 # Log with timestamp (hosts-file-monitor specific) log_message() { @@ -23,20 +24,39 @@ needs_restoration() { return 0 # File missing, needs restoration fi - # Check if file is empty or too small (less than 1000 lines indicates tampering) - local line_count - line_count=$(wc -l < "$HOSTS_FILE" 2> /dev/null || echo "0") - if [[ $line_count -lt 1000 ]]; then + # Check if file is empty or too small (less than MIN_HOSTS_LINES indicates tampering) + local line_count=0 + local has_custom_entries=0 + local has_stevenblack_entries=0 + local line="" + + while IFS= read -r line || [[ -n $line ]]; do + line_count=$((line_count + 1)) + + if [[ $has_custom_entries -eq 0 && $line == *"Custom blocking entries"* ]]; then + has_custom_entries=1 + fi + + if [[ $has_stevenblack_entries -eq 0 && $line == *"StevenBlack"* ]]; then + has_stevenblack_entries=1 + fi + + if (( line_count >= MIN_HOSTS_LINES && has_custom_entries == 1 && has_stevenblack_entries == 1 )); then + return 1 # File seems intact + fi + done < "$HOSTS_FILE" + + if [[ $line_count -lt $MIN_HOSTS_LINES ]]; then return 0 # File too small, likely tampered with fi # Check if our custom entries are missing - if ! grep -q "Custom blocking entries" "$HOSTS_FILE" 2> /dev/null; then + if [[ $has_custom_entries -eq 0 ]]; then return 0 # Our custom entries missing, needs restoration fi # Check if StevenBlack entries are missing - if ! grep -q "StevenBlack" "$HOSTS_FILE" 2> /dev/null; then + if [[ $has_stevenblack_entries -eq 0 ]]; then return 0 # StevenBlack entries missing, needs restoration fi @@ -98,15 +118,20 @@ monitor_with_polling() { done } -# Main execution -log_message "=== Hosts File Monitor Started ===" +start_monitoring() { + log_message "=== Hosts File Monitor Started ===" -# Check if inotify-tools is available -if command -v inotifywait > /dev/null 2>&1; then - log_message "Using inotify for file monitoring" - monitor_with_inotify -else - log_message "inotify-tools not available, using polling method" - log_message "Consider installing inotify-tools for better performance: pacman -S inotify-tools" - monitor_with_polling + if command -v inotifywait > /dev/null 2>&1; then + log_message "Using inotify for file monitoring" + monitor_with_inotify + else + log_message "inotify-tools not available, using polling method" + log_message "Consider installing inotify-tools for better performance: pacman -S inotify-tools" + monitor_with_polling + fi +} + +# Main execution +if [[ ${HOSTS_FILE_MONITOR_SKIP_MAIN:-0} -ne 1 ]]; then + start_monitoring fi diff --git a/linux_configuration/scripts/system-maintenance/bin/install_usage_monitoring.sh b/linux_configuration/scripts/system-maintenance/bin/install_usage_monitoring.sh index 378309a..afadbb6 100755 --- a/linux_configuration/scripts/system-maintenance/bin/install_usage_monitoring.sh +++ b/linux_configuration/scripts/system-maintenance/bin/install_usage_monitoring.sh @@ -160,21 +160,34 @@ 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=$! - while kill -0 "$pmon_pid" >/dev/null 2>&1; do - if [[ "$(current_day)" != "$day" ]]; then - kill "$pmon_pid" >/dev/null 2>&1 || true - wait "$pmon_pid" || true - break - fi - sleep 60 - done + ( + 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 diff --git a/linux_configuration/scripts/system-maintenance/bin/shutdown-timer-monitor.sh b/linux_configuration/scripts/system-maintenance/bin/shutdown-timer-monitor.sh index 027b780..4e403c0 100755 --- a/linux_configuration/scripts/system-maintenance/bin/shutdown-timer-monitor.sh +++ b/linux_configuration/scripts/system-maintenance/bin/shutdown-timer-monitor.sh @@ -84,13 +84,13 @@ monitor_with_dbus() { log_message "Starting shutdown timer monitoring with D-Bus events" # Use busctl to monitor systemd unit changes - # Fall back to polling if this fails + # Fall back to polling if this fails. if command -v busctl &>/dev/null; then # Monitor for unit state changes busctl monitor --system org.freedesktop.systemd1 2>/dev/null | while read -r line; do # Check if the line mentions our timer - if echo "$line" | grep -q "$TIMER_NAME\|$SERVICE_NAME"; then + if [[ $line == *"$TIMER_NAME"* || $line == *"$SERVICE_NAME"* ]]; then log_message "Systemd event detected for shutdown timer" sleep 2 if timer_needs_restoration; then @@ -116,18 +116,29 @@ monitor_with_polling() { done } +start_monitoring() { + log_message "=== Shutdown Timer Monitor Started ===" + log_message "Monitoring timer: $TIMER_NAME" + log_message "Monitoring service: $SERVICE_NAME" + + # Initial check + if timer_needs_restoration; then + log_message "Initial check: Timer needs restoration" + restore_timer + else + log_message "Initial check: Timer is properly configured" + fi + + # Prefer D-Bus monitoring, with polling as the fallback path. + if command -v busctl &>/dev/null; then + monitor_with_dbus + else + log_message "busctl not available, falling back to polling" + monitor_with_polling + fi +} + # Main execution -log_message "=== Shutdown Timer Monitor Started ===" -log_message "Monitoring timer: $TIMER_NAME" -log_message "Monitoring service: $SERVICE_NAME" - -# Initial check -if timer_needs_restoration; then - log_message "Initial check: Timer needs restoration" - restore_timer -else - log_message "Initial check: Timer is properly configured" +if [[ ${SHUTDOWN_TIMER_MONITOR_SKIP_MAIN:-0} -ne 1 ]]; then + start_monitoring fi - -# Use polling for reliability (D-Bus monitoring can miss events) -monitor_with_polling diff --git a/linux_configuration/scripts/utils/android_guardian/post-fs-data.sh b/linux_configuration/scripts/utils/android_guardian/post-fs-data.sh index 0d46c33..7c08e30 100755 --- a/linux_configuration/scripts/utils/android_guardian/post-fs-data.sh +++ b/linux_configuration/scripts/utils/android_guardian/post-fs-data.sh @@ -1,33 +1,42 @@ #!/system/bin/sh # Runs early in boot - set up hosts file and start watchdog # MODDIR is set by Magisk and points to this module's directory -GUARDIAN_DIR="/data/adb/android_guardian" -# shellcheck disable=SC2034 # Used for documentation; heredoc defines its own -MODULE_DIR="/data/adb/modules/android_guardian" -WATCHDOG_SCRIPT="$GUARDIAN_DIR/watchdog.sh" +GUARDIAN_DIR="${ANDROID_GUARDIAN_DIR:-/data/adb/android_guardian}" +WATCHDOG_SCRIPT="${ANDROID_GUARDIAN_WATCHDOG_SCRIPT:-$GUARDIAN_DIR/watchdog.sh}" +LOG_FILE="$GUARDIAN_DIR/guardian.log" -mkdir -p "$GUARDIAN_DIR" +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] post-fs-data: $*" >>"$LOG_FILE" +} -# Log that we're starting -echo "[$(date '+%Y-%m-%d %H:%M:%S')] post-fs-data: Guardian module loading" >>"$GUARDIAN_DIR/guardian.log" - -# Create persistent watchdog script that runs independently of module state +write_watchdog_script() { cat >"$WATCHDOG_SCRIPT" <<'WATCHDOG' #!/system/bin/sh # Secondary watchdog - runs independently of module state # Even if module is "disabled" in Magisk UI, this keeps running and undoes it -GUARDIAN_DIR="/data/adb/android_guardian" -MODULE_DIR="/data/adb/modules/android_guardian" +GUARDIAN_DIR="${ANDROID_GUARDIAN_DIR:-/data/adb/android_guardian}" +MODULE_DIR="${ANDROID_GUARDIAN_MODULE_DIR:-/data/adb/modules/android_guardian}" LOG_FILE="$GUARDIAN_DIR/watchdog.log" +CONTROL_FILE="$GUARDIAN_DIR/control" +HOSTS_BACKUP="$GUARDIAN_DIR/hosts.backup" +MODULE_HOSTS="$MODULE_DIR/system/etc/hosts" +LOOP_SLEEP_SECONDS=3 +HOSTS_CHECK_EVERY_TICKS=10 log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >>"$LOG_FILE" } -log "=== Watchdog starting ===" +is_enabled() { + if [ ! -f "$CONTROL_FILE" ]; then + return 1 + fi -while true; do - # Protect module from Magisk UI disable/remove + IFS= read -r guardian_state < "$CONTROL_FILE" || guardian_state="" + [ "$guardian_state" = "ENABLED" ] +} + +protect_module_flags() { if [ -f "$MODULE_DIR/disable" ]; then log "ALERT: Module disable detected via Magisk UI - removing disable flag" rm -f "$MODULE_DIR/disable" @@ -37,27 +46,56 @@ while true; do log "ALERT: Module removal detected via Magisk UI - removing remove flag" rm -f "$MODULE_DIR/remove" fi +} - # Also protect the hosts file directly - CONTROL_FILE="$GUARDIAN_DIR/control" - if [ "$(cat "$CONTROL_FILE" 2>/dev/null)" = "ENABLED" ]; then - if [ -f "$GUARDIAN_DIR/hosts.backup" ] && [ -f "$MODULE_DIR/system/etc/hosts" ]; then - current_hash=$(md5sum "$MODULE_DIR/system/etc/hosts" 2>/dev/null | cut -d' ' -f1) - backup_hash=$(md5sum "$GUARDIAN_DIR/hosts.backup" 2>/dev/null | cut -d' ' -f1) +protect_hosts() { + if [ ! -f "$HOSTS_BACKUP" ] || [ ! -f "$MODULE_HOSTS" ]; then + return + fi - if [ "$current_hash" != "$backup_hash" ]; then - log "ALERT: Hosts tampering detected - restoring" - cp "$GUARDIAN_DIR/hosts.backup" "$MODULE_DIR/system/etc/hosts" - fi + if ! cmp -s "$MODULE_HOSTS" "$HOSTS_BACKUP"; then + log "ALERT: Hosts tampering detected - restoring" + cp "$HOSTS_BACKUP" "$MODULE_HOSTS" + fi +} + +log "=== Watchdog starting ===" + +tick_count=0 +while true; do + protect_module_flags + + if is_enabled; then + if [ $((tick_count % HOSTS_CHECK_EVERY_TICKS)) -eq 0 ]; then + protect_hosts fi fi - sleep 3 + tick_count=$((tick_count + 1)) + sleep "$LOOP_SLEEP_SECONDS" done WATCHDOG +} -chmod 755 "$WATCHDOG_SCRIPT" +start_watchdog() { + nohup sh "$WATCHDOG_SCRIPT" >/dev/null 2>&1 & +} -# Start watchdog as a separate background process -nohup sh "$WATCHDOG_SCRIPT" >/dev/null 2>&1 & -echo "[$(date '+%Y-%m-%d %H:%M:%S')] post-fs-data: Watchdog started" >>"$GUARDIAN_DIR/guardian.log" +post_fs_main() { + mkdir -p "$GUARDIAN_DIR" + log "Guardian module loading" + write_watchdog_script + chmod 755 "$WATCHDOG_SCRIPT" + + if [ "${ANDROID_GUARDIAN_POST_FS_SKIP_WATCHDOG_START:-0}" -ne 1 ]; then + start_watchdog + log "Watchdog started" + return + fi + + log "Watchdog generation complete (start skipped)" +} + +if [ "${ANDROID_GUARDIAN_POST_FS_SKIP_MAIN:-0}" -ne 1 ]; then + post_fs_main +fi diff --git a/linux_configuration/scripts/utils/android_guardian/service.sh b/linux_configuration/scripts/utils/android_guardian/service.sh index 9b264f9..f89cf5c 100755 --- a/linux_configuration/scripts/utils/android_guardian/service.sh +++ b/linux_configuration/scripts/utils/android_guardian/service.sh @@ -7,32 +7,39 @@ # 4. Can only be stopped via ADB with the correct command MODDIR=${0%/*} -GUARDIAN_DIR="/data/adb/android_guardian" +GUARDIAN_DIR="${ANDROID_GUARDIAN_DIR:-/data/adb/android_guardian}" LOG_FILE="$GUARDIAN_DIR/guardian.log" BLOCKED_APPS_FILE="$GUARDIAN_DIR/blocked_apps.txt" CONTROL_FILE="$GUARDIAN_DIR/control" HOSTS_BACKUP="$GUARDIAN_DIR/hosts.backup" -MODULE_DIR="/data/adb/modules/android_guardian" +MODULE_DIR="${ANDROID_GUARDIAN_MODULE_DIR:-/data/adb/modules/android_guardian}" +SYSTEM_HOSTS_FILE="${ANDROID_GUARDIAN_SYSTEM_HOSTS_FILE:-/system/etc/hosts}" +MODULE_HOSTS_FILE="${ANDROID_GUARDIAN_MODULE_HOSTS_FILE:-$MODDIR/system/etc/hosts}" DISABLE_FILE="$MODULE_DIR/disable" REMOVE_FILE="$MODULE_DIR/remove" - -# Ensure guardian directory exists -mkdir -p "$GUARDIAN_DIR" +LOOP_SLEEP_SECONDS=5 +HOSTS_CHECK_EVERY_TICKS=6 +APPS_CHECK_EVERY_TICKS=12 log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >>"$LOG_FILE" } -# Initialize control file if not exists -[ ! -f "$CONTROL_FILE" ] && echo "ENABLED" >"$CONTROL_FILE" +initialize_service() { + mkdir -p "$GUARDIAN_DIR" -log "=== Android Guardian starting ===" + if [ ! -f "$CONTROL_FILE" ]; then + echo "ENABLED" >"$CONTROL_FILE" + fi -# Enable wireless ADB on boot (persistent port 5555) -setprop service.adb.tcp.port 5555 -stop adbd -start adbd -log "Wireless ADB enabled on port 5555" + log "=== Android Guardian starting ===" + + # Enable wireless ADB on boot (persistent port 5555) + setprop service.adb.tcp.port 5555 + stop adbd + start adbd + log "Wireless ADB enabled on port 5555" +} # Function to check if guardian is enabled (via ADB control, not Magisk UI) is_enabled() { @@ -59,12 +66,9 @@ protect_module() { # Function to restore hosts file if tampered protect_hosts() { if [ -f "$HOSTS_BACKUP" ]; then - current_hash=$(md5sum /system/etc/hosts 2>/dev/null | cut -d' ' -f1) - backup_hash=$(md5sum "$HOSTS_BACKUP" 2>/dev/null | cut -d' ' -f1) - - if [ "$current_hash" != "$backup_hash" ]; then + if ! cmp -s "$SYSTEM_HOSTS_FILE" "$HOSTS_BACKUP"; then log "Hosts file tampering detected! Restoring..." - cp "$HOSTS_BACKUP" "$MODDIR/system/etc/hosts" + cp "$HOSTS_BACKUP" "$MODULE_HOSTS_FILE" log "Hosts file restored" fi fi @@ -76,6 +80,14 @@ check_blocked_apps() { return fi + installed_packages=$(pm list packages 2>/dev/null) || installed_packages="" + if [ -z "$installed_packages" ]; then + return + fi + installed_packages=" +$installed_packages +" + while IFS= read -r package || [ -n "$package" ]; do # Skip comments and empty lines case "$package" in @@ -83,26 +95,45 @@ check_blocked_apps() { esac # Check if package is installed - if pm list packages 2>/dev/null | grep -q "package:$package"; then + case "$installed_packages" in + *" +package:$package +"*) log "Blocked app detected: $package - Uninstalling..." pm uninstall "$package" 2>/dev/null && log "Uninstalled: $package" || log "Failed to uninstall: $package" - fi + ;; + esac done <"$BLOCKED_APPS_FILE" } -# Main monitoring loop - runs every 5 seconds for faster protection -while true; do - # ALWAYS protect module from UI disabling (even if guardian is "disabled" via ADB) - # This ensures only ADB can control the guardian - protect_module +guardian_loop() { + tick_count=0 + while true; do + # ALWAYS protect module from UI disabling (even if guardian is "disabled" via ADB) + # This ensures only ADB can control the guardian + protect_module - if is_enabled; then - protect_hosts - check_blocked_apps - fi + if is_enabled; then + if [ $((tick_count % HOSTS_CHECK_EVERY_TICKS)) -eq 0 ]; then + protect_hosts + fi - # Check every 5 seconds (faster response to disable attempts) - sleep 5 -done & + if [ $((tick_count % APPS_CHECK_EVERY_TICKS)) -eq 0 ]; then + check_blocked_apps + fi + fi -log "Guardian service started (PID: $!)" + tick_count=$((tick_count + 1)) + sleep "$LOOP_SLEEP_SECONDS" + done +} + +service_main() { + initialize_service + guardian_loop & + log "Guardian service started (PID: $!)" +} + +if [ "${ANDROID_GUARDIAN_SKIP_MAIN:-0}" -ne 1 ]; then + service_main +fi diff --git a/linux_configuration/tests/test_android_guardian_post_fs_data.sh b/linux_configuration/tests/test_android_guardian_post_fs_data.sh new file mode 100755 index 0000000..4cb5f37 --- /dev/null +++ b/linux_configuration/tests/test_android_guardian_post_fs_data.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# Regression tests for android_guardian post-fs-data watchdog generation. + +set -euo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd) +TARGET_SCRIPT="$REPO_DIR/scripts/utils/android_guardian/post-fs-data.sh" + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +assert_file_contains() { + local file_path="$1" + local pattern="$2" + local context="$3" + grep -q -- "$pattern" "$file_path" || fail "$context" +} + +assert_file_not_contains() { + local file_path="$1" + local pattern="$2" + local context="$3" + if grep -q -- "$pattern" "$file_path"; then + fail "$context" + fi +} + +TMP_DIR=$(mktemp -d) +cleanup() { + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +GUARDIAN_DIR="$TMP_DIR/guardian" +MODULE_DIR="$TMP_DIR/module" +WATCHDOG_SCRIPT="$GUARDIAN_DIR/watchdog.sh" +mkdir -p "$GUARDIAN_DIR" "$MODULE_DIR/system/etc" + +printf 'Generating watchdog script in a temp Android guardian directory...\n' +ANDROID_GUARDIAN_DIR="$GUARDIAN_DIR" \ +ANDROID_GUARDIAN_MODULE_DIR="$MODULE_DIR" \ +ANDROID_GUARDIAN_WATCHDOG_SCRIPT="$WATCHDOG_SCRIPT" \ +ANDROID_GUARDIAN_POST_FS_SKIP_WATCHDOG_START=1 \ +sh "$TARGET_SCRIPT" + +[[ -f "$WATCHDOG_SCRIPT" ]] || fail 'watchdog script should be generated' +[[ -x "$WATCHDOG_SCRIPT" ]] || fail 'watchdog script should be executable' + +printf 'Checking generated watchdog cadence and lower-fork host protection...\n' +assert_file_contains "$WATCHDOG_SCRIPT" '^HOSTS_CHECK_EVERY_TICKS=10$' \ + 'watchdog should throttle host protection to every 10 ticks' +assert_file_contains "$WATCHDOG_SCRIPT" '^LOOP_SLEEP_SECONDS=3$' \ + 'watchdog should keep the 3 second base sleep' +assert_file_contains "$WATCHDOG_SCRIPT" 'cmp -s' \ + 'watchdog should use cmp for host integrity checks' +assert_file_not_contains "$WATCHDOG_SCRIPT" 'md5sum' \ + 'watchdog should not use md5sum in the hot loop anymore' +assert_file_not_contains "$WATCHDOG_SCRIPT" 'cut -d' \ + 'watchdog should not pipe hashes through cut anymore' + +printf 'Checking test hooks and generation log entry...\n' +assert_file_contains "$TARGET_SCRIPT" 'ANDROID_GUARDIAN_POST_FS_SKIP_MAIN' \ + 'post-fs-data should support skipping main for tests' +assert_file_contains "$TARGET_SCRIPT" 'ANDROID_GUARDIAN_POST_FS_SKIP_WATCHDOG_START' \ + 'post-fs-data should support generating without starting the watchdog' +assert_file_contains "$GUARDIAN_DIR/guardian.log" 'Watchdog generation complete (start skipped)' \ + 'post-fs-data should log skipped watchdog starts during tests' + +printf 'android_guardian post-fs-data regression checks passed.\n' diff --git a/linux_configuration/tests/test_android_guardian_service.sh b/linux_configuration/tests/test_android_guardian_service.sh new file mode 100755 index 0000000..7d174a5 --- /dev/null +++ b/linux_configuration/tests/test_android_guardian_service.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# Regression tests for android_guardian service loop cadence. + +set -euo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd) +TARGET_SCRIPT="$REPO_DIR/scripts/utils/android_guardian/service.sh" + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +assert_equals() { + local expected="$1" + local actual="$2" + local context="$3" + if [[ "$expected" != "$actual" ]]; then + fail "$context (expected: '$expected', actual: '$actual')" + fi +} + +TMP_DIR=$(mktemp -d) +cleanup() { + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +WORKTREE="$TMP_DIR/worktree" +mkdir -p "$WORKTREE/scripts/utils/android_guardian" +cp "$TARGET_SCRIPT" "$WORKTREE/scripts/utils/android_guardian/service.sh" + +printf 'Checking skip-main avoids boot side effects...\n' +SKIP_MAIN_GUARDIAN_DIR="$TMP_DIR/skip-main-guardian" +ANDROID_GUARDIAN_SKIP_MAIN=1 \ +ANDROID_GUARDIAN_DIR="$SKIP_MAIN_GUARDIAN_DIR" \ +ANDROID_GUARDIAN_MODULE_DIR="$TMP_DIR/skip-main-module" \ +sh "$TARGET_SCRIPT" + +[[ ! -d "$SKIP_MAIN_GUARDIAN_DIR" ]] \ + || fail 'skip-main should prevent guardian boot setup side effects' + +printf 'Checking guardian loop cadence constants...\n' +hosts_ticks=$(grep '^HOSTS_CHECK_EVERY_TICKS=' "$WORKTREE/scripts/utils/android_guardian/service.sh" | cut -d= -f2) +apps_ticks=$(grep '^APPS_CHECK_EVERY_TICKS=' "$WORKTREE/scripts/utils/android_guardian/service.sh" | cut -d= -f2) +sleep_seconds=$(grep '^LOOP_SLEEP_SECONDS=' "$WORKTREE/scripts/utils/android_guardian/service.sh" | cut -d= -f2) + +assert_equals '6' "$hosts_ticks" 'hosts protection should run every 6 ticks' +assert_equals '12' "$apps_ticks" 'blocked-app scan should run every 12 ticks' +assert_equals '5' "$sleep_seconds" 'guardian loop should keep the 5 second base sleep' + +printf 'Checking guardian loop protects module every tick...\n' +grep -q 'protect_module' "$WORKTREE/scripts/utils/android_guardian/service.sh" \ + || fail 'guardian loop must always protect the module each tick' + +printf 'Checking blocked-app scans are cached per pass...\n' +grep -q 'installed_packages=$(pm list packages 2>/dev/null)' "$WORKTREE/scripts/utils/android_guardian/service.sh" \ + || fail 'guardian service should cache installed packages once per blocked-app scan' + +if grep -q 'pm list packages 2>/dev/null | grep -q' "$WORKTREE/scripts/utils/android_guardian/service.sh"; then + fail 'guardian service should not re-run pm list packages through grep for every blocked app' +fi + +printf 'Checking guardian loop uses skip-main guard for tests...\n' +grep -q 'ANDROID_GUARDIAN_SKIP_MAIN' "$WORKTREE/scripts/utils/android_guardian/service.sh" \ + || fail 'guardian service should support skip-main testing' + +printf 'android_guardian service regression checks passed.\n' diff --git a/linux_configuration/tests/test_hosts_file_monitor.sh b/linux_configuration/tests/test_hosts_file_monitor.sh new file mode 100755 index 0000000..c8d85f6 --- /dev/null +++ b/linux_configuration/tests/test_hosts_file_monitor.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# Regression tests for hosts-file-monitor.sh behavior. + +set -euo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd) +TARGET_SCRIPT="$REPO_DIR/scripts/system-maintenance/bin/hosts-file-monitor.sh" + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +assert_equals() { + local expected="$1" + local actual="$2" + local context="$3" + if [[ "$expected" != "$actual" ]]; then + fail "$context (expected: '$expected', actual: '$actual')" + fi +} + +TMP_DIR=$(mktemp -d) +cleanup() { + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +WORKTREE="$TMP_DIR/worktree" +BIN_DIR="$TMP_DIR/bin" +mkdir -p "$WORKTREE/scripts/system-maintenance/bin" "$BIN_DIR" +cp "$TARGET_SCRIPT" "$WORKTREE/scripts/system-maintenance/bin/hosts-file-monitor.sh" + +cat >"$BIN_DIR/tee" <<'EOF' +#!/bin/bash +while IFS= read -r _; do + : +done +EOF +chmod +x "$BIN_DIR/tee" + +cat >"$BIN_DIR/sleep" <<'EOF' +#!/bin/bash +printf '%s\n' "$1" >> "${SLEEP_LOG:?}" +exit 99 +EOF +chmod +x "$BIN_DIR/sleep" + +cat >"$BIN_DIR/inotifywait" <<'EOF' +#!/bin/bash +while IFS= read -r line; do + printf '%s\n' "$line" +done <<< "${MOCK_INOTIFY_OUTPUT:-}" +EOF +chmod +x "$BIN_DIR/inotifywait" + +run_shell() { + env -i PATH="$BIN_DIR" HOSTS_FILE_MONITOR_SKIP_MAIN=1 /bin/bash -c "$1" +} + +make_hosts_file() { + local file_path="$1" + local include_custom="$2" + local include_stevenblack="$3" + local i + + : >"$file_path" + for ((i = 1; i <= 1005; i++)); do + printf '127.0.0.1 example-%d.test\n' "$i" >>"$file_path" + done + + if [[ $include_custom -eq 1 ]]; then + printf '# Custom blocking entries\n' >>"$file_path" + fi + + if [[ $include_stevenblack -eq 1 ]]; then + printf '# StevenBlack hosts\n' >>"$file_path" + fi +} + +printf 'Checking intact hosts files are accepted...\n' +hosts_ok="$TMP_DIR/hosts-ok" +make_hosts_file "$hosts_ok" 1 1 +ok_result=$(run_shell "source '$WORKTREE/scripts/system-maintenance/bin/hosts-file-monitor.sh'; HOSTS_FILE='$hosts_ok'; if needs_restoration; then printf restore; else printf ok; fi") +assert_equals 'ok' "$ok_result" 'needs_restoration should accept intact hosts files' + +printf 'Checking missing markers trigger restoration...\n' +hosts_missing="$TMP_DIR/hosts-missing" +make_hosts_file "$hosts_missing" 1 0 +missing_result=$(run_shell "source '$WORKTREE/scripts/system-maintenance/bin/hosts-file-monitor.sh'; HOSTS_FILE='$hosts_missing'; if needs_restoration; then printf restore; else printf ok; fi") +assert_equals 'restore' "$missing_result" 'needs_restoration should reject files missing required markers' + +printf 'Checking inotify path is preferred when available...\n' +inotify_mode=$(run_shell "source '$WORKTREE/scripts/system-maintenance/bin/hosts-file-monitor.sh'; monitor_with_inotify() { printf inotify; }; monitor_with_polling() { printf polling; }; start_monitoring") +assert_equals 'inotify' "$inotify_mode" 'start_monitoring should prefer inotifywait when present' + +printf 'Checking polling fallback is used without inotifywait...\n' +mv "$BIN_DIR/inotifywait" "$BIN_DIR/inotifywait.off" +poll_mode=$(run_shell "source '$WORKTREE/scripts/system-maintenance/bin/hosts-file-monitor.sh'; monitor_with_inotify() { printf inotify; }; monitor_with_polling() { printf polling; }; start_monitoring") +mv "$BIN_DIR/inotifywait.off" "$BIN_DIR/inotifywait" +assert_equals 'polling' "$poll_mode" 'start_monitoring should fall back to polling when inotifywait is absent' + +printf 'hosts-file-monitor.sh regression checks passed.\n' diff --git a/linux_configuration/tests/test_i3blocks_efficiency.sh b/linux_configuration/tests/test_i3blocks_efficiency.sh index dedb22f..5da4ba4 100755 --- a/linux_configuration/tests/test_i3blocks_efficiency.sh +++ b/linux_configuration/tests/test_i3blocks_efficiency.sh @@ -193,6 +193,8 @@ printf 'Checking focus detection path avoids extra xdotool lookups...\n' printf 'Checking ActivityWatch persist strategy avoids /proc event storm...\n' ! grep -Fq 'inotifywait -m -q -e create -e delete /proc' "$I3BLOCKS_DIR/activitywatch_status.sh" \ || fail 'activitywatch persist mode should avoid noisy /proc inotify stream' +grep -Fq 'HEARTBEAT_INTERVAL_S=60' "$I3BLOCKS_DIR/activitywatch_status.sh" \ + || fail 'activitywatch persist mode should use a 60 second calm heartbeat' printf 'Checking GPU/WARP dedupe guards exist...\n' grep -Fq 'emit_if_changed()' "$I3BLOCKS_DIR/gpu_monitor.sh" \ @@ -219,6 +221,8 @@ grep -Fq 'i3blocks_update_if_changed_key "ethernet_output"' "$I3BLOCKS_DIR/ether || fail 'ethernet script should dedupe unchanged output' grep -Fq 'i3blocks_update_if_changed_key "activitywatch_state"' "$I3BLOCKS_DIR/activitywatch_status.sh" \ || fail 'activitywatch script should dedupe unchanged state' +grep -Fq 'WARP_POLL_INTERVAL_S=120' "$I3BLOCKS_DIR/warp_status.sh" \ + || fail 'warp status should poll at a calmer 120 second interval' printf 'Checking bluetooth block behavior and fork count...\n' bluetooth_output=$(PATH="$BIN_DIR:$PATH" bash "$I3BLOCKS_DIR/bluetooth.sh") diff --git a/linux_configuration/tests/test_music_parallelism.sh b/linux_configuration/tests/test_music_parallelism.sh new file mode 100755 index 0000000..e307863 --- /dev/null +++ b/linux_configuration/tests/test_music_parallelism.sh @@ -0,0 +1,108 @@ +#!/bin/bash +# Regression tests for the music parallelism daemon's polling cadence. + +set -euo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd) +TARGET_SCRIPT="$REPO_DIR/scripts/digital_wellbeing/music_parallelism.sh" + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +assert_equals() { + local expected="$1" + local actual="$2" + local context="$3" + if [[ "$expected" != "$actual" ]]; then + fail "$context (expected: '$expected', actual: '$actual')" + fi +} + +TMP_DIR=$(mktemp -d) +cleanup() { + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +WORKTREE="$TMP_DIR/worktree" +BIN_DIR="$TMP_DIR/bin" +mkdir -p "$WORKTREE/scripts/digital_wellbeing" "$WORKTREE/scripts/lib" "$BIN_DIR" +cp "$TARGET_SCRIPT" "$WORKTREE/scripts/digital_wellbeing/music_parallelism.sh" + +cat >"$WORKTREE/scripts/lib/common.sh" <<'EOF' +#!/bin/bash + +FOCUS_APPS_WINDOWS=("Mock Focus App") +FOCUS_APPS_PROCESSES=("mock-focus-proc") + +is_focus_app_running() { + if [[ ${MOCK_FOCUS_ACTIVE:-0} -eq 1 ]]; then + printf '%s\n' "Mock Focus App" + return 0 + fi + + return 1 +} + +get_timestamp() { + printf '%s\n' "${MOCK_TIMESTAMP:-1000}" +} + +log_message() { + : +} +EOF +chmod +x "$WORKTREE/scripts/lib/common.sh" + +cat >"$BIN_DIR/pgrep" <<'EOF' +#!/bin/bash +if [[ ${MOCK_MUSIC_RUNNING:-0} -eq 1 ]]; then + exit 0 +fi +exit 1 +EOF +chmod +x "$BIN_DIR/pgrep" + +cat >"$BIN_DIR/pkill" <<'EOF' +#!/bin/bash +exit 0 +EOF +chmod +x "$BIN_DIR/pkill" + +cat >"$BIN_DIR/sleep" <<'EOF' +#!/bin/bash +printf '%s\n' "$1" >> "${SLEEP_LOG:?}" +exit 99 +EOF +chmod +x "$BIN_DIR/sleep" + +run_case() { + local expected_sleep="$1" + local focus_active="$2" + local music_running="$3" + local sleep_log="$TMP_DIR/sleep.log" + + : >"$sleep_log" + PATH="$BIN_DIR:$PATH" \ + SLEEP_LOG="$sleep_log" \ + MOCK_FOCUS_ACTIVE="$focus_active" \ + MOCK_MUSIC_RUNNING="$music_running" \ + bash "$WORKTREE/scripts/digital_wellbeing/music_parallelism.sh" instant \ + >/dev/null 2>&1 || true + + assert_equals "$expected_sleep" "$(<"$sleep_log")" "music_parallelism.sh should pick the expected sleep interval" +} + +printf 'Checking stable-focus backoff uses the slower interval...\n' +run_case 15 1 0 + +printf 'Checking conflict handling uses the faster retry interval...\n' +run_case 5 1 1 + +printf 'Checking idle mode uses the idle interval...\n' +run_case 30 0 0 + +printf 'music_parallelism.sh regression checks passed.\n' diff --git a/linux_configuration/tests/test_pacman_wrapper_security.sh b/linux_configuration/tests/test_pacman_wrapper_security.sh index 045b0ae..072cf9f 100755 --- a/linux_configuration/tests/test_pacman_wrapper_security.sh +++ b/linux_configuration/tests/test_pacman_wrapper_security.sh @@ -182,6 +182,27 @@ else exit 1 fi +# Test 20: Verify wrapper uses builtin time helpers instead of external date +echo "[TEST 20] Verifying wrapper uses builtin time helpers..." +if grep -q "current_epoch()" "$WRAPPER_DIR/pacman_wrapper.sh" \ + && grep -q "current_day_of_week()" "$WRAPPER_DIR/pacman_wrapper.sh" \ + && grep -q "current_hour_24()" "$WRAPPER_DIR/pacman_wrapper.sh" \ + && grep -q "current_day_name()" "$WRAPPER_DIR/pacman_wrapper.sh"; then + echo "✓ Builtin time helper functions found" +else + echo "✗ Builtin time helper functions missing" + exit 1 +fi + +# Test 21: Verify wrapper avoids external date calls in hot paths +echo "[TEST 21] Verifying wrapper avoids external date calls in hot paths..." +if ! grep -q "date +%s\|date +%u\|date +%H\|date +%A" "$WRAPPER_DIR/pacman_wrapper.sh"; then + echo "✓ External date calls removed from hot paths" +else + echo "✗ External date calls still present in hot paths" + exit 1 +fi + echo "" echo "=== All Tests Passed! ===" echo "" @@ -195,3 +216,4 @@ echo " ✓ Difficult word challenge for VirtualBox installation (7-letter words echo " ✓ makepkg capped runner is integrated via wrapper and installer" echo " ✓ mkpkg convenience helper is deployed by installer" echo " ✓ installer fails fast and handles immutable policy files safely" +echo " ✓ pacman wrapper hot paths use bash builtin time helpers" diff --git a/linux_configuration/tests/test_shutdown_timer_monitor.sh b/linux_configuration/tests/test_shutdown_timer_monitor.sh new file mode 100755 index 0000000..a151476 --- /dev/null +++ b/linux_configuration/tests/test_shutdown_timer_monitor.sh @@ -0,0 +1,123 @@ +#!/bin/bash +# Regression tests for shutdown-timer-monitor.sh dispatcher behavior. + +set -euo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd) +TARGET_SCRIPT="$REPO_DIR/scripts/system-maintenance/bin/shutdown-timer-monitor.sh" +SETUP_SCRIPT="$REPO_DIR/scripts/digital_wellbeing/setup_midnight_shutdown.sh" + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +assert_equals() { + local expected="$1" + local actual="$2" + local context="$3" + if [[ "$expected" != "$actual" ]]; then + fail "$context (expected: '$expected', actual: '$actual')" + fi +} + +TMP_DIR=$(mktemp -d) +cleanup() { + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +WORKTREE="$TMP_DIR/worktree" +BIN_DIR="$TMP_DIR/bin" +mkdir -p "$WORKTREE/scripts/system-maintenance/bin" "$BIN_DIR" +cp "$TARGET_SCRIPT" "$WORKTREE/scripts/system-maintenance/bin/shutdown-timer-monitor.sh" + +cat >"$BIN_DIR/busctl" <<'EOF' +#!/bin/bash +if [[ $1 == monitor ]]; then + printf '%s\n' "${MOCK_BUSCTL_LINE:-no relevant event}" | while read -r line; do + printf '%s\n' "$line" + done + exit 0 +fi + +printf 'unexpected busctl args: %s\n' "$*" >&2 +exit 1 +EOF +chmod +x "$BIN_DIR/busctl" + +cat >"$BIN_DIR/systemctl" <<'EOF' +#!/bin/bash +case "$*" in + 'is-enabled day-specific-shutdown.timer'|'is-active day-specific-shutdown.timer') + exit 0 + ;; + 'daemon-reload'|'enable day-specific-shutdown.timer'|'start day-specific-shutdown.timer') + exit 0 + ;; + *) + printf 'unexpected systemctl args: %s\n' "$*" >&2 + exit 1 + ;; +esac +EOF +chmod +x "$BIN_DIR/systemctl" + +cat >"$BIN_DIR/sleep" <<'EOF' +#!/bin/bash +printf '%s\n' "$1" >> "${SLEEP_LOG:?}" +exit 99 +EOF +chmod +x "$BIN_DIR/sleep" + +cat >"$BIN_DIR/tee" <<'EOF' +#!/bin/bash +while IFS= read -r _; do + : +done +EOF +chmod +x "$BIN_DIR/tee" + +run_case() { + local expected_mode="$1" + local busctl_present="$2" + local sleep_log="$TMP_DIR/sleep.log" + local mode_file="$TMP_DIR/mode.log" + + : >"$sleep_log" + : >"$mode_file" + if [[ $busctl_present -eq 0 ]]; then + mv "$BIN_DIR/busctl" "$BIN_DIR/busctl.off" + fi + + mode=$(env -i PATH="$BIN_DIR" SLEEP_LOG="$sleep_log" SHUTDOWN_TIMER_MONITOR_SKIP_MAIN=1 /bin/bash -c \ + "source '$WORKTREE/scripts/system-maintenance/bin/shutdown-timer-monitor.sh'; \ + timer_needs_restoration() { return 1; }; \ + restore_timer() { :; }; \ + monitor_with_dbus() { printf 'dbus'; }; \ + monitor_with_polling() { printf 'polling'; }; \ + start_monitoring") + + assert_equals "$expected_mode" "$mode" 'shutdown timer monitor should choose the expected dispatcher' + + if [[ -f "$BIN_DIR/busctl.off" ]]; then + mv "$BIN_DIR/busctl.off" "$BIN_DIR/busctl" + fi +} + +printf 'Checking D-Bus path is preferred when busctl exists...\n' +run_case dbus 1 + +printf 'Checking polling fallback is used when busctl is absent...\n' +run_case polling 0 + +printf 'Checking installer template stays in sync with the event-driven monitor...\n' +grep -Fq 'monitor_with_dbus()' "$SETUP_SCRIPT" \ + || fail 'setup_midnight_shutdown.sh should install the D-Bus monitor helper' +grep -Fq 'start_monitoring()' "$SETUP_SCRIPT" \ + || fail 'setup_midnight_shutdown.sh should install the start_monitoring dispatcher' +grep -Fq 'if command -v busctl &>/dev/null; then' "$SETUP_SCRIPT" \ + || fail 'setup_midnight_shutdown.sh should prefer busctl when available' + +printf 'shutdown-timer-monitor.sh regression checks passed.\n' diff --git a/linux_configuration/tests/test_thesis_work_tracker.sh b/linux_configuration/tests/test_thesis_work_tracker.sh new file mode 100755 index 0000000..c79809d --- /dev/null +++ b/linux_configuration/tests/test_thesis_work_tracker.sh @@ -0,0 +1,91 @@ +#!/bin/bash +# Regression tests for thesis_work_tracker.sh helper behavior. + +set -euo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd) +TARGET_SCRIPT="$REPO_DIR/scripts/digital_wellbeing/thesis_work_tracker.sh" + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +assert_equals() { + local expected="$1" + local actual="$2" + local context="$3" + if [[ "$expected" != "$actual" ]]; then + fail "$context (expected: '$expected', actual: '$actual')" + fi +} + +TMP_DIR=$(mktemp -d) +cleanup() { + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +WORKTREE="$TMP_DIR/worktree" +BIN_DIR="$TMP_DIR/bin" +mkdir -p "$WORKTREE/scripts/digital_wellbeing" "$BIN_DIR" +cp "$TARGET_SCRIPT" "$WORKTREE/scripts/digital_wellbeing/thesis_work_tracker.sh" + +cat >"$BIN_DIR/xdotool" <<'EOF' +#!/bin/bash +case "$*" in + 'getactivewindow') + printf '12345\n' + ;; + 'getwindowpid 12345') + printf '6789\n' + ;; + 'getwindowname 12345') + printf '%s\n' "${MOCK_WINDOW_TITLE:-Document - praca_magisterska - Visual Studio Code}" + ;; + *) + printf 'unexpected xdotool args: %s\n' "$*" >&2 + exit 1 + ;; +esac +EOF +chmod +x "$BIN_DIR/xdotool" + +cat >"$BIN_DIR/ps" <<'EOF' +#!/bin/bash +printf '%s\n' "${MOCK_PROCESS_NAME:-Code}" +EOF +chmod +x "$BIN_DIR/ps" + +cat >"$BIN_DIR/pgrep" <<'EOF' +#!/bin/bash +exit 1 +EOF +chmod +x "$BIN_DIR/pgrep" + +cat >"$BIN_DIR/date" <<'EOF' +#!/bin/bash +printf 'date should not be called\n' >&2 +exit 1 +EOF +chmod +x "$BIN_DIR/date" + +source_env() { + PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc "source '$WORKTREE/scripts/digital_wellbeing/thesis_work_tracker.sh'; $1" +} + +printf 'Checking helper output for VS Code on thesis repo...\n' +result=$(source_env 'get_active_window_info') +assert_equals 'Code|Document - praca_magisterska - Visual Studio Code' "$result" \ + 'get_active_window_info should return process and title for VS Code' + +printf 'Checking thesis detection for VS Code thesis repo...\n' +active=$(source_env 'is_thesis_work_active && printf yes || printf no') +assert_equals 'yes' "$active" 'thesis detection should accept VS Code on the thesis repo' + +printf 'Checking thesis detection skips non-thesis VS Code windows...\n' +non_thesis=$(MOCK_WINDOW_TITLE='Document - notes - Visual Studio Code' source_env 'is_thesis_work_active && printf yes || printf no') +assert_equals 'no' "$non_thesis" 'thesis detection should reject VS Code outside the thesis repo' + +printf 'thesis_work_tracker.sh regression checks passed.\n' diff --git a/linux_configuration/tests/test_usage_monitoring_installer_efficiency.sh b/linux_configuration/tests/test_usage_monitoring_installer_efficiency.sh index d93ffd2..7394d84 100755 --- a/linux_configuration/tests/test_usage_monitoring_installer_efficiency.sh +++ b/linux_configuration/tests/test_usage_monitoring_installer_efficiency.sh @@ -26,9 +26,21 @@ printf 'Checking pmon logger template avoids read -t busy-loop pattern...\n' ! grep -q 'read -r -t' <<< "$logger_template" \ || fail 'logger template must not use read -t as sleep surrogate' -printf 'Checking pmon logger template uses sleep-based waiting...\n' -grep -q 'sleep 60' <<< "$logger_template" \ - || fail 'logger template must sleep between day rollover checks' +printf 'Checking pmon logger template uses a sleep-until-midnight helper...\n' +grep -q 'seconds_until_next_day()' <<< "$logger_template" \ + || fail 'logger template must define seconds_until_next_day for rollover timing' + +printf 'Checking pmon logger template avoids minute polling loop...\n' +! grep -q 'sleep 60' <<< "$logger_template" \ + || fail 'logger template must not poll every minute for day rollover' + +printf 'Checking pmon logger template avoids repeated kill -0 probes...\n' +! grep -q 'while kill -0' <<< "$logger_template" \ + || fail 'logger template must not spin on kill -0 for day rollover detection' + +printf 'Checking pmon logger template starts a rollover sleeper...\n' +grep -q 'sleep "\$(seconds_until_next_day)"' <<< "$logger_template" \ + || fail 'logger template must sleep until midnight before rotating pmon' printf 'Checking pmon logger template uses fork-free date builtin...\n' grep -q "printf '%(%Y%m%d)T' -1" <<< "$logger_template" \ diff --git a/phone_focus_mode/config.sh b/phone_focus_mode/config.sh index 5b0fead..d34c698 100755 --- a/phone_focus_mode/config.sh +++ b/phone_focus_mode/config.sh @@ -275,6 +275,7 @@ com.microsoft.office.outlook com.google.android.gm ch.protonmail.android com.microsoft.teams +com.facebook.katana com.facebook.orca # --- App installation alternatives (must stay usable in focus mode) --- diff --git a/python_pkg/steam_backlog_enforcer/game_install.py b/python_pkg/steam_backlog_enforcer/game_install.py index e8330ee..32311d7 100644 --- a/python_pkg/steam_backlog_enforcer/game_install.py +++ b/python_pkg/steam_backlog_enforcer/game_install.py @@ -86,6 +86,10 @@ PROTECTED_APP_IDS = { 220200, 3527290, # Peak 1331550, + 8930, + 1158310, + 440, + 1142710, } STEAMAPPS_PATH = Path("~/.local/share/Steam/steamapps").expanduser()