mirror of
https://github.com/ZmnSCPxj/clboss.git
synced 2026-08-13 12:33:20 +02:00
contrib: cln-plugin-bounce picks up config edits on restart
lightningd parses its config files once, at its own startup; a dynamic "plugin start" hands the plugin the option values memorized back then, so bouncing a plugin after a config edit silently restarted it on stale settings. Snapshot listconfigs before stopping anything to learn which options belong to each bounced plugin and which config files lightningd actually loaded (main, network, includes, config.setconfig), then re-read those files and compare each plugin's current file values against its running values. The restart is two-phase because "plugin start" with option parameters segfaults lightningd through at least v26.04 whenever a configvar names an unregistered option -- precisely the state while a sibling plugin is stopped (configvar_finalize_overrides dereferences opt_find_long() without a NULL check; fix pending as "setconfig: fix crash when a configvar outlives its plugin option"). Phase one is the classic bounce -- ordered stops, reverse bare starts -- which cannot trip the bug. Phase two, only for plugins whose file values differ, stops and restarts each such plugin alone with its file options passed in keyword form on the "plugin start" line, while every other plugin is up. With no edits pending the bounce is exactly the classic single pass. As a final guard, the apply phase is refused with a warning when any option named in the config files is unregistered (e.g. a plugin stopped by hand), and an optioned start that is rejected degrades to a bare restart with a warning: a running plugin on stale values beats a stopped one. Field note: the first, single-phase version of this change crashed prod1's lightningd (v26.04.1) through exactly this path -- plugin_add_params -> configvar_finalize_overrides -- a second reproduction route for the pending fix, needing no version skew, just a multi-plugin bounce with a pending config edit.
This commit is contained in:
parent
0428dbedb4
commit
262afe82ef
1 changed files with 232 additions and 17 deletions
|
|
@ -14,6 +14,36 @@
|
|||
# the restart uses that, so a repointed symlink brings up the new
|
||||
# version.
|
||||
#
|
||||
# lightningd parses its config files once, at its own startup; a
|
||||
# bare "plugin start" hands the restarted plugin the option values
|
||||
# lightningd memorized back then, not what the files say now. So
|
||||
# that a bounce picks up config edits, this script re-reads each
|
||||
# plugin's own options from the config files lightningd loaded (as
|
||||
# reported by listconfigs) and, where they differ from the running
|
||||
# values, passes them on the "plugin start" line, where they
|
||||
# override the memorized ones. The files are concatenated in
|
||||
# sorted-path order and the last occurrence of an option wins; a
|
||||
# bare flag line is passed as "name=true".
|
||||
#
|
||||
# The restart is two-phase because of a lightningd bug present
|
||||
# through at least v26.04 (fix pending: "setconfig: fix crash when a
|
||||
# configvar outlives its plugin option"): a "plugin start" WITH
|
||||
# option parameters segfaults lightningd when any option named in
|
||||
# its configvars is unregistered, which is exactly the state while a
|
||||
# sibling plugin is stopped. Phase one is the classic bounce --
|
||||
# ordered stops, reverse bare starts -- which cannot trip the bug.
|
||||
# Phase two then, for each plugin whose file values differ from its
|
||||
# running values, stops and restarts just that plugin with the
|
||||
# differing values passed as options, while every other plugin is
|
||||
# up. When nothing differs, phase two is a no-op and the bounce is
|
||||
# exactly the classic single pass. As a belt-and-suspenders check,
|
||||
# phase two is skipped entirely (with a warning) if any option named
|
||||
# in the config files is not currently registered -- e.g. some other
|
||||
# plugin was stopped by hand -- since that is the state that crashes
|
||||
# unfixed lightningd. If an optioned start is rejected, the plugin
|
||||
# is restarted bare with a warning: a running plugin on stale values
|
||||
# beats a stopped one.
|
||||
#
|
||||
# Plugin names are the arguments that do not start with "-"; each is
|
||||
# a short name ("clboss", "xrebalance"), or a full versioned basename
|
||||
# if the short name matches more than one running plugin. All other
|
||||
|
|
@ -77,6 +107,109 @@ done <<EOF
|
|||
$names
|
||||
EOF
|
||||
|
||||
tmpd=$(mktemp -d)
|
||||
trap 'rm -rf "$tmpd"' EXIT
|
||||
|
||||
# Gather each plugin's options from the config files lightningd read
|
||||
# at its startup. Must happen before the stops: a stopped plugin's
|
||||
# options vanish from listconfigs.
|
||||
configs=$("$lcli" "$@" listconfigs)
|
||||
|
||||
printf '%s\n' "$configs" \
|
||||
| jq -r '.configs | to_entries[] | .value
|
||||
| (.source? // empty), (.sources[]? // empty)' \
|
||||
| sed -n 's/:[0-9][0-9]*$//p' | sort -u \
|
||||
| while IFS= read -r f; do
|
||||
if [ -r "$f" ]; then
|
||||
cat "$f"
|
||||
else
|
||||
echo "$me: warning: cannot re-read $f" >&2
|
||||
fi
|
||||
done >"$tmpd/cfg"
|
||||
|
||||
while read -r n path; do
|
||||
[ -n "$n" ] || continue
|
||||
optnames=$(printf '%s\n' "$configs" \
|
||||
| jq -r --arg p "$path" '.configs | to_entries[]
|
||||
| select((.value.plugin? // "") == $p) | .key' \
|
||||
| tr '\n' ' ')
|
||||
awk -v names="$optnames" '
|
||||
BEGIN {
|
||||
n = split(names, a, " ")
|
||||
for (i = 1; i <= n; i++)
|
||||
if (a[i] != "") want[a[i]] = 1
|
||||
}
|
||||
{
|
||||
sub(/^[ \t]+/, "")
|
||||
if ($0 == "" || $0 ~ /^#/) next
|
||||
eq = index($0, "=")
|
||||
name = (eq ? substr($0, 1, eq - 1) : $0)
|
||||
if (!(name in want)) next
|
||||
if (!(name in val)) order[++cnt] = name
|
||||
val[name] = (eq ? $0 : $0 "=true")
|
||||
}
|
||||
END { for (i = 1; i <= cnt; i++) print val[order[i]] }
|
||||
' <"$tmpd/cfg" >"$tmpd/$n.opts"
|
||||
done <<EOF
|
||||
$resolved
|
||||
EOF
|
||||
|
||||
# Which plugins have file values differing from the running values?
|
||||
# Only those get the phase-two optioned restart.
|
||||
while read -r n path; do
|
||||
[ -n "$n" ] || continue
|
||||
edits=
|
||||
while IFS= read -r kv; do
|
||||
[ -n "$kv" ] || continue
|
||||
k=${kv%%=*}
|
||||
v=${kv#*=}
|
||||
cur=$(printf '%s\n' "$configs" | jq -r --arg k "$k" \
|
||||
'.configs[$k] | if . == null then ""
|
||||
elif .value_str != null then .value_str
|
||||
elif .value_int != null then (.value_int | tostring)
|
||||
elif .value_bool != null then (.value_bool | tostring)
|
||||
elif .value_msat != null then (.value_msat | tostring)
|
||||
elif .set == true then "true"
|
||||
elif .set == false then "false"
|
||||
else "" end')
|
||||
if [ "$v" != "$cur" ]; then
|
||||
[ -n "$edits" ] || echo "config edits for $n:"
|
||||
echo " $k: ${cur:-unset} -> $v"
|
||||
edits=1
|
||||
fi
|
||||
done <"$tmpd/$n.opts"
|
||||
[ -z "$edits" ] || : >"$tmpd/$n.apply"
|
||||
done <<EOF
|
||||
$resolved
|
||||
EOF
|
||||
|
||||
# Refuse the optioned restarts when any option named in the config
|
||||
# files is not currently registered: on lightningd without the
|
||||
# configvar_finalize_overrides NULL-guard fix, an optioned start in
|
||||
# that state is a segfault.
|
||||
unsafe=
|
||||
while IFS= read -r k; do
|
||||
[ -n "$k" ] || continue
|
||||
case "$k" in *" "*) continue ;; esac
|
||||
if ! printf '%s\n' "$configs" \
|
||||
| jq -e --arg k "$k" '.configs | has($k)' >/dev/null; then
|
||||
if [ -z "$unsafe" ]; then
|
||||
echo "$me: warning: config option(s) not registered (a plugin may be stopped); config edits will not be applied this run:" >&2
|
||||
unsafe=1
|
||||
fi
|
||||
echo " $k" >&2
|
||||
fi
|
||||
done <<EOF
|
||||
$(awk '
|
||||
{
|
||||
sub(/^[ \t]+/, "")
|
||||
if ($0 == "" || $0 ~ /^#/) next
|
||||
eq = index($0, "=")
|
||||
print (eq ? substr($0, 1, eq - 1) : $0)
|
||||
}
|
||||
' <"$tmpd/cfg" | sort -u)
|
||||
EOF
|
||||
|
||||
# Stop in the order given.
|
||||
stopped_names=
|
||||
tostart=
|
||||
|
|
@ -97,31 +230,113 @@ done <<EOF
|
|||
$resolved
|
||||
EOF
|
||||
|
||||
# Start in reverse order, preferring the unversioned sibling; fall
|
||||
# back to the exact path that was stopped.
|
||||
# Start plugin $1 via "plugin start", appending the name=value options
|
||||
# in file $2 in keyword form (empty $2 means none); the rest of the
|
||||
# arguments are the lightning-cli options.
|
||||
try_start() {
|
||||
ts_path=$1
|
||||
ts_optfile=$2
|
||||
shift 2
|
||||
if [ -z "$ts_optfile" ]; then
|
||||
# Positional form only: a keyword-form start hands
|
||||
# lightningd an empty-but-present parameter object, and
|
||||
# plugin_add_params runs configvar_finalize_overrides
|
||||
# for it -- the very crash the phasing steps around.
|
||||
set -- "$@" plugin start "$ts_path"
|
||||
else
|
||||
set -- "$@" -k plugin subcommand=start plugin="$ts_path"
|
||||
while IFS= read -r ts_kv; do
|
||||
[ -n "$ts_kv" ] || continue
|
||||
set -- "$@" "$ts_kv"
|
||||
done <"$ts_optfile"
|
||||
fi
|
||||
"$lcli" "$@" </dev/null >/dev/null
|
||||
}
|
||||
|
||||
# Start plugin (short name $1, stopped path $2), preferring the
|
||||
# unversioned sibling and falling back to the exact path; append the
|
||||
# options in file $3 when non-empty, degrading to a bare start with a
|
||||
# warning when the optioned starts are rejected. The rest of the
|
||||
# arguments are the lightning-cli options.
|
||||
start_plugin() {
|
||||
sp_n=$1
|
||||
sp_path=$2
|
||||
sp_optfile=$3
|
||||
shift 3
|
||||
sp_start=$(dirname "$sp_path")/$sp_n
|
||||
[ -e "$sp_start" ] || sp_start=$sp_path
|
||||
echo "starting $sp_start"
|
||||
[ -z "$sp_optfile" ] || sed 's/^/ /' "$sp_optfile"
|
||||
if try_start "$sp_start" "$sp_optfile" "$@"; then
|
||||
return 0
|
||||
fi
|
||||
if [ "$sp_start" != "$sp_path" ]; then
|
||||
echo "$me: start failed; retrying with $sp_path" >&2
|
||||
if try_start "$sp_path" "$sp_optfile" "$@"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
if [ -n "$sp_optfile" ]; then
|
||||
echo "$me: start failed; retrying without config options" >&2
|
||||
if try_start "$sp_start" "" "$@" \
|
||||
|| { [ "$sp_start" != "$sp_path" ] \
|
||||
&& try_start "$sp_path" "" "$@"; }; then
|
||||
echo "$me: warning: $sp_n started without re-read config options" >&2
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
echo "$me: failed to start $sp_n" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# Phase one: start in reverse order, bare.
|
||||
failed=
|
||||
while read -r n path; do
|
||||
[ -n "$n" ] || continue
|
||||
start=$(dirname "$path")/$n
|
||||
[ -e "$start" ] || start=$path
|
||||
echo "starting $start"
|
||||
if ! "$lcli" "$@" plugin start "$start" >/dev/null; then
|
||||
ok=
|
||||
if [ "$start" != "$path" ]; then
|
||||
echo "$me: start failed; retrying with $path" >&2
|
||||
if "$lcli" "$@" plugin start "$path" >/dev/null; then
|
||||
ok=1
|
||||
fi
|
||||
fi
|
||||
if [ -z "$ok" ]; then
|
||||
echo "$me: failed to start $n" >&2
|
||||
failed=1
|
||||
fi
|
||||
if ! start_plugin "$n" "$path" "" "$@"; then
|
||||
failed=1
|
||||
: >"$tmpd/$n.failed"
|
||||
fi
|
||||
done <<EOF
|
||||
$tostart
|
||||
EOF
|
||||
|
||||
# Phase two: apply config edits one plugin at a time, while every
|
||||
# other plugin is up.
|
||||
while read -r n path; do
|
||||
[ -n "$n" ] || continue
|
||||
[ -e "$tmpd/$n.apply" ] || continue
|
||||
if [ -e "$tmpd/$n.failed" ]; then
|
||||
echo "$me: not applying config edits to $n: not running" >&2
|
||||
continue
|
||||
fi
|
||||
if [ -n "$unsafe" ]; then
|
||||
echo "$me: not applying config edits to $n (see warning above)" >&2
|
||||
failed=1
|
||||
continue
|
||||
fi
|
||||
echo "applying config edits to $n"
|
||||
plugins=$("$lcli" "$@" plugin list)
|
||||
cur=$(printf '%s\n' "$plugins" | jq -r --arg n "$n" "$match")
|
||||
if [ -z "$cur" ] \
|
||||
|| [ "$(printf '%s\n' "$cur" | wc -l)" -gt 1 ]; then
|
||||
echo "$me: cannot re-resolve '$n'; config edits not applied" >&2
|
||||
failed=1
|
||||
continue
|
||||
fi
|
||||
echo "stopping $cur"
|
||||
if ! "$lcli" "$@" plugin stop "$cur" >/dev/null; then
|
||||
echo "$me: stop failed for $cur; config edits not applied" >&2
|
||||
failed=1
|
||||
continue
|
||||
fi
|
||||
if ! start_plugin "$n" "$cur" "$tmpd/$n.opts" "$@"; then
|
||||
failed=1
|
||||
fi
|
||||
done <<EOF
|
||||
$resolved
|
||||
EOF
|
||||
|
||||
plugins=$("$lcli" "$@" plugin list)
|
||||
while IFS= read -r n; do
|
||||
[ -n "$n" ] || continue
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue