Skip to content

Commit a4a1289

Browse files
committed
Retry every engine self-download, not just the two in build.func
The previous commit covered build.func and lxc/install.func. The engine fetches itself from a dozen more places -- api/api.func pulling its four parts, lib/tools.func pulling its five, the incus and VM entry points, the UI loader -- and each of those was still a single attempt. Telemetry counts roughly 240 runs in fourteen days that failed because the engine could not download its own parts; none of them are the fault of the script being installed. The retry options are repeated per file rather than shared. There is nowhere to put a shared copy that does not itself need downloading first, which is the problem being solved. Two things the flags alone got wrong: curl -S prints a line per failed attempt, so a download that recovered on the second try now showed the user two errors and then quietly worked, mid spinner. _cs_curl_retry holds stderr back and releases it only if every attempt failed, so a recovery is silent and a real failure still explains itself. --tries and --waitretry are GNU wget options. The wget fallback exists for the minimal systems that ship BusyBox wget, where an unrecognised option is a hard failure rather than a slower download, so those stay single-attempt. Also, found while auditing the fetch sites: - The debug-MOTD branch fetched ${CORE_URL}/install.func, a path that does not exist -- the file is lxc/install.func. motd_ssh was never defined, so DEV_MODE_MOTD has silently done nothing. - alpine-install.func sourced lib/alpine.func through a bare source <(curl ...), which hides the exit code: a failed download sourced nothing and left every helper below missing with no explanation. It now goes through _bootstrap_source like the rest of that file's bootstrap. - get_header had no timeout at all. It stays without retry, since it only decides whether an ASCII banner appears, but an unbounded connect meant a black-holed network waited out the OS TCP timeout per candidate. Verified against a local server that 503s twice then succeeds: three requests, body returned, nothing on stderr. A 404 still fails on the first request in under a second with curl's message intact.
1 parent 33cddcf commit a4a1289

12 files changed

Lines changed: 110 additions & 30 deletions

File tree

‎api/api.func‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ TELEMETRY_ERROR_MAX_BYTES=10240
8484
# telemetry.func payload, transport and the senders
8585
# ==============================================================================
8686

87+
# Retry options for engine fetches. Repeated in each file that bootstraps
88+
# itself over the network rather than shared, because there is nowhere to put
89+
# a shared copy that does not itself need downloading first. core/build.func
90+
# carries the full reasoning.
91+
_CS_CURL_RETRY=(--retry 3 --retry-delay 1 --retry-connrefused --connect-timeout 10)
92+
8793
_api_source_part() {
8894
local part="$1"
8995

@@ -96,7 +102,7 @@ _api_source_part() {
96102
local base="${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}"
97103
if command -v curl >/dev/null 2>&1; then
98104
# shellcheck disable=SC1090
99-
source <(curl -fsSL "${base}/api/${part}.func")
105+
source <(curl -fsSL "${_CS_CURL_RETRY[@]}" "${base}/api/${part}.func")
100106
else
101107
# shellcheck disable=SC1090
102108
source <(wget -qO- "${base}/api/${part}.func")

‎core/build.func‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,28 @@ _cs_net_log() {
168168
#
169169
# Three attempts with a one second base delay; curl doubles it, so the worst case
170170
# adds about three seconds to a download that was going to fail anyway.
171+
#
172+
# The wget fallbacks deliberately stay a single attempt: --tries/--waitretry are
173+
# GNU options, and the fallback exists for exactly the minimal systems that ship
174+
# BusyBox wget, where an unrecognised option is a hard failure rather than a
175+
# slower download.
171176
_CS_CURL_RETRY=(--retry 3 --retry-delay 1 --retry-connrefused --connect-timeout 10)
172177

178+
# curl -S prints one line per failed attempt, so a download that recovers on the
179+
# second try would show the user two errors and then quietly work — during a
180+
# spinner, no less. Hold stderr back and release it only if every attempt failed.
181+
_cs_curl_retry() {
182+
local url="${1:?url}" err rc=0
183+
err="$(mktemp 2>/dev/null)" || {
184+
curl -fsSL "${_CS_CURL_RETRY[@]}" "$url"
185+
return
186+
}
187+
curl -fsSL "${_CS_CURL_RETRY[@]}" "$url" 2>"$err" || rc=$?
188+
((rc != 0)) && cat "$err" >&2
189+
rm -f "$err"
190+
return "$rc"
191+
}
192+
173193
_cs_download() {
174194
local url="${1:?url}"
175195
local t0 code
@@ -186,9 +206,9 @@ _cs_download() {
186206
return 0
187207
fi
188208
if command -v curl >/dev/null 2>&1; then
189-
curl -fsSL "${_CS_CURL_RETRY[@]}" "$url"
209+
_cs_curl_retry "$url"
190210
else
191-
wget -q --tries=3 --waitretry=1 -O- "$url"
211+
wget -qO- "$url"
192212
fi
193213
}
194214

‎core/core.func‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1073,7 +1073,11 @@ get_header() {
10731073
fi
10741074

10751075
mkdir -p "$(dirname "$cached")"
1076-
if curl -fsSL "${core_url}/headers/${rel}" -o "$cached" && [[ -s "$cached" ]]; then
1076+
# No retry here, unlike every other engine fetch: this one only decides
1077+
# whether an ASCII banner appears, and the loop has several candidates to
1078+
# get through. It does need a bound, though — without one a black-holed
1079+
# network waits out the OS TCP timeout per candidate before drawing nothing.
1080+
if curl -fsSL --connect-timeout 5 --max-time 15 "${core_url}/headers/${rel}" -o "$cached" && [[ -s "$cached" ]]; then
10771081
cat "$cached" 2>/dev/null || true
10781082
return 0
10791083
fi

‎incus/build.func‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ elif [[ -S /dev/incus/sock ]] || [[ -f /etc/profile.d/incus-motd.sh ]] || [[ -f
2222
_INCUS_INSIDE_CONTAINER=1
2323
fi
2424

25+
# Retry options for engine fetches. Repeated in each file that bootstraps
26+
# itself over the network rather than shared, because there is nowhere to put
27+
# a shared copy that does not itself need downloading first. core/build.func
28+
# carries the full reasoning.
29+
_CS_CURL_RETRY=(--retry 3 --retry-delay 1 --retry-connrefused --connect-timeout 10)
30+
2531
# Prefer the dispatcher's resolver; the fallback must use the ENGINE base,
2632
# since every path passed in here is a core file, not a script.
2733
_incus_source() {
@@ -33,7 +39,7 @@ _incus_source() {
3339
local base="${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}"
3440
if command -v curl >/dev/null 2>&1; then
3541
# shellcheck disable=SC1090
36-
source <(curl -fsSL "$base/$rel")
42+
source <(curl -fsSL "${_CS_CURL_RETRY[@]}" "$base/$rel")
3743
else
3844
# shellcheck disable=SC1090
3945
source <(wget -qO- "$base/$rel")

‎incus/vm-core.func‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ _INCUS_VM_CORE_LOADED=1
1919

2020
_CORE_BASE="${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}"
2121

22+
# Retry options for engine fetches. Repeated in each file that bootstraps
23+
# itself over the network rather than shared, because there is nowhere to put
24+
# a shared copy that does not itself need downloading first. core/build.func
25+
# carries the full reasoning.
26+
_CS_CURL_RETRY=(--retry 3 --retry-delay 1 --retry-connrefused --connect-timeout 10)
27+
2228
# Prefer the dispatcher's resolver so a local checkout and forks keep working.
2329
_incus_vm_source() {
2430
local rel="$1"
@@ -32,7 +38,7 @@ _incus_vm_source() {
3238
return
3339
fi
3440
# shellcheck disable=SC1090
35-
source <(curl -fsSL "${_CORE_BASE}/${rel}")
41+
source <(curl -fsSL "${_CS_CURL_RETRY[@]}" "${_CORE_BASE}/${rel}")
3642
}
3743

3844
# core/core.func first: it owns prompt_confirm/prompt_input/prompt_select,

‎lib/tools.func‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ _TOOLS_FUNC_LOADED=1
5252
# hwaccel.func GPU detection and hardware acceleration
5353
# ==============================================================================
5454

55+
# Retry options for engine fetches. Repeated in each file that bootstraps
56+
# itself over the network rather than shared, because there is nowhere to put
57+
# a shared copy that does not itself need downloading first. core/build.func
58+
# carries the full reasoning.
59+
_CS_CURL_RETRY=(--retry 3 --retry-delay 1 --retry-connrefused --connect-timeout 10)
60+
5561
_tools_source_part() {
5662
local part="$1"
5763

@@ -66,7 +72,7 @@ _tools_source_part() {
6672
local base="${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}"
6773
if command -v curl >/dev/null 2>&1; then
6874
# shellcheck disable=SC1090
69-
source <(curl -fsSL "${base}/lib/${part}.func")
75+
source <(curl -fsSL "${_CS_CURL_RETRY[@]}" "${base}/lib/${part}.func")
7076
else
7177
# shellcheck disable=SC1090
7278
source <(wget -qO- "${base}/lib/${part}.func")

‎lxc/alpine-install.func‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,10 @@ EOF
192192
exit 1
193193
fi
194194
fi
195-
source <(curl -fsSL "$(_cs_core_url lib/alpine.func)")
195+
# Same treatment as the bootstrap above: a bare `source <(curl ...)` hides the
196+
# exit code, so a failed download used to source nothing and leave every
197+
# helper below missing with no explanation.
198+
_bootstrap_source "$(_cs_core_url lib/alpine.func)" fetch_and_deploy_gh_release
196199
msg_ok "Updated Container OS"
197200
}
198201

‎lxc/install.func‎

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,26 @@ _cs_core_url() {
4545
echo "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/${1:?func path}"
4646
}
4747

48-
# Retry options for every engine fetch on this side. Defined again rather than
49-
# taken from build.func because that file stays on the host; this one is piped
50-
# into the container and has to stand alone.
51-
#
52-
# A container is at its most fragile right after boot — DNS may not have come up
53-
# yet — and exit 115 ("download failed") is one of the larger failure buckets in
54-
# telemetry. curl's own --retry knows to skip a 404, so a genuinely missing file
55-
# still fails immediately instead of costing three attempts.
56-
_CS_CT_CURL_RETRY=(--retry 3 --retry-delay 1 --retry-connrefused --connect-timeout 10)
48+
# Retry options for engine fetches. Repeated here rather than taken from
49+
# core/build.func, which stays on the host while this file is piped into the
50+
# container and has to stand alone. A container is at its most fragile right
51+
# after boot, before DNS is up; core/build.func carries the full reasoning.
52+
_CS_CURL_RETRY=(--retry 3 --retry-delay 1 --retry-connrefused --connect-timeout 10)
53+
54+
# curl -S prints one line per failed attempt, so a download that recovers on the
55+
# second try would show the user two errors and then quietly work — during a
56+
# spinner, no less. Hold stderr back and release it only if every attempt failed.
57+
_cs_curl_retry() {
58+
local url="${1:?url}" err rc=0
59+
err="$(mktemp 2>/dev/null)" || {
60+
curl -fsSL "${_CS_CURL_RETRY[@]}" "$url"
61+
return
62+
}
63+
curl -fsSL "${_CS_CURL_RETRY[@]}" "$url" 2>"$err" || rc=$?
64+
((rc != 0)) && cat "$err" >&2
65+
rm -f "$err"
66+
return "$rc"
67+
}
5768

5869
# The engine reaches a container in nine pieces. The host's prefetch cannot
5970
# help here — a container has its own filesystem — so fetch them concurrently
@@ -93,7 +104,7 @@ _cs_ct_prefetch_engine() {
93104
done
94105

95106
curl -fsSL --parallel --parallel-immediate --parallel-max 16 \
96-
"${_CS_CT_CURL_RETRY[@]}" --max-time 60 "${args[@]}" 2>/dev/null
107+
"${_CS_CURL_RETRY[@]}" --max-time 60 "${args[@]}" 2>/dev/null
97108

98109
local kept=0
99110
for rel in "${_CS_CT_ENGINE_FILES[@]}"; do
@@ -122,9 +133,9 @@ _cs_engine_read() {
122133
return 0
123134
fi
124135
if command -v curl >/dev/null 2>&1; then
125-
curl -fsSL "${_CS_CT_CURL_RETRY[@]}" "$(_cs_core_url "$name")"
136+
_cs_curl_retry "$(_cs_core_url "$name")"
126137
else
127-
wget -q --tries=3 --waitretry=1 -O- "$(_cs_core_url "$name")"
138+
wget -qO- "$(_cs_core_url "$name")"
128139
fi
129140
}
130141

‎pve/backend.func‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1424,7 +1424,7 @@ PROFILE
14241424
if [[ "${DEV_MODE_MOTD:-false}" == "true" ]]; then
14251425
echo -e "${TAB}${HOLD}${DGN}Setting up MOTD and SSH for debugging...${CL}"
14261426
if pct exec "$CTID" -- bash -c "
1427-
source <(curl -fsSL \"${COMMUNITY_SCRIPTS_CORE_URL}/install.func\")
1427+
source <(curl -fsSL --retry 3 --retry-delay 1 --retry-connrefused --connect-timeout 10 \"${COMMUNITY_SCRIPTS_CORE_URL}/lxc/install.func\")
14281428
declare -f motd_ssh >/dev/null 2>&1 && motd_ssh || true
14291429
" >/dev/null 2>&1; then
14301430
local ct_ip=$(pct exec "$CTID" ip a s dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1)

‎pve/vm-app.func‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@
3232
#
3333
# ==============================================================================
3434

35+
# Retry options for engine fetches. Repeated in each file that bootstraps
36+
# itself over the network rather than shared, because there is nowhere to put
37+
# a shared copy that does not itself need downloading first. core/build.func
38+
# carries the full reasoning.
39+
_CS_CURL_RETRY=(--retry 3 --retry-delay 1 --retry-connrefused --connect-timeout 10)
40+
3541
# ==============================================================================
3642
# SECTION 1: APP DISCOVERY & SELECTION
3743
# ==============================================================================
@@ -374,8 +380,8 @@ customize_vm_image() {
374380
local tmp_install_func tmp_tools_func
375381
tmp_install_func=$(mktemp)
376382
tmp_tools_func=$(mktemp)
377-
curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/lxc/install.func" >"$tmp_install_func"
378-
curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/lib/tools.func" >"$tmp_tools_func"
383+
curl -fsSL "${_CS_CURL_RETRY[@]}" "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/lxc/install.func" >"$tmp_install_func"
384+
curl -fsSL "${_CS_CURL_RETRY[@]}" "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/lib/tools.func" >"$tmp_tools_func"
379385

380386
virt-customize -q -a "$WORK_FILE" \
381387
--mkdir /opt/community-scripts \

0 commit comments

Comments
 (0)