fix(modules): make service/session ops chroot-safe to prevent install aborts

When the TUI modules run inside the archiso installer chroot, the new
system's systemd is not the running init and there is no user session bus.
Operations like `systemctl start`, `enable --now`, `systemctl --user`, and
`gsettings` fail there and, under `set -e`, abort the whole module.

- logging.sh: add in_chroot/have_user_bus/enable_service/start_service
  helpers. enable_service warns instead of aborting; start_service skips in
  a chroot (unit starts on first boot via its enable symlink).
- core.sh, hyprland.sh, hyprlua.sh, niri.sh: route enables through
  enable_service, starts through start_service, and guard gsettings behind
  have_user_bus. Fixes the cronie-enable failure and the ly/DM setup abort.
- app modules (tlp, timeshift, open-webui, mysql, qemu, ollama, cockpit,
  docker, ssh-server): convert `enable --now`/plain enables to
  enable_service + start_service so they no longer abort during chroot install.
- tui-install.sh: run modules with output to a file plus a pid-bound tail,
  waiting on the module PID, so a daemon child inheriting the pipe can no
  longer hang the installer after a module (e.g. flatpak) finishes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R5kHioUMK3mtf2eiLEozCM
main
Amir Alexander Abdelbaki 2026-06-26 20:58:37 +02:00
parent f0db333fa4
commit d445f965ce
15 changed files with 106 additions and 47 deletions

View File

@ -150,7 +150,7 @@ sudo pacman -Syu --noconfirm --needed -- "${HYPRLAND_PACKAGES[@]}"
log "Enabling essential services..." log "Enabling essential services..."
# NetworkManager must be active on boot for network connectivity. # NetworkManager must be active on boot for network connectivity.
sudo systemctl enable NetworkManager.service enable_service NetworkManager.service
# getty@tty1 is the default text-mode login prompt; we replace it with ly. # getty@tty1 is the default text-mode login prompt; we replace it with ly.
# '|| true' prevents abort if the unit is already disabled or doesn't exist. # '|| true' prevents abort if the unit is already disabled or doesn't exist.
@ -158,11 +158,11 @@ sudo systemctl disable getty@tty1.service || true
# ly is the TUI display manager that runs on tty1 and launches Hyprland after # ly is the TUI display manager that runs on tty1 and launches Hyprland after
# the user logs in. # the user logs in.
sudo systemctl enable ly@tty1.service enable_service ly@tty1.service
# udisks2 provides the D-Bus API for block devices; required by udiskie for # udisks2 provides the D-Bus API for block devices; required by udiskie for
# automatic USB/external drive mounting. # automatic USB/external drive mounting.
sudo systemctl enable udisks2.service enable_service udisks2.service
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 4. Install AUR packages # 4. Install AUR packages
@ -285,11 +285,11 @@ tar -zxf ~/install-tmp/Nordzy-cursors-lefthand.tar.gz -C ~/.icons/
log "Enabling Bluetooth and wireless services..." log "Enabling Bluetooth and wireless services..."
# bluez: core Bluetooth protocol stack (powers and manages adapters on boot) # bluez: core Bluetooth protocol stack (powers and manages adapters on boot)
sudo systemctl enable bluez enable_service bluez
# bluetooth.service: high-level service handling pairing and profiles # bluetooth.service: high-level service handling pairing and profiles
sudo systemctl enable bluetooth.service enable_service bluetooth.service
# iwd: modern Wi-Fi daemon from Intel; used by iwmenu for the bar Wi-Fi picker # iwd: modern Wi-Fi daemon from Intel; used by iwmenu for the bar Wi-Fi picker
sudo systemctl enable iwd.service enable_service iwd.service
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 9. Hyprland plugins (must be run from inside a live Hyprland session) # 9. Hyprland plugins (must be run from inside a live Hyprland session)
@ -378,8 +378,8 @@ fi
# Enable the systemd user unit and start it immediately so this session already # Enable the systemd user unit and start it immediately so this session already
# benefits from auto-mounting without requiring a reboot. # benefits from auto-mounting without requiring a reboot.
log "Enabling udiskie service..." log "Enabling udiskie service..."
sudo systemctl enable udiskie.service enable_service udiskie.service
sudo systemctl start udiskie.service start_service udiskie.service
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 15. Install config updater and theme script # 15. Install config updater and theme script

View File

@ -153,17 +153,17 @@ sudo pacman -Syu --noconfirm --needed -- "${HYPRLUA_PACKAGES[@]}"
log "Enabling essential services..." log "Enabling essential services..."
# NetworkManager: manages all network connections (wired, Wi-Fi, VPN). # NetworkManager: manages all network connections (wired, Wi-Fi, VPN).
sudo systemctl enable NetworkManager.service enable_service NetworkManager.service
# Disable the default getty login prompt on tty1 so ly can own that TTY. # Disable the default getty login prompt on tty1 so ly can own that TTY.
# '|| true' prevents abort if the unit is already disabled. # '|| true' prevents abort if the unit is already disabled.
sudo systemctl disable getty@tty1.service || true sudo systemctl disable getty@tty1.service || true
# ly: TUI display manager that presents the login screen on tty1. # ly: TUI display manager that presents the login screen on tty1.
sudo systemctl enable ly@tty1.service enable_service ly@tty1.service
# udisks2: D-Bus block-device service required by udiskie for auto-mounting. # udisks2: D-Bus block-device service required by udiskie for auto-mounting.
sudo systemctl enable udisks2.service enable_service udisks2.service
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 4. Install AUR packages # 4. Install AUR packages
@ -227,7 +227,7 @@ case $doit in
# evdev-right-click-emulation enables long-press → right-click on touch # evdev-right-click-emulation enables long-press → right-click on touch
# screens; its systemd service must be started alongside the session. # screens; its systemd service must be started alongside the session.
yay -S evdev-right-click-emulation yay -S evdev-right-click-emulation
sudo systemctl enable --now evdev-rce.service ;; enable_service evdev-rce.service; start_service evdev-rce.service ;;
# Unrecognised key: leave ~/.config/eww absent; user must copy manually. # Unrecognised key: leave ~/.config/eww absent; user must copy manually.
*) warn "No valid choice — skipping EWW copy. Run manually later." ;; *) warn "No valid choice — skipping EWW copy. Run manually later." ;;
esac esac
@ -278,7 +278,13 @@ sudo ln -sf /usr/bin/ksshaskpass /usr/lib/ssh/ssh-askpass
# Instruct GTK apps that prefer the system colour-scheme to use dark mode. # Instruct GTK apps that prefer the system colour-scheme to use dark mode.
# This is particularly important under Hyprland where there is no GNOME # This is particularly important under Hyprland where there is no GNOME
# settings daemon propagating the user's preference automatically. # settings daemon propagating the user's preference automatically.
gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' # gsettings needs a session D-Bus (dconf) — absent in the installer chroot, where
# it would hang/fail — so only apply it when a user bus is reachable.
if have_user_bus; then
gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' || true
else
skip "No session bus — set GTK dark mode after first login (gsettings)."
fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 7. Cursor setup # 7. Cursor setup
@ -299,11 +305,11 @@ tar -zxf ~/install-tmp/Nordzy-cursors-lefthand.tar.gz -C ~/.icons/
log "Enabling Bluetooth and wireless services..." log "Enabling Bluetooth and wireless services..."
# bluez: core Bluetooth daemon; powers adapters and makes them available on boot # bluez: core Bluetooth daemon; powers adapters and makes them available on boot
sudo systemctl enable bluez enable_service bluez
# bluetooth.service: handles pairing, profiles, and device reconnection # bluetooth.service: handles pairing, profiles, and device reconnection
sudo systemctl enable bluetooth.service enable_service bluetooth.service
# iwd: Intel Wireless Daemon; the Wi-Fi back-end used by iwmenu in the bar # iwd: Intel Wireless Daemon; the Wi-Fi back-end used by iwmenu in the bar
sudo systemctl enable iwd.service enable_service iwd.service
#systemctl --user enable --now hyprmoncfgd #systemctl --user enable --now hyprmoncfgd
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -390,8 +396,8 @@ fi
# Enable the systemd unit for future boots and start it immediately so USB # Enable the systemd unit for future boots and start it immediately so USB
# devices inserted during this session are handled right away. # devices inserted during this session are handled right away.
log "Enabling udiskie service..." log "Enabling udiskie service..."
sudo systemctl enable udiskie.service enable_service udiskie.service
sudo systemctl start udiskie.service start_service udiskie.service
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 15. Install config updater and theme script # 15. Install config updater and theme script

View File

@ -31,10 +31,10 @@ sudo pacman -Syu --noconfirm --needed \
# 3. Enable essential services # 3. Enable essential services
log "Enabling essential services..." log "Enabling essential services..."
sudo systemctl enable NetworkManager.service enable_service NetworkManager.service
sudo systemctl disable getty@tty1.service || true sudo systemctl disable getty@tty1.service || true
sudo systemctl enable ly@tty1.service enable_service ly@tty1.service
sudo systemctl enable udisks2.service enable_service udisks2.service
# 4. Install AUR packages # 4. Install AUR packages
log "Installing AUR packages..." log "Installing AUR packages..."
@ -72,7 +72,11 @@ sudo cp -r ~/Dotfiles/gtk-themes/cyberqueer /usr/share/themes
sudo cp ~/Dotfiles/desktopenvs/niri/btop/themes/cyberqueer.theme /usr/share/btop/themes sudo cp ~/Dotfiles/desktopenvs/niri/btop/themes/cyberqueer.theme /usr/share/btop/themes
sudo cp -f ~/Dotfiles/etc-ly-config.ini /etc/ly/config.ini sudo cp -f ~/Dotfiles/etc-ly-config.ini /etc/ly/config.ini
sudo ln -sf /usr/bin/kitty /usr/bin/xdg-terminal-exec sudo ln -sf /usr/bin/kitty /usr/bin/xdg-terminal-exec
gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' if have_user_bus; then
gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' || true
else
skip "No session bus — set GNOME color-scheme to dark after first login."
fi
# 7. Cursor setup # 7. Cursor setup
log "Installing cursor theme..." log "Installing cursor theme..."
@ -83,9 +87,9 @@ tar -zxf ~/install-tmp/Nordzy-cursors-lefthand.tar.gz -C ~/.icons/
# 8. Enable Bluetooth and wireless services # 8. Enable Bluetooth and wireless services
log "Enabling Bluetooth and wireless services..." log "Enabling Bluetooth and wireless services..."
sudo systemctl enable bluez enable_service bluez
sudo systemctl enable bluetooth.service enable_service bluetooth.service
sudo systemctl enable iwd.service enable_service iwd.service
# 9. Copy configs # 9. Copy configs
log "Copying configs..." log "Copying configs..."
@ -101,7 +105,7 @@ cp ~/Dotfiles/colors.conf ~/.config/colors.conf
log "Deploying greetd config for niri..." log "Deploying greetd config for niri..."
sudo mkdir -p /etc/greetd sudo mkdir -p /etc/greetd
sudo cp -f ~/Dotfiles/desktopenvs/niri/greetd-tuigreet/config.toml /etc/greetd/config.toml sudo cp -f ~/Dotfiles/desktopenvs/niri/greetd-tuigreet/config.toml /etc/greetd/config.toml
sudo systemctl enable greetd.service enable_service greetd.service
# 11. Wallpaper and resources # 11. Wallpaper and resources
log "Copying wallpaper and resources..." log "Copying wallpaper and resources..."
@ -128,8 +132,8 @@ fi
# 14. Enable udiskie # 14. Enable udiskie
log "Enabling udiskie service..." log "Enabling udiskie service..."
sudo systemctl enable udiskie.service enable_service udiskie.service
sudo systemctl start udiskie.service start_service udiskie.service
# 15. Make scripts executable # 15. Make scripts executable
log "Setting script permissions..." log "Setting script permissions..."

View File

@ -31,13 +31,13 @@ source "$(dirname "${BASH_SOURCE[0]}")/lib/logging.sh"
# the most user-friendly option — handles DHCP, WiFi, VPN, and has applets. # the most user-friendly option — handles DHCP, WiFi, VPN, and has applets.
# NOTE: systemctl enable only marks it to start at boot; it doesn't start it now. # NOTE: systemctl enable only marks it to start at boot; it doesn't start it now.
log "Enabling NetworkManager..." log "Enabling NetworkManager..."
sudo systemctl enable NetworkManager.service enable_service NetworkManager.service
# ── cronie ──────────────────────────────────────────────────────────────────── # ── cronie ────────────────────────────────────────────────────────────────────
# WHY: Provides the system cron daemon. Some tools (backups, vnstat stats, # WHY: Provides the system cron daemon. Some tools (backups, vnstat stats,
# fail2ban cleanup) rely on cron jobs. Arch does not enable it by default. # fail2ban cleanup) rely on cron jobs. Arch does not enable it by default.
log "Enabling cronie..." log "Enabling cronie..."
sudo systemctl enable cronie.service enable_service cronie.service
# ── greetd / tuigreet ───────────────────────────────────────────────────────── # ── greetd / tuigreet ─────────────────────────────────────────────────────────
# WHY: greetd is a minimal, standards-compliant display manager (login screen). # WHY: greetd is a minimal, standards-compliant display manager (login screen).
@ -50,20 +50,20 @@ sudo systemctl enable cronie.service
# -f flag forces overwrite of any existing config. # -f flag forces overwrite of any existing config.
log "Deploying greetd config..." log "Deploying greetd config..."
sudo cp -f ~/Dotfiles/desktopenvs/hyprland/greetd-tuigreet/config.toml /etc/greetd/config.toml sudo cp -f ~/Dotfiles/desktopenvs/hyprland/greetd-tuigreet/config.toml /etc/greetd/config.toml
sudo systemctl enable greetd.service enable_service greetd.service
# ── fail2ban ────────────────────────────────────────────────────────────────── # ── fail2ban ──────────────────────────────────────────────────────────────────
# WHY: Protects against brute-force attacks by monitoring log files and # WHY: Protects against brute-force attacks by monitoring log files and
# temporarily banning IPs that show malicious signs (too many failed logins). # temporarily banning IPs that show malicious signs (too many failed logins).
# Important on any machine with SSH open to the network. # Important on any machine with SSH open to the network.
log "Enabling fail2ban..." log "Enabling fail2ban..."
sudo systemctl enable fail2ban.service enable_service fail2ban.service
# ── udisks2 ─────────────────────────────────────────────────────────────────── # ── udisks2 ───────────────────────────────────────────────────────────────────
# WHY: udisks2 provides automatic mounting of USB drives and other removable # WHY: udisks2 provides automatic mounting of USB drives and other removable
# media via D-Bus. Required by file managers (Thunar, pcmanfm) and desktop # media via D-Bus. Required by file managers (Thunar, pcmanfm) and desktop
# utilities that want to offer "Open when inserted" functionality. # utilities that want to offer "Open when inserted" functionality.
log "Enabling udisks2..." log "Enabling udisks2..."
sudo systemctl enable udisks2.service enable_service udisks2.service
log "Core services enabled." log "Core services enabled."

View File

@ -48,6 +48,44 @@ warn() { printf "${YELLOW}[!] %s${RESET}\n" "$*" >&2; }
# Used for: unrecoverable failures (caller decides whether to exit) # Used for: unrecoverable failures (caller decides whether to exit)
err() { printf "${RED}[✖] %s${RESET}\n" "$*" >&2; } err() { printf "${RED}[✖] %s${RESET}\n" "$*" >&2; }
# ── Chroot-/session-aware operation helpers ────────────────────────────────────
# Modules are frequently run inside the archiso installer's chroot, where the
# new system's systemd is NOT the running init and there is no user session bus.
# In that environment:
# * `systemctl enable` works (it only writes symlinks), and
# * `systemctl start` / `--now` / `systemctl --user` / `gsettings` FAIL,
# which would abort a module running under `set -e`. These helpers make those
# operations safe so an install completes; the runtime bits take effect on first
# boot / first login instead.
# in_chroot: true when running inside a chroot (no booted system manager here).
in_chroot() { systemd-detect-virt --chroot 2>/dev/null; }
# have_user_bus: true when a user session D-Bus is reachable (needed by
# `systemctl --user`, gsettings/dconf, flatpak --user overrides, etc.).
have_user_bus() {
[[ -n "${DBUS_SESSION_BUS_ADDRESS:-}" ]] && return 0
[[ -S "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/bus" ]]
}
# enable_service: enable units to start at boot. Best-effort — a failure is
# warned, never fatal, so one unenroll-able unit can't abort the whole module.
enable_service() {
sudo systemctl enable "$@" 2>/dev/null \
|| warn "Could not enable: $* — enable it after first boot."
}
# start_service: start units only when a system manager is actually running.
# Inside the installer chroot there is none, so we skip — the unit starts on
# first boot via its enable symlink.
start_service() {
if in_chroot; then
skip "Not starting '$*' now (install chroot) — it starts on first boot."
return 0
fi
sudo systemctl start "$@" 2>/dev/null || warn "Could not start: $*"
}
# ── ensure_flatpak ───────────────────────────────────────────────────────────── # ── ensure_flatpak ─────────────────────────────────────────────────────────────
# WHY: Many optional app modules install via Flatpak. Rather than duplicating # WHY: Many optional app modules install via Flatpak. Rather than duplicating
# the bootstrap code in every app script, this function handles it once. # the bootstrap code in every app script, this function handles it once.

View File

@ -55,6 +55,6 @@ yay -S --answerdiff None --answerclean All --noconfirm \
# port 9090 and starts cockpit.service only when an incoming connection arrives. # port 9090 and starts cockpit.service only when an incoming connection arrives.
# This is more efficient than keeping the full web server running at all times. # This is more efficient than keeping the full web server running at all times.
log "Enabling Cockpit socket..." log "Enabling Cockpit socket..."
sudo systemctl enable cockpit.socket enable_service cockpit.socket
log "Cockpit enabled. Web UI available at https://localhost:9090" log "Cockpit enabled. Web UI available at https://localhost:9090"

View File

@ -33,7 +33,7 @@ sudo pacman -S --noconfirm --needed docker docker-compose
# docker.service starts the Docker daemon at boot. Without this, running # docker.service starts the Docker daemon at boot. Without this, running
# `docker` commands would fail with "Cannot connect to the Docker daemon". # `docker` commands would fail with "Cannot connect to the Docker daemon".
log "Enabling Docker service..." log "Enabling Docker service..."
sudo systemctl enable docker.service enable_service docker.service
# ── Add current user to docker group ───────────────────────────────────────── # ── Add current user to docker group ─────────────────────────────────────────
# By default, only root can communicate with the Docker socket at # By default, only root can communicate with the Docker socket at

View File

@ -21,5 +21,5 @@ fi
log "Enabling MariaDB service..." log "Enabling MariaDB service..."
# enable --now: persist across reboots and start the daemon in the current session. # enable --now: persist across reboots and start the daemon in the current session.
sudo systemctl enable --now mariadb.service enable_service mariadb.service; start_service mariadb.service
log "MariaDB installed and running." log "MariaDB installed and running."

View File

@ -13,7 +13,7 @@ sudo pacman -S --noconfirm --needed ollama
log "Enabling Ollama service..." log "Enabling Ollama service..."
# The ollama systemd service exposes a REST API on port 11434 used by frontends # The ollama systemd service exposes a REST API on port 11434 used by frontends
# such as open-webui; enable --now starts it immediately without a reboot. # such as open-webui; enable --now starts it immediately without a reboot.
sudo systemctl enable --now ollama.service enable_service ollama.service; start_service ollama.service
log "Ollama running on http://localhost:11434" log "Ollama running on http://localhost:11434"
log "Pull models with: ollama pull <model>" log "Pull models with: ollama pull <model>"

View File

@ -13,6 +13,6 @@ yay -S --answerdiff None --answerclean All --noconfirm open-webui
log "Enabling Open WebUI service..." log "Enabling Open WebUI service..."
# The systemd service proxies requests to Ollama at localhost:11434 and serves # The systemd service proxies requests to Ollama at localhost:11434 and serves
# the web interface on port 8080; enable --now starts it without a reboot. # the web interface on port 8080; enable --now starts it without a reboot.
sudo systemctl enable --now open-webui.service enable_service open-webui.service; start_service open-webui.service
log "Open WebUI running at http://localhost:8080" log "Open WebUI running at http://localhost:8080"

View File

@ -24,7 +24,7 @@ sudo pacman -S --noconfirm --needed \
log "Enabling libvirtd service..." log "Enabling libvirtd service..."
# libvirtd must run as root to manage KVM devices and network namespaces. # libvirtd must run as root to manage KVM devices and network namespaces.
sudo systemctl enable --now libvirtd.service enable_service libvirtd.service; start_service libvirtd.service
log "Configuring default NAT network for autostart..." log "Configuring default NAT network for autostart..."
# The 'default' NAT network is created by libvirt on first start; net-autostart # The 'default' NAT network is created by libvirt on first start; net-autostart

View File

@ -12,6 +12,6 @@ sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' "$SSHD_C
sudo sed -i 's/^#\?PubkeyAuthentication.*/PubkeyAuthentication yes/' "$SSHD_CONF" sudo sed -i 's/^#\?PubkeyAuthentication.*/PubkeyAuthentication yes/' "$SSHD_CONF"
log "Enabling sshd service..." log "Enabling sshd service..."
sudo systemctl enable sshd.service enable_service sshd.service
log "SSH server installed and enabled (key auth only, root login disabled)." log "SSH server installed and enabled (key auth only, root login disabled)."
warn "Add your public key to ~/.ssh/authorized_keys before first use." warn "Add your public key to ~/.ssh/authorized_keys before first use."

View File

@ -9,5 +9,5 @@ log "Installing timeshift-autosnap (AUR)..."
yay -S --answerdiff None --answerclean All --noconfirm timeshift-autosnap yay -S --answerdiff None --answerclean All --noconfirm timeshift-autosnap
log "Enabling cronie service..." log "Enabling cronie service..."
sudo systemctl enable --now cronie.service enable_service cronie.service; start_service cronie.service
log "Timeshift installed with autosnap on pacman transactions." log "Timeshift installed with autosnap on pacman transactions."

View File

@ -6,7 +6,7 @@ log "Installing TLP (laptop power management)..."
sudo pacman -S --noconfirm --needed tlp tlp-rdw sudo pacman -S --noconfirm --needed tlp tlp-rdw
log "Enabling TLP and masking rfkill to avoid conflicts..." log "Enabling TLP and masking rfkill to avoid conflicts..."
sudo systemctl enable --now tlp.service enable_service tlp.service; start_service tlp.service
sudo systemctl enable NetworkManager-dispatcher.service enable_service NetworkManager-dispatcher.service
sudo systemctl mask systemd-rfkill.service systemd-rfkill.socket sudo systemctl mask systemd-rfkill.service systemd-rfkill.socket
log "TLP installed." log "TLP installed."

View File

@ -263,11 +263,22 @@ run_module() {
printf "\n\033[1;35m [$STEP/$TOTAL] %s\033[0m\n" "$label" printf "\n\033[1;35m [$STEP/$TOTAL] %s\033[0m\n" "$label"
printf "\033[35m ─────────────────────────────────────────────\033[0m\n\n" printf "\033[35m ─────────────────────────────────────────────\033[0m\n\n"
# Run the module script, merging stderr into stdout, and tee to the log. # Run the module, combining stderr into stdout. We append to the log FILE
# PIPESTATUS[0] captures the exit code of 'bash "$script"' even though # (not through a 'tee' pipe) and tail it for live console output, waiting only
# 'tee' is the last command in the pipe (and would always succeed). # on the module's own PID. A 'bash "$script" | tee' pipeline would block AFTER
# the module finished whenever a daemon it spawned (flatpak's system helper,
# gpg-agent, dbus, …) inherited the pipe's write end and held it open — that is
# the "module done but it pauses" symptom. Writing to a real file avoids the
# wait: lingering daemons hold a harmless file descriptor, not the pipe.
local rc=0 local rc=0
bash "$script" 2>&1 | tee -a "$LOG" || rc=${PIPESTATUS[0]} bash "$script" >>"$LOG" 2>&1 &
local _modpid=$!
# Mirror newly appended log lines to the console while the module runs.
# --pid makes tail exit once the module process is gone.
tail -n0 --pid="$_modpid" -f "$LOG" 2>/dev/null &
local _tailpid=$!
wait "$_modpid"; rc=$?
wait "$_tailpid" 2>/dev/null || true
if [[ $rc -ne 0 ]]; then if [[ $rc -ne 0 ]]; then
if [[ $ANSWERFILE_MODE == true ]]; then if [[ $ANSWERFILE_MODE == true ]]; then