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.
This commit is contained in:
Krzysztof kuhy Rudnicki 2026-05-06 21:41:07 +02:00
parent 00c383008a
commit f84135f6d7
11 changed files with 425 additions and 157 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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