Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down Expand Up @@ -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. |

Expand Down
109 changes: 87 additions & 22 deletions host-setup.sh
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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=()
Expand Down Expand Up @@ -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 ---

Expand Down
98 changes: 98 additions & 0 deletions tests/host_setup.bats
Original file line number Diff line number Diff line change
@@ -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" ]
}
Loading