From 2ffe1150c9138a14213fa5822e53a482f4256e89 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:45:04 +0200 Subject: [PATCH 1/4] Stop calling exit 1 "Operation not permitted" and name a stale pnpm lockfile Exit status 1 is the command's own verdict; errno 1 is unrelated. The host also offered a DNS retry on every exit-1 failure because the word DNS matched the network check line in the log (ProxmoxVE #17913). --- .github/workflows/verify-split-files.yml | 5 +++ api/exitcodes.func | 2 +- core/error_handler.func | 6 +++- incus/backend.func | 2 +- pve/backend.func | 2 +- tests/exit-code-hints.sh | 45 ++++++++++++++++++++++++ 6 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 tests/exit-code-hints.sh diff --git a/.github/workflows/verify-split-files.yml b/.github/workflows/verify-split-files.yml index 9f4cb45..d5f18bb 100644 --- a/.github/workflows/verify-split-files.yml +++ b/.github/workflows/verify-split-files.yml @@ -33,6 +33,7 @@ on: - "tests/nodejs-key-refresh.sh" - "tests/forge-connection.sh" - "tests/failed-command-output.sh" + - "tests/exit-code-hints.sh" - "tests/forge-release-json.sh" - ".github/workflows/verify-split-files.yml" push: @@ -58,6 +59,7 @@ on: - "tests/nodejs-key-refresh.sh" - "tests/forge-connection.sh" - "tests/failed-command-output.sh" + - "tests/exit-code-hints.sh" - "tests/forge-release-json.sh" - ".github/workflows/verify-split-files.yml" workflow_dispatch: @@ -113,6 +115,9 @@ jobs: - name: Failed command output tests run: bash tests/failed-command-output.sh + - name: Exit code hint tests + run: bash tests/exit-code-hints.sh + - name: Forge release lookup tests run: bash tests/forge-release-json.sh diff --git a/api/exitcodes.func b/api/exitcodes.func index 8277532..369f7fe 100644 --- a/api/exitcodes.func +++ b/api/exitcodes.func @@ -39,7 +39,7 @@ explain_exit_code() { # never reported success is someone backing out of a dialog. Every one of # those records read "Unknown error". 0) echo "Cancelled before completion (no error)" ;; - 1) echo "General error / Operation not permitted" ;; + 1) echo "The command failed; its output says why" ;; 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)" ;; diff --git a/core/error_handler.func b/core/error_handler.func index 4eb7f4d..4d5cc43 100644 --- a/core/error_handler.func +++ b/core/error_handler.func @@ -39,7 +39,7 @@ 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" ;; + 1) echo "The command failed; its output says why" ;; 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)" ;; @@ -540,6 +540,10 @@ _eh_hint_from_log() { _eh_warn "Hint: Run 'apt-get install -f' inside the container or check that all required repositories are configured for your distribution." fi + elif grep -qE "ERR_PNPM_OUTDATED_LOCKFILE|ERR_PNPM_LOCKFILE_CONFIG_MISMATCH|npm ERR! code ELOCKVERIFY" <<<"$tail_txt"; then + _eh_warn "The release's lockfile does not match its package.json: an upstream packaging slip, not this container." + _eh_warn "Hint: the install script can fall back to 'pnpm install --no-frozen-lockfile' for this release." + elif grep -qE "sqv|KEYEXPIRED|NO_PUBKEY|key is not certified|signature verification failed|is not signed|Sub-process.*sqv" <<<"$tail_txt"; then _eh_warn "APT repository signature error detected." _eh_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.)." diff --git a/incus/backend.func b/incus/backend.func index c483c32..c464a2d 100644 --- a/incus/backend.func +++ b/incus/backend.func @@ -1240,7 +1240,7 @@ incus_run_install_script_with_recovery() { if [[ $install_exit_code -eq 1 && -f "$combined_log" ]]; then grep -qiE 'E: Unable to|broken packages|unmet dependencies|dpkg --configure' "$combined_log" && is_apt_issue=true grep -qiE 'Cannot allocate memory|Out of memory|oom-killer|JavaScript heap' "$combined_log" && is_oom=true - grep -qiE 'Could not resolve|DNS|Network is unreachable|Failed to fetch' "$combined_log" && is_network_issue=true + grep -qiE 'Could not resolve|DNS resolution failed|Temporary failure resolving|Network is unreachable|Failed to fetch' "$combined_log" && is_network_issue=true grep -qiE ': command not found' "$combined_log" && is_cmd_not_found=true fi diff --git a/pve/backend.func b/pve/backend.func index ee523c9..cf9f830 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -1497,7 +1497,7 @@ PROFILE if grep -qiE 'Cannot allocate memory|Out of memory|oom-killer|Killed process|JavaScript heap' "$combined_log"; then is_oom=true fi - if grep -qiE 'Could not resolve|DNS|Connection refused|Network is unreachable|No route to host|Temporary failure resolving|Failed to fetch' "$combined_log"; then + if grep -qiE 'Could not resolve|DNS resolution failed|Connection refused|Network is unreachable|No route to host|Temporary failure resolving|Failed to fetch' "$combined_log"; then is_network_issue=true fi if grep -qiE ': command not found|No such file or directory.*/s?bin/' "$combined_log"; then diff --git a/tests/exit-code-hints.sh b/tests/exit-code-hints.sh new file mode 100644 index 0000000..9006c1a --- /dev/null +++ b/tests/exit-code-hints.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -uo pipefail + +CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +eval "$(awk '/^explain_exit_code[(][)] [{]/,/^}/' "$CORE_DIR/api/exitcodes.func")" +eval "$(awk '/^_eh_hint_from_log[(][)] [{]/,/^}/' "$CORE_DIR/core/error_handler.func")" + +TEST_DIR="$(mktemp -d)" +trap 'rm -rf -- "$TEST_DIR"' EXIT +LOG="$TEST_DIR/log" +WARN="$TEST_DIR/warn" +passed=0 failed=0 +_eh_warn() { echo "$1" >>"$WARN"; } + +expect() { + if [[ "$2" == "$3" ]]; then + echo "PASS $1" + passed=$((passed + 1)) + else + echo "FAIL $1: expected <$2>, got <$3>" >&2 + failed=$((failed + 1)) + fi +} + +# Exit status 1 is the command's own verdict; errno 1 (EPERM) is something else. +expect "exit 1 is not explained as EPERM" 0 "$(explain_exit_code 1 | grep -c 'Operation not permitted')" +expect "the container fallback table agrees" 0 "$(grep -E '^ *1[)] echo' "$CORE_DIR/core/error_handler.func" | grep -c 'Operation not permitted')" + +: >"$WARN" +printf '%s\n' 'Scope: all 36 workspace projects' ' ERR_PNPM_OUTDATED_LOCKFILE Cannot install with "frozen-lockfile" because pnpm-lock.yaml is not up to date with /server/package.json' >"$LOG" +_eh_hint_from_log "$LOG" +expect "a stale pnpm lockfile is named" 1 "$(grep -c 'lockfile does not match' "$WARN")" +expect "the fallback is suggested" 1 "$(grep -c 'no-frozen-lockfile' "$WARN")" + +: >"$WARN" +printf '%s\n' '[OK] Git DNS: github.com:(ok)' '[ERROR] in line 34: exit code 1' >"$LOG" +_eh_hint_from_log "$LOG" +expect "the network check line raises no hint" 0 "$(wc -l <"$WARN" | tr -d ' ')" + +# The host classifies the combined log itself; a bare DNS used to match the network check line. +expect "pve host grep no longer matches the word DNS alone" 0 "$(grep -c 'resolve|DNS|' "$CORE_DIR/pve/backend.func")" +expect "incus host grep no longer matches the word DNS alone" 0 "$(grep -c 'resolve|DNS|' "$CORE_DIR/incus/backend.func")" + +printf '%s passed, %s failed\n' "$passed" "$failed" +((failed == 0)) From 32abbb26ce900d5e501c7facff9f3a08a0c18539 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:54:05 +0200 Subject: [PATCH 2/4] Apply tool-specific exit code meanings only to that tool Only the curl entries were guarded; cargo's 101 read as an APT configuration error and git's 128 as an invalid exit argument. The host now reads the failing command from .errinfo so its explanation and the APT repair offer see the same guard. --- api/exitcodes.func | 6 +++--- core/error_handler.func | 34 +++++++++++++++++++++++++++++----- incus/backend.func | 15 ++++++++++++--- pve/backend.func | 14 +++++++++++--- tests/exit-code-hints.sh | 14 +++++++++++++- ui/API.txt | 2 ++ 6 files changed, 70 insertions(+), 15 deletions(-) diff --git a/api/exitcodes.func b/api/exitcodes.func index 369f7fe..f0b24bc 100644 --- a/api/exitcodes.func +++ b/api/exitcodes.func @@ -40,8 +40,8 @@ explain_exit_code() { # those records read "Unknown error". 0) echo "Cancelled before completion (no error)" ;; 1) echo "The command failed; its output says why" ;; - 2) echo "Misuse of shell builtins (e.g. syntax error)" ;; - 3) echo "General syntax or argument error" ;; + 2) echo "The command failed with status 2 (a usage error for most tools); its output says why" ;; + 3) echo "The command failed with status 3; its output says why" ;; 10) echo "Docker / privileged mode required (unsupported environment)" ;; # --- curl / wget errors --- @@ -133,7 +133,7 @@ explain_exit_code() { 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" ;; + 128) echo "git: Fatal error (repository, network or permission problem)" ;; 129) echo "Killed by SIGHUP (terminal closed / hangup)" ;; 130) echo "Aborted by user (SIGINT)" ;; 131) echo "Killed by SIGQUIT (core dumped)" ;; diff --git a/core/error_handler.func b/core/error_handler.func index 4d5cc43..75d0bdb 100644 --- a/core/error_handler.func +++ b/core/error_handler.func @@ -40,8 +40,8 @@ if ! declare -f explain_exit_code &>/dev/null; then local code="$1" case "$code" in 1) echo "The command failed; its output says why" ;; - 2) echo "Misuse of shell builtins (e.g. syntax error)" ;; - 3) echo "General syntax or argument error" ;; + 2) echo "The command failed with status 2 (a usage error for most tools); its output says why" ;; + 3) echo "The command failed with status 3; its output says why" ;; 10) echo "Docker / privileged mode required (unsupported environment)" ;; 4) echo "curl: Feature not supported or protocol error" ;; 5) echo "curl: Could not resolve proxy" ;; @@ -123,7 +123,7 @@ if ! declare -f explain_exit_code &>/dev/null; then 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" ;; + 128) echo "git: Fatal error (repository, network or permission problem)" ;; 129) echo "Killed by SIGHUP (terminal closed / hangup)" ;; 130) echo "Aborted by user (SIGINT)" ;; 131) echo "Killed by SIGQUIT (core dumped)" ;; @@ -334,15 +334,39 @@ _eh_ok() { # readings are only right if curl is what actually ran, so they are dropped when # the failing command was something else. # ------------------------------------------------------------------------------ +# A tool-specific explanation only holds when the failing command is that tool: +# cargo's 101 is a panic, not an APT configuration error. _eh_explanation() { - local exit_code="$1" command="$2" explanation + local exit_code="$1" command="$2" explanation tools explanation="$(explain_exit_code "$exit_code")" - if [[ "$explanation" == curl:* && ! "$command" =~ (^|[[:space:]])([^[:space:]]*/)?curl([[:space:]]|$) ]]; then + tools="$(_eh_tools_for "${explanation%%:*}")" + if [[ -n "$tools" ]] && ! _eh_command_is "$command" "$tools"; then explanation="Command failed with exit status ${exit_code}" fi printf '%s' "$explanation" } +# The prefix of an explanation names the tool family it belongs to. +_eh_tools_for() { + case "$1" in + curl) echo "curl" ;; + APT | DPKG) echo "apt|apt-get|apt-cache|aptitude|dpkg|dpkg-query|dpkg-reconfigure" ;; + Node.js | npm/Node.js | npm/pnpm/yarn) echo "node|npm|npx|pnpm|yarn|corepack|bun" ;; + Python) echo "python|python3|pip|pip3|pipx|uv|uvx" ;; + PostgreSQL) echo "psql|pg_[a-z_]+|postgres" ;; + MySQL/MariaDB) echo "mysql|mariadb|mysqladmin|mariadb-admin" ;; + MongoDB) echo "mongosh|mongo|mongod" ;; + Systemd) echo "systemctl" ;; + git) echo "git" ;; + esac +} + +# Does the command line run one of the tools (first word or any word, with or without a path)? +_eh_command_is() { + local command="$1" tools="$2" + [[ "$command" =~ (^|[[:space:]]|/)(${tools})([[:space:]]|$) ]] +} + # ------------------------------------------------------------------------------ # _eh_report() - the one terminal event for this execution # diff --git a/incus/backend.func b/incus/backend.func index c464a2d..402c7f0 100644 --- a/incus/backend.func +++ b/incus/backend.func @@ -1220,12 +1220,21 @@ incus_run_install_script_with_recovery() { echo "" local is_oom=false is_network_issue=false is_apt_issue=false is_cmd_not_found=false - local error_explanation="" - declare -f explain_exit_code >/dev/null 2>&1 && error_explanation="$(explain_exit_code "$install_exit_code")" + local error_explanation="" failed_cmd="" + [[ -s "${TELEMETRY_ERRINFO:-}" ]] && failed_cmd="$(sed -n 's/^COMMAND=//p' "$TELEMETRY_ERRINFO" | head -n1)" + if declare -f _eh_explanation >/dev/null 2>&1; then + error_explanation="$(_eh_explanation "$install_exit_code" "$failed_cmd")" + elif declare -f explain_exit_code >/dev/null 2>&1; then + error_explanation="$(explain_exit_code "$install_exit_code")" + fi [[ $install_exit_code -eq 134 || $install_exit_code -eq 137 || $install_exit_code -eq 243 ]] && is_oom=true case "$install_exit_code" in - 100 | 101 | 102) is_apt_issue=true ;; + 100 | 101 | 102) + if [[ -z "$failed_cmd" ]] || ! declare -f _eh_command_is >/dev/null 2>&1 || _eh_command_is "$failed_cmd" "$(_eh_tools_for APT)"; then + is_apt_issue=true + fi + ;; 255) [[ -f "$combined_log" ]] && grep -qiE 'dpkg|apt-get|broken packages|unmet dependencies' "$combined_log" && is_apt_issue=true ;; diff --git a/pve/backend.func b/pve/backend.func index cf9f830..cdd1b72 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -1550,8 +1550,11 @@ PROFILE echo "" - local error_explanation="" - if declare -f explain_exit_code >/dev/null 2>&1; then + local error_explanation="" failed_cmd="" + [[ -s "${TELEMETRY_ERRINFO:-}" ]] && failed_cmd="$(sed -n 's/^COMMAND=//p' "$TELEMETRY_ERRINFO" | head -n1)" + if declare -f _eh_explanation >/dev/null 2>&1; then + error_explanation="$(_eh_explanation "$install_exit_code" "$failed_cmd")" + elif declare -f explain_exit_code >/dev/null 2>&1; then error_explanation="$(explain_exit_code "$install_exit_code")" fi @@ -1560,7 +1563,12 @@ PROFILE fi case "$install_exit_code" in - 100 | 101 | 102) is_apt_issue=true ;; + 100 | 101 | 102) + # cargo exits 101 on a panic; only an apt/dpkg command makes this an APT problem + if [[ -z "$failed_cmd" ]] || ! declare -f _eh_command_is >/dev/null 2>&1 || _eh_command_is "$failed_cmd" "$(_eh_tools_for APT)"; then + is_apt_issue=true + fi + ;; 255) if [[ -f "$combined_log" ]] && grep -qiE 'dpkg|apt-get|apt\.conf|broken packages|unmet dependencies|E: Sub-process|E: Failed' "$combined_log"; then is_apt_issue=true diff --git a/tests/exit-code-hints.sh b/tests/exit-code-hints.sh index 9006c1a..7f5b80b 100644 --- a/tests/exit-code-hints.sh +++ b/tests/exit-code-hints.sh @@ -3,7 +3,7 @@ set -uo pipefail CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" eval "$(awk '/^explain_exit_code[(][)] [{]/,/^}/' "$CORE_DIR/api/exitcodes.func")" -eval "$(awk '/^_eh_hint_from_log[(][)] [{]/,/^}/' "$CORE_DIR/core/error_handler.func")" +eval "$(awk '/^_eh_(hint_from_log|explanation|tools_for|command_is)[(][)] [{]/,/^}/' "$CORE_DIR/core/error_handler.func")" TEST_DIR="$(mktemp -d)" trap 'rm -rf -- "$TEST_DIR"' EXIT @@ -26,6 +26,18 @@ expect() { expect "exit 1 is not explained as EPERM" 0 "$(explain_exit_code 1 | grep -c 'Operation not permitted')" expect "the container fallback table agrees" 0 "$(grep -E '^ *1[)] echo' "$CORE_DIR/core/error_handler.func" | grep -c 'Operation not permitted')" +# A tool-specific meaning holds only for that tool; everything else gets the plain status. +expect "cargo 101 is not an APT error" "Command failed with exit status 101" "$(_eh_explanation 101 'cargo build --release')" +expect "apt-get 101 keeps the APT text" APT "$(_eh_explanation 101 'apt-get install -y jq' | cut -d: -f1)" +expect "a core wrapper returning 7 is not curl" "Command failed with exit status 7" "$(_eh_explanation 7 'fetch_and_deploy_gh_release app owner/repo')" +expect "curl 7 with a path keeps the curl text" curl "$(_eh_explanation 7 '/usr/bin/curl -fsSL https://x' | cut -d: -f1)" +expect "git 128 is a git fatal error" git "$(_eh_explanation 128 'git clone https://x' | cut -d: -f1)" +expect "pnpm 128 is not" "Command failed with exit status 128" "$(_eh_explanation 128 'pnpm build')" +expect "node 243 keeps the Node text" Node.js "$(_eh_explanation 243 'node server.js' | cut -d: -f1)" +expect "python 243 does not" "Command failed with exit status 243" "$(_eh_explanation 243 'python3 app.py')" +expect "core codes stay as they are" Proxmox "$(_eh_explanation 209 '' | cut -d: -f1)" +expect "unprefixed codes stay as they are" "Command not found" "$(_eh_explanation 127 'foo')" + : >"$WARN" printf '%s\n' 'Scope: all 36 workspace projects' ' ERR_PNPM_OUTDATED_LOCKFILE Cannot install with "frozen-lockfile" because pnpm-lock.yaml is not up to date with /server/package.json' >"$LOG" _eh_hint_from_log "$LOG" diff --git a/ui/API.txt b/ui/API.txt index 4df57b1..795bb83 100644 --- a/ui/API.txt +++ b/ui/API.txt @@ -17,6 +17,7 @@ _dev_step_end _dev_step_key _dev_step_start _dev_timing_dir +_eh_command_is _eh_destroy_container _eh_explanation _eh_hint_from_log @@ -31,6 +32,7 @@ _eh_quiet_spinner _eh_report _eh_resolve_log _eh_signal_exit +_eh_tools_for _eh_warn _eh_write_debug_log _get_current_ip From f15d891879c9cc2383f289501ae018481fe63f8d Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:46:34 +0200 Subject: [PATCH 3/4] Show the captured output once, from the container handler The handler printed the raw log tail and the host repeated the same output from .errinfo right below it. The container now prints the captured output of the failing command and keeps the raw tail only for failures that did not go through silent(). --- core/error_handler.func | 9 ++++++++- incus/backend.func | 1 - pve/backend.func | 1 - tests/failed-command-output.sh | 27 +++++++++++++++++++++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/core/error_handler.func b/core/error_handler.func index 75d0bdb..62363e8 100644 --- a/core/error_handler.func +++ b/core/error_handler.func @@ -665,10 +665,12 @@ error_handler() { command="${command//\$STD/}" # silent() knows the real failing command; the trap only sees the wrapper. + local from_silent=0 if [[ -n "${_SILENT_FAILED_RC:-}" ]]; then exit_code="$_SILENT_FAILED_RC" command="$_SILENT_FAILED_CMD" line_number="$_SILENT_FAILED_LINE" + from_silent=1 unset _SILENT_FAILED_RC _SILENT_FAILED_CMD _SILENT_FAILED_LINE fi @@ -697,7 +699,12 @@ error_handler() { _eh_write_debug_log "$exit_code" "$command" "$line_number" "$explanation" _eh_resolve_log - _eh_print_log_tail "$_EH_ACTIVE_LOG" + # silent() captured exactly this command's output; the raw log tail is the fallback. + if ((from_silent)) && [[ -s "${_EH_ACTIVE_LOG}.errinfo" ]]; then + _eh_print_failed_output "${_EH_ACTIVE_LOG}.errinfo" + else + _eh_print_log_tail "$_EH_ACTIVE_LOG" + fi _eh_hint_missing_command "$exit_code" "$command" _eh_hint_node_oom "$exit_code" "$command" "$_EH_ACTIVE_LOG" _eh_hint_from_log "$_EH_ACTIVE_LOG" diff --git a/incus/backend.func b/incus/backend.func index 402c7f0..896344d 100644 --- a/incus/backend.func +++ b/incus/backend.func @@ -1254,7 +1254,6 @@ incus_run_install_script_with_recovery() { fi [[ -n "$error_explanation" ]] && echo -e "${TAB}${RD}Error: ${error_explanation}${CL}" && echo "" - declare -f _eh_print_failed_output >/dev/null 2>&1 && _eh_print_failed_output "${TELEMETRY_ERRINFO:-}" if [[ $install_exit_code -eq 10 ]]; then echo -e "${TAB}${INFO} Privileged / nesting / device hint:" echo -e "${TAB}${INFO} • Privileged: ${GN}security.privileged=true${CL} (USB, Docker-in-LXC)" diff --git a/pve/backend.func b/pve/backend.func index cdd1b72..c6bd081 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -1605,7 +1605,6 @@ PROFILE echo -e "${TAB}${RD}Error: ${error_explanation}${CL}" echo "" fi - declare -f _eh_print_failed_output >/dev/null 2>&1 && _eh_print_failed_output "${TELEMETRY_ERRINFO:-}" if [[ $install_exit_code -eq 10 ]]; then echo -e "${TAB}${INFO} This error usually means the container needs ${GN}privileged${CL} mode or Docker/nesting support." diff --git a/tests/failed-command-output.sh b/tests/failed-command-output.sh index 55cca1b..adbb722 100644 --- a/tests/failed-command-output.sh +++ b/tests/failed-command-output.sh @@ -86,5 +86,32 @@ expect "an empty output section prints nothing" "" "$(_eh_print_failed_output "$ expect "a missing errinfo prints nothing" "" "$(_eh_print_failed_output "$TEST_DIR/nope" 2>&1)" expect "no errinfo path prints nothing" "" "$(_eh_print_failed_output "" 2>&1)" +# --- the container handler prefers the captured output over the raw log tail --- +eval "$(awk '/^(error_handler|_eh_resolve_log|_eh_print_log_tail)[(][)] [{]/,/^}/' "$CORE_DIR/core/error_handler.func")" +_eh_explanation() { echo "explained"; } +_eh_report() { :; } +_eh_write_debug_log() { :; } +_eh_hint_missing_command() { :; } +_eh_hint_node_oom() { :; } +_eh_hint_from_log() { :; } +_is_container_context() { return 0; } +msg_error() { echo "error: $*"; } +handler_output() { + # error_handler exits, so it runs in a subshell with the state silent() leaves behind. + ( + export _SILENT_FAILED_RC=1 _SILENT_FAILED_CMD="pnpm install" _SILENT_FAILED_LINE=34 _SILENT_FAILED_LOG="$LOGFILE" + error_handler 1 "silent pnpm install" 2>&1 + ) +} +reset +printf '%s\n' "[INFO] Building" "cargo noise" "ERR_PNPM_OUTDATED_LOCKFILE" >"$LOGFILE" +printf 'EXIT_CODE=1\nLINE=34\nCOMMAND=pnpm install\n--- OUTPUT ---\nERR_PNPM_OUTDATED_LOCKFILE\n' >"$LOGFILE.errinfo" +out="$(handler_output)" +expect "with errinfo the handler shows the command output" 1 "$(printf '%s\n' "$out" | grep -c 'Output of the failing command')" +expect "and leaves out the raw log tail" 0 "$(printf '%s\n' "$out" | grep -c 'Last 20 lines of log')" +rm -f "$LOGFILE.errinfo" +out="$(handler_output)" +expect "without errinfo the raw tail remains" 1 "$(printf '%s\n' "$out" | grep -c 'Last 20 lines of log')" + printf '%s passed, %s failed\n' "$passed" "$failed" ((failed == 0)) From bde3564fc103d881ed849759640436d1c9b7f979 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:10:55 +0200 Subject: [PATCH 4/4] Declining an external installer is a user abort, not exit 10 Exit 10 reads as "needs privileged mode" everywhere, which is the hint BunkerWeb users got after var_external_installer=no. The message now names the variable when that is where the refusal came from (ProxmoxVE #17937). --- core/core.func | 11 ++++++++--- tests/external-installer.sh | 17 ++++++++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/core/core.func b/core/core.func index 3b979c8..4e933d5 100644 --- a/core/core.func +++ b/core/core.func @@ -1871,13 +1871,18 @@ confirm_external_installer() { msg_warn "If you have any doubts or concerns, please review the installer code before proceeding:" msg_custom "${TAB3}${GATEWAY}${BGN}${CL}" "\e[1;34m" "→ ${installer}" echo + # 254 is the user declining; 10 would read as "needs privileged mode". if [[ -z "$answer" ]] && ! read -r -p "${TAB3}Do you want to continue? [y/N]: " answer; then msg_error "No terminal to confirm on. Set var_external_installer=yes to run it unattended." - exit 10 + exit 254 fi if [[ ! "${answer,,}" =~ ^(y|yes|true)$ ]]; then - msg_error "Aborted by user. No changes have been made." - exit 10 + if [[ -n "${var_external_installer:-}" ]]; then + msg_error "var_external_installer=${var_external_installer} declines the upstream installer, and this script has no other way to install. Set it to yes, or leave it unset to be asked." + else + msg_error "Aborted by user. No changes have been made." + fi + exit 254 fi } diff --git a/tests/external-installer.sh b/tests/external-installer.sh index 8a4d12e..ca74952 100644 --- a/tests/external-installer.sh +++ b/tests/external-installer.sh @@ -36,13 +36,20 @@ run_eof() { check "var yes" 0 "$(run yes "")" check "var YES" 0 "$(run YES "")" check "var true" 0 "$(run true "")" -check "var no" 10 "$(run no "y")" -check "var wins over input" 10 "$(run no "yes")" +check "var no" 254 "$(run no "y")" +check "var wins over input" 254 "$(run no "yes")" check "typed y" 0 "$(run "" y)" check "typed Yes" 0 "$(run "" Yes)" -check "typed n" 10 "$(run "" n)" -check "just enter" 10 "$(run "" "")" -check "no stdin" 10 "$(run_eof)" +check "typed n" 254 "$(run "" n)" +check "just enter" 254 "$(run "" "")" +check "no stdin" 254 "$(run_eof)" + +# what the message says; $1 is var_external_installer, $2 what the user types +said() { + (var_external_installer="$1" confirm_external_installer src url) <<<"$2" 2>&1 >/dev/null || true +} +check "declining via the variable names the variable" 1 "$(said no "" | grep -c 'var_external_installer=no')" +check "declining at the prompt is a user abort" 1 "$(said "" n | grep -c 'Aborted by user')" printf '%s passed, %s failed\n' "$passed" "$failed" ((failed == 0))