@@ -44,30 +44,135 @@ mkdir -p "$RUNE_HOME"
4444LOCK_DIR=" $RUNE_HOME /bootstrap.lock.d"
4545TMP=" "
4646SUMS=" "
47+ OWNER_TOKEN=" "
4748cleanup () {
4849 [ -n " $TMP " ] && rm -f " $TMP "
4950 [ -n " $SUMS " ] && rm -f " $SUMS "
50- rmdir " $LOCK_DIR " 2> /dev/null || true
51+ # Release lock after checking token is valid or not
52+ if [ -n " $OWNER_TOKEN " ] && [ " $( cat " $LOCK_DIR /owner" 2> /dev/null || true) " = " $OWNER_TOKEN " ]; then
53+ rm -f " $LOCK_DIR /owner" 2> /dev/null || true
54+ rmdir " $LOCK_DIR " 2> /dev/null || true
55+ fi
56+ }
57+
58+ # Network time budget
59+ # - `mcp-server`: MCP entrypoint run by Claude Code session with ~30s timeout.
60+ # SIGKILL after timeout skip cleanup which leave unreleased bootstrap lock;
61+ # so overall time should be less than 30s.
62+ # Worst case: API resolve up to 7s + binary download up to 13s + checksum up to 7s
63+ # - other: matched with each downloaded binaries' deadline
64+ if [ " ${1:- } " = mcp-server ]; then
65+ NET_RETRY=3; NET_RETRY_DELAY=1; NET_RETRY_MAXTIME=3
66+ NET_API_MAXTIME=4; NET_BIN_MAXTIME=10; NET_CHECKSUM_MAXTIME=4
67+ else
68+ NET_RETRY=3; NET_RETRY_DELAY=2; NET_RETRY_MAXTIME=60
69+ NET_API_MAXTIME=20; NET_BIN_MAXTIME=120; NET_CHECKSUM_MAXTIME=30
70+ fi
71+
72+ # retries fast transient errors such as Github CDN failures (504, timeouts) only;
73+ # slow/hung requests are intentionally not retried to stay within the spawn budget.
74+ # Caller add NET_{API|BIN|CHECKSUM}_MAXTIME properly on each step
75+ fetch () {
76+ curl --fail --silent --show-error --location --connect-timeout 5 \
77+ --retry " $NET_RETRY " --retry-delay " $NET_RETRY_DELAY " \
78+ --retry-max-time " $NET_RETRY_MAXTIME " " $@ "
79+ }
80+
81+ # Lock waiting budget to exit before Claude code MCP spawn timeout (~30s)
82+ LOCK_WAIT_BUDGET=" ${RUNE_LOCK_WAIT_BUDGET:- 20} "
83+ # Lock's wall-clock age to prevent alive but stuck holder
84+ # Worst case: NET_RETRY_MAXTIME + NET_{API|BIN|CHECKSUM}_MAXTIME (about 350s) when !mcp-server
85+ LOCK_STALE_AFTER=" ${RUNE_LOCK_STALE_AFTER:- 360} "
86+
87+ # Atomically take stale lock and remove it
88+ clear_stale_lock () {
89+ if mv " $LOCK_DIR " " $LOCK_DIR .reclaim.$$ " 2> /dev/null; then
90+ rm -rf " $LOCK_DIR .reclaim.$$ " 2> /dev/null || true
91+ fi
92+ return 0
5193}
5294
5395waited=0
54- while ! mkdir " $LOCK_DIR " 2> /dev/null; do # another session hold lock
96+ wait_count=0
97+ while true ; do
98+ # Claim lock atomically
99+ if mkdir " $LOCK_DIR " 2> /dev/null; then
100+ OWNER_TOKEN=" $$ $( date +%s) " # "<pid> <timestamp>"
101+ if ( set -C; printf ' %s\n' " $OWNER_TOKEN " > " $LOCK_DIR /owner" ) 2> /dev/null; then
102+ trap cleanup EXIT INT TERM
103+ # Double-check if mkdir -> write gap affect lock
104+ if [ " $( cat " $LOCK_DIR /owner" 2> /dev/null || true) " = " $OWNER_TOKEN " ]; then
105+ break
106+ fi
107+ trap - EXIT INT TERM
108+ OWNER_TOKEN=" "
109+ continue
110+ fi
111+
112+ OWNER_TOKEN=" "
113+ if [ ! -d " $LOCK_DIR " ]; then
114+ continue # lock is cleared, retry claim
115+ fi
116+
117+ # Real write error (disk full, permission, or others)
118+ if [ ! -e " $LOCK_DIR /owner" ]; then
119+ echo " rune: cannot record install bootstrap lock owner (file write failed)" >&2
120+ exit 1
121+ fi
122+ fi
123+
124+ # Wait for another process as we failed to claim lock
55125 if [ -x " $TARGET " ]; then
56126 exec " $TARGET " " $@ " # bootstrap finished
57127 fi
58128
129+ # Validate owner
130+ owner=" $( cat " $LOCK_DIR /owner" 2> /dev/null || true) "
131+ pid=" ${owner%% * } "
132+ case " $owner " in
133+ * " " * ) ts=" ${owner##* } " ;;
134+ * ) ts=" " ;;
135+ esac
136+
137+ if [ -z " $owner " ]; then
138+ # Dir is created but no owner yet; holder in the middle of claim or died
139+ wait_count=$(( wait_count + 1 ))
140+ if [ " $wait_count " -ge 5 ]; then
141+ echo " rune: bootstrap lock not claimed for ${wait_count} s; reclaiming" >&2
142+ clear_stale_lock; wait_count=0; continue
143+ fi
144+ else
145+ wait_count=0
146+ if [ -n " $pid " ] && ! kill -0 " $pid " 2> /dev/null; then
147+ # Holder process not found; lock is leaked
148+ echo " rune: bootstrap lock holder (pid $pid ) is not found; reclaiming" >&2
149+ clear_stale_lock; continue
150+ fi
151+
152+ # Check wall-clock age
153+ case " $ts " in
154+ ' ' |* [!0-9]* ) age=0 ;;
155+ * ) age=$(( $(date +% s) - ts )) ;;
156+ esac
157+
158+ if [ " $age " -ge " $LOCK_STALE_AFTER " ]; then
159+ echo " rune: bootstrap lock stale (${age} s); reclaiming" >&2
160+ clear_stale_lock; continue
161+ fi
162+
163+ if [ " $waited " -ge " $LOCK_WAIT_BUDGET " ]; then
164+ echo " rune: another rune bootstrap is in progress over MCP spawn budget." >&2
165+ echo " Retry in a moment, or run it out-of-band:" >&2
166+ echo " bash -c \"\$ {CLAUDE_PLUGIN_ROOT:-.}/bin/rune install\" " >&2
167+ exit 1
168+ fi
169+ fi
170+
59171 sleep 1
60172 waited=$(( waited + 1 ))
61- if [ " $waited " -ge 120 ]; then
62- echo " rune: bootstrap lock held >120s, reclaiming" >&2
63- rmdir " $LOCK_DIR " 2> /dev/null || true
64- waited=0
65- fi
66173done
67174
68- # Check error
69- trap cleanup EXIT INT TERM
70-
175+ # Double-check: install is completed right before we won the lock
71176if [ -x " $TARGET " ]; then
72177 cleanup
73178 trap - EXIT INT TERM
@@ -85,12 +190,9 @@ if [ -z "$RUNE_VERSION" ]; then
85190 # Use token if exist
86191 token=" ${GITHUB_TOKEN:- ${GH_TOKEN:- } } "
87192 if [ -n " $token " ]; then
88- body=" $( curl --fail --silent --show-error --location --connect-timeout 10 --max-time 20 \
89- --retry 3 --retry-delay 2 \
90- --header " Authorization: Bearer $token " " $api " || true) "
193+ body=" $( fetch --max-time " $NET_API_MAXTIME " --header " Authorization: Bearer $token " " $api " || true) "
91194 else
92- body=" $( curl --fail --silent --show-error --location --connect-timeout 10 --max-time 20 \
93- --retry 3 --retry-delay 2 " $api " || true) "
195+ body=" $( fetch --max-time " $NET_API_MAXTIME " " $api " || true) "
94196 fi
95197
96198 RUNE_VERSION=" $( printf ' %s' " $body " \
@@ -128,12 +230,22 @@ mkdir -p "$(dirname "$TARGET")"
128230TMP=" $( mktemp " $( dirname " $TARGET " ) /.rune-bootstrap-XXXXXX" ) "
129231SUMS=" $( mktemp -t rune-bootstrap-sums-XXXXXX) "
130232
131- # --retry rides out transient GitHub CDN failures (504, timeouts) instead
132- # of aborting the whole bootstrap on the first blip.
133- curl --fail --silent --show-error --location --connect-timeout 10 --max-time 120 --retry 3 --retry-delay 2 " $RELEASE_BASE /$ASSET " -o " $TMP "
134- curl --fail --silent --show-error --location --connect-timeout 10 --max-time 30 --retry 3 --retry-delay 2 " $RELEASE_BASE /checksums.txt" -o " $SUMS "
233+ if ! fetch --max-time " $NET_BIN_MAXTIME " " $RELEASE_BASE /$ASSET " -o " $TMP " ; then
234+ echo " rune: could not download $ASSET ($RUNE_VERSION ) after retries." >&2
235+ echo " The release endpoint may be slow or temporarily unavailable (e.g. HTTP 504)." >&2
236+ echo " Recover out-of-band, then reconnect /mcp:" >&2
237+ echo " bash -c \"\$ {CLAUDE_PLUGIN_ROOT:-.}/bin/rune install\" " >&2
238+ exit 1
239+ fi
240+ if ! fetch --max-time " $NET_CHECKSUM_MAXTIME " " $RELEASE_BASE /checksums.txt" -o " $SUMS " ; then
241+ echo " rune: could not download checksums.txt ($RUNE_VERSION ) after retries." >&2
242+ echo " The release endpoint may be slow or temporarily unavailable (e.g. HTTP 504)." >&2
243+ echo " Recover out-of-band, then reconnect /mcp:" >&2
244+ echo " bash -c \"\$ {CLAUDE_PLUGIN_ROOT:-.}/bin/rune install\" " >&2
245+ exit 1
246+ fi
135247
136- EXPECTED=" $( grep " $ASSET \$ " " $SUMS " | cut -d' ' -f1) "
248+ EXPECTED=" $( grep " $ASSET \$ " " $SUMS " | cut -d' ' -f1 || true ) "
137249if [ -z " $EXPECTED " ]; then
138250 echo " rune: $ASSET not listed in checksums.txt for $RUNE_VERSION " >&2
139251 exit 1
0 commit comments