diff --git a/.templates/ha_entrypoint.sh b/.templates/ha_entrypoint.sh index cabf17dc9..7621af626 100755 --- a/.templates/ha_entrypoint.sh +++ b/.templates/ha_entrypoint.sh @@ -1,152 +1,248 @@ -#!/command/with-contenv bashio +#!/bin/bash # shellcheck shell=bash +set -euo pipefail + +########################################## +# Detect if this is PID1 (main process) # +########################################## -# Detect if this is PID1 (main container process) — do this once at the start PID1=false if [ "$$" -eq 1 ]; then - PID1=true - echo "Starting as entrypoint" - # Allow s6 commands - if [ -d /command ]; then - ln -sf /command/* /usr/bin/ - fi + PID1=true + echo "Starting as entrypoint" + if [ -d /command ]; then + ln -sf /command/* /usr/bin/ 2>/dev/null || true + fi else - echo "Starting custom scripts" + echo "Starting custom scripts" +fi + +########################################## +# Pick an exec-capable directory # +########################################## + +pick_exec_dir() { + # Prefer locations that are commonly exec-capable in containers + # and writable. Avoid /tmp because it may be mounted noexec. + local d + for d in /dev/shm /run /var/run /mnt /root /; do + if [ -d "$d" ] && [ -w "$d" ]; then + # Create a tiny test executable to confirm "exec" works + local t="${d%/}/.exec_test_$$" + printf '#!/bin/sh\necho ok\n' > "$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; } + chmod 700 "$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; } + if "$t" >/dev/null 2>&1; then + rm -f "$t" 2>/dev/null || true + echo "$d" + return 0 + fi + rm -f "$t" 2>/dev/null || true + fi + done + return 1 +} + +EXEC_DIR="$(pick_exec_dir || true)" +if [ -z "${EXEC_DIR:-}" ]; then + echo "ERROR: Could not find an exec-capable writable directory (e.g., /dev/shm,/run)." + echo "Your environment likely mounts all writable dirs as noexec; shebang validation cannot run safely." + exit 1 fi ###################### # Select the shebang # ###################### -# List of candidate shebangs, prioritize with-contenv if PID1 -candidate_shebangs=() -if $PID1; then - candidate_shebangs+=("/command/with-contenv bashio" "/usr/bin/with-contenv bashio") -fi -candidate_shebangs+=( - "/usr/bin/env bashio" - "/usr/bin/bashio" - "/usr/bin/bash" - "/usr/bin/sh" - "/bin/bash" - "/bin/sh" +candidate_shebangs=( + "/command/with-contenv bashio" + "/usr/bin/with-contenv bashio" + "/usr/bin/env bashio" + "/usr/bin/bashio" + "/usr/bin/bash" + "/bin/bash" + "/usr/bin/sh" + "/bin/sh" ) -# Find the first valid shebang interpreter in candidate list +probe_script_content=' +set -e + +if ! command -v bashio::addon.version >/dev/null 2>&1; then + for f in \ + /usr/lib/bashio/bashio.sh \ + /usr/lib/bashio/lib.sh \ + /usr/src/bashio/bashio.sh \ + /usr/local/lib/bashio/bashio.sh + do + if [ -f "$f" ]; then + # shellcheck disable=SC1090 + . "$f" + break + fi + done +fi + +bashio::addon.version +' + +validate_shebang() { + local candidate="$1" + local tmp out rc + + # shellcheck disable=SC2206 + local cmd=( $candidate ) + local exe="${cmd[0]}" + + if [ ! -x "$exe" ]; then + echo " - FAIL (not executable): #!$candidate" >&2 + return 1 + fi + + tmp="${EXEC_DIR%/}/shebang_test.$$.$RANDOM" + { + printf '#!%s\n' "$candidate" + printf '%s\n' "$probe_script_content" + } > "$tmp" + chmod 700 "$tmp" 2>/dev/null || true + + set +e + out="$("$tmp" 2>"${EXEC_DIR%/}/shebang_probe_err.$$")" + rc=$? + set -e + + rm -f "$tmp" 2>/dev/null || true + + if [ "$rc" -eq 0 ] && [ -n "${out:-}" ] && [ "$out" != "null" ]; then + rm -f "${EXEC_DIR%/}/shebang_probe_err.$$" 2>/dev/null || true + return 0 + fi + + { + echo " - FAIL: #!$candidate" + echo " rc=$rc, stdout='${out:-}'" + if [ -s "${EXEC_DIR%/}/shebang_probe_err.$$" ]; then + echo " stderr:" + sed -n '1,8p' "${EXEC_DIR%/}/shebang_probe_err.$$" + else + echo " stderr: " + fi + } >&2 + rm -f "${EXEC_DIR%/}/shebang_probe_err.$$" 2>/dev/null || true + return 1 +} + shebang="" for candidate in "${candidate_shebangs[@]}"; do - command_path="${candidate%% *}" - # Test if command exists and can actually execute a shell command (for shells) - if [ -x "$command_path" ]; then - # Try as both 'sh -c' and 'bashio echo' style - if "$command_path" -c 'echo yes' > /dev/null 2>&1 || "$command_path" echo "yes" > /dev/null 2>&1; then - shebang="$candidate" - break - fi - fi + if validate_shebang "$candidate"; then + shebang="$candidate" + break + fi done + if [ -z "$shebang" ]; then - echo "ERROR: No valid shebang found!" - exit 1 + echo "ERROR: No valid shebang found (unable to execute bashio::addon.version via candidates)." >&2 + echo "Tried:" >&2 + printf ' - %s\n' "${candidate_shebangs[@]}" >&2 + exit 1 fi +echo "Selected shebang: #!$shebang" + #################### # Starting scripts # #################### -# Loop through /etc/cont-init.d/* scripts and execute them -for SCRIPTS in /etc/cont-init.d/*; do +run_one_script() { + local script="$1" + + echo "$script: executing" + + if [ "$(id -u)" -eq 0 ]; then + chown "$(id -u)":"$(id -g)" "$script" || true + chmod a+x "$script" || true + else + echo -e "\e[38;5;214m$(date) WARNING: Script executed with user $(id -u):$(id -g), things can break and chown won't work\e[0m" + sed -i "s/^[[:space:]]*chown /true # chown /g" "$script" + sed -i "s/^[[:space:]]*chmod /true # chmod /g" "$script" + fi + + sed -i "1s|^.*|#!$shebang|" "$script" + chmod +x "$script" + + if [ "${ha_entry_source:-null}" = "true" ]; then + sed -i -E 's/^[[:space:]]*exit ([0-9]+)/return \1 \|\| exit \1/g' "$script" + sed -i 's/bashio::exit\.nok/return 1/g' "$script" + sed -i 's/bashio::exit\.ok/return 0/g' "$script" + # shellcheck disable=SC1090 + source "$script" || echo -e "\033[0;31mError\033[0m : $script exiting $?" + else + "$script" || echo -e "\033[0;31mError\033[0m : $script exiting $?" + fi + + sed -i '1a exit 0' "$script" +} + +if [ -d /etc/cont-init.d ]; then + for SCRIPTS in /etc/cont-init.d/*; do [ -e "$SCRIPTS" ] || continue - echo "$SCRIPTS: executing" + run_one_script "$SCRIPTS" + done +fi - # Check if run as root (UID 0) - if [ "$(id -u)" -eq 0 ]; then - # Fix permissions for root user - chown "$(id -u)":"$(id -g)" "$SCRIPTS" - chmod a+x "$SCRIPTS" - else - echo -e "\e[38;5;214m$(date) WARNING: Script executed with user $(id -u):$(id -g), things can break and chown won't work\e[0m" - # Disable chown and chmod commands inside the script for non-root users - sed -i "s/^\s*chown /true # chown /g" "$SCRIPTS" - sed -i "s/^\s*chmod /true # chmod /g" "$SCRIPTS" - fi - - # Prepare to run - sed -i "1s|^.*|#!$shebang|" "$SCRIPTS" - chmod +x "$SCRIPTS" - - # Optionally use 'source' to share env variables, when requested - if [ "${ha_entry_source:-null}" = true ]; then - # Replace exit with return, so sourced scripts can return errors - sed -i -E 's/^\s*exit ([0-9]+)/return \1 \|\| exit \1/g' "$SCRIPTS" - sed -i 's/bashio::exit\.nok/return 1/g' "$SCRIPTS" - sed -i 's/bashio::exit\.ok/return 0/g' "$SCRIPTS" - # shellcheck disable=SC1090 - source "$SCRIPTS" || echo -e "\033[0;31mError\033[0m : $SCRIPTS exiting $?" - else - "$SCRIPTS" || echo -e "\033[0;31mError\033[0m : $SCRIPTS exiting $?" - fi - - # Cleanup after execution - sed -i '1a exit 0' "$SCRIPTS" -done - -# Start run scripts in services.d and s6-overlay/s6-rc.d if PID1 if $PID1; then - # Run services - shopt -s nullglob - for runfile in /etc/services.d/*/run /etc/s6-overlay/s6-rc.d/*/run; do - [ -f "$runfile" ] || continue - echo "Starting: $runfile" - sed -i "1s|^.*|#!$shebang|" "$runfile" - chmod +x "$runfile" - (exec "$runfile") & - true - done - shopt -u nullglob + shopt -s nullglob + for runfile in /etc/services.d/*/run /etc/s6-overlay/s6-rc.d/*/run; do + [ -f "$runfile" ] || continue + echo "Starting: $runfile" + sed -i "1s|^.*|#!$shebang|" "$runfile" + chmod +x "$runfile" + (exec "$runfile") & + true + done + shopt -u nullglob fi ###################### # Starting container # ###################### -# If this is PID 1, keep alive and manage sigterm for clean shutdown if $PID1; then - echo " " - echo -e "\033[0;32mEverything started!\033[0m" - terminate() { - echo "Termination signal received, forwarding to subprocesses..." - # Terminate all direct child processes - if command -v pgrep &> /dev/null; then - for pid in $(pgrep -P $$); do - echo "Terminating child PID $pid" - kill -TERM "$pid" 2> /dev/null || echo "Failed to terminate PID $pid" - done - else - # Fallback: Scan /proc for children - for pid in /proc/[0-9]*/; do - pid=${pid#/proc/} - pid=${pid%/} - if [[ "$pid" -ne 1 ]] && grep -q "^PPid:\s*$$" "/proc/$pid/status" 2> /dev/null; then - echo "Terminating child PID $pid" - kill -TERM "$pid" 2> /dev/null || echo "Failed to terminate PID $pid" - fi - done + echo " " + echo -e "\033[0;32mEverything started!\033[0m" + + terminate() { + echo "Termination signal received, forwarding to subprocesses..." + if command -v pgrep >/dev/null 2>&1; then + while read -r pid; do + [ -n "$pid" ] || continue + echo "Terminating child PID $pid" + kill -TERM "$pid" 2>/dev/null || echo "Failed to terminate PID $pid" + done < <(pgrep -P "$$" || true) + else + for p in /proc/[0-9]*/; do + local_pid="${p#/proc/}" + local_pid="${local_pid%/}" + if [ "$local_pid" -ne 1 ] && grep -q "^PPid:[[:space:]]*$$" "/proc/$local_pid/status" 2>/dev/null; then + echo "Terminating child PID $local_pid" + kill -TERM "$local_pid" 2>/dev/null || echo "Failed to terminate PID $local_pid" fi - wait - echo "All subprocesses terminated. Exiting." - exit 0 - } - trap terminate SIGTERM SIGINT - # Main keep-alive loop - while :; do - sleep infinity & - wait $! - done + done + fi + wait || true + echo "All subprocesses terminated. Exiting." + exit 0 + } + + trap terminate SIGTERM SIGINT + while :; do + sleep infinity & + wait $! + done else - echo " " - echo -e "\033[0;32mStarting the upstream container\033[0m" - echo " " - # Launch optional mods script if present - if [ -f /docker-mods ]; then exec /docker-mods; fi + echo " " + echo -e "\033[0;32mStarting the upstream container\033[0m" + echo " " + if [ -f /docker-mods ]; then + exec /docker-mods + fi fi