2025-12-04 07:52:18 +01:00
#!/usr/bin/env bash
# ------------------------------------------------------------------------------
# ERROR HANDLER - ERROR & SIGNAL MANAGEMENT
# ------------------------------------------------------------------------------
2026-01-06 13:28:12 +01:00
# Copyright (c) 2021-2026 community-scripts ORG
2025-12-04 07:52:18 +01:00
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# ------------------------------------------------------------------------------
#
2026-07-20 20:32:36 +02:00
# Provides error handling and signal management for all scripts.
#
# TELEMETRY CONTRACT:
# - HOST context: this file reports terminal statuses via post_update_to_api
# (full metadata + focused error trace).
# - CONTAINER context: this file NEVER talks to the telemetry API. It writes
# two local artifacts that the host picks up after lxc-attach returns:
# /root/.install-<SESSION_ID>.failed → exit code (flag file)
# /root/.install-<SESSION_ID>.log.errinfo → structured error capture
# This guarantees the server only ever sees ONE terminal event per
# execution - the host's complete one.
2025-12-04 07:52:18 +01:00
#
# Usage:
# source <(curl -fsSL .../error_handler.func)
# catch_errors
#
# ------------------------------------------------------------------------------
# ==============================================================================
2026-07-20 20:32:36 +02:00
# SECTION 1: EXIT CODE EXPLANATIONS (fallback)
2025-12-04 07:52:18 +01:00
# ==============================================================================
# ------------------------------------------------------------------------------
# explain_exit_code()
#
2026-07-20 20:32:36 +02:00
# - Canonical version lives in api.func (sourced before this file on the host)
# - This fallback covers the container context where api.func is not sourced
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
2026-02-12 11:55:13 +01:00
if ! declare -f explain_exit_code & >/dev/null; then
explain_exit_code() {
local code = " $1 "
case " $code " in
1) echo "General error / Operation not permitted" ;;
2) echo "Misuse of shell builtins (e.g. syntax error)" ;;
2026-02-17 12:14:46 +01:00
3) echo "General syntax or argument error" ;;
2026-02-16 17:14:00 +01:00
10) echo "Docker / privileged mode required (unsupported environment)" ;;
4) echo "curl: Feature not supported or protocol error" ;;
5) echo "curl: Could not resolve proxy" ;;
2026-02-12 11:55:13 +01:00
6) echo "curl: DNS resolution failed (could not resolve host)" ;;
7) echo "curl: Failed to connect (network unreachable / host down)" ;;
2026-02-17 12:14:46 +01:00
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)" ;;
2026-02-12 11:55:13 +01:00
22) echo "curl: HTTP error returned (404, 429, 500+)" ;;
2026-02-16 17:14:00 +01:00
23) echo "curl: Write error (disk full or permissions)" ;;
2026-02-17 12:14:46 +01:00
24) echo "curl: Write to local file failed" ;;
2026-02-16 17:14:00 +01:00
25) echo "curl: Upload failed" ;;
2026-02-17 12:14:46 +01:00
26) echo "curl: Read error on local file (I/O)" ;;
27) echo "curl: Out of memory (memory allocation failed)" ;;
2026-02-12 11:55:13 +01:00
28) echo "curl: Operation timeout (network slow or server not responding)" ;;
2026-02-16 17:14:00 +01:00
30) echo "curl: FTP port command failed" ;;
2026-02-17 12:14:46 +01:00
32) echo "curl: FTP SIZE command failed" ;;
33) echo "curl: HTTP range error" ;;
34) echo "curl: HTTP post error" ;;
2026-02-12 11:55:13 +01:00
35) echo "curl: SSL/TLS handshake failed (certificate error)" ;;
2026-02-17 12:14:46 +01:00
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" ;;
2026-02-16 17:14:00 +01:00
56) echo "curl: Receive error (connection reset by peer)" ;;
2026-02-17 12:14:46 +01:00
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" ;;
2026-02-16 17:14:00 +01:00
75) echo "Temporary failure (retry later)" ;;
78) echo "curl: Remote file not found (404 on FTP/file)" ;;
2026-02-17 12:14:46 +01:00
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" ;;
2026-02-16 17:14:00 +01:00
64) echo "Usage error (wrong arguments)" ;;
65) echo "Data format error (bad input data)" ;;
66) echo "Input file not found (cannot open input)" ;;
67) echo "User not found (addressee unknown)" ;;
68) echo "Host not found (hostname unknown)" ;;
69) echo "Service unavailable" ;;
70) echo "Internal software error" ;;
71) echo "System error (OS-level failure)" ;;
72) echo "Critical OS file missing" ;;
73) echo "Cannot create output file" ;;
74) echo "I/O error" ;;
76) echo "Remote protocol error" ;;
77) echo "Permission denied" ;;
2026-02-12 11:55:13 +01:00
100) echo "APT: Package manager error (broken packages / dependency problems)" ;;
101) echo "APT: Configuration error (bad sources.list, malformed config)" ;;
102) echo "APT: Lock held by another process (dpkg/apt still running)" ;;
2026-03-02 10:55:20 +01:00
103) echo "Validation: Shell is not Bash" ;;
104) echo "Validation: Not running as root (or invoked via sudo)" ;;
105) echo "Validation: Proxmox VE version not supported" ;;
2026-06-11 16:14:32 +10:00
106) echo "Validation: Unsupported architecture (requires amd64 or arm64)" ;;
2026-03-02 10:55:20 +01:00
107) echo "Validation: Kernel key parameters unreadable" ;;
108) echo "Validation: Kernel key limits exceeded" ;;
109) echo "Proxmox: No available container ID after max attempts" ;;
110) echo "Proxmox: Failed to apply default.vars" ;;
111) echo "Proxmox: App defaults file not available" ;;
112) echo "Proxmox: Invalid install menu option" ;;
113) echo "LXC: Under-provisioned — user aborted update" ;;
114) echo "LXC: Storage too low — user aborted update" ;;
115) echo "Download: install.func download failed or incomplete" ;;
116) echo "Proxmox: Default bridge vmbr0 not found" ;;
117) echo "LXC: Container did not reach running state" ;;
118) echo "LXC: No IP assigned to container after timeout" ;;
119) echo "Proxmox: No valid storage for rootdir content" ;;
120) echo "Proxmox: No valid storage for vztmpl content" ;;
121) echo "LXC: Container network not ready (no IP after retries)" ;;
122) echo "LXC: No internet connectivity — user declined to continue" ;;
123) echo "LXC: Local IP detection failed" ;;
2026-02-12 11:55:13 +01:00
124) echo "Command timed out (timeout command)" ;;
2026-02-17 12:14:46 +01:00
125) echo "Command failed to start (Docker daemon or execution error)" ;;
2026-02-12 11:55:13 +01:00
126) echo "Command invoked cannot execute (permission problem?)" ;;
127) echo "Command not found" ;;
128) echo "Invalid argument to exit" ;;
2026-02-17 12:14:46 +01:00
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)" ;;
2026-02-12 11:55:13 +01:00
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)" ;;
2026-02-17 12:14:46 +01:00
144) echo "Killed by signal 16 (SIGUSR1 / SIGSTKFLT)" ;;
146) echo "Killed by signal 18 (SIGTSTP)" ;;
2026-02-12 11:55:13 +01:00
150) echo "Systemd: Service failed to start" ;;
151) echo "Systemd: Service unit not found" ;;
152) echo "Permission denied (EACCES)" ;;
153) echo "Build/compile failed (make/gcc/cmake)" ;;
154) echo "Node.js: Native addon build failed (node-gyp)" ;;
160) echo "Python: Virtualenv / uv environment missing or broken" ;;
161) echo "Python: Dependency resolution failed" ;;
162) echo "Python: Installation aborted (permissions or EXTERNALLY-MANAGED)" ;;
170) echo "PostgreSQL: Connection failed (server not running / wrong socket)" ;;
171) echo "PostgreSQL: Authentication failed (bad user/password)" ;;
172) echo "PostgreSQL: Database does not exist" ;;
173) echo "PostgreSQL: Fatal error in query / syntax" ;;
180) echo "MySQL/MariaDB: Connection failed (server not running / wrong socket)" ;;
181) echo "MySQL/MariaDB: Authentication failed (bad user/password)" ;;
182) echo "MySQL/MariaDB: Database does not exist" ;;
183) echo "MySQL/MariaDB: Fatal error in query / syntax" ;;
190) echo "MongoDB: Connection failed (server not running)" ;;
191) echo "MongoDB: Authentication failed (bad user/password)" ;;
192) echo "MongoDB: Database not found" ;;
193) echo "MongoDB: Fatal query error" ;;
200) echo "Proxmox: Failed to create lock file" ;;
203) echo "Proxmox: Missing CTID variable" ;;
204) echo "Proxmox: Missing PCT_OSTYPE variable" ;;
205) echo "Proxmox: Invalid CTID (<100)" ;;
206) echo "Proxmox: CTID already in use" ;;
207) echo "Proxmox: Password contains unescaped special characters" ;;
208) echo "Proxmox: Invalid configuration (DNS/MAC/Network format)" ;;
209) echo "Proxmox: Container creation failed" ;;
210) echo "Proxmox: Cluster not quorate" ;;
211) echo "Proxmox: Timeout waiting for template lock" ;;
212) echo "Proxmox: Storage type 'iscsidirect' does not support containers (VMs only)" ;;
213) echo "Proxmox: Storage type does not support 'rootdir' content" ;;
214) echo "Proxmox: Not enough storage space" ;;
215) echo "Proxmox: Container created but not listed (ghost state)" ;;
216) echo "Proxmox: RootFS entry missing in config" ;;
217) echo "Proxmox: Storage not accessible" ;;
218) echo "Proxmox: Template file corrupted or incomplete" ;;
219) echo "Proxmox: CephFS does not support containers - use RBD" ;;
220) echo "Proxmox: Unable to resolve template path" ;;
221) echo "Proxmox: Template file not readable" ;;
222) echo "Proxmox: Template download failed" ;;
223) echo "Proxmox: Template not available after download" ;;
224) echo "Proxmox: PBS storage is for backups only" ;;
225) echo "Proxmox: No template available for OS/Version" ;;
2026-07-20 20:32:36 +02:00
226) echo "Proxmox: VM disk import or post-creation setup failed" ;;
2026-02-12 11:55:13 +01:00
231) echo "Proxmox: LXC stack upgrade failed" ;;
2026-03-02 10:55:20 +01:00
232) echo "Tools: Wrong execution environment (run on PVE host, not inside LXC)" ;;
233) echo "Tools: Application not installed (update prerequisite missing)" ;;
234) echo "Tools: No LXC containers found or available" ;;
235) echo "Tools: Backup or restore operation failed" ;;
236) echo "Tools: Required hardware not detected" ;;
237) echo "Tools: Dependency package installation failed" ;;
238) echo "Tools: OS or distribution not supported for this addon" ;;
2026-02-17 12:14:46 +01:00
239) echo "npm/Node.js: Unexpected runtime error or dependency failure" ;;
2026-02-12 11:55:13 +01:00
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" ;;
247) echo "Node.js: Fatal internal error" ;;
248) echo "Node.js: Invalid C++ addon / N-API failure" ;;
249) echo "npm/pnpm/yarn: Unknown fatal error" ;;
2026-03-02 13:57:42 +01:00
250) echo "App: Download failed or version not determined" ;;
251) echo "App: File extraction failed (corrupt or incomplete archive)" ;;
252) echo "App: Required file or resource not found" ;;
253) echo "App: Data migration required — update aborted" ;;
254) echo "App: User declined prompt or input timed out" ;;
2026-02-12 11:55:13 +01:00
255) echo "DPKG: Fatal internal error" ;;
*) echo "Unknown error" ;;
esac
}
fi
2025-12-04 07:52:18 +01:00
# ==============================================================================
2026-07-20 20:32:36 +02:00
# SECTION 2: CONTEXT DETECTION & CONTAINER ARTIFACTS
# ==============================================================================
# ------------------------------------------------------------------------------
# _is_container_context()
#
# - Returns 0 (true) when running INSIDE the LXC container being installed
# - TELEMETRY_CONTEXT can override the heuristic ("host" / "container");
# install.func sets TELEMETRY_CONTEXT=container during bootstrap
# ------------------------------------------------------------------------------
_is_container_context() {
case " ${ TELEMETRY_CONTEXT :- } " in
container) return 0 ;;
host) return 1 ;;
esac
# Proxmox/Incus tooling exists only on the host
command -v pveversion & >/dev/null && return 1
command -v pct & >/dev/null && return 1
command -v incus & >/dev/null && return 1
# systemd-detect-virt reports lxc inside containers
if command -v systemd-detect-virt & >/dev/null; then
case " $( systemd-detect-virt -c 2>/dev/null) " in
lxc | lxc-libvirt | openvz) return 0 ;;
esac
fi
# PCT_OSTYPE is exported into the install environment by the host
[[ -n " ${ PCT_OSTYPE :- } " ]] && return 0
return 1
}
# ------------------------------------------------------------------------------
# _container_write_failure()
#
# - Writes the failure artifacts inside the container for the host to pick up:
# * flag file with the exit code
# * .errinfo capture (if silent() has not already written a better one)
# * copy of the install log
# - This REPLACES any direct telemetry send from the container
# - Arguments: $1 = exit_code, $2 = command (optional), $3 = line (optional)
# ------------------------------------------------------------------------------
_container_write_failure() {
local exit_code = " ${ 1 :- 1 } "
local command = " ${ 2 :- } "
local line = " ${ 3 :- } "
local sid = " ${ SESSION_ID :- error } "
# Flag file with exit code (host reads this after lxc-attach returns)
echo " $exit_code " >"/root/.install- ${ sid } .failed" 2>/dev/null || true
# Keep the install log where the host expects it
if [[ -n " ${ INSTALL_LOG :- } " && -f " ${ INSTALL_LOG } " && " ${ INSTALL_LOG } " != "/root/.install- ${ sid } .log" ]] ; then
cp " ${ INSTALL_LOG } " "/root/.install- ${ sid } .log" 2>/dev/null || true
fi
# Structured error capture (skip if silent() already wrote the exact
# output segment of the failing command - that one is always better).
# Self-contained: api.func is NOT sourced inside containers.
local errinfo = " ${ INSTALL_LOG :- /root/.install- ${ sid } .log } .errinfo"
if [[ ! -s " $errinfo " ]] ; then
local flat_cmd
flat_cmd = $( printf '%s' " ${ command :- unknown } " | tr '\n' ' ' | head -c 300)
{
echo "EXIT_CODE= ${ exit_code } "
echo "LINE= ${ line :- 0 } "
echo "COMMAND= ${ flat_cmd } "
echo "--- OUTPUT ---"
if [[ -n " ${ INSTALL_LOG :- } " && -s " ${ INSTALL_LOG } " ]] ; then
tail -n 80 " ${ INSTALL_LOG } " 2>/dev/null |
sed 's/\r$//' |
sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' |
grep -avE '^(Get:|Hit:|Ign:|Fetched |Reading package lists|Reading state information|Building dependency tree|Selecting previously|Preparing to unpack|Unpacking |Processing triggers for|\(Reading database|[0-9]+%[[:space:]]*\[)' |
grep -avE '^[[:space:]]*$' |
tail -n 60 | head -c 10240
fi
} >" $errinfo " 2>/dev/null || true
fi
}
# ==============================================================================
# SECTION 3: ERROR HANDLER
2025-12-04 07:52:18 +01:00
# ==============================================================================
# ------------------------------------------------------------------------------
# error_handler()
#
# - Main error handler triggered by ERR trap
2026-07-20 20:32:36 +02:00
# - Displays error message with line number, exit code, explanation, command
# - Shows last 20 lines of the active log
# - Emits actionable hints for common failure patterns (OOM, APT, network...)
# - HOST: reports "failed" to telemetry (full payload via api.func)
# - CONTAINER: writes failure artifacts, sends nothing
# - Exits with original exit code
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
error_handler() {
local exit_code = ${ 1 :- $? }
local command = ${ 2 :- ${ BASH_COMMAND :- unknown }}
local line_number = ${ BASH_LINENO [0] :- unknown }
command = " ${ command // \$ STD / } "
2026-04-05 21:02:56 +02:00
# If error originated from silent(), use its captured metadata
if [[ -n " ${ _SILENT_FAILED_RC :- } " ]] ; then
exit_code = " $_SILENT_FAILED_RC "
command = " $_SILENT_FAILED_CMD "
line_number = " $_SILENT_FAILED_LINE "
unset _SILENT_FAILED_RC _SILENT_FAILED_CMD _SILENT_FAILED_LINE
fi
2025-12-04 07:52:18 +01:00
if [[ " $exit_code " -eq 0 ]] ; then
return 0
fi
2026-07-20 20:32:36 +02:00
# Export the failure location so telemetry can include the "where"
FAILED_COMMAND = " $command "
FAILED_LINE = " $line_number "
export FAILED_COMMAND FAILED_LINE
# Stop spinner and restore cursor FIRST - before any output
2026-02-25 14:08:24 +01:00
if declare -f stop_spinner >/dev/null 2>& 1; then
stop_spinner 2>/dev/null || true
fi
printf "\e[?25h"
2025-12-04 07:52:18 +01:00
local explanation
explanation = " $( explain_exit_code " $exit_code " ) "
2026-07-20 20:32:36 +02:00
if [[ " $explanation " == curl:* && ! " $command " = ~ ( ^| [[ :space:]])([ ^[ :space:]] */) ?curl([[ :space:]] | $) ]] ; then
explanation = "Command failed with exit status ${ exit_code } "
fi
2025-12-04 07:52:18 +01:00
2026-07-20 20:32:36 +02:00
# ── Telemetry / failure artifacts ──
if _is_container_context; then
_container_write_failure " $exit_code " " $command " " $line_number "
elif declare -f post_update_to_api & >/dev/null; then
2026-02-18 16:19:19 +01:00
post_update_to_api "failed" " $exit_code " 2>/dev/null || true
fi
2026-07-20 20:32:36 +02:00
# ── Display ──
2025-12-04 07:52:18 +01:00
if declare -f msg_error >/dev/null 2>& 1; then
msg_error "in line ${ line_number } : exit code ${ exit_code } ( ${ explanation } ): while executing command ${ command } "
else
echo -e "\n ${ RD } [ERROR] ${ CL } in line ${ RD }${ line_number }${ CL } : exit code ${ RD }${ exit_code }${ CL } ( ${ explanation } ): while executing command ${ YWB }${ command }${ CL } \n"
fi
if [[ -n " ${ DEBUG_LOGFILE :- } " ]] ; then
{
echo "------ ERROR ------"
echo "Timestamp : $( date '+%Y-%m-%d %H:%M:%S' ) "
echo "Exit Code : $exit_code ( $explanation )"
echo "Line : $line_number "
echo "Command : $command "
echo "-------------------"
} >>" $DEBUG_LOGFILE "
fi
2026-07-20 20:32:36 +02:00
# Get active log file (prefer silent()'s logfile when available)
2025-12-04 07:52:18 +01:00
local active_log = ""
2026-04-05 21:02:56 +02:00
if [[ -n " ${ _SILENT_FAILED_LOG :- } " && -s " ${ _SILENT_FAILED_LOG } " ]] ; then
active_log = " $_SILENT_FAILED_LOG "
unset _SILENT_FAILED_LOG
elif declare -f get_active_logfile >/dev/null 2>& 1; then
2025-12-04 07:52:18 +01:00
active_log = " $( get_active_logfile) "
elif [[ -n " ${ SILENT_LOGFILE :- } " ]] ; then
active_log = " $SILENT_LOGFILE "
fi
2026-02-23 13:22:09 +01:00
if [[ -n " $active_log " && ! -s " $active_log " && -n " ${ BUILD_LOG :- } " && -s " ${ BUILD_LOG } " ]] ; then
active_log = " $BUILD_LOG "
fi
2025-12-04 07:52:18 +01:00
if [[ -n " $active_log " && -s " $active_log " ]] ; then
2026-02-14 15:28:30 +01:00
echo -e "\n ${ TAB } --- Last 20 lines of log ---"
2025-12-04 07:52:18 +01:00
tail -n 20 " $active_log "
2026-02-14 15:28:30 +01:00
echo -e " ${ TAB } -----------------------------------\n"
2026-02-23 13:22:09 +01:00
fi
2025-12-04 07:52:18 +01:00
2026-07-20 20:32:36 +02:00
# ── Node.js heap OOM detection with actionable guidance ──
2026-04-23 22:17:25 +02:00
local node_oom_detected = "false"
local node_build_context = "false"
if [[ " $command " = ~ ( npm| pnpm| yarn| node| vite| turbo) ]] ; then
node_build_context = "true"
fi
if [[ " $exit_code " == "243" ]] ; then
node_oom_detected = "true"
elif [[ -n " $active_log " && -s " $active_log " ]] ; then
if tail -n 200 " $active_log " 2>/dev/null | grep -Eqi 'Reached heap limit|JavaScript heap out of memory|Allocation failed - JavaScript heap out of memory|FATAL ERROR: Reached heap limit' ; then
node_oom_detected = "true"
fi
fi
if [[ " $node_oom_detected " == "true" ]] || { [[ " $node_build_context " == "true" ]] && [[ " $exit_code " = ~ ^( 134| 137) $ ]] ; } ; then
local heap_hint_mb = ""
if [[ -n " ${ NODE_OPTIONS :- } " ]] && [[ " ${ NODE_OPTIONS } " = ~ max-old-space-size=([ 0-9] +) ]] ; then
heap_hint_mb = " ${ BASH_REMATCH [1] } "
elif [[ -n " ${ var_ram :- } " ]] && [[ " ${ var_ram } " = ~ ^[ 0-9] +$ ]] ; then
heap_hint_mb = $(( var_ram * 75 / 100 ))
else
local mem_kb = ""
mem_kb = $( awk '/^MemTotal:/ {print $2; exit}' /proc/meminfo 2>/dev/null || echo "" )
if [[ " $mem_kb " = ~ ^[ 0-9] +$ ]] ; then
local mem_mb = $(( mem_kb / 1024 ))
heap_hint_mb = $(( mem_mb * 75 / 100 ))
fi
fi
if [[ -z " $heap_hint_mb " ]] || (( heap_hint_mb < 1024)) ; then
heap_hint_mb = 1024
elif (( heap_hint_mb > 12288)) ; then
heap_hint_mb = 12288
fi
if declare -f msg_warn >/dev/null 2>& 1; then
msg_warn "Possible Node.js heap OOM. Try: export NODE_OPTIONS=\"--max-old-space-size= ${ heap_hint_mb } \" and rerun the build."
else
echo -e " ${ YW } Possible Node.js heap OOM. Try: export NODE_OPTIONS=\"--max-old-space-size= ${ heap_hint_mb } \" and rerun the build. ${ CL } "
fi
fi
2026-07-20 20:32:36 +02:00
# ── Log-pattern analysis: actionable hints for common failure causes ──
2026-05-26 08:06:08 +02:00
if [[ -n " $active_log " && -s " $active_log " ]] ; then
local _log_tail
_log_tail = $( tail -n 60 " $active_log " 2>/dev/null || true )
# 1. APT/dpkg dependency conflict
if echo " $_log_tail " | grep -qE "Depends:|depends on.*but.*not installed|broken packages|unmet dep|dependency problems" ; then
local _pg_conflict
_pg_conflict = $( echo " $_log_tail " | grep -oE 'postgresql-[0-9]+ but.*installed' | head -1 || true )
if [[ -n " $_pg_conflict " ]] ; then
if declare -f msg_warn >/dev/null 2>& 1; then
msg_warn "PostgreSQL version conflict: ${ _pg_conflict } "
msg_warn "Hint: A package requires a specific PostgreSQL version that is not installed. Your distribution may have installed a different PG version than expected."
fi
else
if declare -f msg_warn >/dev/null 2>& 1; then
msg_warn "APT dependency conflict detected. A required package is not available or is the wrong version for this system."
msg_warn "Hint: Run 'apt-get install -f' inside the container or check that all required repositories are configured for your distribution."
fi
fi
# 2. APT/GPG signature verification failure
elif echo " $_log_tail " | grep -qE "sqv|KEYEXPIRED|NO_PUBKEY|key is not certified|signature verification failed|is not signed|Sub-process.*sqv" ; then
if declare -f msg_warn >/dev/null 2>& 1; then
msg_warn "APT repository signature error detected."
msg_warn "Hint: A repository GPG key may be missing, expired, or the keyring file is not yet present (/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc etc.)."
msg_warn "Hint: Install the 'postgresql-common' package first, or re-add the repository with its correct signing key."
fi
2026-07-20 20:32:36 +02:00
# 3. Network / DNS failure
2026-05-26 08:06:08 +02:00
elif echo " $_log_tail " | grep -qE "Could not resolve|Failed to fetch|Unable to connect|Name or service not known|Network is unreachable|curl.*resolve" ; then
if declare -f msg_warn >/dev/null 2>& 1; then
msg_warn "Network or DNS failure detected."
msg_warn "Hint: Check the container's network connectivity, DNS settings, and whether any firewall or ad-blocker is intercepting traffic."
fi
# 4. APT lock held by another process
elif echo " $_log_tail " | grep -qE "Could not get lock|dpkg frontend lock|waiting for it to exit|E: Unable to lock" ; then
if declare -f msg_warn >/dev/null 2>& 1; then
msg_warn "APT or dpkg lock conflict detected."
msg_warn "Hint: Another package manager process may be running. Try 'rm /var/lib/dpkg/lock-frontend && dpkg --configure -a' inside the container."
fi
# 5. Disk space exhaustion
elif echo " $_log_tail " | grep -qE "No space left on device|disk quota exceeded|ENOSPC" ; then
if declare -f msg_warn >/dev/null 2>& 1; then
msg_warn "Disk space exhausted during installation."
msg_warn "Hint: Increase the container's disk size (pct resize <ctid> rootfs +2G) or clean up space first."
fi
fi
fi
2026-07-20 20:32:36 +02:00
# ── Context-specific cleanup ──
if _is_container_context; then
# Container: artifacts already written above; nothing more to do
:
2026-02-23 13:22:09 +01:00
else
2026-07-20 20:32:36 +02:00
# HOST: show log path and offer container cleanup
2026-02-23 13:22:09 +01:00
if [[ -n " $active_log " && -s " $active_log " ]] ; then
2025-12-04 07:52:18 +01:00
if declare -f msg_custom >/dev/null 2>& 1; then
msg_custom "📋" " ${ YW } " "Full log: ${ active_log } "
else
echo -e " ${ YW } Full log: ${ CL } ${ BL }${ active_log }${ CL } "
fi
2026-02-23 13:22:09 +01:00
fi
2025-12-04 07:52:18 +01:00
2026-02-23 13:22:09 +01:00
# Offer to remove container if it exists (build errors after container creation)
if [[ -n " ${ CTID :- } " ]] && command -v pct & >/dev/null && pct status " $CTID " & >/dev/null; then
echo ""
if declare -f msg_custom >/dev/null 2>& 1; then
echo -en " ${ TAB } ❓ ${ TAB }${ YW } Remove broken container ${ CTID } ? (Y/n) [auto-remove in 60s]: ${ CL } "
else
echo -en " ${ YW } Remove broken container ${ CTID } ? (Y/n) [auto-remove in 60s]: ${ CL } "
fi
2025-12-04 07:52:18 +01:00
2026-03-02 10:25:19 +01:00
local response = ""
2026-07-21 23:32:19 +02:00
local read_rc
read -t 60 -r response
read_rc = $?
if [[ $read_rc -eq 0 ]] ; then
2026-02-23 13:22:09 +01:00
if [[ -z " $response " || " $response " = ~ ^[ Yy] $ ]] ; then
2026-02-14 15:28:30 +01:00
echo ""
if declare -f msg_info >/dev/null 2>& 1; then
2026-02-23 13:22:09 +01:00
msg_info "Removing container ${ CTID } "
2026-02-14 15:28:30 +01:00
else
2026-02-23 13:22:09 +01:00
echo -e " ${ YW } Removing container ${ CTID }${ CL } "
2026-02-14 15:28:30 +01:00
fi
2025-12-04 07:52:18 +01:00
pct stop " $CTID " & >/dev/null || true
pct destroy " $CTID " & >/dev/null || true
2026-02-14 15:28:30 +01:00
if declare -f msg_ok >/dev/null 2>& 1; then
msg_ok "Container ${ CTID } removed"
else
echo -e " ${ GN } ✔ ${ CL } Container ${ CTID } removed"
fi
2026-02-23 13:22:09 +01:00
elif [[ " $response " = ~ ^[ Nn] $ ]] ; then
echo ""
if declare -f msg_warn >/dev/null 2>& 1; then
msg_warn "Container ${ CTID } kept for debugging"
else
echo -e " ${ YW } Container ${ CTID } kept for debugging ${ CL } "
fi
2025-12-04 07:52:18 +01:00
fi
2026-07-21 23:32:19 +02:00
elif [[ $read_rc -gt 128 ]] ; then
2026-02-23 13:22:09 +01:00
# Timeout - auto-remove
echo ""
if declare -f msg_info >/dev/null 2>& 1; then
msg_info "No response - removing container ${ CTID } "
else
echo -e " ${ YW } No response - removing container ${ CTID }${ CL } "
fi
pct stop " $CTID " & >/dev/null || true
pct destroy " $CTID " & >/dev/null || true
if declare -f msg_ok >/dev/null 2>& 1; then
msg_ok "Container ${ CTID } removed"
else
echo -e " ${ GN } ✔ ${ CL } Container ${ CTID } removed"
2026-02-13 13:49:49 +01:00
fi
2026-07-21 23:32:19 +02:00
else
# read itself failed (e.g. broken/closed stdin, I/O error) rather than
# timing out - don't guess and destroy the container on a read we
# couldn't actually capture; keep it since that's the reversible choice.
echo ""
if declare -f msg_error >/dev/null 2>& 1; then
msg_error "Could not read your response (stdin error) - keeping container ${ CTID } for safety."
msg_error "Remove it manually if not needed: pct destroy ${ CTID } "
else
echo -e " ${ YW } Could not read your response (stdin error) - keeping container ${ CTID } for safety. ${ CL } "
echo -e " ${ YW } Remove it manually if not needed: pct destroy ${ CTID }${ CL } "
fi
2025-12-04 07:52:18 +01:00
fi
fi
fi
exit " $exit_code "
}
# ==============================================================================
2026-07-20 20:32:36 +02:00
# SECTION 4: TELEMETRY & CLEANUP HELPERS FOR SIGNAL HANDLERS
2026-02-23 14:30:48 +01:00
# ==============================================================================
# ------------------------------------------------------------------------------
2026-07-20 20:32:36 +02:00
# _send_abort_telemetry() (compatibility name)
2026-02-23 14:30:48 +01:00
#
2026-07-20 20:32:36 +02:00
# - HOST: reports via post_update_to_api (signals map to "aborted" there)
# - CONTAINER: writes failure artifacts instead of sending anything
2026-02-23 14:30:48 +01:00
# - Arguments: $1 = exit_code
# ------------------------------------------------------------------------------
_send_abort_telemetry() {
local exit_code = " ${ 1 :- 1 } "
2026-07-20 20:32:36 +02:00
if _is_container_context; then
_container_write_failure " $exit_code "
return 0
fi
2026-02-23 14:30:48 +01:00
if declare -f post_update_to_api & >/dev/null; then
post_update_to_api "failed" " $exit_code " 2>/dev/null || true
2026-03-02 14:38:42 +01:00
fi
2026-02-25 14:08:24 +01:00
return 0
2026-02-23 14:30:48 +01:00
}
# ------------------------------------------------------------------------------
# _stop_container_if_installing()
#
2026-07-20 20:32:36 +02:00
# - Stops the LXC container if we're in the install phase (host only)
2026-02-23 14:30:48 +01:00
# - Prevents orphaned container processes when the host exits due to a signal
# ------------------------------------------------------------------------------
_stop_container_if_installing() {
[[ " ${ CONTAINER_INSTALLING :- } " == "true" ]] || return 0
[[ -n " ${ CTID :- } " ]] || return 0
command -v pct & >/dev/null || return 0
pct stop " $CTID " 2>/dev/null || true
}
# ==============================================================================
2026-07-20 20:32:36 +02:00
# SECTION 5: SIGNAL HANDLERS
2025-12-04 07:52:18 +01:00
# ==============================================================================
# ------------------------------------------------------------------------------
# on_exit()
#
2026-02-23 14:30:48 +01:00
# - EXIT trap handler — runs on EVERY script termination
2026-07-20 20:32:36 +02:00
# - CONTAINER: ensures failure artifacts exist on non-zero exit (this also
# covers silent()'s direct `exit $rc`, which bypasses the ERR trap)
# - HOST: catches executions that never sent a final status:
# * non-zero exit → "failed" (signal codes map to "aborted")
# * zero exit with an "installing" record but no final → "aborted"
# (e.g. user cancelled a whiptail dialog, script exited cleanly)
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
on_exit() {
local exit_code = $?
2026-02-23 14:30:48 +01:00
2026-07-20 20:32:36 +02:00
if _is_container_context; then
if [[ $exit_code -ne 0 ]] ; then
_container_write_failure " $exit_code " " ${ FAILED_COMMAND :- } " " ${ FAILED_LINE :- } "
fi
else
if [[ " ${ POST_UPDATE_DONE :- } " != "true" ]] && declare -f post_update_to_api >/dev/null 2>& 1; then
2026-03-24 09:57:43 +01:00
if [[ $exit_code -ne 0 ]] ; then
2026-07-20 20:32:36 +02:00
post_update_to_api "failed" " $exit_code " 2>/dev/null || true
elif [[ " ${ POST_TO_API_DONE :- } " == "true" ]] ; then
# Clean exit but no success was ever reported: the user backed out
# somewhere. Report as aborted so the record doesn't stay "installing".
post_update_to_api "aborted" "0" 2>/dev/null || true
2026-03-24 09:57:43 +01:00
fi
2026-02-12 20:05:27 +01:00
fi
2026-02-23 14:30:48 +01:00
2026-07-20 20:32:36 +02:00
# Best-effort log collection on failure
if [[ $exit_code -ne 0 ]] && declare -f ensure_log_on_host >/dev/null 2>& 1; then
ensure_log_on_host 2>/dev/null || true
fi
2026-02-23 14:30:48 +01:00
2026-07-20 20:32:36 +02:00
# Stop orphaned container if we're in the install phase
if [[ $exit_code -ne 0 ]] ; then
_stop_container_if_installing
fi
2026-02-23 14:30:48 +01:00
fi
2025-12-04 07:52:18 +01:00
[[ -n " ${ lockfile :- } " && -e " $lockfile " ]] && rm -f " $lockfile "
exit " $exit_code "
}
# ------------------------------------------------------------------------------
2026-07-20 20:32:36 +02:00
# on_interrupt() - SIGINT (Ctrl+C)
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
on_interrupt() {
2026-02-25 14:08:24 +01:00
if declare -f stop_spinner >/dev/null 2>& 1; then
stop_spinner 2>/dev/null || true
fi
printf "\e[?25h" 2>/dev/null || true
2026-07-20 20:32:36 +02:00
if _is_container_context; then
_container_write_failure "130"
elif declare -f post_update_to_api & >/dev/null; then
post_update_to_api "aborted" "130" 2>/dev/null || true
fi
2026-02-23 14:30:48 +01:00
_stop_container_if_installing
2025-12-04 07:52:18 +01:00
if declare -f msg_error >/dev/null 2>& 1; then
2026-02-18 10:24:06 +01:00
msg_error "Interrupted by user (SIGINT)" 2>/dev/null || true
2025-12-04 07:52:18 +01:00
else
2026-02-18 10:24:06 +01:00
echo -e "\n ${ RD } Interrupted by user (SIGINT) ${ CL } " 2>/dev/null || true
2025-12-04 07:52:18 +01:00
fi
exit 130
}
# ------------------------------------------------------------------------------
2026-07-20 20:32:36 +02:00
# on_terminate() - SIGTERM
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
on_terminate() {
2026-02-25 14:08:24 +01:00
if declare -f stop_spinner >/dev/null 2>& 1; then
stop_spinner 2>/dev/null || true
fi
printf "\e[?25h" 2>/dev/null || true
2026-07-20 20:32:36 +02:00
if _is_container_context; then
_container_write_failure "143"
elif declare -f post_update_to_api & >/dev/null; then
post_update_to_api "aborted" "143" 2>/dev/null || true
fi
2026-02-23 14:30:48 +01:00
_stop_container_if_installing
2025-12-04 07:52:18 +01:00
if declare -f msg_error >/dev/null 2>& 1; then
2026-02-18 10:24:06 +01:00
msg_error "Terminated by signal (SIGTERM)" 2>/dev/null || true
2025-12-04 07:52:18 +01:00
else
2026-02-18 10:24:06 +01:00
echo -e "\n ${ RD } Terminated by signal (SIGTERM) ${ CL } " 2>/dev/null || true
2025-12-04 07:52:18 +01:00
fi
exit 143
}
2026-02-23 14:30:48 +01:00
# ------------------------------------------------------------------------------
2026-07-20 20:32:36 +02:00
# on_hangup() - SIGHUP (SSH disconnect, terminal closed)
2026-02-23 14:30:48 +01:00
# ------------------------------------------------------------------------------
on_hangup() {
2026-02-25 14:08:24 +01:00
if declare -f stop_spinner >/dev/null 2>& 1; then
stop_spinner 2>/dev/null || true
fi
2026-07-20 20:32:36 +02:00
if _is_container_context; then
_container_write_failure "129"
elif declare -f post_update_to_api & >/dev/null; then
post_update_to_api "aborted" "129" 2>/dev/null || true
fi
2026-02-23 14:30:48 +01:00
_stop_container_if_installing
exit 129
}
2025-12-04 07:52:18 +01:00
# ==============================================================================
2026-07-20 20:32:36 +02:00
# SECTION 6: INITIALIZATION
2025-12-04 07:52:18 +01:00
# ==============================================================================
# ------------------------------------------------------------------------------
# catch_errors()
#
# - Initializes error handling and signal traps
2026-07-20 20:32:36 +02:00
# - set -Ee -o pipefail (+ set -u when STRICT_UNSET=1)
# - Traps: ERR → error_handler, EXIT → on_exit, INT/TERM/HUP → signal handlers
2025-12-04 07:52:18 +01:00
# - Call this function early in every script
# ------------------------------------------------------------------------------
catch_errors() {
set -Ee -o pipefail
if [ " ${ STRICT_UNSET :- 0 } " = "1" ] ; then
set -u
fi
trap 'error_handler' ERR
trap on_exit EXIT
trap on_interrupt INT
trap on_terminate TERM
2026-02-23 14:30:48 +01:00
trap on_hangup HUP
2025-12-04 07:52:18 +01:00
}