From f1283c2a6fe7375140eb957b16eae4526bd1705a Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:36:01 +0100 Subject: [PATCH 1/6] Run the host checks' probes during load, and report nothing a check stopped The checks before the settings menu waited on pveversion, two pvesm status calls, pvesh and, on clusters, pvecm, one after the other. core/build.func now starts them in the background before it downloads the engine; the checks read the results through _cs_probe_read and run a command themselves only when its probe failed or never ran. The storage check trusts a probe's yes and asks a no again live. Telemetry starts with the install record the build sends. A run that ends before it, in the checks or a dialog, reports nothing: a host that is not ready is not a failed install, and these runs made up a third of the failures since 27 October. Tools and add-ons have no such point, so their "requirement not met" codes send nothing either. root_check now runs before pve_check. As another user /etc/pve/storage.cfg is unreadable, and the cluster check reported a damaged cluster. log_msg and log_section no longer end a run when the log cannot be written. --- .github/workflows/verify-split-files.yml | 5 +++ api/API.txt | 1 + api/telemetry.func | 26 ++++++++------ core/build.func | 33 +++++++++++++++++- core/core.func | 37 +++++++++++++++----- docs/api.md | 13 +++++-- pve/backend.func | 12 +++++-- pve/vm-core.func | 2 +- tests/host-probes.sh | 44 ++++++++++++++++++++++++ tests/telemetry-trace.sh | 20 ++++++++--- ui/API.txt | 3 ++ ui/menu.func | 10 ++++-- 12 files changed, 174 insertions(+), 32 deletions(-) create mode 100644 tests/host-probes.sh diff --git a/.github/workflows/verify-split-files.yml b/.github/workflows/verify-split-files.yml index 3fa8997..e37088a 100644 --- a/.github/workflows/verify-split-files.yml +++ b/.github/workflows/verify-split-files.yml @@ -37,6 +37,7 @@ on: - "tests/stale-silent-record.sh" - "tests/exit-code-hints.sh" - "tests/telemetry-trace.sh" + - "tests/host-probes.sh" - "tests/forge-release-json.sh" - "tests/install-cleanup.sh" - "tests/console-hangup.sh" @@ -73,6 +74,7 @@ on: - "tests/stale-silent-record.sh" - "tests/exit-code-hints.sh" - "tests/telemetry-trace.sh" + - "tests/host-probes.sh" - "tests/forge-release-json.sh" - "tests/install-cleanup.sh" - "tests/console-hangup.sh" @@ -146,6 +148,9 @@ jobs: - name: Telemetry trace tests run: bash tests/telemetry-trace.sh + - name: Host probe tests + run: bash tests/host-probes.sh + - name: Forge release lookup tests run: bash tests/forge-release-json.sh diff --git a/api/API.txt b/api/API.txt index d708af0..951e9eb 100644 --- a/api/API.txt +++ b/api/API.txt @@ -14,6 +14,7 @@ _tm_post_progress _tm_post_run _tm_post_update _tm_quiet +_tm_requirement_code _tm_send _tm_step_output build_error_string diff --git a/api/telemetry.func b/api/telemetry.func index d27ab91..7652c48 100644 --- a/api/telemetry.func +++ b/api/telemetry.func @@ -259,16 +259,10 @@ _tm_post_update() { ;; esac - # Unset: no install was reported yet. A host run that failed in its checks - # (storage, ID, template) reports one now; an update run inside a container - # or a cancelled dialog does not. "false" means that send failed. - if [[ -z "${POST_TO_API_DONE:-}" ]]; then - if [[ "$pb_status" == "failed" && -n "${NSAPP:-}" ]] && - ! { declare -f _is_container_context >/dev/null 2>&1 && _is_container_context; }; then - post_to_api - fi - [[ -z "${POST_TO_API_DONE:-}" ]] && return 0 - fi + # Unset: the run ended before its build reported the install, in the checks + # or a dialog. Those report nothing; a host that is not ready is no failed + # install. "false" means the install was due and its send failed. + [[ -z "${POST_TO_API_DONE:-}" ]] && return 0 if [[ "$pb_status" == "failed" || "$pb_status" == "unknown" ]]; then _tm_error_context "$exit_code" @@ -302,6 +296,17 @@ _tm_post_update() { return 0 } +# Tools and add-ons have no build step that starts their telemetry, so their +# checks are known by the code they exit with: wrong host or OS, nothing to work +# on, a prerequisite missing. Like the checks before a container build, those +# report nothing. +_tm_requirement_code() { + case "$1" in + 10 | 103 | 104 | 106 | 232 | 233 | 234 | 236 | 238) return 0 ;; + esac + return 1 +} + # ------------------------------------------------------------------------------ # _tm_post_run - tool and add-on runs # @@ -318,6 +323,7 @@ _tm_post_run() { uuid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen 2>/dev/null || echo "${type}-$(date +%s)") [[ "$status" == "done" ]] && status="success" [[ "$exit_code" =~ ^[0-9]+$ ]] || exit_code=199 + [[ "$status" == "failed" ]] && _tm_requirement_code "$exit_code" && return 0 if [[ "$status" == "failed" ]]; then _tm_error_context "$exit_code" diff --git a/core/build.func b/core/build.func index bf748a4..b675f07 100644 --- a/core/build.func +++ b/core/build.func @@ -268,7 +268,7 @@ _cs_prefetch_engine() { [[ -d "$base" && -w "$base" ]] || continue # An EXIT trap would be the obvious cleanup, but core.func and api.func # install their own later and the last one wins. Sweep instead. - find "$base" -maxdepth 1 \( -name 'cs-engine.*' -o -name 'cs-pveam.*' -o -name 'cs-pveversion.*' \) -mmin +60 -exec rm -rf {} + 2>/dev/null || true + find "$base" -maxdepth 1 \( -name 'cs-engine.*' -o -name 'cs-pveam.*' -o -name 'cs-pveversion.*' -o -name 'cs-probe.*' \) -mmin +60 -exec rm -rf {} + 2>/dev/null || true dir="$(mktemp -d "${base}/cs-engine.XXXXXX" 2>/dev/null)" && break dir="" done @@ -305,6 +305,37 @@ _cs_prefetch_engine() { return 0 } +# The checks before the settings menu wait on pveversion, pvesm and pvesh, each +# a Perl start of up to a second. Start them now, while the engine downloads and +# the header loads; _cs_probe_read hands the checks what they printed. +_cs_probe() { + local dir="$1" name="$2" + shift 2 + : >"${dir}/${name}.out" + ("$@" >"${dir}/${name}.out" 2>/dev/null + echo "$?" >"${dir}/${name}.tmp" && mv -f "${dir}/${name}.tmp" "${dir}/${name}.rc") /dev/null 2>&1 || return 0 + [[ "$(id -u)" == "0" ]] || return 0 + local base dir="" + for base in /dev/shm "${TMPDIR:-/tmp}" /tmp; do + [[ -d "$base" && -w "$base" ]] || continue + dir="$(mktemp -d "${base}/cs-probe.XXXXXX" 2>/dev/null)" && break + dir="" + done + [[ -n "$dir" ]] || return 0 + _cs_probe "$dir" pveversion pveversion + _cs_probe "$dir" vztmpl pvesm status -content vztmpl + _cs_probe "$dir" rootdir pvesm status -content rootdir + _cs_probe "$dir" nextid pvesh get /cluster/nextid + [[ -f /etc/pve/corosync.conf ]] && _cs_probe "$dir" pvecm pvecm status + CS_PROBE_DIR="$dir" + return 0 +} + +_cs_start_host_probes _cs_prefetch_engine || true _cs_source_func "lxc/platform.func" diff --git a/core/core.func b/core/core.func index 0e18236..6459856 100644 --- a/core/core.func +++ b/core/core.func @@ -549,16 +549,34 @@ _cs_runtime_cache_dir() { return 1 } +# _cs_probe_read - output of a host probe that core/build.func started at +# load, once it has finished; fails when there is none or it failed, and the +# caller runs the command itself. Polls a file, so it works inside $( ) too. +_cs_probe_read() { + local dir="${CS_PROBE_DIR:-}" n=0 + [[ -n "$dir" && -f "${dir}/$1.out" ]] || return 1 + while [[ ! -f "${dir}/$1.rc" ]]; do + ((n++ < 1200)) || return 1 + sleep 0.05 + done + [[ "$(<"${dir}/$1.rc")" == "0" ]] || return 1 + cat "${dir}/$1.out" +} + +_cs_pveversion_out() { + _cs_probe_read pveversion || pveversion 2>/dev/null +} + _pve_version() { [[ -n "${PVEVERSION:-}" ]] && { printf '%s' "$PVEVERSION"; return 0; } local dir cache dir="$(_cs_runtime_cache_dir)" || { - pveversion 2>/dev/null | awk -F'/' '{print $2}' | awk -F'-' '{print $1}' - return + _cs_pveversion_out | awk -F'/' '{print $2}' | awk -F'-' '{print $1}' + return 0 } cache="${dir}/cs-pveversion.$$" [[ -s "$cache" ]] || - pveversion 2>/dev/null | awk -F'/' '{print $2}' | awk -F'-' '{print $1}' >"$cache" + _cs_pveversion_out | awk -F'/' '{print $2}' | awk -F'-' '{print $1}' >"$cache" cat "$cache" 2>/dev/null } @@ -735,7 +753,7 @@ pmxcfs_check() { exit 228 fi - if [[ -f /etc/pve/corosync.conf ]] && ! pvecm status 2>/dev/null | grep -qE '^Quorate:[[:space:]]+Yes'; then + if [[ -f /etc/pve/corosync.conf ]] && ! { _cs_probe_read pvecm || pvecm status 2>/dev/null; } | grep -qE '^Quorate:[[:space:]]+Yes'; then msg_error "This cluster node has no quorum, so /etc/pve is read-only and nothing can be created." msg_custom "🔧" "${YW}" "Bring enough cluster nodes online, then check: pvecm status" exit 210 @@ -899,8 +917,8 @@ log_msg() { local logfile logfile="$(get_active_logfile)" - [[ -z "$msg" ]] && return - [[ -z "$logfile" ]] && return + [[ -z "$msg" ]] && return 0 + [[ -z "$logfile" ]] && return 0 # Ensure log directory exists mkdir -p "$(dirname "$logfile")" 2>/dev/null || true @@ -908,7 +926,8 @@ log_msg() { # Strip ANSI codes and write with timestamp local clean_msg clean_msg=$(strip_ansi "$msg") - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $clean_msg" >>"$logfile" + # A log that cannot be written (full disk, removed directory) must not end the run. + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $clean_msg" >>"$logfile" 2>/dev/null || true } # ------------------------------------------------------------------------------ @@ -923,7 +942,7 @@ log_section() { local logfile logfile="$(get_active_logfile)" - [[ -z "$logfile" ]] && return + [[ -z "$logfile" ]] && return 0 mkdir -p "$(dirname "$logfile")" 2>/dev/null || true { @@ -931,7 +950,7 @@ log_section() { echo "================================================================================" echo "[$(date '+%Y-%m-%d %H:%M:%S')] $section" echo "================================================================================" - } >>"$logfile" + } >>"$logfile" 2>/dev/null || true } # ------------------------------------------------------------------------------ diff --git a/docs/api.md b/docs/api.md index c1fef0a..708559c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -15,9 +15,16 @@ terminal status: it writes `/root/.install-.failed` and an `.errinfo` capture, which the host picks up after `lxc-attach` returns. See [`core/error_handler.func`](../core/error_handler.func). -A host run that fails before its install record went out (storage, ID or -template checks) sends that record first, then the failure. Cancelled dialogs -and update runs inside a container send nothing. +Telemetry starts with the install record (`post_to_api`, `post_to_api_vm`), +which the build sends once the checks and dialogs are behind it. A run that +ends before that sends nothing: a host that is not ready is not a failed +install. Tools and add-ons have no such point, so a failure with a +"requirement not met" code (10, 103, 104, 106, 232, 233, 234, 236, 238) sends +nothing either. Update runs inside a container send nothing. + +The checks wait on `pveversion`, `pvesm status` and `pvesh`. `core/build.func` +starts these in the background before it downloads the engine, and the checks +read their output through `_cs_probe_read`. ## What a failure carries (payload version 3) diff --git a/pve/backend.func b/pve/backend.func index 390579b..804e81e 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -2245,8 +2245,16 @@ _offer_storage_content() { # content switched off, which can be switched on in place. # ------------------------------------------------------------------------------ storage_content_check() { - check_storage_support "vztmpl" || _offer_storage_content "vztmpl" || exit 120 - check_storage_support "rootdir" || _offer_storage_content "rootdir" || exit 119 + _probe_has_storage "vztmpl" || check_storage_support "vztmpl" || _offer_storage_content "vztmpl" || exit 120 + _probe_has_storage "rootdir" || check_storage_support "rootdir" || _offer_storage_content "rootdir" || exit 119 +} + +# The probe started at load answers yes; a no is asked again live, since +# _offer_storage_content may change it and nothing else reads the probe. +_probe_has_storage() { + local out + out="$(_cs_probe_read "$1")" || return 1 + awk 'NR > 1 && $1 != "" { found = 1 } END { exit !found }' <<<"$out" } select_storage() { diff --git a/pve/vm-core.func b/pve/vm-core.func index 011af99..670ecc0 100644 --- a/pve/vm-core.func +++ b/pve/vm-core.func @@ -787,7 +787,7 @@ cleanup() { if [[ -n "${TEMP_DIR:-}" && -d "$TEMP_DIR" ]]; then rm -rf "$TEMP_DIR" fi - # The final status nobody sent yet; post_update_to_api decides whether an early failure counts. + # The final status nobody sent yet; post_update_to_api drops it when the run never reached post_to_api_vm. if [[ "${POST_UPDATE_DONE:-}" != "true" ]] && declare -f post_update_to_api >/dev/null 2>&1; then if [[ $exit_code -ne 0 ]]; then post_update_to_api "failed" "$exit_code" diff --git a/tests/host-probes.sh b/tests/host-probes.sh new file mode 100644 index 0000000..5e43c56 --- /dev/null +++ b/tests/host-probes.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# The host checks read pveversion, pvesm and pvesh output that core/build.func +# collects in the background while the engine loads. A reader must wait for an +# unfinished probe, and fall back to the real command when a probe failed or +# never ran. +set -uo pipefail + +CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +eval "$(awk '/^_cs_probe[(][)] [{]/,/^}/' "$CORE_DIR/core/build.func")" +eval "$(awk '/^_cs_probe_read[(][)] [{]/,/^}/' "$CORE_DIR/core/core.func")" +eval "$(awk '/^_probe_has_storage[(][)] [{]/,/^}/' "$CORE_DIR/pve/backend.func")" + +CS_PROBE_DIR="$(mktemp -d)" +trap 'rm -rf -- "$CS_PROBE_DIR"' EXIT +passed=0 failed=0 + +expect() { + if [[ "$2" == "$3" ]]; then + echo "PASS $1" + passed=$((passed + 1)) + else + echo "FAIL $1: expected <$2>, got <$3>" >&2 + failed=$((failed + 1)) + fi +} + +_cs_probe "$CS_PROBE_DIR" pveversion sh -c 'sleep 0.4; echo pve-manager/9.1.4/5ac30304265fbd8e' +_cs_probe "$CS_PROBE_DIR" broken sh -c 'echo partial; exit 2' +_cs_probe "$CS_PROBE_DIR" rootdir printf 'Name Type Status Total Used Available %%\nlocal-lvm lvmthin active 1 1 1 1%%\n' +_cs_probe "$CS_PROBE_DIR" vztmpl printf 'Name Type Status Total Used Available %%\n' + +expect "a reader waits for a probe still running" "pve-manager/9.1.4/5ac30304265fbd8e" "$(_cs_probe_read pveversion)" +expect "a failed probe gives nothing" "fail" "$(_cs_probe_read broken >/dev/null && echo ok || echo fail)" +expect "a probe never started gives nothing" "fail" "$(_cs_probe_read nextid >/dev/null && echo ok || echo fail)" +expect "a storage listed by the probe counts" yes "$(_probe_has_storage rootdir && echo yes || echo no)" +expect "a header without storages does not" no "$(_probe_has_storage vztmpl && echo yes || echo no)" + +saved="$CS_PROBE_DIR" +CS_PROBE_DIR="" +expect "without a probe dir the caller runs the command" "fail" "$(_cs_probe_read pveversion >/dev/null && echo ok || echo fail)" +CS_PROBE_DIR="$saved" + +printf '%s passed, %s failed\n' "$passed" "$failed" +((failed == 0)) diff --git a/tests/telemetry-trace.sh b/tests/telemetry-trace.sh index ff78b8b..4a0739a 100644 --- a/tests/telemetry-trace.sh +++ b/tests/telemetry-trace.sh @@ -117,7 +117,12 @@ unset POST_TO_API_DONE : >"$TEST_DIR/statuses" CS_ERROR_MSG="Not enough space on 'local'. Required: 500GB, Available: 23.2GB" post_update_to_api failed 214 -expect "an early host failure is reported with its install record" "installing failed" "$(paste -sd' ' "$TEST_DIR/statuses")" +expect "a failed check sends nothing" "" "$(paste -sd' ' "$TEST_DIR/statuses")" +reset +POST_TO_API_DONE=false +: >"$TEST_DIR/statuses" +post_update_to_api failed 214 +expect "a failure after an install record that did not go out still does" failed "$(paste -sd' ' "$TEST_DIR/statuses")" reset unset POST_TO_API_DONE : >"$TEST_DIR/statuses" @@ -133,11 +138,18 @@ expect "message with quotes and backslashes round-trips" 'quote " backslash \ ta # --- tools and add-ons carry the same fields --- reset -CS_ERROR_MSG="No LXC containers found" CS_STEP="Scanning containers" -post_tool_to_api demo-tool failed 234 +CS_ERROR_MSG="Backup of CT 105 failed" CS_STEP="Backing up containers" +post_tool_to_api demo-tool failed 235 expect "tool failures stay failures" failed "$(json_field "$SENT" status)" expect "tool type" pve "$(json_field "$SENT" type)" -expect "tool message" "No LXC containers found" "$(json_field "$SENT" error_message)" +expect "tool message" "Backup of CT 105 failed" "$(json_field "$SENT" error_message)" +reset +CS_ERROR_MSG="No LXC containers found" +post_tool_to_api demo-tool failed 234 +expect "a tool without anything to work on sends nothing" no "$([[ -f "$SENT" ]] && echo yes || echo no)" +reset +post_addon_to_api demo-addon failed 10 +expect "an add-on whose prerequisite is missing sends nothing" no "$([[ -f "$SENT" ]] && echo yes || echo no)" printf '%s passed, %s failed\n' "$passed" "$failed" ((failed == 0)) diff --git a/ui/API.txt b/ui/API.txt index 7e91610..356652e 100644 --- a/ui/API.txt +++ b/ui/API.txt @@ -10,6 +10,8 @@ _cs_host_arch _cs_host_version_line _cs_live_base _cs_os_family +_cs_probe_read +_cs_pveversion_out _cs_reattach_output _cs_ref_line _cs_run_os_section @@ -73,6 +75,7 @@ _tm_post_progress _tm_post_run _tm_post_update _tm_quiet +_tm_requirement_code _tm_send _tm_step_output _vars_file_value diff --git a/ui/menu.func b/ui/menu.func index d33eddd..8463747 100644 --- a/ui/menu.func +++ b/ui/menu.func @@ -189,8 +189,10 @@ install_script() { # pvesh spawns a Perl client and waits on pvedaemon — the slowest thing here # by far, and it needs nothing the checks below produce. Start it now and # collect it when it is actually needed, so it runs while they do. + # core/build.func usually started it at load already (_cs_start_host_probes). local _nextid_file="" _nextid_pid="" - if ! declare -f get_next_container_identifier &>/dev/null && command -v pvesh &>/dev/null; then + if ! declare -f get_next_container_identifier &>/dev/null && [[ ! -f "${CS_PROBE_DIR:-/nonexistent}/nextid.out" ]] && + command -v pvesh &>/dev/null; then _nextid_file="$(mktemp 2>/dev/null)" || _nextid_file="" if [[ -n "$_nextid_file" ]]; then pvesh get /cluster/nextid >"$_nextid_file" 2>/dev/null & @@ -198,9 +200,11 @@ install_script() { fi fi - pve_check + # Root first: for any other user /etc/pve/storage.cfg is unreadable, and the + # cluster check in pve_check took that for a damaged cluster. shell_check root_check + pve_check ensure_whiptail arch_check arm64_notice @@ -215,6 +219,8 @@ install_script() { if declare -f get_next_container_identifier &>/dev/null; then NEXTID="$(get_next_container_identifier)" + elif NEXTID="$(_cs_probe_read nextid | tr -d '[:space:]')" && [[ -n "$NEXTID" ]]; then + : elif [[ -n "$_nextid_pid" ]]; then wait "$_nextid_pid" 2>/dev/null || true NEXTID="$(tr -d '[:space:]' <"$_nextid_file" 2>/dev/null)" From a0c9f1508e50562cdaf3e3bb24fc802a65fcd2be Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Thu, 29 Oct 2026 09:49:20 +0100 Subject: [PATCH 2/6] Answer storage, catalog and ID questions from the load-time probes From the menu to the storage dialog a default install waited about eight seconds on Perl: pvesh /cluster/resources once per ID tried, pveversion once per telemetry send, pvesm status before every storage decision, and pveam for the template catalog. The probes started at load now also take pvesm status and pveam available. check_storage_support, the storage dialogs, the preselect and space checks and the content checks after the dialog read them through _pvesm_status; _offer_storage_content drops them when it changes a storage. validate_container_id and the late ID check look for the guest config under /etc/pve/nodes, which pmxcfs keeps for every node; pvesh is the fallback without it. telemetry_collect_sysinfo takes the cached version, since every payload is built in a subshell and its own cache never reached the caller. variables no longer asks for the version before the menu. --- api/sysinfo.func | 8 +++++++- core/build.func | 8 +++++--- core/core.func | 20 ++++++++++++++++---- pve/backend.func | 44 ++++++++++++++++++++++++-------------------- tests/host-probes.sh | 24 ++++++++++++++++-------- ui/API.txt | 2 ++ ui/build-ui.func | 9 +++------ ui/defaults.func | 2 +- ui/validate.func | 38 +++++++++++++++++--------------------- 9 files changed, 91 insertions(+), 64 deletions(-) diff --git a/api/sysinfo.func b/api/sysinfo.func index beaadd8..bcfb6f4 100644 --- a/api/sysinfo.func +++ b/api/sysinfo.func @@ -235,7 +235,13 @@ telemetry_collect_sysinfo() { TM_PLATFORM="${TELEMETRY_PLATFORM:-}" TM_PVE_VERSION="" if command -v pveversion &>/dev/null; then - TM_PVE_VERSION=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true + # Every payload is built in $( ), so _TM_SYSINFO_DONE never reaches the + # caller and this ran once per send; _pve_version caches in a file. + if declare -f _pve_version >/dev/null 2>&1; then + TM_PVE_VERSION="$(_pve_version)" + else + TM_PVE_VERSION=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true + fi TM_PLATFORM="${TM_PLATFORM:-pve}" elif command -v incus &>/dev/null; then TM_PVE_VERSION="incus-$(incus version 2>/dev/null | head -1 | awk '{print $NF}')" || true diff --git a/core/build.func b/core/build.func index b675f07..ed49028 100644 --- a/core/build.func +++ b/core/build.func @@ -305,9 +305,9 @@ _cs_prefetch_engine() { return 0 } -# The checks before the settings menu wait on pveversion, pvesm and pvesh, each -# a Perl start of up to a second. Start them now, while the engine downloads and -# the header loads; _cs_probe_read hands the checks what they printed. +# The checks, the settings and the storage dialogs wait on pveversion, pvesm, +# pveam and pvesh, each a Perl start of up to a second. Start them now, while the +# engine downloads and the header loads; _cs_probe_read hands over what they printed. _cs_probe() { local dir="$1" name="$2" shift 2 @@ -327,8 +327,10 @@ _cs_start_host_probes() { done [[ -n "$dir" ]] || return 0 _cs_probe "$dir" pveversion pveversion + _cs_probe "$dir" pvesm pvesm status _cs_probe "$dir" vztmpl pvesm status -content vztmpl _cs_probe "$dir" rootdir pvesm status -content rootdir + _cs_probe "$dir" pveam pveam available -section system _cs_probe "$dir" nextid pvesh get /cluster/nextid [[ -f /etc/pve/corosync.conf ]] && _cs_probe "$dir" pvecm pvecm status CS_PROBE_DIR="$dir" diff --git a/core/core.func b/core/core.func index 6459856..aefaa93 100644 --- a/core/core.func +++ b/core/core.func @@ -563,6 +563,15 @@ _cs_probe_read() { cat "${dir}/$1.out" } +# _cs_probe_drop ... - forget probes whose answer a change made stale +_cs_probe_drop() { + local name + for name in "$@"; do + rm -f "${CS_PROBE_DIR:-/nonexistent}/${name}.out" "${CS_PROBE_DIR:-/nonexistent}/${name}.rc" 2>/dev/null + done + return 0 +} + _cs_pveversion_out() { _cs_probe_read pveversion || pveversion 2>/dev/null } @@ -584,7 +593,9 @@ _pve_version() { # reports it verbatim as pve_version. A fixed "PVE Version" prefix therefore # printed "PVE Version Incus 7.3" on Incus hosts. _cs_host_version_line() { - local v="${PVEVERSION:-unknown}" + local v="${PVEVERSION:-}" + [[ -z "$v" ]] && command -v pveversion >/dev/null 2>&1 && v="$(_pve_version)" + v="${v:-unknown}" if [[ "$v" == Incus* ]]; then v="${v#Incus}" v="${v# }" @@ -661,11 +672,11 @@ _cs_scripts_ref_line() { _pveam_available() { local dir cache dir="$(_cs_runtime_cache_dir)" || { - pveam available -section system 2>/dev/null - return + _cs_probe_read pveam || pveam available -section system 2>/dev/null + return 0 } cache="${dir}/cs-pveam.$$" - [[ -s "$cache" ]] || pveam available -section system 2>/dev/null >"$cache" + [[ -s "$cache" ]] || { _cs_probe_read pveam || pveam available -section system 2>/dev/null; } >"$cache" cat "$cache" 2>/dev/null } @@ -674,6 +685,7 @@ _pveam_available() { _pveam_update() { local dir dir="$(_cs_runtime_cache_dir)" && rm -f "${dir}/cs-pveam.$$" + _cs_probe_drop pveam if command -v timeout &>/dev/null; then timeout 30 pveam update >/dev/null 2>&1 || return 1 else diff --git a/pve/backend.func b/pve/backend.func index 804e81e..29d2db8 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -2084,13 +2084,13 @@ resolve_storage_preselect() { *) return 1 ;; esac [[ -z "$preselect" ]] && return 1 - if ! pvesm status -content "$required_content" | awk 'NR>1{print $1}' | grep -qx -- "$preselect"; then + if ! _pvesm_status "$required_content" | awk 'NR>1{print $1}' | grep -qx -- "$preselect"; then msg_warn "Preselected storage '${preselect}' does not support content '${required_content}' (or not found)" return 1 fi local line total used free - line="$(pvesm status | awk -v s="$preselect" 'NR>1 && $1==s {print $0}')" + line="$(_pvesm_status | awk -v s="$preselect" 'NR>1 && $1==s {print $0}')" if [[ -z "$line" ]]; then STORAGE_INFO="n/a" else @@ -2172,13 +2172,24 @@ fix_gpu_gids() { fi } +# _pvesm_status [content] - `pvesm status [-content ]`, from the probe +# core/build.func started at load (all, vztmpl, rootdir) while it holds. +# _offer_storage_content drops the probes when it changes a storage. +_pvesm_status() { + if [[ -n "${1:-}" ]]; then + _cs_probe_read "$1" || pvesm status -content "$1" + else + _cs_probe_read pvesm || pvesm status + fi +} + check_storage_support() { local CONTENT="$1" VALID=0 while IFS= read -r line; do local STORAGE_NAME STORAGE_NAME=$(awk '{print $1}' <<<"$line") [[ -n "$STORAGE_NAME" ]] && VALID=1 - done < <(pvesm status -content "$CONTENT" 2>/dev/null | awk 'NR>1') + done < <(_pvesm_status "$CONTENT" 2>/dev/null | awk 'NR>1') [[ $VALID -eq 1 ]] } @@ -2228,7 +2239,8 @@ _offer_storage_content() { current="$(_storage_content_of "$choice")" [[ "$current" == "none" ]] && current="" - if pvesm set "$choice" --content "${current:+${current},}${content}" >/dev/null 2>&1 && check_storage_support "$content"; then + if pvesm set "$choice" --content "${current:+${current},}${content}" >/dev/null 2>&1 && + _cs_probe_drop "$content" pvesm && check_storage_support "$content"; then msg_ok "Enabled ${label} on storage '${choice}'" return 0 fi @@ -2245,16 +2257,8 @@ _offer_storage_content() { # content switched off, which can be switched on in place. # ------------------------------------------------------------------------------ storage_content_check() { - _probe_has_storage "vztmpl" || check_storage_support "vztmpl" || _offer_storage_content "vztmpl" || exit 120 - _probe_has_storage "rootdir" || check_storage_support "rootdir" || _offer_storage_content "rootdir" || exit 119 -} - -# The probe started at load answers yes; a no is asked again live, since -# _offer_storage_content may change it and nothing else reads the probe. -_probe_has_storage() { - local out - out="$(_cs_probe_read "$1")" || return 1 - awk 'NR > 1 && $1 != "" { found = 1 } END { exit !found }' <<<"$out" + check_storage_support "vztmpl" || _offer_storage_content "vztmpl" || exit 120 + check_storage_support "rootdir" || _offer_storage_content "rootdir" || exit 119 } select_storage() { @@ -2303,7 +2307,7 @@ select_storage() { STORAGE_MAP["$DISPLAY"]="$TAG" MENU+=("$DISPLAY" "$INFO" "OFF") ((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY} - done < <(pvesm status -content "$CONTENT" | awk 'NR>1') + done < <(_pvesm_status "$CONTENT" | awk 'NR>1') if [[ ${#MENU[@]} -eq 0 ]]; then msg_error "No storage found for content type '$CONTENT'." @@ -2447,7 +2451,7 @@ validate_storage_space() { # Get full storage line from pvesm status local storage_line - storage_line=$(pvesm status 2>/dev/null | awk -v s="$storage" '$1 == s {print $0}') + storage_line=$(_pvesm_status 2>/dev/null | awk -v s="$storage" '$1 == s {print $0}') if [[ -z "$storage_line" ]]; then [[ "$show_dialog" == "yes" ]] && whiptail --msgbox "⚠️ Warning: Storage '$storage' not found! @@ -2503,7 +2507,7 @@ check_storage_health() { [[ -z "$storage" ]] && return 0 local storage_line storage_type total_kb avail_kb used_pct free_gb_fmt - storage_line=$(pvesm status 2>/dev/null | awk -v s="$storage" '$1 == s {print $0}') + storage_line=$(_pvesm_status 2>/dev/null | awk -v s="$storage" '$1 == s {print $0}') [[ -z "$storage_line" ]] && return 0 storage_type=$(awk '{print $2}' <<<"$storage_line") @@ -3077,7 +3081,7 @@ create_lxc_container() { msg_error "ID cannot be less than 100." exit 205 } - if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then + if _vmid_config_exists "$CTID"; then msg_warn "Container/VM ID $CTID is already in use (detected late). Reassigning..." CTID=$(get_valid_container_id "$((CTID + 1))") export CTID @@ -3175,7 +3179,7 @@ create_lxc_container() { ;; esac - if ! pvesm status -content rootdir 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$CONTAINER_STORAGE"; then + if ! _pvesm_status rootdir 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$CONTAINER_STORAGE"; then msg_error "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) does not support 'rootdir' content." msg_custom "💡" "${YW}" "Enable 'Disk image' (rootdir) for storage '${CONTAINER_STORAGE}' in:" msg_custom " " "${YW}" "Datacenter → Storage → ${CONTAINER_STORAGE} → Edit → Content" @@ -3191,7 +3195,7 @@ create_lxc_container() { msg_info "Validating template storage '$TEMPLATE_STORAGE'" TEMPLATE_TYPE=$(grep -E "^[^:]+: $TEMPLATE_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1 || true) - if ! pvesm status -content vztmpl 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$TEMPLATE_STORAGE"; then + if ! _pvesm_status vztmpl 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$TEMPLATE_STORAGE"; then msg_warn "Template storage '$TEMPLATE_STORAGE' may not support 'vztmpl'" fi msg_ok "Template storage '$TEMPLATE_STORAGE' validated${TEMPLATE_STORAGE_INFO:+ - ${TEMPLATE_STORAGE_INFO}}" diff --git a/tests/host-probes.sh b/tests/host-probes.sh index 5e43c56..20c0dae 100644 --- a/tests/host-probes.sh +++ b/tests/host-probes.sh @@ -1,14 +1,14 @@ #!/usr/bin/env bash -# The host checks read pveversion, pvesm and pvesh output that core/build.func -# collects in the background while the engine loads. A reader must wait for an -# unfinished probe, and fall back to the real command when a probe failed or -# never ran. +# The checks, settings and storage dialogs read pveversion, pvesm, pveam and +# pvesh output that core/build.func collects in the background while the engine +# loads. A reader must wait for an unfinished probe, and fall back to the real +# command when a probe failed, never ran or was dropped after a change. set -uo pipefail CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" eval "$(awk '/^_cs_probe[(][)] [{]/,/^}/' "$CORE_DIR/core/build.func")" -eval "$(awk '/^_cs_probe_read[(][)] [{]/,/^}/' "$CORE_DIR/core/core.func")" -eval "$(awk '/^_probe_has_storage[(][)] [{]/,/^}/' "$CORE_DIR/pve/backend.func")" +eval "$(awk '/^_cs_probe_(read|drop)[(][)] [{]/,/^}/' "$CORE_DIR/core/core.func")" +eval "$(awk '/^(_pvesm_status|check_storage_support)[(][)] [{]/,/^}/' "$CORE_DIR/pve/backend.func")" CS_PROBE_DIR="$(mktemp -d)" trap 'rm -rf -- "$CS_PROBE_DIR"' EXIT @@ -24,6 +24,9 @@ expect() { fi } +# Stands in for the live command, so a fallback is visible. +pvesm() { printf 'Name Type Status Total Used Available %%\nlive dir active 1 1 1 1%%\n'; } + _cs_probe "$CS_PROBE_DIR" pveversion sh -c 'sleep 0.4; echo pve-manager/9.1.4/5ac30304265fbd8e' _cs_probe "$CS_PROBE_DIR" broken sh -c 'echo partial; exit 2' _cs_probe "$CS_PROBE_DIR" rootdir printf 'Name Type Status Total Used Available %%\nlocal-lvm lvmthin active 1 1 1 1%%\n' @@ -32,8 +35,13 @@ _cs_probe "$CS_PROBE_DIR" vztmpl printf 'Name Type Status Total Used Available % expect "a reader waits for a probe still running" "pve-manager/9.1.4/5ac30304265fbd8e" "$(_cs_probe_read pveversion)" expect "a failed probe gives nothing" "fail" "$(_cs_probe_read broken >/dev/null && echo ok || echo fail)" expect "a probe never started gives nothing" "fail" "$(_cs_probe_read nextid >/dev/null && echo ok || echo fail)" -expect "a storage listed by the probe counts" yes "$(_probe_has_storage rootdir && echo yes || echo no)" -expect "a header without storages does not" no "$(_probe_has_storage vztmpl && echo yes || echo no)" +expect "pvesm status for a content comes from its probe" "local-lvm" "$(_pvesm_status rootdir | awk 'NR == 2 {print $1}')" +expect "a storage listed by the probe counts" yes "$(check_storage_support rootdir && echo yes || echo no)" +expect "a probe header without storages does not" no "$(check_storage_support vztmpl && echo yes || echo no)" +expect "without a probe for it pvesm runs live" "live" "$(_pvesm_status | awk 'NR == 2 {print $1}')" + +_cs_probe_drop rootdir +expect "a dropped probe is asked live" "live" "$(_pvesm_status rootdir | awk 'NR == 2 {print $1}')" saved="$CS_PROBE_DIR" CS_PROBE_DIR="" diff --git a/ui/API.txt b/ui/API.txt index 356652e..1e970af 100644 --- a/ui/API.txt +++ b/ui/API.txt @@ -10,6 +10,7 @@ _cs_host_arch _cs_host_version_line _cs_live_base _cs_os_family +_cs_probe_drop _cs_probe_read _cs_pveversion_out _cs_reattach_output @@ -79,6 +80,7 @@ _tm_requirement_code _tm_send _tm_step_output _vars_file_value +_vmid_config_exists _write_storage_to_vars advanced_settings apt_conffile_guard diff --git a/ui/build-ui.func b/ui/build-ui.func index 9cb54cd..5476b91 100644 --- a/ui/build-ui.func +++ b/ui/build-ui.func @@ -90,12 +90,9 @@ variables() { combined_log="/var/log/community-scripts/install-${SESSION_ID}-combined-$(date +%Y%m%d_%H%M%S).log" fi - # Get Proxmox VE version and kernel version - if command -v pveversion >/dev/null 2>&1; then - PVEVERSION="$(_pve_version)" - else - PVEVERSION="N/A" - fi + # On a PVE host the version is read where it is shown (_pve_version, cached): + # asking here held the header for the whole pveversion start. + command -v pveversion >/dev/null 2>&1 || PVEVERSION="N/A" KERNEL_VERSION=$(uname -r) # Capture app-declared defaults (for precedence logic) diff --git a/ui/defaults.func b/ui/defaults.func index d379651..d7ffc7b 100644 --- a/ui/defaults.func +++ b/ui/defaults.func @@ -172,7 +172,7 @@ resolve_os_version() { ') [[ -n "$candidate" ]] || return 0 - msg_warn "${ostype^} ${want} is not in this host's template catalog (PVE ${PVEVERSION:-unknown})" + msg_warn "${ostype^} ${want} is not in this host's template catalog (PVE ${PVEVERSION:-$(_pve_version)})" if [[ "${PHS_SILENT:-0}" != "1" ]] && command -v whiptail &>/dev/null && [ -t 0 ] && [[ "$TERM" != "dumb" ]]; then local -a menu=() diff --git a/ui/validate.func b/ui/validate.func index b2605a3..53db13a 100644 --- a/ui/validate.func +++ b/ui/validate.func @@ -182,6 +182,16 @@ install_ssh_keys_into_ct() { return 0 } +# _vmid_config_exists - a VM or container config for the ID on any node +_vmid_config_exists() { + local f + for f in /etc/pve/nodes/*/qemu-server/"$1".conf /etc/pve/nodes/*/lxc/"$1".conf \ + /etc/pve/qemu-server/"$1".conf /etc/pve/lxc/"$1".conf; do + [[ -f "$f" ]] && return 0 + done + return 1 +} + # ------------------------------------------------------------------------------ # validate_container_id() # @@ -198,11 +208,13 @@ validate_container_id() { return 1 fi - # Ask the cluster first: /etc/pve/qemu-server and /etc/pve/lxc are symlinks - # into this node's own directory, so checking them alone only sees local - # guests. On a cluster the wizard then hands out an ID already in use - # elsewhere and pct create fails long after the choice was made. - if command -v pvesh &>/dev/null; then + # /etc/pve/qemu-server and /etc/pve/lxc only point into this node's own + # directory; /etc/pve/nodes holds every node's guests. pvesh's + # /cluster/resources says the same in about a second, and the ID search + # asks once per ID it tries, so it is only the fallback without pmxcfs. + if [[ -d "/etc/pve/nodes" ]]; then + _vmid_config_exists "$ctid" && return 1 + elif command -v pvesh &>/dev/null; then local cluster_ids cluster_ids=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null | grep -oP '"vmid":\s*\K[0-9]+' 2>/dev/null || true) @@ -211,22 +223,6 @@ validate_container_id() { fi fi - # Local config, for when pvesh is unavailable or answered with nothing. - if [[ -f "/etc/pve/qemu-server/${ctid}.conf" ]] || [[ -f "/etc/pve/lxc/${ctid}.conf" ]]; then - return 1 - fi - - # Every node's config directly, which also covers the window where pmxcfs has - # the file but /cluster/resources has not caught up. - if [[ -d "/etc/pve/nodes" ]]; then - local node_dir - for node_dir in /etc/pve/nodes/*/; do - if [[ -f "${node_dir}qemu-server/${ctid}.conf" ]] || [[ -f "${node_dir}lxc/${ctid}.conf" ]]; then - return 1 - fi - done - fi - # Check if ID is used in LVM logical volumes if lvs --noheadings -o lv_name 2>/dev/null | grep -qE "(^|[-_])${ctid}($|[-_])"; then return 1 From 79ed6c53bed3e4ac4c2cc323a234bf53dea1b3bd Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Thu, 29 Oct 2026 09:52:58 +0100 Subject: [PATCH 3/6] Offer the checked container ID in the advanced settings base_settings already found a free ID before advanced_settings runs. The dialog offered NEXTID instead, which a leftover disk can hold, and then answered its own suggestion with 'ID already in use'. --- ui/advanced.func | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/advanced.func b/ui/advanced.func index f8c31ee..080ba4b 100644 --- a/ui/advanced.func +++ b/ui/advanced.func @@ -43,7 +43,8 @@ advanced_settings() { local _ct_type="${var_unprivileged:-1}" local _pw="" local _pw_display="Automatic Login" - local _ct_id="$NEXTID" + # base_settings ran first and checked it; NEXTID can be taken by a leftover disk. + local _ct_id="${CT_ID:-$NEXTID}" local _hostname="$NSAPP" local _disk_size="${var_disk:-4}" local _core_count="${var_cpu:-1}" From d6f5589fab4e9791ebb1efbfbaa93f455a5e3f68 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Thu, 29 Oct 2026 10:03:51 +0100 Subject: [PATCH 4/6] Keep the VM path from starting pveversion and qm per telemetry send The VM engine has no _pve_version, so every telemetry payload, built in a subshell, started pveversion again; pve_check now keeps the version it read and telemetry_collect_sysinfo uses it. cleanup asked qm config whether the VM exists; its config file says the same. --- api/sysinfo.func | 5 ++++- pve/vm-core.func | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/api/sysinfo.func b/api/sysinfo.func index bcfb6f4..32b7fc2 100644 --- a/api/sysinfo.func +++ b/api/sysinfo.func @@ -236,9 +236,12 @@ telemetry_collect_sysinfo() { TM_PVE_VERSION="" if command -v pveversion &>/dev/null; then # Every payload is built in $( ), so _TM_SYSINFO_DONE never reaches the - # caller and this ran once per send; _pve_version caches in a file. + # caller and this ran once per send; _pve_version caches in a file, and + # the VM engine, which lacks it, keeps what its pve_check read. if declare -f _pve_version >/dev/null 2>&1; then TM_PVE_VERSION="$(_pve_version)" + elif [[ "${PVEVERSION:-}" =~ ^[0-9] ]]; then + TM_PVE_VERSION="$PVEVERSION" else TM_PVE_VERSION=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true fi diff --git a/pve/vm-core.func b/pve/vm-core.func index 670ecc0..9041aea 100644 --- a/pve/vm-core.func +++ b/pve/vm-core.func @@ -792,7 +792,7 @@ cleanup() { if [[ $exit_code -ne 0 ]]; then post_update_to_api "failed" "$exit_code" elif [[ -n "${POST_TO_API_DONE:-}" ]]; then - if [[ -n "${VMID:-}" ]] && qm config "$VMID" &>/dev/null; then + if [[ -n "${VMID:-}" && -f "/etc/pve/qemu-server/${VMID}.conf" ]]; then # Clean exit with the VM in place: a script that never said so itself. post_update_to_api "done" "none" else @@ -816,6 +816,8 @@ check_root() { pve_check() { local pve_ver pve_ver="$(declare -f _pve_version >/dev/null 2>&1 && _pve_version || pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')" + # Telemetry builds each payload in a subshell and would start pveversion again. + PVEVERSION="${PVEVERSION:-$pve_ver}" if [[ "$pve_ver" =~ ^8\.([0-9]+) ]]; then local minor="${BASH_REMATCH[1]}" From ebd7ff5bd8b96d93b7a5d6e8fac9eb633ae81a1e Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Thu, 29 Oct 2026 10:15:27 +0100 Subject: [PATCH 5/6] Start the probes with one fork, ask the OS check early, skip pulls from no container The probes forked six times from the script's own shell before the engine download; on the arm64 host that held the download back by a fifth of a second. One subshell now starts them. lxc_os_support_check asks pve-container through Perl whether it can set up the release, between "Creating" and the storage dialog. variables() knows the release and starts that question as a probe; a release changed in the advanced settings is asked live. ensure_log_on_host pulled the install log and the error capture from the container after every failure, including those before pct create, where each pull waited for its timeout to find no container. --- core/build.func | 37 +++++++++++++++++++++++++------------ core/core.func | 11 +++++++++-- pve/backend.func | 14 +++++++++++--- tests/host-probes.sh | 6 ++++-- ui/API.txt | 1 + ui/build-ui.func | 9 +++++++++ 6 files changed, 59 insertions(+), 19 deletions(-) diff --git a/core/build.func b/core/build.func index ed49028..458a4bd 100644 --- a/core/build.func +++ b/core/build.func @@ -308,31 +308,44 @@ _cs_prefetch_engine() { # The checks, the settings and the storage dialogs wait on pveversion, pvesm, # pveam and pvesh, each a Perl start of up to a second. Start them now, while the # engine downloads and the header loads; _cs_probe_read hands over what they printed. -_cs_probe() { +# _cs_probe_run - run it, keep its output and exit code +_cs_probe_run() { local dir="$1" name="$2" shift 2 - : >"${dir}/${name}.out" - ("$@" >"${dir}/${name}.out" 2>/dev/null - echo "$?" >"${dir}/${name}.tmp" && mv -f "${dir}/${name}.tmp" "${dir}/${name}.rc") "${dir}/${name}.out" 2>/dev/null + echo "$?" >"${dir}/${name}.tmp" && mv -f "${dir}/${name}.tmp" "${dir}/${name}.rc" +} + +# _cs_probe - the same in the background. The empty +# .out tells readers a result is coming before the subshell has started. +_cs_probe() { + : >"${1}/${2}.out" + _cs_probe_run "$@" /dev/null 2>&1 || return 0 [[ "$(id -u)" == "0" ]] || return 0 - local base dir="" + local base dir="" name names=(pveversion pvesm vztmpl rootdir pveam nextid) for base in /dev/shm "${TMPDIR:-/tmp}" /tmp; do [[ -d "$base" && -w "$base" ]] || continue dir="$(mktemp -d "${base}/cs-probe.XXXXXX" 2>/dev/null)" && break dir="" done [[ -n "$dir" ]] || return 0 - _cs_probe "$dir" pveversion pveversion - _cs_probe "$dir" pvesm pvesm status - _cs_probe "$dir" vztmpl pvesm status -content vztmpl - _cs_probe "$dir" rootdir pvesm status -content rootdir - _cs_probe "$dir" pveam pveam available -section system - _cs_probe "$dir" nextid pvesh get /cluster/nextid - [[ -f /etc/pve/corosync.conf ]] && _cs_probe "$dir" pvecm pvecm status + [[ -f /etc/pve/corosync.conf ]] && names+=(pvecm) + for name in "${names[@]}"; do : >"${dir}/${name}.out"; done + # One fork from this shell; the probes branch off inside it. + ( + _cs_probe_run "$dir" pveversion pveversion & + _cs_probe_run "$dir" pvesm pvesm status & + _cs_probe_run "$dir" vztmpl pvesm status -content vztmpl & + _cs_probe_run "$dir" rootdir pvesm status -content rootdir & + _cs_probe_run "$dir" pveam pveam available -section system & + _cs_probe_run "$dir" nextid pvesh get /cluster/nextid & + [[ -f "${dir}/pvecm.out" ]] && _cs_probe_run "$dir" pvecm pvecm status & + wait + ) - a finished probe's exit code, for checks that answer +# with one; fails when there is no such probe +_cs_probe_rc() { local dir="${CS_PROBE_DIR:-}" n=0 [[ -n "$dir" && -f "${dir}/$1.out" ]] || return 1 while [[ ! -f "${dir}/$1.rc" ]]; do ((n++ < 1200)) || return 1 sleep 0.05 done - [[ "$(<"${dir}/$1.rc")" == "0" ]] || return 1 - cat "${dir}/$1.out" + cat "${dir}/$1.rc" } # _cs_probe_drop ... - forget probes whose answer a change made stale diff --git a/pve/backend.func b/pve/backend.func index 29d2db8..009326b 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -313,7 +313,11 @@ lxc_os_supported() { lxc_os_support_check() { local os="${var_os:-}" ver="${var_version:-}" rc=0 [[ -n "$os" && -n "$ver" ]] || return 0 - lxc_os_supported "$os" "$ver" || rc=$? + # variables() asked in the background; a release changed since is asked now. + rc="$(_cs_probe_rc "lxcos-${os}-${ver}")" || { + rc=0 + lxc_os_supported "$os" "$ver" || rc=$? + } [[ $rc -eq 1 ]] || return 0 local installed candidate="" older="" v @@ -3981,6 +3985,10 @@ ensure_log_on_host() { # Try pulling from container and creating combined log if [[ -n "${CTID:-}" && -n "${SESSION_ID:-}" ]] && command -v pct &>/dev/null; then + # A run that failed before pct create has no container to pull from, and + # each pull below waited up to 8 s to find that out. + local has_ct=0 + [[ -f "/etc/pve/lxc/${CTID}.conf" ]] && has_ct=1 local host_combined="" if [[ -n "${combined_log:-}" && -s "${combined_log}" ]]; then host_combined="$combined_log" @@ -4012,7 +4020,7 @@ ensure_log_on_host() { fi # Pull INSTALL_LOG from container (with timeout to prevent hangs on dead containers) local temp_log="/tmp/.install-temp-${SESSION_ID}.log" - if timeout 8 pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "$temp_log" 2>/dev/null; then + if ((has_ct)) && timeout 8 pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "$temp_log" 2>/dev/null; then { echo "================================================================================" echo "PHASE 2: APPLICATION INSTALLATION (Container)" @@ -4026,7 +4034,7 @@ ensure_log_on_host() { # Also pull the structured error capture for the telemetry error trace if [[ -z "${TELEMETRY_ERRINFO:-}" || ! -s "${TELEMETRY_ERRINFO:-}" ]]; then local host_errinfo="/tmp/.errinfo-${SESSION_ID}" - if timeout 8 pct pull "$CTID" "/root/.install-${SESSION_ID}.log.errinfo" "$host_errinfo" 2>/dev/null && [[ -s "$host_errinfo" ]]; then + if ((has_ct)) && timeout 8 pct pull "$CTID" "/root/.install-${SESSION_ID}.log.errinfo" "$host_errinfo" 2>/dev/null && [[ -s "$host_errinfo" ]]; then TELEMETRY_ERRINFO="$host_errinfo" export TELEMETRY_ERRINFO fi diff --git a/tests/host-probes.sh b/tests/host-probes.sh index 20c0dae..14f5ba3 100644 --- a/tests/host-probes.sh +++ b/tests/host-probes.sh @@ -6,8 +6,8 @@ set -uo pipefail CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" -eval "$(awk '/^_cs_probe[(][)] [{]/,/^}/' "$CORE_DIR/core/build.func")" -eval "$(awk '/^_cs_probe_(read|drop)[(][)] [{]/,/^}/' "$CORE_DIR/core/core.func")" +eval "$(awk '/^_cs_probe(_run)?[(][)] [{]/,/^}/' "$CORE_DIR/core/build.func")" +eval "$(awk '/^_cs_probe_(read|drop|rc)[(][)] [{]/,/^}/' "$CORE_DIR/core/core.func")" eval "$(awk '/^(_pvesm_status|check_storage_support)[(][)] [{]/,/^}/' "$CORE_DIR/pve/backend.func")" CS_PROBE_DIR="$(mktemp -d)" @@ -29,11 +29,13 @@ pvesm() { printf 'Name Type Status Total Used Available %%\nlive dir active 1 1 _cs_probe "$CS_PROBE_DIR" pveversion sh -c 'sleep 0.4; echo pve-manager/9.1.4/5ac30304265fbd8e' _cs_probe "$CS_PROBE_DIR" broken sh -c 'echo partial; exit 2' +_cs_probe "$CS_PROBE_DIR" osfail sh -c 'exit 1' _cs_probe "$CS_PROBE_DIR" rootdir printf 'Name Type Status Total Used Available %%\nlocal-lvm lvmthin active 1 1 1 1%%\n' _cs_probe "$CS_PROBE_DIR" vztmpl printf 'Name Type Status Total Used Available %%\n' expect "a reader waits for a probe still running" "pve-manager/9.1.4/5ac30304265fbd8e" "$(_cs_probe_read pveversion)" expect "a failed probe gives nothing" "fail" "$(_cs_probe_read broken >/dev/null && echo ok || echo fail)" +expect "a check probe hands over its exit code" "1" "$(_cs_probe_rc osfail)" expect "a probe never started gives nothing" "fail" "$(_cs_probe_read nextid >/dev/null && echo ok || echo fail)" expect "pvesm status for a content comes from its probe" "local-lvm" "$(_pvesm_status rootdir | awk 'NR == 2 {print $1}')" expect "a storage listed by the probe counts" yes "$(check_storage_support rootdir && echo yes || echo no)" diff --git a/ui/API.txt b/ui/API.txt index 1e970af..8514e9e 100644 --- a/ui/API.txt +++ b/ui/API.txt @@ -11,6 +11,7 @@ _cs_host_version_line _cs_live_base _cs_os_family _cs_probe_drop +_cs_probe_rc _cs_probe_read _cs_pveversion_out _cs_reattach_output diff --git a/ui/build-ui.func b/ui/build-ui.func index 5476b91..beae560 100644 --- a/ui/build-ui.func +++ b/ui/build-ui.func @@ -95,6 +95,15 @@ variables() { command -v pveversion >/dev/null 2>&1 || PVEVERSION="N/A" KERNEL_VERSION=$(uname -r) + # pve-container's verdict on the release is a Perl start too; the script set + # the release before calling this, and lxc_os_support_check reads the answer. + case "${var_os:-}" in + debian | ubuntu) + [[ -n "${CS_PROBE_DIR:-}" && -n "${var_version:-}" ]] && declare -f lxc_os_supported >/dev/null 2>&1 && + _cs_probe "$CS_PROBE_DIR" "lxcos-${var_os}-${var_version}" lxc_os_supported "$var_os" "$var_version" + ;; + esac + # Capture app-declared defaults (for precedence logic) # These values are set by the app script BEFORE default.vars is loaded # If app declares higher values than default.vars, app values take precedence From 72eaddb3fd95adc2dd9b864ab513c4208108fd09 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Thu, 29 Oct 2026 10:20:34 +0100 Subject: [PATCH 6/6] Describe the load-time probes and who reads them --- docs/api.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/api.md b/docs/api.md index 708559c..ecb53ed 100644 --- a/docs/api.md +++ b/docs/api.md @@ -22,9 +22,14 @@ install. Tools and add-ons have no such point, so a failure with a "requirement not met" code (10, 103, 104, 106, 232, 233, 234, 236, 238) sends nothing either. Update runs inside a container send nothing. -The checks wait on `pveversion`, `pvesm status` and `pvesh`. `core/build.func` -starts these in the background before it downloads the engine, and the checks -read their output through `_cs_probe_read`. +The checks, the settings and the storage dialogs wait on `pveversion`, +`pvesm status` (all, `vztmpl`, `rootdir`), `pveam available` and `pvesh`. +`core/build.func` starts these in the background before it downloads the +engine, `variables` adds pve-container's verdict on the release, and the +readers take them through `_cs_probe_read` / `_cs_probe_rc` (`_pvesm_status` +for storage). `_offer_storage_content` and `_pveam_update` drop what they +change with `_cs_probe_drop`. Guest IDs are checked against the configs under +`/etc/pve/nodes`, which pmxcfs keeps for every node. ## What a failure carries (payload version 3)