From 68509f92dc99b90c8a7edf4b65e92e871e486601 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Fri, 18 Sep 2026 12:21:51 +0200 Subject: [PATCH 1/3] Stop a modified conffile from aborting the whole update dpkg's conffile prompt has no tty during an update. It aborts the run, leaves the package half-configured, and stops every other pending package on the container behind an error that never mentions conffiles. 79 of 608 ct scripts run an unguarded apt upgrade in update_script. apt_conffile_guard points APT_CONFIG at a throwaway file carrying --force-confdef --force-confold. apt reads it after apt.conf.d, and it dies with the process, so an early exit cannot leave a drop-in behind. apt_conffile_report names what upstream shipped alongside the file that was kept. It runs from on_exit, not after update_script: 575 of 604 update_script bodies exit before returning, so anything placed there is unreachable. The three identical update branches in start() are now one function, which is what made a single guard call site possible. Supersedes community-scripts/ProxmoxVE#16438, which targeted the retired misc/build.func and paired begin/end calls that never ran. --- core/core.func | 51 +++++++++++++++++++ core/error_handler.func | 3 ++ ui/menu.func | 105 +++++++++++++--------------------------- 3 files changed, 87 insertions(+), 72 deletions(-) diff --git a/core/core.func b/core/core.func index f98604a..a8da901 100644 --- a/core/core.func +++ b/core/core.func @@ -2409,6 +2409,57 @@ apt_update_safe() { return 0 } +# ------------------------------------------------------------------------------ +# apt_conffile_guard() / apt_conffile_report() +# +# - dpkg's "modified conffile" prompt has no tty during an update: it aborts the +# run, leaves the package half-configured, and stops every other pending +# package on the container with an error that never mentions conffiles +# - Points APT_CONFIG at a throwaway file. apt reads it after apt.conf.d, and it +# dies with the process, so no drop-in can be left behind on an early exit +# - Keeping the operator's file silently would trade a loud failure for a quiet +# one, so apt_conffile_report names what upstream shipped alongside it +# ------------------------------------------------------------------------------ +_cs_conffile_list() { + find "${_CS_CONFFILE_ROOT:-/etc}" -maxdepth 6 \( -name '*.dpkg-dist' -o -name '*.dpkg-new' \) 2>/dev/null | sort +} + +apt_conffile_guard() { + command -v apt-get >/dev/null 2>&1 || return 0 + [[ -n "${_CS_CONFFILE_GUARD:-}" ]] && return 0 + + local conf + conf="$(mktemp 2>/dev/null)" || return 0 + printf 'Dpkg::Options { "--force-confdef"; "--force-confold"; };\n' >"$conf" 2>/dev/null || { + rm -f "$conf" + return 0 + } + + export APT_CONFIG="$conf" + _CS_CONFFILE_GUARD="$conf" + _CS_CONFFILE_BASELINE="$(_cs_conffile_list)" + return 0 +} + +apt_conffile_report() { + [[ -n "${_CS_CONFFILE_GUARD:-}" ]] || return 0 + + local added + added="$(comm -13 <(printf '%s\n' "${_CS_CONFFILE_BASELINE:-}") <(_cs_conffile_list) 2>/dev/null)" + rm -f "$_CS_CONFFILE_GUARD" + _CS_CONFFILE_GUARD="" + unset APT_CONFIG + [[ -n "${added//[[:space:]]/}" ]] || return 0 + + msg_warn "Kept your version of these config files - upstream shipped changes to them too:" + local f + while IFS= read -r f; do + [[ -n "$f" ]] || continue + echo -e "${TAB}${YW}${f%.dpkg-*}${CL} - new upstream version at ${YW}${f}${CL}" + done <<<"$added" + return 0 +} + # ------------------------------------------------------------------------------ # ensure_whiptail() # diff --git a/core/error_handler.func b/core/error_handler.func index 23e4117..e0b5a51 100644 --- a/core/error_handler.func +++ b/core/error_handler.func @@ -712,6 +712,9 @@ on_exit() { # Before anything else: the timings are only useful if they survive a failure. declare -f dev_mode_timing_summary >/dev/null 2>&1 && dev_mode_timing_summary + # Here rather than after update_script, which most ct scripts exit inside. + declare -f apt_conffile_report >/dev/null 2>&1 && apt_conffile_report + if _is_container_context; then if [[ $exit_code -ne 0 ]]; then _container_write_failure "$exit_code" "${FAILED_COMMAND:-}" "${FAILED_LINE:-}" diff --git a/ui/menu.func b/ui/menu.func index 8c73065..d613824 100644 --- a/ui/menu.func +++ b/ui/menu.func @@ -1111,6 +1111,36 @@ check_breaking_change_guard() { fi return 1 } + +# The update path, shared by the three branches of start() that reach it. +_cs_run_update() { + ensure_profile_loaded + ensure_recorded_toolchains + get_lxc_ip + # Move legacy containers onto the helper-based /usr/bin/update the next time + # they update. No-op once migrated; preserves the container's original source. + migrate_update_entrypoint + # exit, not return, for the same reason as below: returning falls through + # into the ct script's host-only tail. + runtime_script_status_guard update || exit 0 + check_container_os_guard || exit 0 + check_breaking_change_guard || exit 0 + apt_conffile_guard + update_script + run_addon_updates + update_motd_ip + cleanup_lxc + # An update run ends here. Returning would continue in the ct script, whose + # next lines are build_container and description -- host code, which inside a + # container fails as "You need to set 'CTID' variable", "MAC: unbound + # variable", or on Alpine "dpkg: command not found". + # + # Every ct script papers over this with an exit at the end of its own + # update_script, which is why the three calls above have been unreachable. + # Those scripts exit before getting here, so this changes nothing for them. + exit 0 +} + start() { # Through the resolver, not a bare curl: this way a local checkout and the # prefetched copy are used. Hardcoding the URL here meant tools.func and its @@ -1138,58 +1168,12 @@ start() { elif [ ! -z ${PHS_SILENT+x} ] && [[ "${PHS_SILENT}" == "1" ]]; then VERBOSE="no" set_std_mode - ensure_profile_loaded - ensure_recorded_toolchains - get_lxc_ip - # Move legacy containers onto the helper-based /usr/bin/update the next time - # they update. No-op once migrated; preserves the container's original source. - migrate_update_entrypoint - # exit, not return, for the same reason as below: returning falls through - # into the ct script's host-only tail. - runtime_script_status_guard update || exit 0 - check_container_os_guard || exit 0 - check_breaking_change_guard || exit 0 - update_script - run_addon_updates - update_motd_ip - cleanup_lxc - # An update run ends here. Returning would continue in the ct script, whose - # next lines are build_container and description -- host code, which inside a - # container fails as "You need to set 'CTID' variable", "MAC: unbound - # variable", or on Alpine "dpkg: command not found". - # - # Every ct script papers over this with an exit at the end of its own - # update_script, which is why the three calls above have been unreachable. - # Those scripts exit before getting here, so this changes nothing for them. - exit 0 + _cs_run_update elif ! command -v whiptail &>/dev/null || ! [ -t 0 ] || [[ "$TERM" == "dumb" ]]; then msg_info "No interactive terminal detected – defaulting to silent update mode" VERBOSE="no" set_std_mode - ensure_profile_loaded - ensure_recorded_toolchains - get_lxc_ip - # Move legacy containers onto the helper-based /usr/bin/update the next time - # they update. No-op once migrated; preserves the container's original source. - migrate_update_entrypoint - # exit, not return, for the same reason as below: returning falls through - # into the ct script's host-only tail. - runtime_script_status_guard update || exit 0 - check_container_os_guard || exit 0 - check_breaking_change_guard || exit 0 - update_script - run_addon_updates - update_motd_ip - cleanup_lxc - # An update run ends here. Returning would continue in the ct script, whose - # next lines are build_container and description -- host code, which inside a - # container fails as "You need to set 'CTID' variable", "MAC: unbound - # variable", or on Alpine "dpkg: command not found". - # - # Every ct script papers over this with an exit at the end of its own - # update_script, which is why the three calls above have been unreachable. - # Those scripts exit before getting here, so this changes nothing for them. - exit 0 + _cs_run_update else CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \ "Support/Update functions for ${APP} LXC. Choose an option:" \ @@ -1213,29 +1197,6 @@ start() { exit ;; esac - ensure_profile_loaded - ensure_recorded_toolchains - get_lxc_ip - # Move legacy containers onto the helper-based /usr/bin/update the next time - # they update. No-op once migrated; preserves the container's original source. - migrate_update_entrypoint - # exit, not return, for the same reason as below: returning falls through - # into the ct script's host-only tail. - runtime_script_status_guard update || exit 0 - check_container_os_guard || exit 0 - check_breaking_change_guard || exit 0 - update_script - run_addon_updates - update_motd_ip - cleanup_lxc - # An update run ends here. Returning would continue in the ct script, whose - # next lines are build_container and description -- host code, which inside a - # container fails as "You need to set 'CTID' variable", "MAC: unbound - # variable", or on Alpine "dpkg: command not found". - # - # Every ct script papers over this with an exit at the end of its own - # update_script, which is why the three calls above have been unreachable. - # Those scripts exit before getting here, so this changes nothing for them. - exit 0 + _cs_run_update fi } From 2a6bc6533d445e405d6289dc37e3a23de1e7d01a Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Fri, 18 Sep 2026 12:23:58 +0200 Subject: [PATCH 2/3] smaller comments --- core/core.func | 14 +++----------- core/error_handler.func | 2 +- ui/menu.func | 15 +++------------ 3 files changed, 7 insertions(+), 24 deletions(-) diff --git a/core/core.func b/core/core.func index a8da901..e4238d0 100644 --- a/core/core.func +++ b/core/core.func @@ -2409,17 +2409,9 @@ apt_update_safe() { return 0 } -# ------------------------------------------------------------------------------ -# apt_conffile_guard() / apt_conffile_report() -# -# - dpkg's "modified conffile" prompt has no tty during an update: it aborts the -# run, leaves the package half-configured, and stops every other pending -# package on the container with an error that never mentions conffiles -# - Points APT_CONFIG at a throwaway file. apt reads it after apt.conf.d, and it -# dies with the process, so no drop-in can be left behind on an early exit -# - Keeping the operator's file silently would trade a loud failure for a quiet -# one, so apt_conffile_report names what upstream shipped alongside it -# ------------------------------------------------------------------------------ +# dpkg's conffile prompt has no tty during an update and takes the whole run +# with it. APT_CONFIG is read after apt.conf.d and dies with the process, so an +# early exit cannot leave a drop-in behind. _cs_conffile_list() { find "${_CS_CONFFILE_ROOT:-/etc}" -maxdepth 6 \( -name '*.dpkg-dist' -o -name '*.dpkg-new' \) 2>/dev/null | sort } diff --git a/core/error_handler.func b/core/error_handler.func index e0b5a51..7df538e 100644 --- a/core/error_handler.func +++ b/core/error_handler.func @@ -712,7 +712,7 @@ on_exit() { # Before anything else: the timings are only useful if they survive a failure. declare -f dev_mode_timing_summary >/dev/null 2>&1 && dev_mode_timing_summary - # Here rather than after update_script, which most ct scripts exit inside. + # Here, because most ct scripts exit inside update_script. declare -f apt_conffile_report >/dev/null 2>&1 && apt_conffile_report if _is_container_context; then diff --git a/ui/menu.func b/ui/menu.func index d613824..83d066c 100644 --- a/ui/menu.func +++ b/ui/menu.func @@ -1113,15 +1113,14 @@ check_breaking_change_guard() { } # The update path, shared by the three branches of start() that reach it. +# exit, not return: returning continues into the ct script's host-only tail. +# Most ct scripts exit inside update_script, so the three calls after it are +# unreachable for them. _cs_run_update() { ensure_profile_loaded ensure_recorded_toolchains get_lxc_ip - # Move legacy containers onto the helper-based /usr/bin/update the next time - # they update. No-op once migrated; preserves the container's original source. migrate_update_entrypoint - # exit, not return, for the same reason as below: returning falls through - # into the ct script's host-only tail. runtime_script_status_guard update || exit 0 check_container_os_guard || exit 0 check_breaking_change_guard || exit 0 @@ -1130,14 +1129,6 @@ _cs_run_update() { run_addon_updates update_motd_ip cleanup_lxc - # An update run ends here. Returning would continue in the ct script, whose - # next lines are build_container and description -- host code, which inside a - # container fails as "You need to set 'CTID' variable", "MAC: unbound - # variable", or on Alpine "dpkg: command not found". - # - # Every ct script papers over this with an exit at the end of its own - # update_script, which is why the three calls above have been unreachable. - # Those scripts exit before getting here, so this changes nothing for them. exit 0 } From 5f27bb6440ac782e2f3e19c372c79e34b09dc6ac Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Fri, 18 Sep 2026 12:25:51 +0200 Subject: [PATCH 3/3] Regenerate ui/API.txt --- ui/API.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/API.txt b/ui/API.txt index 7a1fa4f..e30d22f 100644 --- a/ui/API.txt +++ b/ui/API.txt @@ -2,12 +2,14 @@ _build_current_app_vars_tmp _build_vars_diff _container_write_failure _cs_clear +_cs_conffile_list _cs_engine_ref_line _cs_host_version_line _cs_live_base _cs_os_family _cs_ref_line _cs_run_os_section +_cs_run_update _cs_runtime_cache_dir _cs_scripts_ref_line _dev_step_end @@ -50,6 +52,8 @@ _tm_pick_logfile _tm_send _write_storage_to_vars advanced_settings +apt_conffile_guard +apt_conffile_report apt_update_safe arch_check arch_resolve