11# Copyright (c) 2021-2026 community-scripts ORG
2- # Author: michelroegl-brunner
3- # License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVED /main/LICENSE
2+ # Author: michelroegl-brunner | MickLesk
3+ # License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE /main/LICENSE
44
55# ==============================================================================
66# API.FUNC - TELEMETRY & DIAGNOSTICS API
@@ -153,7 +153,7 @@ explain_exit_code() {
153153 126) echo "Command invoked cannot execute (permission problem?)" ;;
154154 127) echo "Command not found" ;;
155155 128) echo "Invalid argument to exit" ;;
156- 130) echo "Terminated by Ctrl+C (SIGINT)" ;;
156+ 130) echo "Aborted by user (SIGINT)" ;;
157157 134) echo "Process aborted (SIGABRT - possibly Node.js heap overflow)" ;;
158158 137) echo "Killed (SIGKILL / Out of memory?)" ;;
159159 139) echo "Segmentation fault (core dumped)" ;;
@@ -233,6 +233,60 @@ explain_exit_code() {
233233 esac
234234}
235235
236+ # ------------------------------------------------------------------------------
237+ # json_escape()
238+ #
239+ # - Escapes a string for safe JSON embedding
240+ # - Handles backslashes, quotes, newlines, tabs, and carriage returns
241+ # ------------------------------------------------------------------------------
242+ json_escape() {
243+ local s="$1"
244+ s=${s//\\/\\\\}
245+ s=${s//"/\\"/}
246+ s=${s//$'\n'/\\n}
247+ s=${s//$'\r'/}
248+ s=${s//$'\t'/\\t}
249+ echo "$s"
250+ }
251+
252+ # ------------------------------------------------------------------------------
253+ # get_error_text()
254+ #
255+ # - Returns last 20 lines of the active log (INSTALL_LOG or BUILD_LOG)
256+ # - Falls back to combined log or BUILD_LOG if primary is not accessible
257+ # - Handles container paths that don't exist on the host
258+ # ------------------------------------------------------------------------------
259+ get_error_text() {
260+ local logfile=""
261+ if declare -f get_active_logfile >/dev/null 2>&1; then
262+ logfile=$(get_active_logfile)
263+ elif [[ -n "${INSTALL_LOG:-}" ]]; then
264+ logfile="$INSTALL_LOG"
265+ elif [[ -n "${BUILD_LOG:-}" ]]; then
266+ logfile="$BUILD_LOG"
267+ fi
268+
269+ # If logfile is inside container (e.g. /root/.install-*), try the host copy
270+ if [[ -n "$logfile" && ! -s "$logfile" ]]; then
271+ # Try combined log: /tmp/<app>-<CTID>-<SESSION_ID>.log
272+ if [[ -n "${CTID:-}" && -n "${SESSION_ID:-}" ]]; then
273+ local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log"
274+ if [[ -s "$combined_log" ]]; then
275+ logfile="$combined_log"
276+ fi
277+ fi
278+ fi
279+
280+ # Also try BUILD_LOG as fallback if primary log is empty/missing
281+ if [[ -z "$logfile" || ! -s "$logfile" ]] && [[ -n "${BUILD_LOG:-}" && -s "${BUILD_LOG}" ]]; then
282+ logfile="$BUILD_LOG"
283+ fi
284+
285+ if [[ -n "$logfile" && -s "$logfile" ]]; then
286+ tail -n 20 "$logfile" 2>/dev/null | sed 's/\r$//'
287+ fi
288+ }
289+
236290# ==============================================================================
237291# SECTION 2: TELEMETRY FUNCTIONS
238292# ==============================================================================
@@ -353,6 +407,9 @@ detect_ram() {
353407# - Never blocks or fails script execution
354408# ------------------------------------------------------------------------------
355409post_to_api() {
410+ # Prevent duplicate submissions (post_to_api is called from multiple places)
411+ [[ "${POST_TO_API_DONE:-}" == "true" ]] && return 0
412+
356413 # Silent fail - telemetry should never break scripts
357414 command -v curl &>/dev/null || {
358415 [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] curl not found, skipping" >&2
@@ -382,15 +439,17 @@ post_to_api() {
382439 detect_gpu
383440 fi
384441 local gpu_vendor="${GPU_VENDOR:-unknown}"
385- local gpu_model="${GPU_MODEL:-}"
442+ local gpu_model
443+ gpu_model=$(json_escape "${GPU_MODEL:-}")
386444 local gpu_passthrough="${GPU_PASSTHROUGH:-unknown}"
387445
388446 # Detect CPU if not already set
389447 if [[ -z "${CPU_VENDOR:-}" ]]; then
390448 detect_cpu
391449 fi
392450 local cpu_vendor="${CPU_VENDOR:-unknown}"
393- local cpu_model="${CPU_MODEL:-}"
451+ local cpu_model
452+ cpu_model=$(json_escape "${CPU_MODEL:-}")
394453
395454 # Detect RAM if not already set
396455 if [[ -z "${RAM_SPEED:-}" ]]; then
440499 -H "Content-Type: application/json" \
441500 -d "$JSON_PAYLOAD" &>/dev/null || true
442501 fi
502+
503+ POST_TO_API_DONE=true
443504}
444505
445506# ------------------------------------------------------------------------------
451512# * ct_type=2 (VM instead of LXC)
452513# * type="vm"
453514# * Disk size without 'G' suffix
515+ # - Includes hardware detection: CPU, GPU, RAM speed
454516# - Only executes if DIAGNOSTICS=yes and RANDOM_UUID is set
455517# - Never blocks or fails script execution
456518# ------------------------------------------------------------------------------
@@ -473,6 +535,29 @@ post_to_api_vm() {
473535 pve_version=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true
474536 fi
475537
538+ # Detect GPU if not already set
539+ if [[ -z "${GPU_VENDOR:-}" ]]; then
540+ detect_gpu
541+ fi
542+ local gpu_vendor="${GPU_VENDOR:-unknown}"
543+ local gpu_model
544+ gpu_model=$(json_escape "${GPU_MODEL:-}")
545+ local gpu_passthrough="${GPU_PASSTHROUGH:-unknown}"
546+
547+ # Detect CPU if not already set
548+ if [[ -z "${CPU_VENDOR:-}" ]]; then
549+ detect_cpu
550+ fi
551+ local cpu_vendor="${CPU_VENDOR:-unknown}"
552+ local cpu_model
553+ cpu_model=$(json_escape "${CPU_MODEL:-}")
554+
555+ # Detect RAM if not already set
556+ if [[ -z "${RAM_SPEED:-}" ]]; then
557+ detect_ram
558+ fi
559+ local ram_speed="${RAM_SPEED:-}"
560+
476561 # Remove 'G' suffix from disk size
477562 local DISK_SIZE_API="${DISK_SIZE%G}"
478563
@@ -492,6 +577,12 @@ post_to_api_vm() {
492577 "os_version": "${var_version:-}",
493578 "pve_version": "${pve_version}",
494579 "method": "${METHOD:-default}",
580+ "cpu_vendor": "${cpu_vendor}",
581+ "cpu_model": "${cpu_model}",
582+ "gpu_vendor": "${gpu_vendor}",
583+ "gpu_model": "${gpu_model}",
584+ "gpu_passthrough": "${gpu_passthrough}",
585+ "ram_speed": "${ram_speed}",
495586 "repo_source": "${REPO_SOURCE}"
496587}
497588EOF
@@ -522,9 +613,12 @@ post_update_to_api() {
522613 # Silent fail - telemetry should never break scripts
523614 command -v curl &>/dev/null || return 0
524615
525- # Prevent duplicate submissions
616+ # Support "force" mode (3rd arg) to bypass duplicate check for retries after cleanup
617+ local force="${3:-}"
526618 POST_UPDATE_DONE=${POST_UPDATE_DONE:-false}
527- [[ "$POST_UPDATE_DONE" == "true" ]] && return 0
619+ if [[ "$POST_UPDATE_DONE" == "true" && "$force" != "force" ]]; then
620+ return 0
621+ fi
528622
529623 [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0
530624 [[ -z "${RANDOM_UUID:-}" ]] && return 0
@@ -535,12 +629,14 @@ post_update_to_api() {
535629
536630 # Get GPU info (if detected)
537631 local gpu_vendor="${GPU_VENDOR:-unknown}"
538- local gpu_model="${GPU_MODEL:-}"
632+ local gpu_model
633+ gpu_model=$(json_escape "${GPU_MODEL:-}")
539634 local gpu_passthrough="${GPU_PASSTHROUGH:-unknown}"
540635
541636 # Get CPU info (if detected)
542637 local cpu_vendor="${CPU_VENDOR:-unknown}"
543- local cpu_model="${CPU_MODEL:-}"
638+ local cpu_model
639+ cpu_model=$(json_escape "${CPU_MODEL:-}")
544640
545641 # Get RAM info (if detected)
546642 local ram_speed="${RAM_SPEED:-}"
@@ -562,13 +658,21 @@ post_update_to_api() {
562658 esac
563659
564660 # For failed/unknown status, resolve exit code and error description
661+ local short_error=""
565662 if [[ "$pb_status" == "failed" ]] || [[ "$pb_status" == "unknown" ]]; then
566663 if [[ "$raw_exit_code" =~ ^[0-9]+$ ]]; then
567664 exit_code="$raw_exit_code"
568665 else
569666 exit_code=1
570667 fi
571- error=$(explain_exit_code "$exit_code")
668+ local error_text=""
669+ error_text=$(get_error_text)
670+ if [[ -n "$error_text" ]]; then
671+ error=$(json_escape "$error_text")
672+ else
673+ error=$(json_escape "$(explain_exit_code "$exit_code")")
674+ fi
675+ short_error=$(json_escape "$(explain_exit_code "$exit_code")")
572676 error_category=$(categorize_error "$exit_code")
573677 [[ -z "$error" ]] && error="Unknown error"
574678 fi
@@ -585,8 +689,9 @@ post_update_to_api() {
585689 pve_version=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true
586690 fi
587691
588- # Full payload including all fields - allows record creation if initial call failed
589- # The Go service will find the record by random_id and PATCH, or create if not found
692+ local http_code=""
693+
694+ # ── Attempt 1: Full payload with complete error text ──
590695 local JSON_PAYLOAD
591696 JSON_PAYLOAD=$(
592697 cat <<EOF
@@ -618,11 +723,80 @@ post_update_to_api() {
618723EOF
619724 )
620725
621- # Fire-and-forget: never block, never fail
726+ http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
727+ -H "Content-Type: application/json" \
728+ -d "$JSON_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000"
729+
730+ if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then
731+ POST_UPDATE_DONE=true
732+ return 0
733+ fi
734+
735+ # ── Attempt 2: Short error text (no full log) ──
736+ sleep 1
737+ local RETRY_PAYLOAD
738+ RETRY_PAYLOAD=$(
739+ cat <<EOF
740+ {
741+ "random_id": "${RANDOM_UUID}",
742+ "type": "${TELEMETRY_TYPE:-lxc}",
743+ "nsapp": "${NSAPP:-unknown}",
744+ "status": "${pb_status}",
745+ "ct_type": ${CT_TYPE:-1},
746+ "disk_size": ${DISK_SIZE:-0},
747+ "core_count": ${CORE_COUNT:-0},
748+ "ram_size": ${RAM_SIZE:-0},
749+ "os_type": "${var_os:-}",
750+ "os_version": "${var_version:-}",
751+ "pve_version": "${pve_version}",
752+ "method": "${METHOD:-default}",
753+ "exit_code": ${exit_code},
754+ "error": "${short_error}",
755+ "error_category": "${error_category}",
756+ "install_duration": ${duration},
757+ "cpu_vendor": "${cpu_vendor}",
758+ "cpu_model": "${cpu_model}",
759+ "gpu_vendor": "${gpu_vendor}",
760+ "gpu_model": "${gpu_model}",
761+ "gpu_passthrough": "${gpu_passthrough}",
762+ "ram_speed": "${ram_speed}",
763+ "repo_source": "${REPO_SOURCE}"
764+ }
765+ EOF
766+ )
767+
768+ http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
769+ -H "Content-Type: application/json" \
770+ -d "$RETRY_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000"
771+
772+ if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then
773+ POST_UPDATE_DONE=true
774+ return 0
775+ fi
776+
777+ # ── Attempt 3: Minimal payload (bare minimum to set status) ──
778+ sleep 2
779+ local MINIMAL_PAYLOAD
780+ MINIMAL_PAYLOAD=$(
781+ cat <<EOF
782+ {
783+ "random_id": "${RANDOM_UUID}",
784+ "type": "${TELEMETRY_TYPE:-lxc}",
785+ "nsapp": "${NSAPP:-unknown}",
786+ "status": "${pb_status}",
787+ "exit_code": ${exit_code},
788+ "error": "${short_error}",
789+ "error_category": "${error_category}",
790+ "install_duration": ${duration}
791+ }
792+ EOF
793+ )
794+
622795 curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
623796 -H "Content-Type: application/json" \
624- -d "$JSON_PAYLOAD " -o /dev/null 2>&1 || true
797+ -d "$MINIMAL_PAYLOAD " -o /dev/null 2>/dev/null || true
625798
799+ # Tried 3 times - mark as done regardless to prevent infinite loops
626800 POST_UPDATE_DONE=true
627801}
628802
@@ -658,6 +832,9 @@ categorize_error() {
658832 # Configuration errors
659833 203 | 204 | 205 | 206 | 207 | 208) echo "config" ;;
660834
835+ # Aborted by user
836+ 130) echo "aborted" ;;
837+
661838 # Resource errors (OOM, etc)
662839 137 | 134) echo "resource" ;;
663840
@@ -722,7 +899,13 @@ post_tool_to_api() {
722899
723900 if [[ "$status" == "failed" ]]; then
724901 [[ ! "$exit_code" =~ ^[0-9]+$ ]] && exit_code=1
725- error=$(explain_exit_code "$exit_code")
902+ local error_text=""
903+ error_text=$(get_error_text)
904+ if [[ -n "$error_text" ]]; then
905+ error=$(json_escape "$error_text")
906+ else
907+ error=$(json_escape "$(explain_exit_code "$exit_code")")
908+ fi
726909 error_category=$(categorize_error "$exit_code")
727910 fi
728911
@@ -783,7 +966,13 @@ post_addon_to_api() {
783966
784967 if [[ "$status" == "failed" ]]; then
785968 [[ ! "$exit_code" =~ ^[0-9]+$ ]] && exit_code=1
786- error=$(explain_exit_code "$exit_code")
969+ local error_text=""
970+ error_text=$(get_error_text)
971+ if [[ -n "$error_text" ]]; then
972+ error=$(json_escape "$error_text")
973+ else
974+ error=$(json_escape "$(explain_exit_code "$exit_code")")
975+ fi
787976 error_category=$(categorize_error "$exit_code")
788977 fi
789978
@@ -876,7 +1065,13 @@ post_update_to_api_extended() {
8761065 else
8771066 exit_code=1
8781067 fi
879- error=$(explain_exit_code "$exit_code")
1068+ local error_text=""
1069+ error_text=$(get_error_text)
1070+ if [[ -n "$error_text" ]]; then
1071+ error=$(json_escape "$error_text")
1072+ else
1073+ error=$(json_escape "$(explain_exit_code "$exit_code")")
1074+ fi
8801075 error_category=$(categorize_error "$exit_code")
8811076 [[ -z "$error" ]] && error="Unknown error"
8821077 fi
0 commit comments