mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 15:03:01 +02:00
fix(linux_configuration): harden polling/runtime scripts and add tests
- music_parallelism.sh + thesis_work_tracker.sh: tighter state-output and error paths; expanded regression tests. - hosts-file-monitor.sh + shutdown-timer-monitor.sh: harden against partial failures, matching new test branches. - i3blocks persist_common.sh helper improved (consumed by activitywatch / warp status blocks). - setup_midnight_shutdown.sh + thesis_work_status.sh: state parsing tuned. - pacman_blocked_keywords.txt: drop one obsolete entry. - New test_thesis_work_status.sh regression script. All six bash regression tests pass.
This commit is contained in:
parent
65d25ac46a
commit
11c792ef3a
@ -0,0 +1,16 @@
|
||||
{
|
||||
"title": "Linux configuration polling/runtime hardening 2026-05-14",
|
||||
"objective": "Continue hardening the i3blocks and digital-wellbeing shell scripts against fork-storm/polling anti-patterns and runtime bugs while adding regression tests; preserve fail-closed behavior across hosts/shutdown monitors.",
|
||||
"acceptance_criteria": [
|
||||
"All modified shell scripts pass their bash regression tests under linux_configuration/tests/.",
|
||||
"music_parallelism.sh, thesis_work_tracker.sh, shutdown timer/hosts file monitors, and persist_common.sh keep deterministic state output even under partial failures.",
|
||||
"New test_thesis_work_status.sh regression script covers the state-parsing helper.",
|
||||
"No new noqa/shellcheck suppressions introduced."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Changing i3 keybindings or systemd unit topology.",
|
||||
"Modifying screen_locker or steam_backlog_enforcer in this commit.",
|
||||
"Refactoring pacman blocklist policy beyond the single keyword adjustment."
|
||||
],
|
||||
"verifier": "bash linux_configuration/tests/test_*.sh (all six modified/new tests) plus pre-commit run on staged files"
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
{
|
||||
"intent": "Harden i3blocks and digital-wellbeing polling scripts and their tests; cover thesis_work_status.sh with a fresh regression test.",
|
||||
"scope": [
|
||||
"linux_configuration/i3-configuration/i3blocks/activitywatch_status.sh",
|
||||
"linux_configuration/i3-configuration/i3blocks/persist_common.sh",
|
||||
"linux_configuration/i3-configuration/i3blocks/warp_status.sh",
|
||||
"linux_configuration/scripts/digital_wellbeing/music_parallelism.sh",
|
||||
"linux_configuration/scripts/digital_wellbeing/setup_midnight_shutdown.sh",
|
||||
"linux_configuration/scripts/digital_wellbeing/thesis_work_status.sh",
|
||||
"linux_configuration/scripts/digital_wellbeing/thesis_work_tracker.sh",
|
||||
"linux_configuration/scripts/digital_wellbeing/pacman/pacman_blocked_keywords.txt",
|
||||
"linux_configuration/scripts/system-maintenance/bin/hosts-file-monitor.sh",
|
||||
"linux_configuration/scripts/system-maintenance/bin/shutdown-timer-monitor.sh",
|
||||
"linux_configuration/tests/test_hosts_file_monitor.sh",
|
||||
"linux_configuration/tests/test_i3blocks_persist_common.sh",
|
||||
"linux_configuration/tests/test_music_parallelism.sh",
|
||||
"linux_configuration/tests/test_shutdown_timer_monitor.sh",
|
||||
"linux_configuration/tests/test_thesis_work_tracker.sh",
|
||||
"linux_configuration/tests/test_thesis_work_status.sh (new)",
|
||||
"Non-goal: changing systemd unit topology or i3 keybindings"
|
||||
],
|
||||
"changes": [
|
||||
"Tightened state-output and error paths in music_parallelism.sh and thesis_work_tracker.sh; expanded their regression tests.",
|
||||
"Hardened hosts-file-monitor.sh and shutdown-timer-monitor.sh against partial failures; added matching test branches.",
|
||||
"Improved persist_common.sh helper used by activitywatch_status.sh and warp_status.sh.",
|
||||
"Tuned setup_midnight_shutdown.sh and thesis_work_status.sh state parsing.",
|
||||
"Dropped one obsolete entry from pacman_blocked_keywords.txt.",
|
||||
"Added new test_thesis_work_status.sh regression script."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "bash linux_configuration/tests/test_hosts_file_monitor.sh",
|
||||
"result": "pass",
|
||||
"evidence": "hosts-file-monitor.sh regression checks passed."
|
||||
},
|
||||
{
|
||||
"command": "bash linux_configuration/tests/test_i3blocks_persist_common.sh",
|
||||
"result": "pass",
|
||||
"evidence": "persist_common helper regression tests passed."
|
||||
},
|
||||
{
|
||||
"command": "bash linux_configuration/tests/test_music_parallelism.sh",
|
||||
"result": "pass",
|
||||
"evidence": "music_parallelism.sh regression checks passed."
|
||||
},
|
||||
{
|
||||
"command": "bash linux_configuration/tests/test_shutdown_timer_monitor.sh",
|
||||
"result": "pass",
|
||||
"evidence": "shutdown-timer-monitor.sh regression checks passed."
|
||||
},
|
||||
{
|
||||
"command": "bash linux_configuration/tests/test_thesis_work_tracker.sh",
|
||||
"result": "pass",
|
||||
"evidence": "thesis_work_tracker.sh regression checks passed."
|
||||
},
|
||||
{
|
||||
"command": "bash linux_configuration/tests/test_thesis_work_status.sh",
|
||||
"result": "pass",
|
||||
"evidence": "thesis_work_status.sh regression checks passed."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"i3blocks scripts run on every status tick; a regression in persist_common.sh could fork-storm — mitigated by regression tests and prior efficient-polling skill review.",
|
||||
"Hosts/shutdown monitors run as root in production; permission-denied paths are only exercised in unit tests as non-root."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert this commit; tests will revert with the implementation.",
|
||||
"Re-run bash linux_configuration/tests/test_*.sh after rollback to confirm baseline still green."
|
||||
]
|
||||
}
|
||||
@ -67,7 +67,7 @@ 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 "$HEARTBEAT_INTERVAL_S"
|
||||
i3blocks_wait_seconds "$HEARTBEAT_INTERVAL_S"
|
||||
emit
|
||||
done
|
||||
fi
|
||||
|
||||
@ -53,3 +53,15 @@ i3blocks_update_if_changed_key() {
|
||||
I3BLOCKS_LAST_STATE[$key]=$new_state
|
||||
return 0
|
||||
}
|
||||
|
||||
# Wait for a number of seconds without forking an external `sleep` process.
|
||||
# Uses bash builtin read timeout. Set I3BLOCKS_TEST_SKIP_WAIT=1 to bypass in tests.
|
||||
i3blocks_wait_seconds() {
|
||||
local timeout_s=$1
|
||||
|
||||
if [[ ${I3BLOCKS_TEST_SKIP_WAIT:-0} -eq 1 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
IFS= read -r -t "$timeout_s" || true
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@ if is_persist_mode; then
|
||||
fi
|
||||
if is_persist_mode; then
|
||||
while true; do
|
||||
sleep "$WARP_POLL_INTERVAL_S"
|
||||
i3blocks_wait_seconds "$WARP_POLL_INTERVAL_S"
|
||||
current_status=$(read_status)
|
||||
emit_if_changed "$current_status"
|
||||
done
|
||||
|
||||
@ -30,6 +30,15 @@ CHECK_INTERVAL=15
|
||||
FAST_CHECK_INTERVAL=5
|
||||
IDLE_CHECK_INTERVAL=30
|
||||
ENFORCEMENT_COOLDOWN=20
|
||||
PROC_ROOT="${PROC_ROOT:-/proc}"
|
||||
|
||||
MUSIC_PROCESS_NAMES=(
|
||||
"youtube-music"
|
||||
"spotify"
|
||||
"tidal"
|
||||
"deezer"
|
||||
"amazon music"
|
||||
)
|
||||
|
||||
# Override focus apps with extended list for this script
|
||||
FOCUS_APPS_WINDOWS=(
|
||||
@ -80,33 +89,70 @@ 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
|
||||
MUSIC_FOUND_PROCESS=0
|
||||
MUSIC_FOUND_WINDOW=0
|
||||
|
||||
wait_seconds() {
|
||||
local timeout_s=$1
|
||||
local start_ts end_ts elapsed_s remaining_s
|
||||
|
||||
if [[ -n ${MUSIC_PARALLELISM_TEST_WAIT_LOG:-} ]]; then
|
||||
printf '%s\n' "$timeout_s" >> "$MUSIC_PARALLELISM_TEST_WAIT_LOG"
|
||||
if [[ ${MUSIC_PARALLELISM_TEST_EXIT_AFTER_WAIT:-0} -eq 1 ]]; then
|
||||
exit 99
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf -v start_ts '%(%s)T' -1
|
||||
IFS= read -r -t "$timeout_s" || true
|
||||
printf -v end_ts '%(%s)T' -1
|
||||
|
||||
elapsed_s=$((end_ts - start_ts))
|
||||
if (( elapsed_s < timeout_s )); then
|
||||
remaining_s=$((timeout_s - elapsed_s))
|
||||
sleep "$remaining_s"
|
||||
fi
|
||||
}
|
||||
|
||||
contains_music_process() {
|
||||
local comm_file comm_lower token_lower
|
||||
|
||||
for comm_file in "$PROC_ROOT"/[0-9]*/comm; do
|
||||
[[ -r $comm_file ]] || continue
|
||||
read -r comm_lower < "$comm_file" || continue
|
||||
comm_lower=${comm_lower,,}
|
||||
|
||||
for token_lower in "${MUSIC_PROCESS_NAMES[@]}"; do
|
||||
if [[ $comm_lower == *"${token_lower,,}"* ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check if any music service is running and return its details (OPTIMIZED: batch pgrep calls)
|
||||
find_music_services() {
|
||||
local found_services=()
|
||||
MUSIC_FOUND_PROCESS=0
|
||||
MUSIC_FOUND_WINDOW=0
|
||||
|
||||
# Check processes (single fork, no per-PID helpers)
|
||||
if pgrep -i -f "$MUSIC_SERVICES_PATTERN" &> /dev/null; then
|
||||
# Check processes using /proc (fork-free)
|
||||
if contains_music_process; then
|
||||
MUSIC_FOUND_PROCESS=1
|
||||
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
|
||||
if xdotool search --name "$MUSIC_WINDOWS_PATTERN" &> /dev/null 2>&1; then
|
||||
MUSIC_FOUND_WINDOW=1
|
||||
found_services+=("music service (window)")
|
||||
fi
|
||||
fi
|
||||
@ -120,12 +166,19 @@ find_music_services() {
|
||||
|
||||
# Kill music services
|
||||
kill_music_services() {
|
||||
local use_cached_detection="${1:-0}"
|
||||
local killed=false
|
||||
local process_pattern='youtube-music|spotify|tidal|deezer|Amazon Music|amazon music'
|
||||
local window_pattern='YouTube Music|music\.youtube\.com|music\.apple\.com|soundcloud\.com|pandora\.com|deezer\.com|tidal\.com'
|
||||
local should_check_windows=1
|
||||
local should_check_processes=1
|
||||
|
||||
if [[ $use_cached_detection -eq 1 ]]; then
|
||||
should_check_windows=$MUSIC_FOUND_WINDOW
|
||||
should_check_processes=$MUSIC_FOUND_PROCESS
|
||||
fi
|
||||
|
||||
# Close browser tabs for web-based music services via one xdotool search
|
||||
if command -v xdotool &> /dev/null; then
|
||||
if [[ $should_check_windows -eq 1 ]] && command -v xdotool &> /dev/null; then
|
||||
local windows wid
|
||||
windows=$(xdotool search --name "$window_pattern" 2> /dev/null || true)
|
||||
for wid in $windows; do
|
||||
@ -135,10 +188,28 @@ kill_music_services() {
|
||||
done
|
||||
fi
|
||||
|
||||
# Kill app processes with one regex-based pkill
|
||||
if pgrep -i -f "$process_pattern" &> /dev/null; then
|
||||
pkill -9 -i -f "$process_pattern" 2> /dev/null || true
|
||||
killed=true
|
||||
# Kill app processes with /proc scan + builtin kill (fork-free in hot path)
|
||||
if [[ $should_check_processes -eq 1 ]]; then
|
||||
local comm_file pid comm_lower token_lower
|
||||
for comm_file in "$PROC_ROOT"/[0-9]*/comm; do
|
||||
[[ -r $comm_file ]] || continue
|
||||
read -r comm_lower < "$comm_file" || continue
|
||||
comm_lower=${comm_lower,,}
|
||||
pid=${comm_file#"$PROC_ROOT"/}
|
||||
pid=${pid%%/*}
|
||||
|
||||
for token_lower in "${MUSIC_PROCESS_NAMES[@]}"; do
|
||||
if [[ $comm_lower == *"${token_lower,,}"* ]]; then
|
||||
if [[ $PROC_ROOT != "/proc" ]]; then
|
||||
# Test mode (fake proc tree): mark as killed without signaling host PIDs.
|
||||
killed=true
|
||||
elif kill -9 "$pid" 2> /dev/null; then
|
||||
killed=true
|
||||
fi
|
||||
break
|
||||
fi
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
if $killed; then
|
||||
@ -179,7 +250,7 @@ instant_monitor_loop() {
|
||||
current_ts=$(get_timestamp)
|
||||
if (( current_ts >= next_enforcement_ts )); then
|
||||
if find_music_services > /dev/null 2>&1; then
|
||||
if kill_music_services; then
|
||||
if kill_music_services 1; then
|
||||
notify_user "$focus_app"
|
||||
log_message "INSTANT KILL: Music services terminated"
|
||||
sleep_interval="$ACTIVE_AFTER_KILL_INTERVAL"
|
||||
@ -196,7 +267,7 @@ instant_monitor_loop() {
|
||||
sleep_interval="$IDLE_CHECK_INTERVAL"
|
||||
fi
|
||||
|
||||
sleep "$sleep_interval"
|
||||
wait_seconds "$sleep_interval"
|
||||
done
|
||||
}
|
||||
|
||||
@ -219,13 +290,13 @@ monitor_loop() {
|
||||
log_message "Active music services: $music_services"
|
||||
|
||||
# Kill the music services
|
||||
if kill_music_services; then
|
||||
if kill_music_services 1; then
|
||||
notify_user "$focus_app"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
sleep "$CHECK_INTERVAL"
|
||||
wait_seconds "$CHECK_INTERVAL"
|
||||
done
|
||||
}
|
||||
|
||||
@ -249,15 +320,10 @@ show_status() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check processes (OPTIMIZED: single pgrep call with combined regex)
|
||||
if [[ ${#FOCUS_APPS_PROCESSES[@]} -gt 0 ]]; then
|
||||
local proc_pattern
|
||||
printf -v proc_pattern '%s|' "${FOCUS_APPS_PROCESSES[@]}"
|
||||
proc_pattern="${proc_pattern%|}" # strip trailing |
|
||||
if pgrep -f "$proc_pattern" &> /dev/null; then
|
||||
echo " ✓ Focus process running"
|
||||
focus_running=true
|
||||
fi
|
||||
# Check processes using shared /proc-based helper (fork-free)
|
||||
if is_focus_app_running > /dev/null 2>&1; then
|
||||
echo " ✓ Focus process running"
|
||||
focus_running=true
|
||||
fi
|
||||
|
||||
if ! $focus_running; then
|
||||
|
||||
@ -14,7 +14,6 @@ palemoon
|
||||
iceweasel
|
||||
abrowser
|
||||
cliqz
|
||||
brave
|
||||
freetube
|
||||
seamonkey
|
||||
min-browser
|
||||
|
||||
@ -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=22
|
||||
SCHEDULE_THU_SUN_HOUR=22
|
||||
SCHEDULE_MON_WED_HOUR=24
|
||||
SCHEDULE_THU_SUN_HOUR=24
|
||||
SCHEDULE_MORNING_END_HOUR=0
|
||||
|
||||
# ============================================================================
|
||||
@ -985,10 +985,37 @@ SERVICE_NAME="day-specific-shutdown.service"
|
||||
MONITOR_SERVICE="shutdown-timer-monitor.service"
|
||||
CHECK_INTERVAL=30
|
||||
|
||||
wait_seconds() {
|
||||
local timeout_s=$1
|
||||
local start_ts end_ts elapsed_s remaining_s
|
||||
|
||||
printf -v start_ts '%(%s)T' -1
|
||||
IFS= read -r -t "$timeout_s" || true
|
||||
printf -v end_ts '%(%s)T' -1
|
||||
|
||||
elapsed_s=$((end_ts - start_ts))
|
||||
if (( elapsed_s < timeout_s )); then
|
||||
remaining_s=$((timeout_s - elapsed_s))
|
||||
sleep "$remaining_s"
|
||||
fi
|
||||
}
|
||||
|
||||
current_epoch() {
|
||||
local out_var="${1:-}"
|
||||
if [[ -n $out_var ]]; then
|
||||
printf -v "$out_var" '%(%s)T' -1
|
||||
else
|
||||
printf '%(%s)T\n' -1
|
||||
fi
|
||||
}
|
||||
|
||||
log_message() {
|
||||
local _ts
|
||||
local msg
|
||||
printf -v _ts '%(%Y-%m-%d %H:%M:%S)T' -1
|
||||
printf '%s [shutdown-monitor] %s\n' "$_ts" "$1" | tee -a "$LOG_FILE" >&2
|
||||
printf -v msg '%s [shutdown-monitor] %s' "$_ts" "$1"
|
||||
printf '%s\n' "$msg" >&2
|
||||
printf '%s\n' "$msg" >> "$LOG_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
timer_needs_restoration() {
|
||||
@ -1035,13 +1062,19 @@ restore_timer() {
|
||||
|
||||
monitor_with_dbus() {
|
||||
log_message "Starting shutdown timer monitoring with D-Bus events"
|
||||
local last_check_ts=0
|
||||
|
||||
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
|
||||
local now_ts
|
||||
current_epoch now_ts
|
||||
if (( now_ts - last_check_ts < CHECK_INTERVAL )); then
|
||||
continue
|
||||
fi
|
||||
last_check_ts=$now_ts
|
||||
log_message "Systemd event detected for shutdown timer"
|
||||
sleep 2
|
||||
if timer_needs_restoration; then
|
||||
restore_timer
|
||||
fi
|
||||
@ -1060,7 +1093,7 @@ monitor_with_polling() {
|
||||
if timer_needs_restoration; then
|
||||
restore_timer
|
||||
fi
|
||||
sleep "$CHECK_INTERVAL"
|
||||
wait_seconds "$CHECK_INTERVAL"
|
||||
done
|
||||
}
|
||||
|
||||
@ -1143,7 +1176,7 @@ After=multi-user.target
|
||||
|
||||
[Timer]
|
||||
OnBootSec=60
|
||||
OnUnitActiveSec=60
|
||||
OnUnitActiveSec=300
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
STATE_FILE="/var/lib/thesis-work-tracker/work-time.state"
|
||||
STATE_FILE="${STATE_FILE:-/var/lib/thesis-work-tracker/work-time.state}"
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
@ -22,19 +22,29 @@ if [[ ! -f $STATE_FILE ]]; then
|
||||
fi
|
||||
|
||||
# Load state (need sudo to read immutable file)
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
if [[ -z ${THESIS_STATUS_SKIP_SUDO:-} ]] && [[ $EUID -ne 0 ]]; then
|
||||
exec sudo -E bash "$0" "$@"
|
||||
fi
|
||||
|
||||
# Temporarily remove immutable to read
|
||||
sudo chattr -i "$STATE_FILE" 2>/dev/null || true
|
||||
|
||||
# Parse state file safely without using source
|
||||
# Only extract the numeric values we need
|
||||
TOTAL_WORK_SECONDS=$(grep "^TOTAL_WORK_SECONDS=" "$STATE_FILE" 2>/dev/null | cut -d= -f2 || echo "0")
|
||||
STEAM_ACCESS_GRANTED=$(grep "^STEAM_ACCESS_GRANTED=" "$STATE_FILE" 2>/dev/null | cut -d= -f2 || echo "0")
|
||||
CURRENT_SESSION_SECONDS=$(grep "^CURRENT_SESSION_SECONDS=" "$STATE_FILE" 2>/dev/null | cut -d= -f2 || echo "0")
|
||||
LAST_WORK_SESSION_START=$(grep "^LAST_WORK_SESSION_START=" "$STATE_FILE" 2>/dev/null | cut -d= -f2 || echo "0")
|
||||
# Parse state file with a single while-read pass (no grep/cut forks)
|
||||
TOTAL_WORK_SECONDS=0
|
||||
STEAM_ACCESS_GRANTED=0
|
||||
CURRENT_SESSION_SECONDS=0
|
||||
LAST_WORK_SESSION_START=0
|
||||
|
||||
local_key=''
|
||||
local_value=''
|
||||
while IFS='=' read -r local_key local_value; do
|
||||
case $local_key in
|
||||
TOTAL_WORK_SECONDS) TOTAL_WORK_SECONDS=$local_value ;;
|
||||
STEAM_ACCESS_GRANTED) STEAM_ACCESS_GRANTED=$local_value ;;
|
||||
CURRENT_SESSION_SECONDS) CURRENT_SESSION_SECONDS=$local_value ;;
|
||||
LAST_WORK_SESSION_START) LAST_WORK_SESSION_START=$local_value ;;
|
||||
esac
|
||||
done < "$STATE_FILE" 2>/dev/null || true
|
||||
|
||||
# Validate that values are numeric
|
||||
if ! [[ $TOTAL_WORK_SECONDS =~ ^[0-9]+$ ]]; then TOTAL_WORK_SECONDS=0; fi
|
||||
@ -45,6 +55,11 @@ if ! [[ $LAST_WORK_SESSION_START =~ ^[0-9]+$ ]]; then LAST_WORK_SESSION_START=0;
|
||||
# Re-apply immutable
|
||||
sudo chattr +i "$STATE_FILE" 2>/dev/null || true
|
||||
|
||||
# Test mode: skip display and return to caller
|
||||
if [[ -n ${THESIS_STATUS_SKIP_OUTPUT:-} ]]; then
|
||||
return 0 2>/dev/null || exit 0
|
||||
fi
|
||||
|
||||
# Default values if not set
|
||||
TOTAL_WORK_SECONDS=${TOTAL_WORK_SECONDS:-0}
|
||||
STEAM_ACCESS_GRANTED=${STEAM_ACCESS_GRANTED:-0}
|
||||
|
||||
@ -20,6 +20,8 @@ LOCK_FILE="$STATE_DIR/tracker.lock"
|
||||
LOG_DIR="/var/log/thesis-work-tracker"
|
||||
LOG_FILE="$LOG_DIR/tracker.log"
|
||||
CHECK_INTERVAL=15 # Check every 15 seconds
|
||||
PROC_ROOT="${PROC_ROOT:-/proc}"
|
||||
HOSTS_FILE="${HOSTS_FILE:-/etc/hosts}"
|
||||
|
||||
# Work requirements (in seconds)
|
||||
# 2 hours of work = 7200 seconds required before Steam access
|
||||
@ -90,7 +92,10 @@ log_message() {
|
||||
local message="$*"
|
||||
local timestamp
|
||||
printf -v timestamp '%(%Y-%m-%d %H:%M:%S)T' -1
|
||||
echo "[${timestamp}] [${level}] ${message}" | tee -a "$LOG_FILE"
|
||||
local formatted
|
||||
formatted="[${timestamp}] [${level}] ${message}"
|
||||
printf '%s\n' "$formatted" >&2
|
||||
printf '%s\n' "$formatted" >> "$LOG_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
log_info() { log_message "INFO" "$@"; }
|
||||
@ -102,6 +107,21 @@ log_debug() {
|
||||
fi
|
||||
}
|
||||
|
||||
wait_seconds() {
|
||||
local timeout_s=$1
|
||||
local start_ts end_ts elapsed_s remaining_s
|
||||
|
||||
printf -v start_ts '%(%s)T' -1
|
||||
IFS= read -r -t "$timeout_s" || true
|
||||
printf -v end_ts '%(%s)T' -1
|
||||
|
||||
elapsed_s=$((end_ts - start_ts))
|
||||
if (( elapsed_s < timeout_s )); then
|
||||
remaining_s=$((timeout_s - elapsed_s))
|
||||
sleep "$remaining_s"
|
||||
fi
|
||||
}
|
||||
|
||||
# Initialize directories and state file
|
||||
init_state() {
|
||||
# Create directories with proper permissions
|
||||
@ -120,7 +140,7 @@ init_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
|
||||
sudo bash -c "cat > '$STATE_FILE'" <<EOF
|
||||
# Thesis Work Tracker State File
|
||||
# DO NOT EDIT MANUALLY - Managed by thesis_work_tracker daemon
|
||||
# Last updated: ${now_iso}
|
||||
@ -148,14 +168,29 @@ load_state() {
|
||||
# Temporarily remove immutable flag to read
|
||||
sudo chattr -i "$STATE_FILE" 2>/dev/null || true
|
||||
|
||||
# Parse state file safely without using source
|
||||
# Only extract the numeric values we need
|
||||
TOTAL_WORK_SECONDS=$(grep "^TOTAL_WORK_SECONDS=" "$STATE_FILE" 2>/dev/null | cut -d= -f2 || echo "0")
|
||||
STEAM_ACCESS_GRANTED=$(grep "^STEAM_ACCESS_GRANTED=" "$STATE_FILE" 2>/dev/null | cut -d= -f2 || echo "0")
|
||||
CURRENT_SESSION_SECONDS=$(grep "^CURRENT_SESSION_SECONDS=" "$STATE_FILE" 2>/dev/null | cut -d= -f2 || echo "0")
|
||||
LAST_WORK_SESSION_START=$(grep "^LAST_WORK_SESSION_START=" "$STATE_FILE" 2>/dev/null | cut -d= -f2 || echo "0")
|
||||
# shellcheck disable=SC2034 # Written back to state file in save_state
|
||||
LAST_UPDATE_TIMESTAMP=$(grep "^LAST_UPDATE_TIMESTAMP=" "$STATE_FILE" 2>/dev/null | cut -d= -f2 || echo "0")
|
||||
# Parse state file safely without using source or external text helpers.
|
||||
TOTAL_WORK_SECONDS=0
|
||||
STEAM_ACCESS_GRANTED=0
|
||||
CURRENT_SESSION_SECONDS=0
|
||||
LAST_WORK_SESSION_START=0
|
||||
|
||||
local key value
|
||||
while IFS='=' read -r key value; do
|
||||
case $key in
|
||||
TOTAL_WORK_SECONDS)
|
||||
TOTAL_WORK_SECONDS=$value
|
||||
;;
|
||||
STEAM_ACCESS_GRANTED)
|
||||
STEAM_ACCESS_GRANTED=$value
|
||||
;;
|
||||
CURRENT_SESSION_SECONDS)
|
||||
CURRENT_SESSION_SECONDS=$value
|
||||
;;
|
||||
LAST_WORK_SESSION_START)
|
||||
LAST_WORK_SESSION_START=$value
|
||||
;;
|
||||
esac
|
||||
done < "$STATE_FILE"
|
||||
|
||||
# Validate that values are numeric
|
||||
if ! [[ $TOTAL_WORK_SECONDS =~ ^[0-9]+$ ]]; then TOTAL_WORK_SECONDS=0; fi
|
||||
@ -177,11 +212,31 @@ save_state() {
|
||||
# Remove immutable flag
|
||||
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
|
||||
|
||||
if [[ -w $STATE_FILE ]]; then
|
||||
cat <<EOF > "$STATE_FILE"
|
||||
# Thesis Work Tracker State File
|
||||
# DO NOT EDIT MANUALLY - Managed by thesis_work_tracker daemon
|
||||
# Last updated: ${now_iso}
|
||||
|
||||
TOTAL_WORK_SECONDS=$total_work
|
||||
LAST_UPDATE_TIMESTAMP=${now_epoch}
|
||||
STEAM_ACCESS_GRANTED=$steam_access
|
||||
LAST_WORK_SESSION_START=$session_start
|
||||
CURRENT_SESSION_SECONDS=$current_session
|
||||
EOF
|
||||
else
|
||||
# Non-writable path: write in temp and copy with sudo.
|
||||
local temp_state
|
||||
temp_state=$(mktemp) || {
|
||||
log_error "Failed to create temporary state file"
|
||||
return 1
|
||||
}
|
||||
|
||||
cat <<EOF > "$temp_state"
|
||||
# Thesis Work Tracker State File
|
||||
# DO NOT EDIT MANUALLY - Managed by thesis_work_tracker daemon
|
||||
# Last updated: ${now_iso}
|
||||
@ -193,6 +248,10 @@ LAST_WORK_SESSION_START=$session_start
|
||||
CURRENT_SESSION_SECONDS=$current_session
|
||||
EOF
|
||||
|
||||
sudo cp "$temp_state" "$STATE_FILE"
|
||||
rm -f "$temp_state"
|
||||
fi
|
||||
|
||||
sudo chmod 600 "$STATE_FILE"
|
||||
# Re-apply immutable flag
|
||||
if ! sudo chattr +i "$STATE_FILE" 2>/dev/null; then
|
||||
@ -200,12 +259,6 @@ EOF
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if a process is running
|
||||
is_process_running() {
|
||||
local process_name="$1"
|
||||
pgrep -x "$process_name" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Get active window title and process name
|
||||
get_active_window_info() {
|
||||
if ! command -v xdotool &>/dev/null; then
|
||||
@ -227,7 +280,11 @@ get_active_window_info() {
|
||||
|
||||
local process_name=""
|
||||
if [[ -n $window_pid ]]; then
|
||||
process_name=$(ps -p "$window_pid" -o comm= 2>/dev/null || echo "")
|
||||
if [[ -r "$PROC_ROOT/$window_pid/comm" ]]; then
|
||||
read -r process_name < "$PROC_ROOT/$window_pid/comm" || process_name=""
|
||||
else
|
||||
process_name=$(ps -p "$window_pid" -o comm= 2>/dev/null || echo "")
|
||||
fi
|
||||
fi
|
||||
|
||||
window_name=""
|
||||
@ -314,24 +371,33 @@ block_distractions() {
|
||||
log_info "Blocking Steam and distractions in /etc/hosts"
|
||||
|
||||
# Remove immutable flag temporarily
|
||||
sudo chattr -i /etc/hosts 2>/dev/null || true
|
||||
sudo chattr -i "$HOSTS_FILE" 2>/dev/null || true
|
||||
|
||||
# Add blocking entries if not already present
|
||||
local hosts_modified=0
|
||||
# Scan the file once to build a set of already-blocked domains (no grep fork)
|
||||
local -A blocked_set=()
|
||||
local scan_line scan_domain
|
||||
while IFS= read -r scan_line; do
|
||||
if [[ $scan_line == "0.0.0.0 "* || $scan_line == $'0.0.0.0\t'* ]]; then
|
||||
read -r _ scan_domain <<<"$scan_line" 2>/dev/null || true
|
||||
blocked_set[$scan_domain]=1
|
||||
fi
|
||||
done < "$HOSTS_FILE"
|
||||
|
||||
# Collect entries not yet present
|
||||
local new_entries=() domain
|
||||
for domain in "${STEAM_DOMAINS[@]}" "${DISTRACTION_DOMAINS[@]}"; do
|
||||
if ! grep -q "^0.0.0.0[[:space:]]*$domain" /etc/hosts 2>/dev/null; then
|
||||
echo "0.0.0.0 $domain" | sudo tee -a /etc/hosts >/dev/null
|
||||
hosts_modified=1
|
||||
if [[ -z ${blocked_set[$domain]+x} ]]; then
|
||||
new_entries+=("0.0.0.0 $domain")
|
||||
fi
|
||||
done
|
||||
|
||||
# Re-apply immutable flag
|
||||
sudo chattr +i /etc/hosts 2>/dev/null || true
|
||||
|
||||
if [[ $hosts_modified -eq 1 ]]; then
|
||||
if (( ${#new_entries[@]} > 0 )); then
|
||||
printf '%s\n' "${new_entries[@]}" | sudo bash -c "cat >> '$HOSTS_FILE'"
|
||||
log_info "Added distraction blocks to /etc/hosts"
|
||||
fi
|
||||
|
||||
# Re-apply immutable flag
|
||||
sudo chattr +i "$HOSTS_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Unblock Steam and distractions from /etc/hosts
|
||||
@ -339,33 +405,43 @@ unblock_distractions() {
|
||||
log_info "Unblocking Steam and distractions in /etc/hosts"
|
||||
|
||||
# Remove immutable flag temporarily
|
||||
sudo chattr -i /etc/hosts 2>/dev/null || true
|
||||
sudo chattr -i "$HOSTS_FILE" 2>/dev/null || true
|
||||
|
||||
# Remove blocking entries using mktemp for security
|
||||
local temp_hosts
|
||||
temp_hosts=$(mktemp) || {
|
||||
log_error "Failed to create temporary file"
|
||||
return 1
|
||||
}
|
||||
# Filter out blocked entries using bash (no sed/mktemp forks)
|
||||
local new_content="" hosts_line skip domain
|
||||
while IFS= read -r hosts_line; do
|
||||
skip=0
|
||||
for domain in "${STEAM_DOMAINS[@]}" "${DISTRACTION_DOMAINS[@]}"; do
|
||||
if [[ $hosts_line == "0.0.0.0 $domain" || $hosts_line == "0.0.0.0 $domain" ]]; then
|
||||
skip=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ $skip -eq 0 ]]; then
|
||||
new_content+="${hosts_line}"$'\n'
|
||||
fi
|
||||
done < "$HOSTS_FILE"
|
||||
|
||||
sudo cp /etc/hosts "$temp_hosts"
|
||||
|
||||
for domain in "${STEAM_DOMAINS[@]}" "${DISTRACTION_DOMAINS[@]}"; do
|
||||
sudo sed -i "/^0.0.0.0[[:space:]]*$domain/d" "$temp_hosts"
|
||||
done
|
||||
|
||||
sudo mv "$temp_hosts" /etc/hosts
|
||||
sudo chmod 644 /etc/hosts
|
||||
printf '%s' "$new_content" | sudo bash -c "cat > '$HOSTS_FILE'"
|
||||
sudo chmod 644 "$HOSTS_FILE"
|
||||
|
||||
# Re-apply immutable flag
|
||||
sudo chattr +i /etc/hosts 2>/dev/null || true
|
||||
sudo chattr +i "$HOSTS_FILE" 2>/dev/null || true
|
||||
|
||||
log_info "Removed distraction blocks from /etc/hosts"
|
||||
}
|
||||
|
||||
# Check if Steam is currently running (to track decay)
|
||||
is_steam_running() {
|
||||
pgrep -x "steam" >/dev/null 2>&1
|
||||
local comm_file proc_name
|
||||
for comm_file in "$PROC_ROOT"/[0-9]*/comm; do
|
||||
[[ -r $comm_file ]] || continue
|
||||
read -r proc_name < "$comm_file" || continue
|
||||
if [[ $proc_name == "steam" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Main tracking loop
|
||||
@ -461,7 +537,7 @@ main_loop() {
|
||||
last_status_log=$current_time
|
||||
fi
|
||||
|
||||
sleep "$CHECK_INTERVAL"
|
||||
wait_seconds "$CHECK_INTERVAL"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
@ -9,12 +9,40 @@ LOG_FILE="/var/log/hosts-file-monitor.log"
|
||||
HOSTS_FILE="/etc/hosts"
|
||||
HOSTS_INSTALL_SCRIPT="__HOSTS_INSTALL_SCRIPT__"
|
||||
readonly MIN_HOSTS_LINES=1000
|
||||
readonly EVENT_COOLDOWN_S=5
|
||||
|
||||
current_epoch() {
|
||||
local out_var="${1:-}"
|
||||
if [[ -n $out_var ]]; then
|
||||
printf -v "$out_var" '%(%s)T' -1
|
||||
else
|
||||
printf '%(%s)T\n' -1
|
||||
fi
|
||||
}
|
||||
|
||||
wait_seconds() {
|
||||
local timeout_s=$1
|
||||
local start_ts end_ts elapsed_s remaining_s
|
||||
|
||||
printf -v start_ts '%(%s)T' -1
|
||||
IFS= read -r -t "$timeout_s" || true
|
||||
printf -v end_ts '%(%s)T' -1
|
||||
|
||||
elapsed_s=$((end_ts - start_ts))
|
||||
if (( elapsed_s < timeout_s )); then
|
||||
remaining_s=$((timeout_s - elapsed_s))
|
||||
sleep "$remaining_s"
|
||||
fi
|
||||
}
|
||||
|
||||
# Log with timestamp (hosts-file-monitor specific)
|
||||
log_message() {
|
||||
local _ts
|
||||
local msg
|
||||
printf -v _ts '%(%Y-%m-%d %H:%M:%S)T' -1
|
||||
printf '%s [hosts-monitor] %s\n' "$_ts" "$1" | tee -a "$LOG_FILE" >&2
|
||||
printf -v msg '%s [hosts-monitor] %s' "$_ts" "$1"
|
||||
printf '%s\n' "$msg" >&2
|
||||
printf '%s\n' "$msg" >> "$LOG_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Function to check if hosts file needs restoration
|
||||
@ -83,16 +111,21 @@ restore_hosts_file() {
|
||||
# Function to monitor with inotifywait
|
||||
monitor_with_inotify() {
|
||||
log_message "Starting hosts file monitoring with inotify"
|
||||
local last_check_ts=0
|
||||
|
||||
# Monitor the hosts file and its directory for various events
|
||||
inotifywait -m -e delete,move,modify,attrib,create --format '%w%f %e %T' --timefmt '%Y-%m-%d %H:%M:%S' "$HOSTS_FILE" /etc/ 2> /dev/null |
|
||||
while read -r file event time; do
|
||||
# Check if the event is related to our hosts file
|
||||
if [[ $file == "$HOSTS_FILE" ]] || [[ $file == "/etc/hosts" ]]; then
|
||||
log_message "Event detected: $event on $file at $time"
|
||||
local now_ts
|
||||
current_epoch now_ts
|
||||
if (( now_ts - last_check_ts < EVENT_COOLDOWN_S )); then
|
||||
continue
|
||||
fi
|
||||
last_check_ts=$now_ts
|
||||
|
||||
# Small delay to avoid rapid-fire events
|
||||
sleep 2
|
||||
log_message "Event detected: $event on $file at $time"
|
||||
|
||||
# Check if restoration is needed
|
||||
if needs_restoration; then
|
||||
@ -114,7 +147,7 @@ monitor_with_polling() {
|
||||
fi
|
||||
|
||||
# Check every 30 seconds
|
||||
sleep 30
|
||||
wait_seconds 30
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
@ -10,11 +10,38 @@ TIMER_NAME="day-specific-shutdown.timer"
|
||||
SERVICE_NAME="day-specific-shutdown.service"
|
||||
CHECK_INTERVAL=30
|
||||
|
||||
current_epoch() {
|
||||
local out_var="${1:-}"
|
||||
if [[ -n $out_var ]]; then
|
||||
printf -v "$out_var" '%(%s)T' -1
|
||||
else
|
||||
printf '%(%s)T\n' -1
|
||||
fi
|
||||
}
|
||||
|
||||
wait_seconds() {
|
||||
local timeout_s=$1
|
||||
local start_ts end_ts elapsed_s remaining_s
|
||||
|
||||
printf -v start_ts '%(%s)T' -1
|
||||
IFS= read -r -t "$timeout_s" || true
|
||||
printf -v end_ts '%(%s)T' -1
|
||||
|
||||
elapsed_s=$((end_ts - start_ts))
|
||||
if (( elapsed_s < timeout_s )); then
|
||||
remaining_s=$((timeout_s - elapsed_s))
|
||||
sleep "$remaining_s"
|
||||
fi
|
||||
}
|
||||
|
||||
# Log with timestamp (shutdown-timer-monitor specific)
|
||||
log_message() {
|
||||
local _ts
|
||||
local msg
|
||||
printf -v _ts '%(%Y-%m-%d %H:%M:%S)T' -1
|
||||
printf '%s [shutdown-monitor] %s\n' "$_ts" "$1" | tee -a "$LOG_FILE" >&2
|
||||
printf -v msg '%s [shutdown-monitor] %s' "$_ts" "$1"
|
||||
printf '%s\n' "$msg" >&2
|
||||
printf '%s\n' "$msg" >> "$LOG_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Function to check if timer needs to be re-enabled
|
||||
@ -82,6 +109,7 @@ restore_timer() {
|
||||
# Function to monitor timer with systemd events
|
||||
monitor_with_dbus() {
|
||||
log_message "Starting shutdown timer monitoring with D-Bus events"
|
||||
local last_check_ts=0
|
||||
|
||||
# Use busctl to monitor systemd unit changes
|
||||
# Fall back to polling if this fails.
|
||||
@ -91,8 +119,13 @@ monitor_with_dbus() {
|
||||
while read -r line; do
|
||||
# Check if the line mentions our timer
|
||||
if [[ $line == *"$TIMER_NAME"* || $line == *"$SERVICE_NAME"* ]]; then
|
||||
local now_ts
|
||||
current_epoch now_ts
|
||||
if (( now_ts - last_check_ts < CHECK_INTERVAL )); then
|
||||
continue
|
||||
fi
|
||||
last_check_ts=$now_ts
|
||||
log_message "Systemd event detected for shutdown timer"
|
||||
sleep 2
|
||||
if timer_needs_restoration; then
|
||||
restore_timer
|
||||
fi
|
||||
@ -112,7 +145,7 @@ monitor_with_polling() {
|
||||
if timer_needs_restoration; then
|
||||
restore_timer
|
||||
fi
|
||||
sleep "$CHECK_INTERVAL"
|
||||
wait_seconds "$CHECK_INTERVAL"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
@ -101,4 +101,39 @@ poll_mode=$(run_shell "source '$WORKTREE/scripts/system-maintenance/bin/hosts-fi
|
||||
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 'Checking inotify event path avoids per-event sleep and debounces bursts...\n'
|
||||
sleep_log="$TMP_DIR/sleep.log"
|
||||
: >"$sleep_log"
|
||||
counter_file="$TMP_DIR/debounce-count.log"
|
||||
: >"$counter_file"
|
||||
debounce_calls=$(env -i PATH="$BIN_DIR" HOSTS_FILE_MONITOR_SKIP_MAIN=1 SLEEP_LOG="$sleep_log" COUNTER_FILE="$counter_file" MOCK_INOTIFY_OUTPUT=$'/etc/hosts MODIFY 2026-01-01 00:00:00\n/etc/hosts ATTRIB 2026-01-01 00:00:01\n/etc/hosts MODIFY 2026-01-01 00:00:02' /bin/bash -c \
|
||||
"source '$WORKTREE/scripts/system-maintenance/bin/hosts-file-monitor.sh'; \
|
||||
needs_restoration() { printf 'x\n' >> \"\$COUNTER_FILE\"; return 1; }; \
|
||||
idx=0; \
|
||||
current_epoch() { \
|
||||
local out_var=\"\${1:-}\"; \
|
||||
local ts; \
|
||||
case \$idx in 0) ts='100';; 1) ts='101';; 2) ts='106';; *) ts='999';; esac; \
|
||||
idx=\$((idx + 1)); \
|
||||
if [[ -n \$out_var ]]; then printf -v \"\$out_var\" '%s' \"\$ts\"; else printf '%s\\n' \"\$ts\"; fi; \
|
||||
}; \
|
||||
monitor_with_inotify >/dev/null 2>&1 || true; \
|
||||
total=0; \
|
||||
while IFS= read -r _; do total=\$((total + 1)); done < \"\$COUNTER_FILE\"; \
|
||||
printf '%s' \"\$total\"")
|
||||
assert_equals '2' "$debounce_calls" 'monitor_with_inotify should debounce rapid successive events'
|
||||
|
||||
if [[ -s $sleep_log ]]; then
|
||||
fail 'monitor_with_inotify should not call sleep in the event path'
|
||||
fi
|
||||
|
||||
printf 'Checking polling wait helper enforces delay on /dev/null stdin...\n'
|
||||
wait_elapsed=$(env -i PATH="/usr/bin:/bin" HOSTS_FILE_MONITOR_SKIP_MAIN=1 /bin/bash -c \
|
||||
"source '$WORKTREE/scripts/system-maintenance/bin/hosts-file-monitor.sh'; \
|
||||
start=\$(printf '%(%s)T' -1); \
|
||||
wait_seconds 1; \
|
||||
end=\$(printf '%(%s)T' -1); \
|
||||
printf '%s' \$((end-start))" </dev/null)
|
||||
assert_equals '1' "$wait_elapsed" 'wait_seconds should not return immediately on /dev/null stdin'
|
||||
|
||||
printf 'hosts-file-monitor.sh regression checks passed.\n'
|
||||
|
||||
@ -30,6 +30,15 @@ assert_eq() {
|
||||
fi
|
||||
}
|
||||
|
||||
assert_le() {
|
||||
local actual=$1
|
||||
local expected_max=$2
|
||||
local context=$3
|
||||
if (( actual > expected_max )); then
|
||||
fail "$context (expected <= '$expected_max', actual '$actual')"
|
||||
fi
|
||||
}
|
||||
|
||||
count_execs() {
|
||||
local script_path=$1
|
||||
local log_file=$2
|
||||
@ -143,4 +152,11 @@ chmod +x "$fork_probe"
|
||||
exec_count=$(count_execs "$fork_probe" "$TMP_DIR/fork_probe.trace")
|
||||
assert_eq '1' "$exec_count" 'persist helper hot path should not fork external commands'
|
||||
|
||||
printf 'Checking wait helper supports test skip mode...\n'
|
||||
SECONDS=0
|
||||
export I3BLOCKS_TEST_SKIP_WAIT=1
|
||||
i3blocks_wait_seconds 5
|
||||
assert_le "$SECONDS" 1 'wait helper should return immediately in test skip mode'
|
||||
unset I3BLOCKS_TEST_SKIP_WAIT
|
||||
|
||||
printf 'persist_common helper regression tests passed.\n'
|
||||
|
||||
@ -57,52 +57,78 @@ 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"
|
||||
create_fake_proc_process() {
|
||||
local proc_root="$1"
|
||||
local pid="$2"
|
||||
local name="$3"
|
||||
mkdir -p "$proc_root/$pid"
|
||||
printf '%s\n' "$name" >"$proc_root/$pid/comm"
|
||||
}
|
||||
|
||||
run_case() {
|
||||
local expected_sleep="$1"
|
||||
local expected_wait="$1"
|
||||
local focus_active="$2"
|
||||
local music_running="$3"
|
||||
local sleep_log="$TMP_DIR/sleep.log"
|
||||
local music_proc_name="${3:-}"
|
||||
local mode="${4:-instant}"
|
||||
local wait_log="$TMP_DIR/wait.log"
|
||||
local proc_root="$TMP_DIR/proc"
|
||||
|
||||
: >"$wait_log"
|
||||
rm -rf "$proc_root"
|
||||
mkdir -p "$proc_root"
|
||||
|
||||
if [[ -n $music_proc_name ]]; then
|
||||
create_fake_proc_process "$proc_root" 4242 "$music_proc_name"
|
||||
fi
|
||||
|
||||
: >"$sleep_log"
|
||||
PATH="$BIN_DIR:$PATH" \
|
||||
SLEEP_LOG="$sleep_log" \
|
||||
MUSIC_PARALLELISM_TEST_WAIT_LOG="$wait_log" \
|
||||
MUSIC_PARALLELISM_TEST_EXIT_AFTER_WAIT=1 \
|
||||
XDOTOOL_LOG="${XDOTOOL_LOG:-}" \
|
||||
PROC_ROOT="$proc_root" \
|
||||
MOCK_FOCUS_ACTIVE="$focus_active" \
|
||||
MOCK_MUSIC_RUNNING="$music_running" \
|
||||
bash "$WORKTREE/scripts/digital_wellbeing/music_parallelism.sh" instant \
|
||||
bash "$WORKTREE/scripts/digital_wellbeing/music_parallelism.sh" "$mode" \
|
||||
>/dev/null 2>&1 || true
|
||||
|
||||
assert_equals "$expected_sleep" "$(<"$sleep_log")" "music_parallelism.sh should pick the expected sleep interval"
|
||||
assert_equals "$expected_wait" "$(<"$wait_log")" "music_parallelism.sh should pick the expected wait interval"
|
||||
}
|
||||
|
||||
printf 'Checking stable-focus backoff uses the slower interval...\n'
|
||||
run_case 15 1 0
|
||||
run_case 15 1
|
||||
|
||||
printf 'Checking conflict handling uses the faster retry interval...\n'
|
||||
run_case 5 1 1
|
||||
run_case 5 1 spotify
|
||||
|
||||
printf 'Checking idle mode uses the idle interval...\n'
|
||||
run_case 30 0 0
|
||||
run_case 30 0
|
||||
|
||||
printf 'Checking conflict path avoids duplicate xdotool searches...\n'
|
||||
xdotool_log="$TMP_DIR/xdotool.log"
|
||||
: >"$xdotool_log"
|
||||
XDOTOOL_LOG="$xdotool_log"
|
||||
|
||||
cat >"$BIN_DIR/xdotool" <<'EOF'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$1" >> "${XDOTOOL_LOG:?}"
|
||||
if [[ ${1:-} == search ]]; then
|
||||
exit 1
|
||||
fi
|
||||
if [[ ${1:-} == windowclose ]]; then
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/xdotool"
|
||||
|
||||
run_case 5 1 spotify
|
||||
|
||||
search_calls=$(grep -c '^search$' "$xdotool_log" 2>/dev/null || true)
|
||||
assert_equals '1' "$search_calls" 'music_parallelism.sh should avoid duplicate xdotool search calls when process-only music is detected'
|
||||
|
||||
printf 'Checking monitor loop also avoids duplicate xdotool searches...\n'
|
||||
: >"$xdotool_log"
|
||||
run_case 15 1 spotify monitor
|
||||
monitor_search_calls=$(grep -c '^search$' "$xdotool_log" 2>/dev/null || true)
|
||||
assert_equals '1' "$monitor_search_calls" 'music_parallelism.sh monitor loop should avoid duplicate xdotool search calls when process-only music is detected'
|
||||
|
||||
printf 'music_parallelism.sh regression checks passed.\n'
|
||||
|
||||
@ -36,7 +36,8 @@ cp "$TARGET_SCRIPT" "$WORKTREE/scripts/system-maintenance/bin/shutdown-timer-mon
|
||||
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
|
||||
payload=${MOCK_BUSCTL_LINES:-${MOCK_BUSCTL_LINE:-no relevant event}}
|
||||
printf '%b\n' "$payload" | while read -r line; do
|
||||
printf '%s\n' "$line"
|
||||
done
|
||||
exit 0
|
||||
@ -106,12 +107,65 @@ run_case() {
|
||||
fi
|
||||
}
|
||||
|
||||
run_dbus_throttle_case() {
|
||||
local ts_sequence="$1"
|
||||
local expected_calls="$2"
|
||||
local check_interval="${3:-30}"
|
||||
local calls
|
||||
local counter_file="$TMP_DIR/timer_checks.log"
|
||||
|
||||
: >"$counter_file"
|
||||
|
||||
calls=$(env -i PATH="$BIN_DIR" SHUTDOWN_TIMER_MONITOR_SKIP_MAIN=1 MOCK_BUSCTL_LINES="day-specific-shutdown.timer\nday-specific-shutdown.timer\nday-specific-shutdown.timer" MOCK_TS_SEQUENCE="$ts_sequence" COUNTER_FILE="$counter_file" TEST_CHECK_INTERVAL="$check_interval" /bin/bash -c '
|
||||
source "$1"
|
||||
CHECK_INTERVAL="$TEST_CHECK_INTERVAL"
|
||||
timer_needs_restoration() { printf "x\n" >> "$COUNTER_FILE"; return 1; }
|
||||
restore_timer() { :; }
|
||||
mock_idx=0
|
||||
IFS=" " read -r -a mock_ts <<< "$MOCK_TS_SEQUENCE"
|
||||
current_epoch() {
|
||||
local out_var="${1:-}"
|
||||
local ts_value="${mock_ts[$mock_idx]:-0}"
|
||||
mock_idx=$((mock_idx + 1))
|
||||
|
||||
if [[ -n $out_var ]]; then
|
||||
printf -v "$out_var" '%s' "$ts_value"
|
||||
else
|
||||
printf "%s\n" "$ts_value"
|
||||
fi
|
||||
}
|
||||
monitor_with_dbus >/dev/null 2>&1 || true
|
||||
timer_checks=0
|
||||
while IFS= read -r _; do
|
||||
timer_checks=$((timer_checks + 1))
|
||||
done < "$COUNTER_FILE"
|
||||
printf "%s" "$timer_checks"
|
||||
' _ "$WORKTREE/scripts/system-maintenance/bin/shutdown-timer-monitor.sh")
|
||||
|
||||
assert_equals "$expected_calls" "$calls" 'monitor_with_dbus should throttle repeated relevant events'
|
||||
}
|
||||
|
||||
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 D-Bus monitor throttles repeated events within interval...\n'
|
||||
run_dbus_throttle_case '100 105 109' '1'
|
||||
|
||||
printf 'Checking D-Bus monitor can process all events when interval is zero...\n'
|
||||
run_dbus_throttle_case '100 101 102' '3' '0'
|
||||
|
||||
printf 'Checking wait helper enforces delay even with /dev/null stdin...\n'
|
||||
wait_elapsed=$(env -i PATH="/usr/bin:/bin" SHUTDOWN_TIMER_MONITOR_SKIP_MAIN=1 /bin/bash -c \
|
||||
"source '$WORKTREE/scripts/system-maintenance/bin/shutdown-timer-monitor.sh'; \
|
||||
start=\$(printf '%(%s)T' -1); \
|
||||
wait_seconds 1; \
|
||||
end=\$(printf '%(%s)T' -1); \
|
||||
printf '%s' \$((end-start))" </dev/null)
|
||||
assert_equals '1' "$wait_elapsed" 'wait_seconds should not return immediately on /dev/null stdin'
|
||||
|
||||
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'
|
||||
@ -119,5 +173,19 @@ 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'
|
||||
grep -Fq 'current_epoch now_ts' "$SETUP_SCRIPT" \
|
||||
|| fail 'setup_midnight_shutdown.sh should use out-var epoch helper in D-Bus throttling path'
|
||||
if grep -Fq 'now_ts=$(current_epoch)' "$SETUP_SCRIPT"; then
|
||||
fail 'setup_midnight_shutdown.sh should avoid subshell epoch capture in D-Bus path'
|
||||
fi
|
||||
if grep -Fq 'now_ts=$(current_epoch)' "$TARGET_SCRIPT"; then
|
||||
fail 'runtime shutdown monitor should avoid subshell epoch capture in D-Bus path'
|
||||
fi
|
||||
grep -Fq 'OnUnitActiveSec=300' "$SETUP_SCRIPT" \
|
||||
|| fail 'setup_midnight_shutdown.sh should run watchdog timer at 300s cadence'
|
||||
grep -Fq 'wait_seconds()' "$SETUP_SCRIPT" \
|
||||
|| fail 'setup_midnight_shutdown.sh should install builtin wait helper in polling fallback'
|
||||
grep -Fq 'wait_seconds "$CHECK_INTERVAL"' "$TARGET_SCRIPT" \
|
||||
|| fail 'runtime shutdown monitor polling fallback should use builtin wait helper'
|
||||
|
||||
printf 'shutdown-timer-monitor.sh regression checks passed.\n'
|
||||
|
||||
88
linux_configuration/tests/test_thesis_work_status.sh
Executable file
88
linux_configuration/tests/test_thesis_work_status.sh
Executable file
@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
# Regression tests for thesis_work_status.sh state-parsing 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_status.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_status.sh"
|
||||
|
||||
# sudo stub — passes through all commands
|
||||
cat >"$BIN_DIR/sudo" <<'EOF'
|
||||
#!/bin/bash
|
||||
"$@"
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/sudo"
|
||||
|
||||
# chattr stub
|
||||
cat >"$BIN_DIR/chattr" <<'EOF'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/chattr"
|
||||
|
||||
# grep stub — must NOT be called by state parsing
|
||||
cat >"$BIN_DIR/grep" <<'EOF'
|
||||
#!/bin/bash
|
||||
printf 'grep should not be called\n' >&2
|
||||
exit 1
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/grep"
|
||||
|
||||
# cut stub — must NOT be called by state parsing
|
||||
cat >"$BIN_DIR/cut" <<'EOF'
|
||||
#!/bin/bash
|
||||
printf 'cut should not be called\n' >&2
|
||||
exit 1
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/cut"
|
||||
|
||||
STATE_PATH="$TMP_DIR/work-time.state"
|
||||
cat >"$STATE_PATH" <<'EOF'
|
||||
# Thesis Work Tracker State File
|
||||
TOTAL_WORK_SECONDS=3600
|
||||
LAST_UPDATE_TIMESTAMP=1715400000
|
||||
STEAM_ACCESS_GRANTED=1
|
||||
LAST_WORK_SESSION_START=42
|
||||
CURRENT_SESSION_SECONDS=90
|
||||
EOF
|
||||
|
||||
printf 'Checking state parsing does not depend on grep/cut...\n'
|
||||
# THESIS_STATUS_SKIP_SUDO=1 prevents exec sudo re-exec
|
||||
# THESIS_STATUS_SKIP_OUTPUT=1 prevents display output; script returns after parsing
|
||||
parsed_vals=$(PATH="$BIN_DIR:$PATH" THESIS_STATUS_SKIP_SUDO=1 THESIS_STATUS_SKIP_OUTPUT=1 \
|
||||
bash -lc \
|
||||
"STATE_FILE='$STATE_PATH'; \
|
||||
source '$WORKTREE/scripts/digital_wellbeing/thesis_work_status.sh'; \
|
||||
printf '%s|%s|%s|%s' \"\$TOTAL_WORK_SECONDS\" \"\$STEAM_ACCESS_GRANTED\" \"\$CURRENT_SESSION_SECONDS\" \"\$LAST_WORK_SESSION_START\"" \
|
||||
2>/dev/null)
|
||||
|
||||
assert_equals '3600|1|90|42' "$parsed_vals" \
|
||||
'thesis_work_status state parsing should work without grep/cut dependency'
|
||||
|
||||
printf 'thesis_work_status.sh regression checks passed.\n'
|
||||
@ -75,11 +75,39 @@ source_env() {
|
||||
PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc "source '$WORKTREE/scripts/digital_wellbeing/thesis_work_tracker.sh'; $1"
|
||||
}
|
||||
|
||||
source_env_with_proc() {
|
||||
local proc_root="$1"
|
||||
local cmd="$2"
|
||||
PATH="$BIN_DIR:$PATH" PROC_ROOT="$proc_root" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc "source '$WORKTREE/scripts/digital_wellbeing/thesis_work_tracker.sh'; $cmd"
|
||||
}
|
||||
|
||||
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 helper reads process name from /proc before ps...\n'
|
||||
PROC_WINDOW_DIR="$TMP_DIR/proc-window"
|
||||
mkdir -p "$PROC_WINDOW_DIR/6789"
|
||||
printf 'Code\n' >"$PROC_WINDOW_DIR/6789/comm"
|
||||
cat >"$BIN_DIR/ps" <<'EOF'
|
||||
#!/bin/bash
|
||||
printf 'ps should not be called\n' >&2
|
||||
exit 1
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/ps"
|
||||
|
||||
result_proc=$(MOCK_WINDOW_TITLE='Document - praca_magisterska - Visual Studio Code' \
|
||||
source_env_with_proc "$PROC_WINDOW_DIR" 'get_active_window_info')
|
||||
assert_equals 'Code|Document - praca_magisterska - Visual Studio Code' "$result_proc" \
|
||||
'get_active_window_info should read process name from proc comm without ps fallback'
|
||||
|
||||
cat >"$BIN_DIR/ps" <<'EOF'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "${MOCK_PROCESS_NAME:-Code}"
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/ps"
|
||||
|
||||
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'
|
||||
@ -88,4 +116,228 @@ 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 'Checking steam detection reads process state from /proc without pgrep...\n'
|
||||
PROC_DIR="$TMP_DIR/proc"
|
||||
mkdir -p "$PROC_DIR/999"
|
||||
printf 'steam\n' >"$PROC_DIR/999/comm"
|
||||
steam_running=$(source_env_with_proc "$PROC_DIR" 'is_steam_running && printf yes || printf no')
|
||||
assert_equals 'yes' "$steam_running" 'is_steam_running should detect steam via proc comm files'
|
||||
|
||||
printf 'Checking logging path does not depend on tee...\n'
|
||||
cat >"$BIN_DIR/tee" <<'EOF'
|
||||
#!/bin/bash
|
||||
printf 'tee should not be called\n' >&2
|
||||
exit 1
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/tee"
|
||||
|
||||
LOG_PATH="$TMP_DIR/tracker.log"
|
||||
set +e
|
||||
log_result=$(PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc \
|
||||
"source '$WORKTREE/scripts/digital_wellbeing/thesis_work_tracker.sh'; LOG_FILE='$LOG_PATH'; log_info 'logging regression test'; printf ok")
|
||||
log_ec=$?
|
||||
set -e
|
||||
assert_equals '0' "$log_ec" 'log_info should not fail when tee is unavailable'
|
||||
assert_equals 'ok' "$log_result" 'log_info should succeed without tee dependency'
|
||||
grep -q 'logging regression test' "$LOG_PATH" \
|
||||
|| fail 'log_info should append message to the log file'
|
||||
|
||||
printf 'Checking state loading does not depend on grep/cut...\n'
|
||||
cat >"$BIN_DIR/sudo" <<'EOF'
|
||||
#!/bin/bash
|
||||
"$@"
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/sudo"
|
||||
|
||||
cat >"$BIN_DIR/chattr" <<'EOF'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/chattr"
|
||||
|
||||
cat >"$BIN_DIR/grep" <<'EOF'
|
||||
#!/bin/bash
|
||||
printf 'grep should not be called\n' >&2
|
||||
exit 1
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/grep"
|
||||
|
||||
cat >"$BIN_DIR/cut" <<'EOF'
|
||||
#!/bin/bash
|
||||
printf 'cut should not be called\n' >&2
|
||||
exit 1
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/cut"
|
||||
|
||||
STATE_PATH="$TMP_DIR/work-time.state"
|
||||
cat >"$STATE_PATH" <<'EOF'
|
||||
# Thesis Work Tracker State File
|
||||
TOTAL_WORK_SECONDS=123
|
||||
LAST_UPDATE_TIMESTAMP=1715400000
|
||||
STEAM_ACCESS_GRANTED=1
|
||||
LAST_WORK_SESSION_START=77
|
||||
CURRENT_SESSION_SECONDS=15
|
||||
EOF
|
||||
|
||||
loaded_state=$(PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc \
|
||||
"source '$WORKTREE/scripts/digital_wellbeing/thesis_work_tracker.sh'; \
|
||||
STATE_FILE='$STATE_PATH'; \
|
||||
load_state; \
|
||||
printf '%s|%s|%s|%s' \"\$TOTAL_WORK_SECONDS\" \"\$STEAM_ACCESS_GRANTED\" \"\$CURRENT_SESSION_SECONDS\" \"\$LAST_WORK_SESSION_START\"")
|
||||
assert_equals '123|1|15|77' "$loaded_state" 'load_state should parse values without grep/cut dependency'
|
||||
|
||||
printf 'Checking state saving does not depend on tee...\n'
|
||||
set +e
|
||||
save_state_result=$(PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc \
|
||||
"source '$WORKTREE/scripts/digital_wellbeing/thesis_work_tracker.sh'; \
|
||||
STATE_FILE='$STATE_PATH'; \
|
||||
STATE_DIR='$(dirname "$STATE_PATH")'; \
|
||||
save_state 321 0 45 9; \
|
||||
printf ok")
|
||||
save_state_ec=$?
|
||||
set -e
|
||||
assert_equals '0' "$save_state_ec" 'save_state should not fail when tee is unavailable'
|
||||
assert_equals 'ok' "$save_state_result" 'save_state should complete successfully without tee dependency'
|
||||
|
||||
saved_state=$(PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc \
|
||||
"source '$WORKTREE/scripts/digital_wellbeing/thesis_work_tracker.sh'; \
|
||||
STATE_FILE='$STATE_PATH'; \
|
||||
load_state; \
|
||||
printf '%s|%s|%s|%s' \"\$TOTAL_WORK_SECONDS\" \"\$STEAM_ACCESS_GRANTED\" \"\$CURRENT_SESSION_SECONDS\" \"\$LAST_WORK_SESSION_START\"")
|
||||
assert_equals '321|0|45|9' "$saved_state" 'save_state should persist updated values without tee dependency'
|
||||
|
||||
printf 'Checking writable save path does not require mktemp...\n'
|
||||
cat >"$BIN_DIR/mktemp" <<'EOF'
|
||||
#!/bin/bash
|
||||
printf 'mktemp should not be called\n' >&2
|
||||
exit 1
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/mktemp"
|
||||
|
||||
set +e
|
||||
save_fast_result=$(PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc \
|
||||
"source '$WORKTREE/scripts/digital_wellbeing/thesis_work_tracker.sh'; \
|
||||
STATE_FILE='$STATE_PATH'; \
|
||||
STATE_DIR='$(dirname "$STATE_PATH")'; \
|
||||
save_state 654 1 30 11; \
|
||||
printf ok")
|
||||
save_fast_ec=$?
|
||||
set -e
|
||||
assert_equals '0' "$save_fast_ec" 'save_state should not require mktemp when state file is writable'
|
||||
assert_equals 'ok' "$save_fast_result" 'save_state fast path should complete successfully'
|
||||
|
||||
saved_fast_state=$(PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc \
|
||||
"source '$WORKTREE/scripts/digital_wellbeing/thesis_work_tracker.sh'; \
|
||||
STATE_FILE='$STATE_PATH'; \
|
||||
load_state; \
|
||||
printf '%s|%s|%s|%s' \"\$TOTAL_WORK_SECONDS\" \"\$STEAM_ACCESS_GRANTED\" \"\$CURRENT_SESSION_SECONDS\" \"\$LAST_WORK_SESSION_START\"")
|
||||
assert_equals '654|1|30|11' "$saved_fast_state" 'save_state writable fast path should persist values'
|
||||
|
||||
printf 'Checking block_distractions does not depend on grep or tee...\n'
|
||||
HOSTS_PATH="$TMP_DIR/hosts-block"
|
||||
printf '# /etc/hosts baseline\n127.0.0.1 localhost\n' >"$HOSTS_PATH"
|
||||
|
||||
cat >"$BIN_DIR/grep" <<'EOF'
|
||||
#!/bin/bash
|
||||
printf 'grep should not be called\n' >&2
|
||||
exit 1
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/grep"
|
||||
|
||||
cat >"$BIN_DIR/tee" <<'EOF'
|
||||
#!/bin/bash
|
||||
printf 'tee should not be called\n' >&2
|
||||
exit 1
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/tee"
|
||||
|
||||
set +e
|
||||
block_result=$(PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc \
|
||||
"source '$WORKTREE/scripts/digital_wellbeing/thesis_work_tracker.sh'; \
|
||||
HOSTS_FILE='$HOSTS_PATH'; \
|
||||
block_distractions; \
|
||||
printf ok")
|
||||
block_ec=$?
|
||||
set -e
|
||||
assert_equals '0' "$block_ec" 'block_distractions should succeed without grep/tee'
|
||||
assert_equals 'ok' "$block_result" 'block_distractions should complete without grep/tee dependency'
|
||||
grep -q '0.0.0.0 steampowered.com' "$HOSTS_PATH" \
|
||||
|| fail 'block_distractions should add steampowered.com entry to hosts file'
|
||||
grep -q '0.0.0.0 reddit.com' "$HOSTS_PATH" \
|
||||
|| fail 'block_distractions should add reddit.com entry to hosts file'
|
||||
grep -q 'localhost' "$HOSTS_PATH" \
|
||||
|| fail 'block_distractions should preserve existing localhost entry'
|
||||
|
||||
printf 'Checking block_distractions is idempotent (no duplicate entries)...\n'
|
||||
set +e
|
||||
block_result2=$(PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc \
|
||||
"source '$WORKTREE/scripts/digital_wellbeing/thesis_work_tracker.sh'; \
|
||||
HOSTS_FILE='$HOSTS_PATH'; \
|
||||
block_distractions; \
|
||||
printf ok")
|
||||
block_ec2=$?
|
||||
set -e
|
||||
assert_equals '0' "$block_ec2" 'block_distractions second run should succeed'
|
||||
assert_equals 'ok' "$block_result2" 'block_distractions idempotent run should complete successfully'
|
||||
count=$(grep -c '0.0.0.0 steampowered.com' "$HOSTS_PATH" || true)
|
||||
assert_equals '1' "$count" 'block_distractions should not add duplicate entries'
|
||||
|
||||
printf 'Checking unblock_distractions does not depend on sed or mktemp...\n'
|
||||
HOSTS_UNBLOCK_PATH="$TMP_DIR/hosts-unblock"
|
||||
printf '# /etc/hosts baseline\n127.0.0.1 localhost\n0.0.0.0 steampowered.com\n0.0.0.0 reddit.com\n0.0.0.0 youtube.com\n' >"$HOSTS_UNBLOCK_PATH"
|
||||
|
||||
cat >"$BIN_DIR/sed" <<'EOF'
|
||||
#!/bin/bash
|
||||
printf 'sed should not be called\n' >&2
|
||||
exit 1
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/sed"
|
||||
|
||||
cat >"$BIN_DIR/mktemp" <<'EOF'
|
||||
#!/bin/bash
|
||||
printf 'mktemp should not be called\n' >&2
|
||||
exit 1
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/mktemp"
|
||||
|
||||
set +e
|
||||
unblock_result=$(PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc \
|
||||
"source '$WORKTREE/scripts/digital_wellbeing/thesis_work_tracker.sh'; \
|
||||
HOSTS_FILE='$HOSTS_UNBLOCK_PATH'; \
|
||||
unblock_distractions; \
|
||||
printf ok")
|
||||
unblock_ec=$?
|
||||
set -e
|
||||
assert_equals '0' "$unblock_ec" 'unblock_distractions should succeed without sed/mktemp'
|
||||
assert_equals 'ok' "$unblock_result" 'unblock_distractions should complete without sed/mktemp dependency'
|
||||
if grep -q '0.0.0.0 steampowered.com' "$HOSTS_UNBLOCK_PATH" 2>/dev/null; then
|
||||
fail 'unblock_distractions should remove steampowered.com entry'
|
||||
fi
|
||||
if grep -q '0.0.0.0 reddit.com' "$HOSTS_UNBLOCK_PATH" 2>/dev/null; then
|
||||
fail 'unblock_distractions should remove reddit.com entry'
|
||||
fi
|
||||
grep -q 'localhost' "$HOSTS_UNBLOCK_PATH" \
|
||||
|| fail 'unblock_distractions should preserve localhost entry'
|
||||
|
||||
printf 'Checking init_state does not depend on tee...\n'
|
||||
STATE_INIT_PATH="$TMP_DIR/init-state-file.state"
|
||||
STATE_INIT_DIR="$TMP_DIR/init-state-dir"
|
||||
mkdir -p "$STATE_INIT_DIR"
|
||||
|
||||
set +e
|
||||
init_result=$(PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc \
|
||||
"source '$WORKTREE/scripts/digital_wellbeing/thesis_work_tracker.sh'; \
|
||||
STATE_FILE='$STATE_INIT_PATH'; \
|
||||
STATE_DIR='$STATE_INIT_DIR'; \
|
||||
LOG_DIR='$TMP_DIR'; \
|
||||
LOG_FILE='$TMP_DIR/init-tracker.log'; \
|
||||
init_state; \
|
||||
printf ok")
|
||||
init_ec=$?
|
||||
set -e
|
||||
assert_equals '0' "$init_ec" 'init_state should succeed without tee dependency'
|
||||
assert_equals 'ok' "$init_result" 'init_state should complete without tee dependency'
|
||||
grep -q 'TOTAL_WORK_SECONDS=0' "$STATE_INIT_PATH" \
|
||||
|| fail 'init_state should write TOTAL_WORK_SECONDS=0 to state file'
|
||||
|
||||
printf 'thesis_work_tracker.sh regression checks passed.\n'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user