From f84135f6d7b51f87117bf402c66ff7e2b6e1e066 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Wed, 6 May 2026 21:41:07 +0200 Subject: [PATCH] linux_configuration: WIP digital_wellbeing + pacman hosts-guard updates Pre-existing local changes from a prior session, committed together for cleanup. Touches: - hosts/guard/: pacman pre-unlock / post-relock hook tweaks and the shared hosts-guard-common.sh helper. - scripts/digital_wellbeing/: block_compulsive_opening, music_parallelism, setup_midnight_shutdown, setup_pc_startup_monitor refinements. - scripts/digital_wellbeing/pacman/pacman_wrapper.sh: substantial rewrite. - scripts/lib/common.sh: shared helpers expanded. - tests/test_hosts_guard_pacman_integration.sh: new integration test. --- .../hosts/guard/install_pacman_hooks.sh | 1 + .../guard/pacman-hooks/hosts-guard-common.sh | 7 + .../pacman-hooks/pacman-post-relock-hosts.sh | 2 + .../pacman-hooks/pacman-pre-unlock-hosts.sh | 7 +- .../block_compulsive_opening.sh | 12 +- .../digital_wellbeing/music_parallelism.sh | 101 +++++++------ .../pacman/pacman_wrapper.sh | 137 ++++++++++++++---- .../setup_midnight_shutdown.sh | 18 +-- .../setup_pc_startup_monitor.sh | 81 ++--------- linux_configuration/scripts/lib/common.sh | 123 +++++++++++++++- .../test_hosts_guard_pacman_integration.sh | 93 ++++++++++++ 11 files changed, 425 insertions(+), 157 deletions(-) create mode 100755 linux_configuration/tests/test_hosts_guard_pacman_integration.sh diff --git a/linux_configuration/hosts/guard/install_pacman_hooks.sh b/linux_configuration/hosts/guard/install_pacman_hooks.sh index 4a1169c..e7f7b19 100755 --- a/linux_configuration/hosts/guard/install_pacman_hooks.sh +++ b/linux_configuration/hosts/guard/install_pacman_hooks.sh @@ -43,6 +43,7 @@ HOOK # Place helper scripts into a shared location install -d -m 755 /usr/local/share/hosts-guard +install -m 755 "$SCRIPT_DIR/pacman-hooks/hosts-guard-common.sh" /usr/local/share/hosts-guard/ install -m 755 "$SCRIPT_DIR/pacman-hooks/pacman-pre-unlock-hosts.sh" /usr/local/share/hosts-guard/ install -m 755 "$SCRIPT_DIR/pacman-hooks/pacman-post-relock-hosts.sh" /usr/local/share/hosts-guard/ diff --git a/linux_configuration/hosts/guard/pacman-hooks/hosts-guard-common.sh b/linux_configuration/hosts/guard/pacman-hooks/hosts-guard-common.sh index 8ab484b..8f329f0 100755 --- a/linux_configuration/hosts/guard/pacman-hooks/hosts-guard-common.sh +++ b/linux_configuration/hosts/guard/pacman-hooks/hosts-guard-common.sh @@ -8,6 +8,13 @@ RESOLVED_CONF=/etc/systemd/resolved.conf RESOLVED_DROPIN=/etc/systemd/resolved.conf.d LOGTAG=hosts-guard-hook +require_root() { + if [[ $EUID -ne 0 ]]; then + echo "hosts-guard pacman hook must run as root" >&2 + exit 1 + fi +} + # Check if target has a read-only mount is_ro_mount() { findmnt -no OPTIONS -T "$TARGET" 2>/dev/null | grep -qw ro diff --git a/linux_configuration/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh b/linux_configuration/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh index 3efe168..d82cd6f 100755 --- a/linux_configuration/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh +++ b/linux_configuration/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh @@ -8,6 +8,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=hosts-guard-common.sh source "$SCRIPT_DIR/hosts-guard-common.sh" +require_root + ENFORCE=/usr/local/sbin/enforce-hosts.sh ENFORCE_NSSWITCH=/usr/local/sbin/enforce-nsswitch.sh ENFORCE_RESOLVED=/usr/local/sbin/enforce-resolved.sh diff --git a/linux_configuration/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh b/linux_configuration/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh index d27c591..332281d 100755 --- a/linux_configuration/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh +++ b/linux_configuration/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh @@ -8,15 +8,16 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=hosts-guard-common.sh source "$SCRIPT_DIR/hosts-guard-common.sh" +require_root + +log_hook "pre" "unlocking(start)" + # Remove protective attributes from all guarded files remove_all_guard_attrs -sudo rm /etc/hosts # Stop guard services (hosts, nsswitch, resolved watchers) stop_units_if_present -log_hook "pre" "unlocking(start)" - # Collapse any existing mount layers collapse_mounts diff --git a/linux_configuration/scripts/digital_wellbeing/block_compulsive_opening.sh b/linux_configuration/scripts/digital_wellbeing/block_compulsive_opening.sh index e2e5dfd..c1593e4 100755 --- a/linux_configuration/scripts/digital_wellbeing/block_compulsive_opening.sh +++ b/linux_configuration/scripts/digital_wellbeing/block_compulsive_opening.sh @@ -61,14 +61,14 @@ ensure_state_dir() { # Log message with timestamp log_message() { local msg - msg="$(date '+%Y-%m-%d %H:%M:%S') - $1" + msg="$(printf '%(%Y-%m-%d %H:%M:%S)T' -1) - $1" echo "$msg" >&2 echo "$msg" >>"$LOG_FILE" 2>/dev/null || true } # Get current hour key (YYYY-MM-DD-HH format) get_hour_key() { - date '+%Y-%m-%d-%H' + printf '%(%Y-%m-%d-%H)T' -1 } # Get state file path for an app @@ -179,7 +179,7 @@ kill_app() { is_autoclose_suspended() { local app="$1" local today - today=$(date '+%Y-%m-%d') + today=$(printf '%(%Y-%m-%d)T' -1) local suspend_file="$STATE_DIR/${app}.suspend-autoclose" if [[ -f $suspend_file ]]; then @@ -220,8 +220,8 @@ launch_with_timer() { # Give Electron apps time to fork before we start polling sleep 2 - # Record state - echo "$app_pid $(date +%s)" >"$running_file" + # Record state (FORK-FREE: use printf %s for timestamp) + echo "$app_pid $(printf '%(%s)T' -1)" >"$running_file" log_message "LAUNCHED: $app with PID $app_pid (auto-close in ${timeout_minutes}m)" # Spawn the auto-close daemon in a completely detached subshell @@ -256,7 +256,7 @@ launch_with_timer() { # Kill all matching processes (handles forked Electron children) kill_app "$real_binary" - echo "$(date '+%Y-%m-%d %H:%M:%S') - AUTO-CLOSED: $app after ${timeout_minutes}m" >>"$LOG_FILE" 2>/dev/null || true + printf '%(%Y-%m-%d %H:%M:%S)T - AUTO-CLOSED: %s after %dm\n' -1 "$app" "${timeout_minutes}" >>"$LOG_FILE" 2>/dev/null || true fi rm -f "$running_file" 2>/dev/null || true diff --git a/linux_configuration/scripts/digital_wellbeing/music_parallelism.sh b/linux_configuration/scripts/digital_wellbeing/music_parallelism.sh index f80c743..dd5eca7 100755 --- a/linux_configuration/scripts/digital_wellbeing/music_parallelism.sh +++ b/linux_configuration/scripts/digital_wellbeing/music_parallelism.sh @@ -11,8 +11,16 @@ set -euo pipefail # Source common library for shared functions SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" -# shellcheck source=../lib/common.sh -source "$SCRIPT_DIR/../lib/common.sh" +if [[ -f "$SCRIPT_DIR/../lib/common.sh" ]]; then + # shellcheck source=../lib/common.sh + source "$SCRIPT_DIR/../lib/common.sh" +elif [[ -f "/usr/local/lib/common.sh" ]]; then + # shellcheck source=/usr/local/lib/common.sh + source "/usr/local/lib/common.sh" +else + echo "ERROR: common.sh library not found" + exit 1 +fi # Configuration LOG_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/music-parallelism" @@ -70,24 +78,34 @@ MUSIC_SERVICES=( "pandora.com" ) -# Check if any music service is running and return its details +# Check if any music service is running and return its details (OPTIMIZED: batch pgrep calls) find_music_services() { local found_services=() - for service in "${MUSIC_SERVICES[@]}"; do - # Check for browser tabs with music services - # This checks window titles which usually contain the URL or tab title - if command -v xdotool &> /dev/null; then - if xdotool search --name "$service" &> /dev/null 2>&1; then - found_services+=("$service (window)") - fi - fi + # 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 for dedicated desktop apps - if pgrep -i -f "$service" &> /dev/null; then - found_services+=("$service (process)") + # Check processes (single fork) + local matching_services + if matching_services=$(pgrep -i -f "$music_pattern" 2>/dev/null); then + while read -r pid; do + local proc_name + proc_name=$(ps -p "$pid" -o comm= 2>/dev/null || echo "unknown") + found_services+=("$proc_name (process)") + done <<< "$matching_services" + 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 + found_services+=("music service (window)") fi - done + fi if [[ ${#found_services[@]} -gt 0 ]]; then printf '%s\n' "${found_services[@]}" @@ -185,6 +203,7 @@ notify_user() { # Instant monitoring loop - uses polling at high frequency ONLY when focus app is detected # When focus app active: checks every 0.5s. When idle: checks every 3s. Reduces fork overhead. +# OPTIMIZATION: Single batched pgrep call instead of multiple separate calls instant_monitor_loop() { log_message "=== Music Parallelism INSTANT Monitor Started ===" log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}" @@ -192,18 +211,14 @@ instant_monitor_loop() { log_message "Polling: 0.5s when focus app active, 3s when idle (optimized for lower fork overhead)" while true; do - # Only check if focus app is running + # Only check if focus app is running (uses optimized is_focus_app_running from common.sh) if is_focus_app_running &> /dev/null; then - # Instant kill youtube-music if detected - if pgrep -f "youtube-music" &> /dev/null; then - pkill -9 -f "youtube-music" 2> /dev/null || true - log_message "INSTANT KILL: YouTube Music terminated" - notify-send -u normal -t 2000 "🎵 YouTube Music killed" "Focus mode active" 2> /dev/null || true - fi - # Also check other music services - if pgrep -x "spotify" &> /dev/null; then - pkill -9 -x "spotify" 2> /dev/null || true - log_message "INSTANT KILL: Spotify terminated" + # OPTIMIZATION: Single pgrep call with regex instead of multiple calls + # Kill youtube-music OR spotify with one command (use pkill to avoid pipe fork) + if pgrep -i -f "youtube-music|spotify" &> /dev/null; then + pkill -i -f "youtube-music|spotify" 2> /dev/null || true + log_message "INSTANT KILL: Music services terminated" + notify-send -u normal -t 2000 "🎵 Music killed" "Focus mode active" 2> /dev/null || true fi sleep "$FAST_CHECK_INTERVAL" # High-frequency check while focus app is active else @@ -251,23 +266,27 @@ show_status() { echo "Focus Applications (window-based detection):" local focus_running=false - # Check windows - if command -v xdotool &> /dev/null; then - for app in "${FOCUS_APPS_WINDOWS[@]}"; do - if xdotool search --name "$app" &> /dev/null 2>&1; then - echo " ✓ $app (WINDOW OPEN)" - focus_running=true - fi - done - fi - - # Check processes - for app in "${FOCUS_APPS_PROCESSES[@]}"; do - if pgrep -f "$app" &> /dev/null; then - echo " ✓ $app (PROCESS RUNNING)" + # Check windows (OPTIMIZED: single xdotool call with combined regex) + if command -v xdotool &> /dev/null && [[ ${#FOCUS_APPS_WINDOWS[@]} -gt 0 ]]; then + local regex + printf -v regex '%s|' "${FOCUS_APPS_WINDOWS[@]}" + regex="${regex%|}" # strip trailing | + if xdotool search --name "$regex" &> /dev/null 2>&1; then + echo " ✓ Focus window detected" focus_running=true fi - done + 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 + fi if ! $focus_running; then echo " (none detected)" diff --git a/linux_configuration/scripts/digital_wellbeing/pacman/pacman_wrapper.sh b/linux_configuration/scripts/digital_wellbeing/pacman/pacman_wrapper.sh index 6e24892..2ea2c4f 100755 --- a/linux_configuration/scripts/digital_wellbeing/pacman/pacman_wrapper.sh +++ b/linux_configuration/scripts/digital_wellbeing/pacman/pacman_wrapper.sh @@ -73,37 +73,40 @@ load_policy_lists() { local whitelist_file="$script_dir/pacman_whitelist.txt" local greylist_file="$script_dir/pacman_greylist.txt" + read_policy_list_file() { + local file_path="$1" + local -n target_array="$2" + local line="" + + target_array=() + while IFS= read -r line || [[ -n $line ]]; do + line="${line%$'\r'}" + if [[ $line =~ ^[[:space:]]*(#|$) ]]; then + continue + fi + target_array+=("${line,,}") + done < "$file_path" + } + if [[ -f $blocked_file ]]; then - mapfile -t BLOCKED_KEYWORDS_LIST < <(sed 's/\r$//' "$blocked_file" | grep -Ev '^[[:space:]]*(#|$)' || true) + read_policy_list_file "$blocked_file" BLOCKED_KEYWORDS_LIST else BLOCKED_KEYWORDS_LIST=() echo -e "${YELLOW}Warning:${NC} Missing blocked keywords file at $blocked_file" >&2 fi if [[ -f $whitelist_file ]]; then - mapfile -t WHITELISTED_NAMES_LIST < <(sed 's/\r$//' "$whitelist_file" | grep -Ev '^[[:space:]]*(#|$)' || true) + read_policy_list_file "$whitelist_file" WHITELISTED_NAMES_LIST else WHITELISTED_NAMES_LIST=() fi if [[ -f $greylist_file ]]; then - mapfile -t GREYLISTED_KEYWORDS_LIST < <(sed 's/\r$//' "$greylist_file" | grep -Ev '^[[:space:]]*(#|$)' || true) + read_policy_list_file "$greylist_file" GREYLISTED_KEYWORDS_LIST else GREYLISTED_KEYWORDS_LIST=() fi - for i in "${!BLOCKED_KEYWORDS_LIST[@]}"; do - BLOCKED_KEYWORDS_LIST[i]="${BLOCKED_KEYWORDS_LIST[i],,}" - done - - for i in "${!WHITELISTED_NAMES_LIST[@]}"; do - WHITELISTED_NAMES_LIST[i]="${WHITELISTED_NAMES_LIST[i],,}" - done - - for i in "${!GREYLISTED_KEYWORDS_LIST[@]}"; do - GREYLISTED_KEYWORDS_LIST[i]="${GREYLISTED_KEYWORDS_LIST[i],,}" - done - POLICY_LISTS_LOADED=1 } # Determine if this invocation may perform a transaction (upgrade/install/remove) @@ -112,7 +115,22 @@ needs_unlock() { # Also include -Su/-Syu/-Syuu when -S is part of the combined flag for arg in "$@"; do case "$arg" in - -S* | -U | -R | --sync | --upgrade | --remove) + -S*) + return 0 + ;; + -U) + return 0 + ;; + -R) + return 0 + ;; + --sync) + return 0 + ;; + --upgrade) + return 0 + ;; + --remove) return 0 ;; esac @@ -120,6 +138,31 @@ needs_unlock() { return 1 } +pacman_hooks_manage_hosts_guard() { + local pre_hook="/etc/pacman.d/hooks/10-unlock-etc-hosts.hook" + local post_hook="/etc/pacman.d/hooks/90-relock-etc-hosts.hook" + local pre_exec="/usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh" + local post_exec="/usr/local/share/hosts-guard/pacman-post-relock-hosts.sh" + + if [[ ! -f $pre_hook || ! -f $post_hook ]]; then + return 1 + fi + + grep -Fq "$pre_exec" "$pre_hook" && grep -Fq "$post_exec" "$post_hook" +} + +should_use_wrapper_hosts_guard_fallback() { + if ! needs_unlock "$@"; then + return 1 + fi + + if pacman_hooks_manage_hosts_guard; then + return 1 + fi + + return 0 +} + # Run pre/post hooks for /etc/hosts guard if present pre_unlock_hosts() { local pre="/usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh" @@ -203,22 +246,55 @@ function show_help() { # Function to display a message before executing function display_operation() { case "$1" in - -S | -Sy | -S\ *) + -S) + echo -e "${BLUE}Installing packages...${NC}" >&2 + ;; + -Sy) + echo -e "${BLUE}Installing packages...${NC}" >&2 + ;; + -S\ *) echo -e "${BLUE}Installing packages...${NC}" >&2 ;; -Syu | -Syyu) echo -e "${BLUE}Updating system...${NC}" >&2 ;; - -R | -Rs | -Rns | -R\ *) + -R) echo -e "${YELLOW}Removing packages...${NC}" >&2 ;; - -Ss | -Ss\ *) + -Rs) + echo -e "${YELLOW}Removing packages...${NC}" >&2 + ;; + -Rns) + echo -e "${YELLOW}Removing packages...${NC}" >&2 + ;; + -R\ *) + echo -e "${YELLOW}Removing packages...${NC}" >&2 + ;; + -Ss) echo -e "${CYAN}Searching for packages...${NC}" >&2 ;; - -Q | -Qs | -Qi | -Ql | -Q\ *) + -Ss\ *) + echo -e "${CYAN}Searching for packages...${NC}" >&2 + ;; + -Q) echo -e "${CYAN}Querying package database...${NC}" >&2 ;; - -U | -U\ *) + -Qs) + echo -e "${CYAN}Querying package database...${NC}" >&2 + ;; + -Qi) + echo -e "${CYAN}Querying package database...${NC}" >&2 + ;; + -Ql) + echo -e "${CYAN}Querying package database...${NC}" >&2 + ;; + -Q\ *) + echo -e "${CYAN}Querying package database...${NC}" >&2 + ;; + -U) + echo -e "${BLUE}Installing local packages...${NC}" >&2 + ;; + -U\ *) echo -e "${BLUE}Installing local packages...${NC}" >&2 ;; -Scc) @@ -280,9 +356,9 @@ get_lock_holders() { local lock_file="$1" holders=() if command -v fuser >/dev/null 2>&1; then - mapfile -t holders < <(fuser "$lock_file" 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+$' || true) + read -r -a holders <<< "$(fuser "$lock_file" 2>/dev/null || true)" elif command -v lsof >/dev/null 2>&1; then - mapfile -t holders < <(lsof -t "$lock_file" 2>/dev/null | grep -E '^[0-9]+$' || true) + mapfile -t holders < <(lsof -t "$lock_file" 2>/dev/null || true) fi # Filter out our own PID if [[ ${#holders[@]} -gt 0 ]]; then @@ -677,20 +753,23 @@ echo -e "${GREEN}Executing:${NC} $PACMAN_BIN $*" >&2 # Record start time for statistics start_time=$(date +%s) -# Execute the real pacman command (with /etc/hosts guard handling) -if needs_unlock "$@"; then - pre_unlock_hosts -fi - # Handle a possible stale DB lock before executing if ! check_and_handle_db_lock "$@"; then exit 1 fi +manual_hosts_guard=0 + +# Execute the real pacman command (with /etc/hosts guard handling) +if should_use_wrapper_hosts_guard_fallback "$@"; then + pre_unlock_hosts + manual_hosts_guard=1 +fi + "$PACMAN_BIN" "$@" exit_code=$? -if needs_unlock "$@"; then +if [[ $manual_hosts_guard -eq 1 ]]; then post_relock_hosts fi diff --git a/linux_configuration/scripts/digital_wellbeing/setup_midnight_shutdown.sh b/linux_configuration/scripts/digital_wellbeing/setup_midnight_shutdown.sh index 7e6619f..3a2e8e4 100755 --- a/linux_configuration/scripts/digital_wellbeing/setup_midnight_shutdown.sh +++ b/linux_configuration/scripts/digital_wellbeing/setup_midnight_shutdown.sh @@ -829,19 +829,19 @@ if [[ -z "${MORNING_END_HOUR:-}" ]]; then exit 1 fi -# Get current time and day -current_hour=$(date +%H) -current_minute=$(date +%M) +# Get current time and day (fork-free bash builtins) +current_hour=$(printf '%(%H)T' -1) +current_minute=$(printf '%(%M)T' -1) current_time_minutes=$((10#$current_hour * 60 + 10#$current_minute)) -day_of_week=$(date +%u) # 1=Monday, 7=Sunday -day_name=$(date +%A) +day_of_week=$(printf '%(%u)T' -1) # 1=Monday, 7=Sunday +day_name=$(printf '%(%A)T' -1) # Calculate minute thresholds from config mon_wed_minutes=$((MON_WED_HOUR * 60)) thu_sun_minutes=$((THU_SUN_HOUR * 60)) morning_end_minutes=$((MORNING_END_HOUR * 60)) -logger -t day-specific-shutdown "Checking shutdown conditions at $(date) - Day: $day_name ($day_of_week), Time: $current_hour:$current_minute" +logger -t day-specific-shutdown "Checking shutdown conditions at $(printf '%(%Y-%m-%d %H:%M:%S)T' -1) - Day: $day_name ($day_of_week), Time: $current_hour:$current_minute" # Determine if we should shutdown based on day and time should_shutdown=false @@ -879,11 +879,11 @@ else fi if [[ $should_shutdown == true ]]; then - echo "$(date): Executing shutdown - current time $current_hour:$current_minute is within shutdown window for $day_name" - logger -t day-specific-shutdown "Executing scheduled shutdown at $(date)" + printf '%(%Y-%m-%d %H:%M:%S)T: Executing shutdown - current time %s:%s is within shutdown window for %s\n' -1 "$current_hour" "$current_minute" "$day_name" + logger -t day-specific-shutdown "Executing scheduled shutdown at $(printf '%(%Y-%m-%d %H:%M:%S)T' -1)" /usr/bin/systemctl poweroff else - echo "$(date): Skipping shutdown - not within shutdown window for $day_name (current: $current_hour:$current_minute)" + printf '%(%Y-%m-%d %H:%M:%S)T: Skipping shutdown - not within shutdown window for %s (current: %s:%s)\n' -1 "$day_name" "$current_hour" "$current_minute" logger -t day-specific-shutdown "Skipped shutdown - not within shutdown window for $day_name (current: $current_hour:$current_minute)" fi EOF diff --git a/linux_configuration/scripts/digital_wellbeing/setup_pc_startup_monitor.sh b/linux_configuration/scripts/digital_wellbeing/setup_pc_startup_monitor.sh index c256206..6e73000 100755 --- a/linux_configuration/scripts/digital_wellbeing/setup_pc_startup_monitor.sh +++ b/linux_configuration/scripts/digital_wellbeing/setup_pc_startup_monitor.sh @@ -16,7 +16,7 @@ shift "$COMMON_ARGS_SHIFT" echo "PC Startup Time Monitor for Arch Linux" echo "======================================" -echo "Current Date: $(date)" +echo "Current Date: $(get_datetime)" echo "User: $(get_actual_user)" if [[ $INTERACTIVE_MODE == "true" ]]; then echo "Mode: Interactive (prompts enabled)" @@ -33,91 +33,36 @@ echo "User home: $USER_HOME" # Function to check if today is a monitored day is_monitored_day() { - local day_of_week - day_of_week=$(date +%u) # 1=Monday, 7=Sunday - - # Check if today is Monday (1), Friday (5), Saturday (6), or Sunday (7) - if [[ $day_of_week == "1" ]] || [[ $day_of_week == "5" ]] || [[ $day_of_week == "6" ]] || [[ $day_of_week == "7" ]]; then - return 0 # Yes, it's a monitored day - else - return 1 # No, it's not a monitored day - fi + is_day_of_week 1 5 6 7 # 1=Monday, 5=Friday, 6=Saturday, 7=Sunday } # Function to check if current time is between 5AM and 8AM is_current_time_in_window() { - local current_hour current_hour_num - current_hour=$(date +%H) - current_hour_num=$((10#$current_hour)) # Convert to decimal to avoid octal issues - - if [[ $current_hour_num -ge 5 ]] && [[ $current_hour_num -lt 8 ]]; then - return 0 # Yes, current time is in the 5AM-8AM window - else - return 1 # No, current time is outside the window - fi + is_hour_in_range 5 8 } # Function to check if PC was booted between 5AM-8AM today was_booted_in_window_today() { - local today boot_time - today=$(date +%Y-%m-%d) - boot_time="" + local boot_datetime boot_date boot_hour boot_hour_num today + today=$(get_date) + boot_datetime=$(get_boot_datetime) - # Get the last boot time using multiple methods for reliability - if command -v uptime &>/dev/null; then - # Method 1: Calculate boot time from uptime - local uptime_seconds - uptime_seconds=$(awk '{print int($1)}' /proc/uptime 2>/dev/null || echo "0") - if [[ $uptime_seconds -gt 0 ]]; then - boot_time=$(date -d "@$(($(date +%s) - uptime_seconds))" +"%Y-%m-%d %H:%M:%S") - fi - fi - - # Method 2: Use systemd if available (fallback) - if [[ -z $boot_time ]] && command -v systemctl &>/dev/null; then - boot_time=$(systemd-analyze | grep "Startup finished" | sed -n 's/.*finished in .* = \(.*\)$/\1/p' 2>/dev/null || echo "") - if [[ -n $boot_time ]]; then - # This gives us relative time, need to calculate absolute time - local current_time uptime_sec - current_time=$(date +%s) - uptime_sec=$(awk '{print int($1)}' /proc/uptime 2>/dev/null || echo "0") - boot_time=$(date -d "@$((current_time - uptime_sec))" +"%Y-%m-%d %H:%M:%S") - fi - fi - - # Method 3: Use who -b (fallback) - if [[ -z $boot_time ]] && command -v who &>/dev/null; then - boot_time=$(who -b | awk '{print $3, $4}' 2>/dev/null || echo "") - if [[ -n $boot_time ]]; then - boot_time="$today $boot_time" - fi - fi - - # Method 4: Use /proc/uptime as final fallback - if [[ -z $boot_time ]]; then - local uptime_seconds - uptime_seconds=$(awk '{print int($1)}' /proc/uptime 2>/dev/null || echo "0") - boot_time=$(date -d "@$(($(date +%s) - uptime_seconds))" +"%Y-%m-%d %H:%M:%S") - fi - - echo "Boot time detected: $boot_time" + echo "Boot time detected: $boot_datetime" # Check if boot time is from today - local boot_date - boot_date=$(echo "$boot_time" | cut -d' ' -f1) + boot_date=$(echo "$boot_datetime" | cut -d' ' -f1) if [[ $boot_date != "$today" ]]; then echo "PC was not booted today (boot date: $boot_date, today: $today)" return 1 # Not booted today fi # Extract hour from boot time - local boot_hour boot_hour_num - boot_hour=$(echo "$boot_time" | cut -d' ' -f2 | cut -d':' -f1) + boot_hour=$(echo "$boot_datetime" | cut -d' ' -f2 | cut -d':' -f1) boot_hour_num=$((10#$boot_hour)) # Convert to decimal echo "Boot hour: $boot_hour_num" - # Check if boot time was between 5AM (5) and 8AM (7, since we want before 8AM) + # Check if boot time was between 5AM (5) and 8AM (8, before 8AM) if [[ $boot_hour_num -ge 5 ]] && [[ $boot_hour_num -lt 8 ]]; then echo "PC was booted in the expected window (5AM-8AM)" return 0 # Yes, booted in window @@ -130,9 +75,9 @@ was_booted_in_window_today() { # Function to show notification/warning show_startup_warning() { local day_name current_time today - day_name=$(date +%A) - current_time=$(date +"%H:%M") - today=$(date +%Y-%m-%d) + day_name=$(get_day_name) + current_time=$(printf '%(%H:%M)T' -1) + today=$(get_date) echo "" echo "⚠️ PC STARTUP TIME WARNING" diff --git a/linux_configuration/scripts/lib/common.sh b/linux_configuration/scripts/lib/common.sh index a7817bd..794e06e 100755 --- a/linux_configuration/scripts/lib/common.sh +++ b/linux_configuration/scripts/lib/common.sh @@ -385,6 +385,125 @@ log_error() { warn() { log_warn "$@"; } err() { log_error "$@"; } +# ============================================================================= +# EFFICIENT TIME FUNCTIONS (zero-fork bash builtins) +# ============================================================================= +# These functions use printf '%(...)'T' bash builtin (NO external commands) +# to avoid fork-storm anti-patterns in polling scripts. +# See: .github/skills/efficient-polling-scripts/SKILL.md + +# Get current Unix timestamp (seconds since epoch) +# Usage: ts=$(get_timestamp) +# FORK-FREE: uses bash builtin printf %s (sec_since_epoch) +get_timestamp() { + printf '%(%s)T' -1 +} + +# Get current date in YYYY-MM-DD format +# Usage: date=$(get_date) +get_date() { + printf '%(%Y-%m-%d)T' -1 +} + +# Get current time in HH:MM:SS format +# Usage: time=$(get_time) +get_time() { + printf '%(%H:%M:%S)T' -1 +} + +# Get current date-time in YYYY-MM-DD HH:MM:SS format +# Usage: dt=$(get_datetime) +get_datetime() { + printf '%(%Y-%m-%d %H:%M:%S)T' -1 +} + +# Get day of week (1=Monday, 7=Sunday) +# Usage: dow=$(get_day_of_week) +get_day_of_week() { + printf '%(%u)T' -1 +} + +# Get day name (Monday, Tuesday, ...) +# Usage: day=$(get_day_name) +get_day_name() { + printf '%(%A)T' -1 +} + +# Get current hour (00-23) +# Usage: hour=$(get_hour) +get_hour() { + printf '%(%H)T' -1 +} + +# Get current minute (00-59) +# Usage: minute=$(get_minute) +get_minute() { + printf '%(%M)T' -1 +} + +# Get current second (00-59) +# Usage: second=$(get_second) +get_second() { + printf '%(%S)T' -1 +} + +# Get Unix timestamp from boot (uptime in seconds) +# Usage: boot_seconds=$(get_uptime_seconds) +get_uptime_seconds() { + read -r uptime_with_fraction _ < /proc/uptime + printf '%.*f\n' 0 "$uptime_with_fraction" +} + +# Get boot time in YYYY-MM-DD HH:MM:SS format +# Usage: boot_time=$(get_boot_datetime) +# Calculates: current_time - uptime_seconds +get_boot_datetime() { + local uptime_seconds + uptime_seconds=$(get_uptime_seconds) + local boot_ts=$(($(get_timestamp) - uptime_seconds)) + printf '%(%Y-%m-%d %H:%M:%S)T' "$boot_ts" +} + +# Get boot time date only (YYYY-MM-DD) +# Usage: boot_date=$(get_boot_date) +get_boot_date() { + local uptime_seconds + uptime_seconds=$(get_uptime_seconds) + local boot_ts=$(($(get_timestamp) - uptime_seconds)) + printf '%(%Y-%m-%d)T' "$boot_ts" +} + +# Get boot time hour only (00-23) +# Usage: boot_hour=$(get_boot_hour) +get_boot_hour() { + local uptime_seconds + uptime_seconds=$(get_uptime_seconds) + local boot_ts=$(($(get_timestamp) - uptime_seconds)) + printf '%(%H)T' "$boot_ts" +} + +# Check if current time is within a given hour range +# Usage: if is_hour_in_range 5 8; then ... # 5AM-8AM +is_hour_in_range() { + local start_hour=$1 + local end_hour=$2 + local current_hour + current_hour=$(get_hour) + local current_hour_num=$((10#$current_hour)) + [[ $current_hour_num -ge $start_hour ]] && [[ $current_hour_num -lt $end_hour ]] +} + +# Check if current day is a specific day of week +# Usage: if is_day_of_week 1 5 6 7; then ... # Monday, Friday, Saturday, Sunday +is_day_of_week() { + local target_day + target_day=$(get_day_of_week) + for day in "$@"; do + [[ $target_day -eq $day ]] && return 0 + done + return 1 +} + # ============================================================================= # INTERACTIVE PROMPTS # ============================================================================= @@ -415,10 +534,12 @@ has_cmd() { # Usage: print_setup_header "Script Name" print_setup_header() { local title="$1" + local current_datetime + current_datetime=$(get_datetime) echo "$title" printf '=%.0s' $(seq 1 ${#title}) echo "" - echo "Current Date: $(date)" + echo "Current Date: $current_datetime" echo "User: $USER" echo "Original user: $(get_actual_user)" if [[ $INTERACTIVE_MODE == "true" ]]; then diff --git a/linux_configuration/tests/test_hosts_guard_pacman_integration.sh b/linux_configuration/tests/test_hosts_guard_pacman_integration.sh new file mode 100755 index 0000000..9013094 --- /dev/null +++ b/linux_configuration/tests/test_hosts_guard_pacman_integration.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# Regression tests for pacman wrapper and hosts-guard hook integration. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(dirname "$SCRIPT_DIR")" +WRAPPER_FILE="$REPO_DIR/scripts/digital_wellbeing/pacman/pacman_wrapper.sh" +PRE_HOOK_FILE="$REPO_DIR/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh" +POST_HOOK_FILE="$REPO_DIR/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh" +COMMON_FILE="$REPO_DIR/hosts/guard/pacman-hooks/hosts-guard-common.sh" +INSTALLER_FILE="$REPO_DIR/hosts/guard/install_pacman_hooks.sh" + +assert_contains() { + local file_path="$1" + local pattern="$2" + local message="$3" + + if grep -Fq "$pattern" "$file_path"; then + echo "PASS: $message" + else + echo "FAIL: $message" + exit 1 + fi +} + +assert_not_regex() { + local file_path="$1" + local pattern="$2" + local message="$3" + + if grep -Eq "$pattern" "$file_path"; then + echo "FAIL: $message" + exit 1 + fi + + echo "PASS: $message" +} + +first_line_number() { + local file_path="$1" + local pattern="$2" + + grep -n -F -m 1 "$pattern" "$file_path" | cut -d: -f1 +} + +assert_order() { + local file_path="$1" + local first_pattern="$2" + local second_pattern="$3" + local message="$4" + local first_line + local second_line + + first_line="$(first_line_number "$file_path" "$first_pattern")" + second_line="$(first_line_number "$file_path" "$second_pattern")" + + if [[ -z "$first_line" || -z "$second_line" ]]; then + echo "FAIL: $message" + exit 1 + fi + + if (( first_line < second_line )); then + echo "PASS: $message" + else + echo "FAIL: $message" + exit 1 + fi +} + +echo "=== Hosts guard pacman integration regression tests ===" + +for file_path in "$WRAPPER_FILE" "$PRE_HOOK_FILE" "$POST_HOOK_FILE" "$COMMON_FILE" "$INSTALLER_FILE"; do + bash -n "$file_path" +done +echo "PASS: shell syntax is valid" + +assert_not_regex "$PRE_HOOK_FILE" '(^|[[:space:]])(sudo[[:space:]]+)?rm[[:space:]]+/etc/hosts([[:space:]]|$)' \ + "pre-transaction hook must not delete /etc/hosts" + +assert_contains "$WRAPPER_FILE" 'pacman_hooks_manage_hosts_guard()' \ + "wrapper detects when pacman hooks already manage hosts guard" +assert_contains "$WRAPPER_FILE" 'should_use_wrapper_hosts_guard_fallback()' \ + "wrapper exposes a dedicated fallback path for hosts guard" +assert_order "$WRAPPER_FILE" 'if ! check_and_handle_db_lock "$@"; then' 'if should_use_wrapper_hosts_guard_fallback "$@"; then' \ + "wrapper checks pacman db lock before any manual hosts unlock fallback" +assert_contains "$WRAPPER_FILE" 'manual_hosts_guard=1' \ + "wrapper tracks whether manual hosts guard fallback was used" + +assert_contains "$INSTALLER_FILE" 'install -m 755 "$SCRIPT_DIR/pacman-hooks/hosts-guard-common.sh" /usr/local/share/hosts-guard/' \ + "installer deploys shared hosts guard hook helpers" + +echo "All hosts guard pacman integration regression tests passed."