diff --git a/misc/api.func b/misc/api.func index dad7a2eb..62635b35 100644 --- a/misc/api.func +++ b/misc/api.func @@ -35,7 +35,11 @@ TELEMETRY_URL="https://telemetry.community-scripts.org/telemetry" # Timeout for telemetry requests (seconds) +# Progress pings (validation/configuring) use the short timeout TELEMETRY_TIMEOUT=5 +# Final status updates (success/failed) use the longer timeout +# PocketBase may need more time under load (FindRecord + UpdateRecord) +STATUS_TIMEOUT=10 # ============================================================================== # SECTION 0: REPOSITORY SOURCE DETECTION @@ -117,16 +121,17 @@ detect_repo_source # - Canonical source of truth for ALL exit code mappings # - Used by both api.func (telemetry) and error_handler.func (error display) # - Supports: -# * Generic/Shell errors (1, 2, 124, 126-130, 134, 137, 139, 141, 143) -# * curl/wget errors (6, 7, 22, 28, 35) +# * Generic/Shell errors (1-3, 10, 124-132, 134, 137, 139, 141, 143-146) +# * curl/wget errors (4-8, 16, 18, 22-28, 30, 32-36, 39, 44-48, 51-52, 55-57, 59, 61, 63, 75, 78-79, 92, 95) # * Package manager errors (APT, DPKG: 100-102, 255) +# * BSD sysexits (64-78) # * Systemd/Service errors (150-154) # * Python/pip/uv errors (160-162) # * PostgreSQL errors (170-173) # * MySQL/MariaDB errors (180-183) # * MongoDB errors (190-193) # * Proxmox custom codes (200-231) -# * Node.js/npm errors (243, 245-249) +# * Node.js/npm errors (239, 243, 245-249) # - Returns description string for given exit code # ------------------------------------------------------------------------------ explain_exit_code() { @@ -135,6 +140,7 @@ explain_exit_code() { # --- Generic / Shell --- 1) echo "General error / Operation not permitted" ;; 2) echo "Misuse of shell builtins (e.g. syntax error)" ;; + 3) echo "General syntax or argument error" ;; 10) echo "Docker / privileged mode required (unsupported environment)" ;; # --- curl / wget errors (commonly seen in downloads) --- @@ -142,16 +148,41 @@ explain_exit_code() { 5) echo "curl: Could not resolve proxy" ;; 6) echo "curl: DNS resolution failed (could not resolve host)" ;; 7) echo "curl: Failed to connect (network unreachable / host down)" ;; - 8) echo "curl: FTP server reply error" ;; + 8) echo "curl: Server reply error (FTP/SFTP or apk untrusted key)" ;; + 16) echo "curl: HTTP/2 framing layer error" ;; + 18) echo "curl: Partial file (transfer not completed)" ;; 22) echo "curl: HTTP error returned (404, 429, 500+)" ;; 23) echo "curl: Write error (disk full or permissions)" ;; + 24) echo "curl: Write to local file failed" ;; 25) echo "curl: Upload failed" ;; + 26) echo "curl: Read error on local file (I/O)" ;; + 27) echo "curl: Out of memory (memory allocation failed)" ;; 28) echo "curl: Operation timeout (network slow or server not responding)" ;; 30) echo "curl: FTP port command failed" ;; + 32) echo "curl: FTP SIZE command failed" ;; + 33) echo "curl: HTTP range error" ;; + 34) echo "curl: HTTP post error" ;; 35) echo "curl: SSL/TLS handshake failed (certificate error)" ;; + 36) echo "curl: FTP bad download resume" ;; + 39) echo "curl: LDAP search failed" ;; + 44) echo "curl: Internal error (bad function call order)" ;; + 45) echo "curl: Interface error (failed to bind to specified interface)" ;; + 46) echo "curl: Bad password entered" ;; + 47) echo "curl: Too many redirects" ;; + 48) echo "curl: Unknown command line option specified" ;; + 51) echo "curl: SSL peer certificate or SSH host key verification failed" ;; + 52) echo "curl: Empty reply from server (got nothing)" ;; + 55) echo "curl: Failed sending network data" ;; 56) echo "curl: Receive error (connection reset by peer)" ;; + 57) echo "curl: Unrecoverable poll/select error (system I/O failure)" ;; + 59) echo "curl: Couldn't use specified SSL cipher" ;; + 61) echo "curl: Bad/unrecognized transfer encoding" ;; + 63) echo "curl: Maximum file size exceeded" ;; 75) echo "Temporary failure (retry later)" ;; 78) echo "curl: Remote file not found (404 on FTP/file)" ;; + 79) echo "curl: SSH session error (key exchange/auth failed)" ;; + 92) echo "curl: HTTP/2 stream error (protocol violation)" ;; + 95) echo "curl: HTTP/3 layer error" ;; # --- Package manager / APT / DPKG --- 100) echo "APT: Package manager error (broken packages / dependency problems)" ;; @@ -175,16 +206,21 @@ explain_exit_code() { # --- Common shell/system errors --- 124) echo "Command timed out (timeout command)" ;; + 125) echo "Command failed to start (Docker daemon or execution error)" ;; 126) echo "Command invoked cannot execute (permission problem?)" ;; 127) echo "Command not found" ;; 128) echo "Invalid argument to exit" ;; 129) echo "Killed by SIGHUP (terminal closed / hangup)" ;; 130) echo "Aborted by user (SIGINT)" ;; + 131) echo "Killed by SIGQUIT (core dumped)" ;; + 132) echo "Killed by SIGILL (illegal CPU instruction)" ;; 134) echo "Process aborted (SIGABRT - possibly Node.js heap overflow)" ;; 137) echo "Killed (SIGKILL / Out of memory?)" ;; 139) echo "Segmentation fault (core dumped)" ;; 141) echo "Broken pipe (SIGPIPE - output closed prematurely)" ;; 143) echo "Terminated (SIGTERM)" ;; + 144) echo "Killed by signal 16 (SIGUSR1 / SIGSTKFLT)" ;; + 146) echo "Killed by signal 18 (SIGTSTP)" ;; # --- Systemd / Service errors (150-154) --- 150) echo "Systemd: Service failed to start" ;; @@ -192,7 +228,6 @@ explain_exit_code() { 152) echo "Permission denied (EACCES)" ;; 153) echo "Build/compile failed (make/gcc/cmake)" ;; 154) echo "Node.js: Native addon build failed (node-gyp)" ;; - # --- Python / pip / uv (160-162) --- 160) echo "Python: Virtualenv / uv environment missing or broken" ;; 161) echo "Python: Dependency resolution failed" ;; @@ -243,7 +278,8 @@ explain_exit_code() { 225) echo "Proxmox: No template available for OS/Version" ;; 231) echo "Proxmox: LXC stack upgrade failed" ;; - # --- Node.js / npm / pnpm / yarn (243-249) --- + # --- Node.js / npm / pnpm / yarn (239-249) --- + 239) echo "npm/Node.js: Unexpected runtime error or dependency failure" ;; 243) echo "Node.js: Out of memory (JavaScript heap out of memory)" ;; 245) echo "Node.js: Invalid command-line option" ;; 246) echo "Node.js: Internal JavaScript Parse Error" ;; @@ -318,6 +354,55 @@ get_error_text() { fi } +# ------------------------------------------------------------------------------ +# get_full_log() +# +# - Returns the FULL installation log (build + install combined) +# - Calls ensure_log_on_host() to pull container log if needed +# - Strips ANSI escape codes and carriage returns +# - Truncates to max_bytes (default: 120KB) to stay within API limits +# - Used for the error telemetry field (full trace instead of 20 lines) +# ------------------------------------------------------------------------------ +get_full_log() { + local max_bytes="${1:-122880}" # 120KB default + local logfile="" + + # Ensure logs are available on host (pulls from container if needed) + if declare -f ensure_log_on_host >/dev/null 2>&1; then + ensure_log_on_host + fi + + # Try combined log first (most complete) + if [[ -n "${CTID:-}" && -n "${SESSION_ID:-}" ]]; then + local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log" + if [[ -s "$combined_log" ]]; then + logfile="$combined_log" + fi + fi + + # Fall back to INSTALL_LOG + if [[ -z "$logfile" || ! -s "$logfile" ]]; then + if [[ -n "${INSTALL_LOG:-}" && -s "${INSTALL_LOG}" ]]; then + logfile="$INSTALL_LOG" + fi + fi + + # Fall back to BUILD_LOG + if [[ -z "$logfile" || ! -s "$logfile" ]]; then + if [[ -n "${BUILD_LOG:-}" && -s "${BUILD_LOG}" ]]; then + logfile="$BUILD_LOG" + fi + fi + + if [[ -n "$logfile" && -s "$logfile" ]]; then + # Strip ANSI codes, carriage returns, and anonymize IP addresses (GDPR) + sed 's/\r$//' "$logfile" 2>/dev/null | + sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' | + sed -E 's/([0-9]{1,3}\.)[0-9]{1,3}\.[0-9]{1,3}/\1x.x/g' | + head -c "$max_bytes" + fi +} + # ------------------------------------------------------------------------------ # build_error_string() # @@ -520,6 +605,7 @@ post_to_api() { cat </dev/null || return 0 + [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0 + [[ -z "${RANDOM_UUID:-}" ]] && return 0 + + local progress_status="${1:-configuring}" + local app_name="${NSAPP:-${app:-unknown}}" + local telemetry_type="${TELEMETRY_TYPE:-lxc}" + + curl -fsS -m 5 -X POST "${TELEMETRY_URL:-https://telemetry.community-scripts.org/telemetry}" \ + -H "Content-Type: application/json" \ + -d "{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"${telemetry_type}\",\"nsapp\":\"${app_name}\",\"status\":\"${progress_status}\"}" &>/dev/null || true +} + # ------------------------------------------------------------------------------ # post_update_to_api() # @@ -725,11 +839,15 @@ post_update_to_api() { else exit_code=1 fi - # Get log lines and build structured error string - local error_text="" - error_text=$(get_error_text) + # Get full installation log for error field + local log_text="" + log_text=$(get_full_log 122880) || true # 120KB max + if [[ -z "$log_text" ]]; then + # Fallback to last 20 lines + log_text=$(get_error_text) + fi local full_error - full_error=$(build_error_string "$exit_code" "$error_text") + full_error=$(build_error_string "$exit_code" "$log_text") error=$(json_escape "$full_error") short_error=$(json_escape "$(explain_exit_code "$exit_code")") error_category=$(categorize_error "$exit_code") @@ -750,12 +868,13 @@ post_update_to_api() { local http_code="" - # ── Attempt 1: Full payload with complete error text ── + # ── Attempt 1: Full payload with complete error text (includes full log) ── local JSON_PAYLOAD JSON_PAYLOAD=$( cat </dev/null) || http_code="000" @@ -798,6 +917,7 @@ EOF cat </dev/null) || http_code="000" @@ -840,6 +960,7 @@ EOF cat </dev/null || true + -d "$MINIMAL_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000" - # Tried 3 times - mark as done regardless to prevent infinite loops - POST_UPDATE_DONE=true + if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then + POST_UPDATE_DONE=true + return 0 + fi + + # All 3 attempts failed — do NOT set POST_UPDATE_DONE=true. + # This allows the EXIT trap (api_exit_script) to retry with 3 fresh attempts. + # No infinite loop risk: EXIT trap fires exactly once. } # ============================================================================== @@ -876,6 +1003,9 @@ categorize_error() { # Network errors (curl/wget) 6 | 7 | 22 | 35) echo "network" ;; + # Docker / Privileged mode required + 10) echo "config" ;; + # Timeout errors 28 | 124 | 211) echo "timeout" ;; @@ -906,14 +1036,14 @@ categorize_error() { # Python environment errors # (already covered: 160-162 under dependency) - # Aborted by user - 130) echo "aborted" ;; + # Aborted by user (SIGHUP=terminal closed, SIGINT=Ctrl+C, SIGTERM=killed) + 129 | 130 | 143) echo "user_aborted" ;; # Resource errors (OOM, SIGKILL, SIGABRT) 134 | 137) echo "resource" ;; - # Signal/Process errors (SIGTERM, SIGPIPE, SIGSEGV) - 129 | 139 | 141 | 143) echo "signal" ;; + # Signal/Process errors (SIGPIPE, SIGSEGV) + 139 | 141) echo "signal" ;; # Shell errors (general error, syntax error) 1 | 2) echo "shell" ;; @@ -950,6 +1080,63 @@ get_install_duration() { echo $((now - INSTALL_START_TIME)) } +# ------------------------------------------------------------------------------ +# _telemetry_report_exit() +# +# - Internal handler called by EXIT trap set in init_tool_telemetry() +# - Determines success/failure from exit code and reports via appropriate API +# - Arguments: +# * $1: exit_code from the script +# ------------------------------------------------------------------------------ +_telemetry_report_exit() { + local ec="${1:-0}" + local status="success" + [[ "$ec" -ne 0 ]] && status="failed" + + # Lazy name resolution: use explicit name, fall back to $APP, then "unknown" + local name="${TELEMETRY_TOOL_NAME:-${APP:-unknown}}" + + if [[ "${TELEMETRY_TOOL_TYPE:-pve}" == "addon" ]]; then + post_addon_to_api "$name" "$status" "$ec" + else + post_tool_to_api "$name" "$status" "$ec" + fi +} + +# ------------------------------------------------------------------------------ +# init_tool_telemetry() +# +# - One-line telemetry setup for tools/addon scripts +# - Reads DIAGNOSTICS from /usr/local/community-scripts/diagnostics +# (persisted on PVE host during first build, and inside containers by install.func) +# - Starts install timer for duration tracking +# - Sets EXIT trap to automatically report success/failure on script exit +# - Arguments: +# * $1: tool_name (optional, falls back to $APP at exit time) +# * $2: type ("pve" for PVE host scripts, "addon" for container addons) +# - Usage: +# source <(curl -fsSL .../misc/api.func) 2>/dev/null || true +# init_tool_telemetry "post-pve-install" "pve" +# init_tool_telemetry "" "addon" # uses $APP at exit time +# ------------------------------------------------------------------------------ +init_tool_telemetry() { + local name="${1:-}" + local type="${2:-pve}" + + [[ -n "$name" ]] && TELEMETRY_TOOL_NAME="$name" + TELEMETRY_TOOL_TYPE="$type" + + # Read diagnostics opt-in/opt-out + if [[ -f /usr/local/community-scripts/diagnostics ]]; then + DIAGNOSTICS=$(grep -i "^DIAGNOSTICS=" /usr/local/community-scripts/diagnostics 2>/dev/null | awk -F'=' '{print $2}') || true + fi + + start_install_timer + + # EXIT trap: automatically report telemetry when script ends + trap '_telemetry_report_exit "$?"' EXIT +} + # ------------------------------------------------------------------------------ # post_tool_to_api() # @@ -997,7 +1184,8 @@ post_tool_to_api() { cat </dev/null || true + -d "$JSON_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000" - POST_UPDATE_DONE=true + if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then + POST_UPDATE_DONE=true + return 0 + fi + + # Retry with minimal payload + sleep 1 + http_code=$(curl -sS -w "%{http_code}" -m "${STATUS_TIMEOUT}" -X POST "${TELEMETRY_URL}" \ + -H "Content-Type: application/json" \ + -d "{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"${TELEMETRY_TYPE:-lxc}\",\"nsapp\":\"${NSAPP:-unknown}\",\"status\":\"${pb_status}\",\"exit_code\":${exit_code},\"install_duration\":${duration:-0}}" \ + -o /dev/null 2>/dev/null) || http_code="000" + + if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then + POST_UPDATE_DONE=true + return 0 + fi + + # Do NOT set POST_UPDATE_DONE=true — let EXIT trap retry }