aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/cmail-setup-finish.sh12
-rwxr-xr-xscripts/import-wireguard-configs.sh8
-rwxr-xr-xscripts/normalize-notify-sounds.sh26
-rwxr-xr-xscripts/testing/debug-vm.sh4
-rwxr-xr-xscripts/testing/lib/vm-utils.sh21
-rwxr-xr-xscripts/testing/run-net-scenarios.sh8
-rwxr-xr-xscripts/testing/run-test-baremetal.sh17
-rwxr-xr-xscripts/testing/run-test.sh18
-rw-r--r--scripts/testing/tests/test_desktop.py8
9 files changed, 90 insertions, 32 deletions
diff --git a/scripts/cmail-setup-finish.sh b/scripts/cmail-setup-finish.sh
index 7f9d3fc..949023f 100755
--- a/scripts/cmail-setup-finish.sh
+++ b/scripts/cmail-setup-finish.sh
@@ -30,6 +30,15 @@ err() { printf 'error: %s\n' "$*" >&2; exit 1; }
info() { printf '==> %s\n' "$*"; }
ok() { printf ' %s\n' "$*"; }
+# Decrypt $1 to $2 with 0600 from the moment of creation. gpg writes its output
+# at the process umask (often 0644), so a bare decrypt leaves the plaintext
+# world-readable until the chmod on the next line. The 0077 umask subshell closes
+# that window; the chmod stays to tighten a looser file left by an earlier run.
+decrypt_to_secure() {
+ ( umask 077; gpg --quiet --yes --decrypt --output "$2" "$1" )
+ chmod 600 "$2"
+}
+
# 1. Pre-reqs
command -v protonmail-bridge >/dev/null 2>&1 \
|| err "protonmail-bridge not found in PATH — install via archsetup first"
@@ -49,8 +58,7 @@ cmailpass_enc="$HOME/.config/.cmailpass.gpg"
# 2. Decrypt cmailpass
info "decrypting $cmailpass_enc"
cmailpass_plain="$HOME/.config/.cmailpass"
-gpg --quiet --yes --decrypt --output "$cmailpass_plain" "$cmailpass_enc"
-chmod 600 "$cmailpass_plain"
+decrypt_to_secure "$cmailpass_enc" "$cmailpass_plain"
ok "wrote $cmailpass_plain (mode 0600)"
# 3. Bridge cert
diff --git a/scripts/import-wireguard-configs.sh b/scripts/import-wireguard-configs.sh
index 9e42033..e1db5a6 100755
--- a/scripts/import-wireguard-configs.sh
+++ b/scripts/import-wireguard-configs.sh
@@ -55,11 +55,13 @@ for conf in "$dir"/*.conf; do
echo " $out" >&2
exit 1
fi
+ # nmcli import auto-activates a full-tunnel (0.0.0.0/0) profile. Bring it
+ # down FIRST, before the rename/modify that could fail under set -e, so a
+ # failed modify can never leave a live unasked-for VPN up. A profile that
+ # didn't activate makes this a harmless no-op.
+ nmcli connection down "$uuid" >/dev/null 2>&1 || true
nmcli connection modify "$uuid" connection.id "$name" \
connection.autoconnect no
- # nmcli import auto-activates; bring it back down so importing never leaves
- # a tunnel up. A profile that didn't activate makes this a harmless no-op.
- nmcli connection down "$uuid" >/dev/null 2>&1 || true
echo "imported: $name (inactive, autoconnect off, iface wgpvpn)"
done
[ "$found" = 1 ] || { echo "no .conf files in $dir" >&2; exit 1; }
diff --git a/scripts/normalize-notify-sounds.sh b/scripts/normalize-notify-sounds.sh
index 72c4c33..7dffbbc 100755
--- a/scripts/normalize-notify-sounds.sh
+++ b/scripts/normalize-notify-sounds.sh
@@ -28,6 +28,12 @@ shopt -s nullglob
files=("$SOUND_DIR"/*.ogg)
(( ${#files[@]} )) || { echo "No .ogg files in $SOUND_DIR" >&2; exit 1; }
+# Clean up the in-flight temp file on any exit (a failed encode under set -e
+# aborts mid-loop, so the trap is what stops the temp from leaking).
+tmp=""
+cleanup() { [ -n "${tmp:-}" ] && rm -f "$tmp"; return 0; }
+trap cleanup EXIT
+
for f in "${files[@]}"; do
mean=$(ffmpeg -hide_banner -nostats -i "$f" -af volumedetect -f null /dev/null 2>&1 \
| grep -oP 'mean_volume: \K[-0-9.]+' || true)
@@ -36,14 +42,22 @@ for f in "${files[@]}"; do
continue
fi
gain=$(awk -v t="$TARGET_DB" -v m="$mean" 'BEGIN { printf "%.1f", t - m }')
- tmp=$(mktemp --suffix=.ogg)
+ # Resolve the real target (SOUND_DIR is often the stow-symlinked ~/.local
+ # copy) and stage the temp beside it, so the mv is atomic on the same
+ # filesystem and replacing the real file leaves the stow symlink pointing
+ # at it. A truncate-in-place (cat > "$f") would have corrupted the tracked
+ # file if the encode produced a short or empty output.
+ target=$(readlink -f "$f")
+ tmp=$(mktemp --suffix=.ogg --tmpdir="$(dirname "$target")")
ffmpeg -hide_banner -loglevel error -y -i "$f" \
-af "volume=${gain}dB" -c:a libvorbis -q:a 6 "$tmp"
- # Write through the file rather than mv over it: when SOUND_DIR is the
- # stow-symlinked ~/.local copy, mv would replace the symlink with a real
- # file and decouple it from the repo. cat preserves the symlink target.
- cat "$tmp" > "$f"
- rm -f "$tmp"
+ if [ ! -s "$tmp" ]; then
+ echo "skip (empty re-encode): $f" >&2
+ rm -f "$tmp"; tmp=""
+ continue
+ fi
+ mv -f "$tmp" "$target"
+ tmp=""
printf "%-14s mean %7s dB gain %+6s dB -> target %s dB\n" \
"$(basename "$f")" "$mean" "$gain" "$TARGET_DB"
done
diff --git a/scripts/testing/debug-vm.sh b/scripts/testing/debug-vm.sh
index b0fa2b9..d05ed59 100755
--- a/scripts/testing/debug-vm.sh
+++ b/scripts/testing/debug-vm.sh
@@ -46,7 +46,6 @@ fi
# Configuration
TIMESTAMP=$(date +'%Y%m%d-%H%M%S')
VM_IMAGES_DIR="$PROJECT_ROOT/vm-images"
-BASE_DISK="$VM_IMAGES_DIR/archsetup-base.qcow2"
ROOT_PASSWORD="archsetup"
OVERLAY_DISK=""
@@ -54,6 +53,9 @@ OVERLAY_DISK=""
LOGFILE="/tmp/debug-vm-$TIMESTAMP.log"
init_logging "$LOGFILE"
init_vm_paths "$VM_IMAGES_DIR"
+# The profile-correct base image comes from init_vm_paths; a hardcoded
+# archsetup-base.qcow2 booted the btrfs base under FS_PROFILE=zfs.
+BASE_DISK="$DISK_PATH"
cleanup_debug() {
if declare -f vm_is_running >/dev/null 2>&1 && vm_is_running; then
diff --git a/scripts/testing/lib/vm-utils.sh b/scripts/testing/lib/vm-utils.sh
index b85e773..b6686e9 100755
--- a/scripts/testing/lib/vm-utils.sh
+++ b/scripts/testing/lib/vm-utils.sh
@@ -66,9 +66,12 @@ init_vm_paths() {
# let a zfs run's ZFSBootMenu entries clobber the btrfs GRUB entry, leaving
# the btrfs base unbootable (no removable ESP fallback to recover from).
OVMF_VARS="$VM_IMAGES_DIR/OVMF_VARS${img_suffix}.fd"
- PID_FILE="$VM_IMAGES_DIR/qemu.pid"
- MONITOR_SOCK="$VM_IMAGES_DIR/qemu-monitor.sock"
- SERIAL_LOG="$VM_IMAGES_DIR/qemu-serial.log"
+ # Runtime paths carry the same suffix: a concurrent btrfs and zfs run
+ # sharing one PID file let the second run's stop logic kill the first
+ # run's VM, and both truncated the same serial log.
+ PID_FILE="$VM_IMAGES_DIR/qemu${img_suffix}.pid"
+ MONITOR_SOCK="$VM_IMAGES_DIR/qemu-monitor${img_suffix}.sock"
+ SERIAL_LOG="$VM_IMAGES_DIR/qemu-serial${img_suffix}.log"
mkdir -p "$VM_IMAGES_DIR"
}
@@ -287,6 +290,18 @@ kill_qemu() {
pid=$(cat "$PID_FILE" 2>/dev/null)
if [ -n "$pid" ]; then
kill -9 "$pid" 2>/dev/null || true
+ # Wait for the process to actually die before returning: the
+ # force-kill fallback is followed by restore_snapshot, and
+ # qemu-img refuses a qcow2 the dying qemu still holds locked.
+ # `wait` reaps it when it's our child; otherwise poll until the
+ # /proc entry is gone or a zombie (fds released either way).
+ wait "$pid" 2>/dev/null || true
+ local i state
+ for i in $(seq 1 50); do
+ state=$(awk '{print $3}' "/proc/$pid/stat" 2>/dev/null) || state=""
+ [ -z "$state" ] || [ "$state" = "Z" ] && break
+ sleep 0.1
+ done
fi
fi
_cleanup_qemu_files
diff --git a/scripts/testing/run-net-scenarios.sh b/scripts/testing/run-net-scenarios.sh
index 7197e37..8d27438 100755
--- a/scripts/testing/run-net-scenarios.sh
+++ b/scripts/testing/run-net-scenarios.sh
@@ -100,13 +100,19 @@ for f in "${S_FILES[@]}"; do
esac
echo "== $name — $SCENARIO_DESC"
scenario_break || { fail "$name: break failed"; exit 1; }
+ # A diagnose miss still runs the fix + assert (the repair result is
+ # worth having either way) but must fail the scenario: rc carries it to
+ # the subshell exit so the run can't report green over a diagnosis
+ # regression.
+ rc=0
if declare -F scenario_diagnose_expect >/dev/null; then
if scenario_diagnose_expect; then pass "$name: diagnose named it"
- else fail "$name: diagnose did NOT name it (inspect net doctor --json)"; fi
+ else fail "$name: diagnose did NOT name it (inspect net doctor --json)"; rc=1; fi
fi
scenario_fix || info "$name: net doctor --fix returned non-zero"
if scenario_assert; then pass "$name: repaired"
else fail "$name: NOT repaired"; exit 1; fi
+ exit "$rc"
) || fails=$((fails + 1))
done
diff --git a/scripts/testing/run-test-baremetal.sh b/scripts/testing/run-test-baremetal.sh
index d22c424..d837389 100755
--- a/scripts/testing/run-test-baremetal.sh
+++ b/scripts/testing/run-test-baremetal.sh
@@ -228,14 +228,17 @@ if ! $VALIDATE_ONLY; then
if [ $POLL_COUNT -ge $MAX_POLLS ]; then
error "ArchSetup timed out after 90 minutes"
- ARCHSETUP_EXIT_CODE=124
+ ARCHSETUP_COMPLETED=timeout
else
- step "Retrieving archsetup exit status"
+ # The installer runs detached with set -e off, so its real exit
+ # status is unavailable; the marker only proves the script reached
+ # its last line. Testinfra is the pass/fail authority.
+ step "Checking for the archsetup completion marker"
if ssh_cmd "grep -q 'ARCHSETUP_EXECUTION_COMPLETE' /var/log/archsetup-*.log 2>/dev/null"; then
- ARCHSETUP_EXIT_CODE=0
+ ARCHSETUP_COMPLETED=yes
success "ArchSetup completed successfully"
else
- ARCHSETUP_EXIT_CODE=1
+ ARCHSETUP_COMPLETED=no
error "ArchSetup may have encountered errors"
fi
fi
@@ -259,7 +262,7 @@ if ! $VALIDATE_ONLY; then
capture_post_install_state "$TEST_RESULTS_DIR"
else
info "Skipping archsetup (--validate-only)"
- ARCHSETUP_EXIT_CODE=0
+ ARCHSETUP_COMPLETED=yes
mkdir -p "$TEST_RESULTS_DIR/pre-install" "$TEST_RESULTS_DIR/post-install"
fi
@@ -302,7 +305,7 @@ Target: $TARGET_HOST
Test Method: Bare Metal ZFS
Results:
- ArchSetup Exit Code: $ARCHSETUP_EXIT_CODE
+ ArchSetup Completed: $ARCHSETUP_COMPLETED (completion marker, not the installer's exit code)
Validation: $(if $TEST_PASSED; then echo "PASSED"; else echo "FAILED"; fi)
Validation Summary:
@@ -335,7 +338,7 @@ fi
# Final summary
section "Test Complete"
-if [ "$ARCHSETUP_EXIT_CODE" -eq 0 ] && $TEST_PASSED; then
+if [ "$ARCHSETUP_COMPLETED" = "yes" ] && $TEST_PASSED; then
success "TEST PASSED"
exit 0
else
diff --git a/scripts/testing/run-test.sh b/scripts/testing/run-test.sh
index f962df3..a5c3691 100755
--- a/scripts/testing/run-test.sh
+++ b/scripts/testing/run-test.sh
@@ -280,15 +280,17 @@ done
if [ $POLL_COUNT -ge $MAX_POLLS ]; then
error "ArchSetup timed out after 150 minutes"
- ARCHSETUP_EXIT_CODE=124
+ ARCHSETUP_COMPLETED=timeout
else
- # Get exit code from the remote log
- step "Retrieving archsetup exit status..."
- ARCHSETUP_EXIT_CODE=$(vm_exec "$ROOT_PASSWORD" \
- "grep -q 'ARCHSETUP_EXECUTION_COMPLETE' /var/log/archsetup-*.log 2>/dev/null && echo 0 || echo 1" \
+ # The installer runs detached with set -e off, so its real exit status
+ # is unavailable here; the completion marker only proves the script ran
+ # to its last line. Testinfra below is the actual pass/fail authority.
+ step "Checking for the archsetup completion marker..."
+ ARCHSETUP_COMPLETED=$(vm_exec "$ROOT_PASSWORD" \
+ "grep -q 'ARCHSETUP_EXECUTION_COMPLETE' /var/log/archsetup-*.log 2>/dev/null && echo yes || echo no" \
2>/dev/null)
- if [ "$ARCHSETUP_EXIT_CODE" = "0" ]; then
+ if [ "$ARCHSETUP_COMPLETED" = "yes" ]; then
success "ArchSetup completed successfully"
else
error "ArchSetup may have encountered errors (check logs)"
@@ -370,7 +372,7 @@ VM Configuration:
SSH: localhost:$SSH_PORT
Results:
- ArchSetup Exit Code: $ARCHSETUP_EXIT_CODE
+ ArchSetup Completed: $ARCHSETUP_COMPLETED (completion marker, not the installer's exit code)
Validation: $(if $TEST_PASSED; then echo "PASSED"; else echo "FAILED"; fi)
Validation Summary:
@@ -424,7 +426,7 @@ CLEANUP_DONE=1
# Final summary
section "Test Complete"
-if [ "$ARCHSETUP_EXIT_CODE" = "0" ] && $TEST_PASSED; then
+if [ "$ARCHSETUP_COMPLETED" = "yes" ] && $TEST_PASSED; then
success "TEST PASSED"
exit 0
else
diff --git a/scripts/testing/tests/test_desktop.py b/scripts/testing/tests/test_desktop.py
index 468bc75..1538d6a 100644
--- a/scripts/testing/tests/test_desktop.py
+++ b/scripts/testing/tests/test_desktop.py
@@ -93,7 +93,13 @@ def test_hyprland_socket(host, hyprland_installed, compositor_running):
pytest.skip("Hyprland not installed")
if not compositor_running:
pytest.skip("Hyprland not running (headless) — socket check not applicable")
- assert host.run("test -S /tmp/hypr/*/.socket.sock").rc == 0
+ # `test -S` on a shell glob breaks at both edges: zero matches leaves the
+ # literal pattern (rc 1) and several instances pass multiple args (rc 2).
+ # find -type s proves exactly one live socket path exists.
+ sock = host.run(
+ "find /tmp/hypr -maxdepth 2 -name .socket.sock -type s | head -1"
+ ).stdout.strip()
+ assert sock, "no Hyprland socket found under /tmp/hypr"
@pytest.mark.attribution("archsetup")