diff --git a/contrib/cln-plugin-bounce b/contrib/cln-plugin-bounce index d5c8a43..b2f20ec 100755 --- a/contrib/cln-plugin-bounce +++ b/contrib/cln-plugin-bounce @@ -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 <&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 < $v" + edits=1 + fi + done <"$tmpd/$n.opts" + [ -z "$edits" ] || : >"$tmpd/$n.apply" +done </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 </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 <&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 <