Optimize polling/runtime scripts, add regressions, and sync verification artifacts

This commit is contained in:
Krzysztof kuhy Rudnicki 2026-05-10 02:58:11 +02:00
parent f4a188068f
commit c9923542fc
24 changed files with 1077 additions and 165 deletions

View File

@ -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"
}

View File

@ -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"
]
}

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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,8 +1033,41 @@ restore_timer() {
fi
}
monitor_with_dbus() {
log_message "Starting shutdown timer monitoring with D-Bus events"
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
}
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"
@ -993,12 +1076,15 @@ else
log_message "Initial check: Timer is properly configured"
fi
while true; do
if timer_needs_restoration; then
restore_timer
if command -v busctl &>/dev/null; then
monitor_with_dbus
else
log_message "busctl not available, falling back to polling"
monitor_with_polling
fi
sleep "$CHECK_INTERVAL"
done
}
start_monitoring
EOF
chmod +x "$monitor_script"

View File

@ -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 <<EOF | sudo tee "$STATE_FILE" >/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 <<EOF | sudo tee "$STATE_FILE" >/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,6 +472,7 @@ cleanup() {
exit 0
}
if [[ ${THESIS_WORK_TRACKER_SKIP_MAIN:-0} -ne 1 ]]; then
trap cleanup SIGTERM SIGINT
# Check for lock file to prevent multiple instances
@ -475,3 +486,4 @@ touch "$LOCK_FILE"
# Run main loop
main_loop
fi

View File

@ -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,10 +118,9 @@ monitor_with_polling() {
done
}
# Main execution
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
@ -110,3 +129,9 @@ else
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

View File

@ -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
(
sleep "$(seconds_until_next_day)"
kill "$pmon_pid" >/dev/null 2>&1 || true
) &
rollover_pid=$!
wait "$pmon_pid" || true
break
if [[ -n $rollover_pid ]]; then
kill "$rollover_pid" >/dev/null 2>&1 || true
wait "$rollover_pid" 2>/dev/null || true
fi
sleep 60
done
done
SCRIPT

View File

@ -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,7 +116,7 @@ monitor_with_polling() {
done
}
# Main execution
start_monitoring() {
log_message "=== Shutdown Timer Monitor Started ==="
log_message "Monitoring timer: $TIMER_NAME"
log_message "Monitoring service: $SERVICE_NAME"
@ -129,5 +129,16 @@ else
log_message "Initial check: Timer is properly configured"
fi
# Use polling for reliability (D-Bus monitoring can miss events)
# 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
if [[ ${SHUTDOWN_TIMER_MONITOR_SKIP_MAIN:-0} -ne 1 ]]; then
start_monitoring
fi

View File

@ -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
if ! cmp -s "$MODULE_HOSTS" "$HOSTS_BACKUP"; then
log "ALERT: Hosts tampering detected - restoring"
cp "$GUARDIAN_DIR/hosts.backup" "$MODULE_DIR/system/etc/hosts"
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
}
start_watchdog() {
nohup sh "$WATCHDOG_SCRIPT" >/dev/null 2>&1 &
}
post_fs_main() {
mkdir -p "$GUARDIAN_DIR"
log "Guardian module loading"
write_watchdog_script
chmod 755 "$WATCHDOG_SCRIPT"
# 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"
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

View File

@ -7,24 +7,30 @@
# 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"
if [ ! -f "$CONTROL_FILE" ]; then
echo "ENABLED" >"$CONTROL_FILE"
fi
log "=== Android Guardian starting ==="
@ -33,6 +39,7 @@ 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
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
if [ $((tick_count % HOSTS_CHECK_EVERY_TICKS)) -eq 0 ]; then
protect_hosts
check_blocked_apps
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
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

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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")

View File

@ -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'

View File

@ -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"

View File

@ -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'

View File

@ -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'

View File

@ -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" \

View File

@ -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) ---

View File

@ -86,6 +86,10 @@ PROTECTED_APP_IDS = {
220200,
3527290, # Peak
1331550,
8930,
1158310,
440,
1142710,
}
STEAMAPPS_PATH = Path("~/.local/share/Steam/steamapps").expanduser()