diff --git a/hosts/guard/README.md b/hosts/guard/README.md index 357dce2..15a6809 100644 --- a/hosts/guard/README.md +++ b/hosts/guard/README.md @@ -10,6 +10,7 @@ Components: - hosts-guard.path (triggers on PathChanged=/etc/hosts) - hosts-bind-mount.service (bind mounts /etc/hosts read-only after boot) 3. psychological/ directory – scripts that add delay + journaling before allowing a maintenance/unlock operation. +4. pacman hooks – automatically unlock/re-lock /etc/hosts around package transactions so pacman never fails due to the read-only bind mount. Install Flow (suggested): 1. After generating /etc/hosts via your existing hosts/install.sh, copy it to /usr/local/share/locked-hosts. @@ -19,6 +20,11 @@ Install Flow (suggested): systemctl enable --now hosts-guard.path systemctl enable --now hosts-bind-mount.service 4. (Optional) Use psychological/unlock-hosts.sh as the ONLY sanctioned way to modify hosts (it removes protections temporarily, launches an editor after a delay, and re-enforces on close). +5. Make pacman automatic (recommended): + ./install_pacman_hooks.sh + This installs hooks under /etc/pacman.d/hooks that: + - PreTransaction: temporarily disable guard and make /etc/hosts writable + - PostTransaction: re-run enforcement and re-enable guard (bind mount + path watcher) Limitations: - A root user can still disable units, remount, remove attributes. diff --git a/hosts/guard/enforce-hosts.sh b/hosts/guard/enforce-hosts.sh old mode 100644 new mode 100755 diff --git a/hosts/guard/install_pacman_hooks.sh b/hosts/guard/install_pacman_hooks.sh new file mode 100755 index 0000000..c4b63c5 --- /dev/null +++ b/hosts/guard/install_pacman_hooks.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +require_root() { if [[ $EUID -ne 0 ]]; then exec sudo -E bash "$0" "$@"; fi } +require_root "$@" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +HOOKS_DIR="/etc/pacman.d/hooks" + +install -d -m 755 "$HOOKS_DIR" + +# Pre-transaction hook +cat >"$HOOKS_DIR/10-unlock-etc-hosts.hook" <<'HOOK' +[Trigger] +Operation = Upgrade +Operation = Install +Operation = Remove +Type = Package +Target = * + +[Action] +Description = Temporarily unlocking /etc/hosts for transaction +When = PreTransaction +Exec = /bin/bash /usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh +NeedsTargets +HOOK + +# Post-transaction hook +cat >"$HOOKS_DIR/90-relock-etc-hosts.hook" <<'HOOK' +[Trigger] +Operation = Upgrade +Operation = Install +Operation = Remove +Type = Package +Target = * + +[Action] +Description = Re-locking /etc/hosts after transaction +When = PostTransaction +Exec = /bin/bash /usr/local/share/hosts-guard/pacman-post-relock-hosts.sh +NeedsTargets +HOOK + +# Place helper scripts into a shared location +install -d -m 755 /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/ + +echo "Pacman hooks installed into $HOOKS_DIR." diff --git a/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh b/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh new file mode 100644 index 0000000..450f4e7 --- /dev/null +++ b/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Post-transaction hook to re-apply hosts guard protections (single-layer ro bind) + +TARGET=/etc/hosts +ENFORCE=/usr/local/sbin/enforce-hosts.sh +LOGTAG=hosts-guard-hook + +mount_layers_count() { awk '$5=="/etc/hosts"{c++} END{print c+0}' /proc/self/mountinfo 2>/dev/null || echo 0; } +collapse_mounts() { + local i=0 + if command -v mountpoint >/devnull 2>&1; then + while mountpoint -q "$TARGET"; do + umount -l "$TARGET" >/dev/null 2>&1 || break + i=$((i+1)) + (( i > 20 )) && break + done + else + local cnt + cnt=$(mount_layers_count) + while (( cnt > 1 )); do + umount -l "$TARGET" >/dev/null 2>&1 || break + i=$((i+1)) + (( i > 20 )) && break + cnt=$(mount_layers_count) + done + fi +} + +# Ensure we end with a single read-only bind mount layer +logger -t "$LOGTAG" "post: relocking /etc/hosts (starting)" +echo "$(date -Is) post-relock(start)" >> /run/hosts-guard-hook.log 2>/dev/null || true +collapse_mounts + +if [[ -x "$ENFORCE" ]]; then + "$ENFORCE" >/dev/null 2>&1 || true +fi + +# Apply exactly one ro bind layer +mount --bind "$TARGET" "$TARGET" >/dev/null 2>&1 || true +mount -o remount,ro,bind "$TARGET" >/dev/null 2>&1 || true + +# Start only the path watcher; avoid bind-mount service (we already bound once) +if command -v systemctl >/dev/null 2>&1; then + systemctl start hosts-guard.path >/dev/null 2>&1 || true +fi + +logger -t "$LOGTAG" "post: relocking /etc/hosts (done)" +echo "$(date -Is) post-relock(done)" >> /run/hosts-guard-hook.log 2>/dev/null || true + +exit 0 diff --git a/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh b/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh new file mode 100644 index 0000000..e44866a --- /dev/null +++ b/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Non-interactive pre-transaction hook to temporarily unlock /etc/hosts + +TARGET=/etc/hosts +LOGTAG=hosts-guard-hook + +stop_units_if_present() { + local units=(hosts-bind-mount.service hosts-guard.path) + for u in "${units[@]}"; do + if command -v systemctl >/dev/null 2>&1; then + if systemctl list-unit-files 2>/dev/null | grep -q "^$u"; then + systemctl stop "$u" >/dev/null 2>&1 || true + fi + fi + done +} + +is_ro_mount() { findmnt -no OPTIONS -T "$TARGET" 2>/dev/null | grep -qw ro; } +is_bind_mount() { findmnt -no OPTIONS -T "$TARGET" 2>/dev/null | grep -qw bind; } + +mount_layers_count() { awk '$5=="/etc/hosts"{c++} END{print c+0}' /proc/self/mountinfo 2>/dev/null || echo 0; } +cleanup_mount_stacks() { + local i=0 + # Unmount bind layers until /etc/hosts is no longer a mountpoint + if command -v mountpoint >/dev/null 2>&1; then + while mountpoint -q "$TARGET"; do + umount -l "$TARGET" >/dev/null 2>&1 || break + i=$((i+1)) + (( i > 20 )) && break + done + else + # Fallback to best-effort using mountinfo count + local cnt + cnt=$(mount_layers_count) + while (( cnt > 1 )); do + umount -l "$TARGET" >/dev/null 2>&1 || break + i=$((i+1)) + (( i > 20 )) && break + cnt=$(mount_layers_count) + done + fi +} + +# Drop protective attributes if present +if command -v lsattr >/dev/null 2>&1; then + attrs=$(lsattr -d "$TARGET" 2>/dev/null || true) + echo "$attrs" | grep -q " i " && chattr -i "$TARGET" >/dev/null 2>&1 || true + echo "$attrs" | grep -q " a " && chattr -a "$TARGET" >/dev/null 2>&1 || true +fi + +stop_units_if_present + +logger -t "$LOGTAG" "pre: unlocking /etc/hosts (starting)" +echo "$(date -Is) pre-unlock" >> /run/hosts-guard-hook.log 2>/dev/null || true + +# Always collapse any existing layers; we'll operate on the plain file +cleanup_mount_stacks + +# If someone managed a ro single-layer mount, ensure rw by remounting or collapsing again +if is_ro_mount; then + mount -o remount,rw "$TARGET" >/dev/null 2>&1 || cleanup_mount_stacks +fi + +logger -t "$LOGTAG" "pre: unlocking /etc/hosts (done)" + +exit 0 diff --git a/hosts/guard/setup_hosts_guard.sh b/hosts/guard/setup_hosts_guard.sh old mode 100644 new mode 100755 diff --git a/scripts/pacman_wrapper.sh b/scripts/pacman_wrapper.sh index 5ef494a..5dd1e8f 100755 --- a/scripts/pacman_wrapper.sh +++ b/scripts/pacman_wrapper.sh @@ -12,6 +12,36 @@ BOLD='\033[1m' NC='\033[0m' # No Color PACMAN_BIN="/usr/bin/pacman" +# Determine if this invocation may perform a transaction (upgrade/install/remove) +needs_unlock() { + # If args include -S (install/upgrade), -U (local install), or -R (remove), we 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) + return 0 ;; + esac + done + return 1 +} + +# 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" + if [[ -x "$pre" ]]; then + echo -e "${CYAN}[hosts-guard] Preparing /etc/hosts for transaction...${NC}" >&2 + /bin/bash "$pre" || true + fi +} + +post_relock_hosts() { + local post="/usr/local/share/hosts-guard/pacman-post-relock-hosts.sh" + if [[ -x "$post" ]]; then + /bin/bash "$post" || true + echo -e "${CYAN}[hosts-guard] Protections re-applied to /etc/hosts.${NC}" >&2 + fi +} + # Ensure periodic system services (timer/monitor) are set up; if not, trigger setup ensure_periodic_maintenance() { @@ -155,7 +185,7 @@ function has_noconfirm_flag() { # Cleanup: remove any installed blocked packages (in addition to the queued operation) function remove_installed_blocked_packages() { - local user_args=("$@") + # args not used; kept for future policy extension # List installed package names mapfile -t installed_names < <("$PACMAN_BIN" -Qq 2>/dev/null) local to_remove=() @@ -223,8 +253,10 @@ function check_for_steam() { # Function to check if current day is a weekday (after 4PM Friday until midnight Sunday) function is_weekday() { - local day_of_week=$(date +%u) # %u gives 1-7 (Monday is 1, Sunday is 7) - local hour=$(date +%H) # %H gives hour in 24-hour format (00-23) + local day_of_week + day_of_week=$(date +%u) # %u gives 1-7 (Monday is 1, Sunday is 7) + local hour + hour=$(date +%H) # %H gives hour in 24-hour format (00-23) # Monday through Thursday are always weekdays if [[ $day_of_week -ge 1 && $day_of_week -le 4 ]]; then @@ -248,7 +280,8 @@ function prompt_for_steam_challenge() { # Check if it's a weekday and block completely if is_weekday; then - local day_name=$(date +%A) + local day_name + day_name=$(date +%A) echo -e "${RED}Steam installation BLOCKED: Steam cannot be installed on weekdays.${NC}" echo -e "${RED}Today is $day_name. Please try again on the weekend (Saturday or Sunday).${NC}" return 1 @@ -522,15 +555,23 @@ fi display_operation "$1" # Echo the command that's about to be executed -echo -e "${GREEN}Executing:${NC} $PACMAN_BIN $@" >&2 +echo -e "${GREEN}Executing:${NC} $PACMAN_BIN $*" >&2 # Record start time for statistics start_time=$(date +%s) -# Execute the real pacman command +# Execute the real pacman command (with /etc/hosts guard handling) +if needs_unlock "$@"; then + pre_unlock_hosts +fi + "$PACMAN_BIN" "$@" exit_code=$? +if needs_unlock "$@"; then + post_relock_hosts +fi + # Record end time for statistics end_time=$(date +%s) duration=$((end_time - start_time)) diff --git a/scripts/toggle_window_manager.sh b/scripts/toggle_window_manager.sh new file mode 100755 index 0000000..748b7fb --- /dev/null +++ b/scripts/toggle_window_manager.sh @@ -0,0 +1,103 @@ +#!/bin/bash + +set -euo pipefail + +# Configuration ----------------------------------------------------------------- +TARGET_SESSION_NAME="Xfce Session" +TARGET_PACKAGES=( + xfwm4 # Compositing window manager with XFCE integration + xfce4-session # Provides the Xfce session entry for display managers + xfce4-panel # Panel with system tray support + xfce4-settings # Settings daemon (enables compositing toggle, theming, etc.) + xfce4-terminal # Handy default terminal for the new environment +) + +# Utility functions -------------------------------------------------------------- +info() { echo "[INFO] $*"; } +warn() { echo "[WARN] $*" >&2; } +error() { echo "[ERROR] $*" >&2; exit 1; } + +require_command() { + local cmd="$1" pkg_hint="${2:-}" + if ! command -v "$cmd" >/dev/null 2>&1; then + if [[ -n "$pkg_hint" ]]; then + warn "Install '$pkg_hint' to obtain the '$cmd' command." + fi + error "Required command '$cmd' not found." + fi +} + +ensure_pacman() { + require_command pacman "pacman" + if ! grep -qi "arch" /etc/os-release 2>/dev/null; then + warn "This script was designed for Arch Linux; continuing anyway." + fi +} + +install_packages() { + local missing=() + for pkg in "${TARGET_PACKAGES[@]}"; do + if ! pacman -Qi "$pkg" >/dev/null 2>&1; then + missing+=("$pkg") + fi + done + + if [[ ${#missing[@]} -eq 0 ]]; then + info "All target packages are already installed." + return + fi + + if ! command -v sudo >/dev/null 2>&1; then + error "sudo is required to install packages. Install sudo or run this script as root." + fi + + info "Installing missing packages: ${missing[*]}" + sudo pacman -S --needed --noconfirm "${missing[@]}" +} + +print_post_install_tips() { + cat </dev/null 2>&1; then + info "Terminating current session (ID: $session_id) via loginctl." + loginctl terminate-session "$session_id" + return + fi + + if loginctl list-sessions 2>/dev/null | awk '{print $1" "$3}' | grep -q " $USER$"; then + info "Terminating all sessions for user '$USER' via loginctl." + loginctl terminate-user "$USER" + return + fi + + warn "loginctl could not terminate the session; attempting fallback logout." + pkill -KILL -u "$USER" || error "Failed to terminate user sessions. Please log out manually." +} + +main() { + ensure_pacman + install_packages + print_post_install_tips + + # Give the user a moment to read the instructions before logging out. + logout_user +} + +main "$@"