From 8d32b466885ae38e2f038f27ca28b7e33bcdb0ad Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 03:17:45 +0000 Subject: [PATCH] host-setup: prefer a persistent journal over the systemd-journal group journald only writes a separate journal per user (readable by that user without extra groups) when logs are stored on disk. The group workaround let users read every log on the system. - ensure_log_access: nothing to do if the user is already in systemd-journal or the journal is persistent (Storage=persistent, or auto with /var/log/journal). Otherwise offer to store the journal on disk via /etc/systemd/journald.conf.d/99-systemd-compose.conf, then restart journald and flush. Check the effective setting afterwards and report when another drop-in still forces volatile storage. - Only if that's declined or fails, offer the systemd-journal group, with a note that it can read all system logs. - The helpers are defined before any action runs, and the script stops after defining them when sourced, so the tests can check them. - 8 tests with fake systemd-analyze/sudo/id/usermod. - README: installation step and troubleshooting entry. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KdsNJV9e2ktZ9gF5kmczEu --- README.md | 4 +- host-setup.sh | 109 +++++++++++++++++++++++++++++++++--------- tests/host_setup.bats | 98 +++++++++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+), 24 deletions(-) create mode 100644 tests/host_setup.bats diff --git a/README.md b/README.md index 6186cc5..357afc2 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ The script asks before changing anything. It: 1. Installs missing packages: `python3`, `python3-venv`, `python3-pip`, `git`, `lsof`, `iproute2`, `dbus-user-session`, `libpam-systemd`. 2. Fixes minimal systems such as DietPi (masked `systemd-logind`, missing `XDG_RUNTIME_DIR`). -3. Offers to add you to the `systemd-journal` group, so `logs` and `status` can show your apps' output on systems that don't keep a per-user journal. This group can read **all** system logs. +3. Makes sure `logs` and `status` can show your apps' output. If the system journal is kept only in memory (common on DietPi and other minimal images), you can't read your own apps' logs. The script offers to store the journal on disk (`/etc/systemd/journald.conf.d/99-systemd-compose.conf`), after which each user can read their own logs. If you decline, it offers the `systemd-journal` group instead, which can read **all** system logs. 4. Enables **linger** for your user, so apps start at boot and keep running after you log out. 5. Links `systemd-compose` into `/usr/local/bin`. @@ -303,7 +303,7 @@ systemctl --user daemon-reload | `Name Conflict!` | Another project folder already uses this `APP_NAME`. Change `APP_NAME` in this project's `config.env`. | | `Port 8000 is already in use` | Another program (or another app) is listening on `PORT`. Change `PORT` in `.env`, or stop the other program. | | `running but not listening on port …` | The app started but isn't using `$PORT`. Check `ARGS` binds to `$PORT`, or raise `STARTUP_TIMEOUT`. | -| `logs` shows nothing / "No journal files were found" | Accept the `systemd-journal` group in `host-setup.sh`, then log out and back in. | +| `logs` shows nothing / "No journal files were found" | Re-run `./host-setup.sh` and accept storing the journal on disk. Check with `systemd-analyze cat-config systemd/journald.conf \| grep Storage=`: it should end with `persistent`, or with `auto` while `/var/log/journal` exists. If you chose the `systemd-journal` group instead, log out and back in. | | Apps stop when you log out / don't start at boot | Linger is off. Run `loginctl enable-linger $USER`. | | `Failed to connect to bus` | Re-run `./host-setup.sh` and open a new shell. | diff --git a/host-setup.sh b/host-setup.sh index 3f9d41b..73bb2b8 100644 --- a/host-setup.sh +++ b/host-setup.sh @@ -1,13 +1,5 @@ #!/bin/bash -# --- OS COMPATIBILITY CHECK --- -if ! command -v apt-get &> /dev/null; then - echo "❌ Error: This script supports Debian/Ubuntu based systems only." - echo " (Raspbian, DietPi, Ubuntu, Mint, etc.)" - echo " Reason: It relies on 'apt' for package management." - exit 1 -fi - # Dependencies: # python3-venv: Critical for creating isolated environments # python3-pip: Required for package management @@ -21,6 +13,91 @@ TARGET_LINK="/usr/local/bin/systemd-compose" SCRIPT_DIR="$(dirname "$(realpath "$0")")" SOURCE_SCRIPT="$SCRIPT_DIR/systemd-compose.sh" +JOURNAL_DIR="${JOURNAL_DIR:-/var/log/journal}" +JOURNALD_DROPIN_DIR="${JOURNALD_DROPIN_DIR:-/etc/systemd/journald.conf.d}" +JOURNALD_DROPIN="$JOURNALD_DROPIN_DIR/99-systemd-compose.conf" + +# --- HELPERS: LOG ACCESS --- + +# Prints journald's effective Storage= setting: auto, persistent, volatile or none +journal_storage_setting() { + local storage + storage=$(systemd-analyze cat-config systemd/journald.conf 2>/dev/null \ + | sed -n 's/^[[:space:]]*Storage=[[:space:]]*//p' | tail -n 1) + echo "${storage:-auto}" +} + +# journald only writes a separate journal per user (which that user may read +# without extra groups) when logs are stored on disk. +journal_is_persistent() { + case "$(journal_storage_setting)" in + persistent) return 0 ;; + auto) [ -d "$JOURNAL_DIR" ] ;; + *) return 1 ;; + esac +} + +enable_persistent_journal() { + sudo mkdir -p "$JOURNALD_DROPIN_DIR" && + printf '# Added by systemd-compose host-setup.sh\n[Journal]\nStorage=persistent\n' \ + | sudo tee "$JOURNALD_DROPIN" >/dev/null && + sudo systemctl restart systemd-journald && + sudo journalctl --flush +} + +# Makes sure `systemd-compose logs` can show the user's app logs. Prefers +# storing the journal on disk (each user reads only their own logs) over the +# systemd-journal group (which can read every log on the system). +ensure_log_access() { + if id -nG "$USER" | grep -qw "systemd-journal"; then + echo "✅ User is in the 'systemd-journal' group (logs visible)." + return 0 + fi + if journal_is_persistent; then + echo "✅ The journal is stored on disk, so you can read your apps' logs." + return 0 + fi + + echo "⚠️ The system journal is only kept in memory (Storage=$(journal_storage_setting))." + echo " In that mode you can't read your own apps' logs without extra permissions." + echo " Recommended: store the journal on disk. Each user can then read their own logs." + echo " (journald limits it to 10% of the disk, 4 GB at most.)" + read -p "❓ Store the journal on disk? (Requires sudo) [y/N] " -n 1 -r + echo "" + if [[ $REPLY =~ ^[Yy]$ ]]; then + if enable_persistent_journal && journal_is_persistent; then + echo "✅ The journal is now stored on disk ($JOURNALD_DROPIN)." + return 0 + fi + echo "❌ Couldn't switch the journal to disk. Another setting may override it:" + echo " systemd-analyze cat-config systemd/journald.conf" + fi + + echo " Alternative: join the 'systemd-journal' group. Note: it can read ALL system logs." + read -p "❓ Add user '$USER' to the group? (Requires sudo) [y/N] " -n 1 -r + echo "" + if [[ $REPLY =~ ^[Yy]$ ]]; then + sudo usermod -aG systemd-journal "$USER" + echo "✅ User added to group." + GROUPS_CHANGED=1 + else + echo "⚠️ Skipped. 'systemd-compose logs' may show nothing." + fi +} + +# The tests source this file to check the helpers above; stop here for them. +if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then + return 0 +fi + +# --- OS COMPATIBILITY CHECK --- +if ! command -v apt-get &> /dev/null; then + echo "❌ Error: This script supports Debian/Ubuntu based systems only." + echo " (Raspbian, DietPi, Ubuntu, Mint, etc.)" + echo " Reason: It relies on 'apt' for package management." + exit 1 +fi + echo "=== 🛠️ Host Dependency Check ===" MISSING_DEPS=() @@ -129,21 +206,9 @@ fix_shell_environment() { ensure_logind_service fix_shell_environment -# --- ACTION: LOG PERMISSIONS --- +# --- ACTION: LOG ACCESS --- -if groups "$USER" | grep -q "systemd-journal"; then - echo "✅ User is in 'systemd-journal' group (Logs visible)." -else - echo "⚠️ User '$USER' is NOT in 'systemd-journal' group." - echo " You won't be able to see app logs without this." - read -p "❓ Add user to group now? (Requires sudo) [y/N] " -n 1 -r - echo "" - if [[ $REPLY =~ ^[Yy]$ ]]; then - sudo usermod -aG systemd-journal "$USER" - echo "✅ User added to group." - GROUPS_CHANGED=1 - fi -fi +ensure_log_access # --- ACTION: ENABLE LINGER --- diff --git a/tests/host_setup.bats b/tests/host_setup.bats new file mode 100644 index 0000000..7728d3f --- /dev/null +++ b/tests/host_setup.bats @@ -0,0 +1,98 @@ +#!/usr/bin/env bats +# host-setup.sh: making app logs readable (persistent journal vs. group) + +# Single quotes below are intentional: the fake tools expand "$@" themselves. +# shellcheck disable=SC2016 + +load test_helper + +setup() { + TEST_ROOT="$(mktemp -d)" + export STUB_STATE="$TEST_ROOT/state" + export JOURNAL_DIR="$TEST_ROOT/var/log/journal" + export JOURNALD_DROPIN_DIR="$TEST_ROOT/etc/journald.conf.d" + export JOURNALD_CONF="$TEST_ROOT/etc/journald.conf" + export STUB_GROUPS="user" + mkdir -p "$STUB_STATE" "$TEST_ROOT/bin" "$TEST_ROOT/etc" + printf '[Journal]\n#Storage=auto\n' > "$JOURNALD_CONF" + + # Fakes for the system tools ensure_log_access uses + cat > "$TEST_ROOT/bin/systemd-analyze" <<'EOF' +#!/bin/bash +# Effective config = main file, then drop-ins in name order (like systemd) +cat "$JOURNALD_CONF" +for f in "$JOURNALD_DROPIN_DIR"/*.conf; do [ -f "$f" ] && cat "$f"; done +exit 0 +EOF + printf '#!/bin/bash\necho "sudo $*" >> "$STUB_STATE/calls"\nexec "$@"\n' > "$TEST_ROOT/bin/sudo" + printf '#!/bin/bash\necho "systemctl $*" >> "$STUB_STATE/calls"\n' > "$TEST_ROOT/bin/systemctl" + printf '#!/bin/bash\necho "usermod $*" >> "$STUB_STATE/calls"\n' > "$TEST_ROOT/bin/usermod" + printf '#!/bin/bash\nif [ "$1" == "-nG" ]; then echo "$STUB_GROUPS"; else exec /usr/bin/id "$@"; fi\n' > "$TEST_ROOT/bin/id" + chmod +x "$TEST_ROOT/bin/"* + export PATH="$TEST_ROOT/bin:$REPO_DIR/tests/stubs:$PATH" +} + +# Runs ensure_log_access with the given answers to its y/N prompts +log_access() { + run bash -c "source '$REPO_DIR/host-setup.sh' && ensure_log_access" <<< "$1" +} + +@test "sourcing host-setup.sh only defines functions" { + run bash -c "source '$REPO_DIR/host-setup.sh' && type -t ensure_log_access" + [ "$status" -eq 0 ] + [ "$output" == "function" ] +} + +@test "Storage=auto with /var/log/journal counts as persistent" { + mkdir -p "$JOURNAL_DIR" + log_access "" + [ "$status" -eq 0 ] + assert_output_contains "stored on disk, so you can read" + [ ! -f "$STUB_STATE/calls" ] +} + +@test "members of systemd-journal are not asked anything" { + STUB_GROUPS="user systemd-journal" log_access "" + [ "$status" -eq 0 ] + assert_output_contains "'systemd-journal' group (logs visible)" +} + +@test "an in-memory journal is switched to disk when accepted" { + log_access "y" + [ "$status" -eq 0 ] + assert_output_contains "only kept in memory (Storage=auto)" + assert_output_contains "now stored on disk" + grep -q '^Storage=persistent$' "$JOURNALD_DROPIN_DIR/99-systemd-compose.conf" + grep -q 'systemctl restart systemd-journald' "$STUB_STATE/calls" + refute_output_contains "Alternative" +} + +@test "an explicit Storage=volatile is overridden by the drop-in" { + printf '[Journal]\nStorage=volatile\n' > "$JOURNALD_CONF" + log_access "y" + assert_output_contains "(Storage=volatile)" + assert_output_contains "now stored on disk" +} + +@test "a later drop-in that keeps volatile storage is reported" { + mkdir -p "$JOURNALD_DROPIN_DIR" + printf '[Journal]\nStorage=volatile\n' > "$JOURNALD_DROPIN_DIR/zz-ramlog.conf" + log_access "yn" + assert_output_contains "Couldn't switch the journal to disk" + assert_output_contains "Alternative: join the 'systemd-journal' group" +} + +@test "declining disk storage offers the group as a fallback" { + log_access "ny" + [ "$status" -eq 0 ] + assert_output_contains "can read ALL system logs" + grep -q 'usermod -aG systemd-journal' "$STUB_STATE/calls" + [ ! -f "$JOURNALD_DROPIN_DIR/99-systemd-compose.conf" ] +} + +@test "declining both changes nothing" { + log_access "nn" + [ "$status" -eq 0 ] + assert_output_contains "may show nothing" + [ ! -f "$STUB_STATE/calls" ] +}