From 66be65e2b384a91cae2d7bfa47275eee1251037d Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Mon, 5 Jun 2023 23:47:21 +0930 Subject: [PATCH 01/12] lightningd: move listconfigs into configs.c It mainly clutters up options.c. The deprecated handling was very tied to it, so it stays there. Signed-off-by: Rusty Russell --- lightningd/Makefile | 1 + lightningd/configs.c | 307 ++++++++++++++++++++++++++++++++++++++ lightningd/options.c | 346 ++++--------------------------------------- lightningd/options.h | 7 + 4 files changed, 344 insertions(+), 317 deletions(-) create mode 100644 lightningd/configs.c diff --git a/lightningd/Makefile b/lightningd/Makefile index b20175d375e0..207e3c6157fe 100644 --- a/lightningd/Makefile +++ b/lightningd/Makefile @@ -40,6 +40,7 @@ LIGHTNINGD_SRC := \ lightningd/watch.c LIGHTNINGD_SRC_NOHDR := \ + lightningd/configs.c \ lightningd/datastore.c \ lightningd/ping.c \ lightningd/offer.c \ diff --git a/lightningd/configs.c b/lightningd/configs.c new file mode 100644 index 000000000000..ad1f50e9e887 --- /dev/null +++ b/lightningd/configs.c @@ -0,0 +1,307 @@ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void json_add_source(struct json_stream *result, + const char *fieldname, + const struct configvar *cv) +{ + const char *source; + + if (!cv) { + source = "default"; + } else { + source = NULL; + switch (cv->src) { + case CONFIGVAR_CMDLINE: + case CONFIGVAR_CMDLINE_SHORT: + source = "cmdline"; + break; + case CONFIGVAR_EXPLICIT_CONF: + case CONFIGVAR_BASE_CONF: + case CONFIGVAR_NETWORK_CONF: + source = tal_fmt(tmpctx, "%s:%u", cv->file, cv->linenum); + break; + case CONFIGVAR_PLUGIN_START: + source = "pluginstart"; + break; + } + } + json_add_string(result, fieldname, source); +} + +static const char *configval_fieldname(const struct opt_table *ot) +{ + bool multi = (ot->type & OPT_MULTI); + if (ot->type & OPT_SHOWBOOL) + return multi ? "values_bool" : "value_bool"; + if (ot->type & OPT_SHOWINT) + return multi ? "values_int" : "value_int"; + if (ot->type & OPT_SHOWMSATS) + return multi ? "values_msat" : "value_msat"; + return multi ? "values_str" : "value_str"; +} + +#define CONFIG_SHOW_BUFSIZE 4096 + +static const char *get_opt_val(const struct opt_table *ot, + char buf[], + const struct configvar *cv) +{ + if (ot->show == (void *)opt_show_charp) { + /* Don't truncate or quote! */ + return *(char **)ot->u.carg; + } + if (ot->show) { + /* Plugins options' show only shows defaults, so show val if + * we have it */ + if (is_plugin_opt(ot) && cv) + return cv->optarg; + strcpy(buf + CONFIG_SHOW_BUFSIZE, "..."); + if (ot->show(buf, CONFIG_SHOW_BUFSIZE, ot->u.carg)) + return buf; + return NULL; + } + + /* For everything else we only display if it's set, + * BUT we check here to make sure you've handled + * everything! */ + if (is_known_opt_cb_arg(ot->cb_arg) + || is_restricted_print_if_nonnull(ot->cb_arg)) { + /* Only if set! */ + if (cv) + return cv->optarg; + else + return NULL; + } + + /* Insert more decodes here! */ + errx(1, "Unknown decode for %s", ot->names); +} + +static void check_literal(const char *name, const char *val) +{ + if (streq(val, "true") || streq(val, "false")) + return; + if (!streq(val, "") && strspn(val, "-0123456789.") == strlen(val)) + return; + errx(1, "Bad literal for %s: %s", name, val); +} + +static void json_add_configval(struct json_stream *result, + const char *fieldname, + const struct opt_table *ot, + const char *str) +{ + if (ot->type & OPT_SHOWBOOL) { + json_add_bool(result, fieldname, opt_canon_bool(str)); + } else if (ot->type & (OPT_SHOWMSATS|OPT_SHOWINT)) { + check_literal(ot->names, str); + json_add_primitive(result, fieldname, str); + } else + json_add_string(result, fieldname, str); +} + +/* Config vars can have multiple names ("--large-channels|--wumbo"), but first + * is preferred */ +static void json_add_config(struct lightningd *ld, + struct json_stream *response, + bool always_include, + const struct opt_table *ot, + const char **names) +{ + char buf[CONFIG_SHOW_BUFSIZE + sizeof("...")]; + const char *val; + struct configvar *cv; + + /* This tells us if they actually set the option */ + cv = configvar_first(ld->configvars, names); + + /* Ignore dev/hidden options (deprecated) unless they actually used it */ + if (!cv + && (ot->desc == opt_hidden || (ot->type & OPT_DEV)) + && !always_include) { + return; + } + + /* Ignore options which simply exit */ + if (ot->type & OPT_EXITS) + return; + + if (ot->type & OPT_NOARG) { + json_object_start(response, names[0]); + json_add_bool(response, "set", cv != NULL); + json_add_source(response, "source", cv); + json_add_config_plugin(response, ld->plugins, "plugin", ot); + json_object_end(response); + return; + } + + assert(ot->type & OPT_HASARG); + if (ot->type & OPT_MULTI) { + json_object_start(response, names[0]); + json_array_start(response, configval_fieldname(ot)); + while (cv) { + val = get_opt_val(ot, buf, cv); + json_add_configval(response, NULL, ot, val); + cv = configvar_next(ld->configvars, cv, names); + } + json_array_end(response); + + /* Iterate again, for sources */ + json_array_start(response, "sources"); + for (cv = configvar_first(ld->configvars, names); + cv; + cv = configvar_next(ld->configvars, cv, names)) { + json_add_source(response, NULL, cv); + } + json_array_end(response); + json_add_config_plugin(response, ld->plugins, "plugin", ot); + json_object_end(response); + return; + } + + /* Returns NULL if we don't want to print it */ + val = get_opt_val(ot, buf, cv); + if (!val) + return; + + json_object_start(response, names[0]); + json_add_configval(response, configval_fieldname(ot), ot, val); + json_add_source(response, "source", cv); + json_add_config_plugin(response, ld->plugins, "plugin", ot); + json_object_end(response); +} + +static struct command_result *param_opt_config(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + const struct opt_table **config) +{ + const char *name0 = json_strdup(tmpctx, buffer, tok); + *config = opt_find_long(name0, NULL); + if (*config) + return NULL; + + return command_fail_badparam(cmd, name, buffer, tok, + "Unknown config option"); +} + +/* FIXME: This is a hack! Expose somehow in ccan/opt.*/ +/* Returns string after first '-'. */ +static const char *first_name(const char *names, unsigned *len) +{ + *len = strcspn(names + 1, "|= "); + return names + 1; +} + +static const char *next_name(const char *names, unsigned *len) +{ + names += *len; + if (names[0] == ' ' || names[0] == '=' || names[0] == '\0') + return NULL; + return first_name(names + 1, len); +} + +static struct command_result *json_listconfigs(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) +{ + struct json_stream *response = NULL; + const struct opt_table *config; + + if (!param(cmd, buffer, params, + p_opt("config", param_opt_config, &config), + NULL)) + return command_param_failed(); + + response = json_stream_success(cmd); + + if (!deprecated_apis) + goto modern; + + if (!config) + json_add_string(response, "# version", version()); + + for (size_t i = 0; i < opt_count; i++) { + unsigned int len; + const char *name; + + /* FIXME: Print out comment somehow? */ + if (opt_table[i].type == OPT_SUBTABLE) + continue; + + for (name = first_name(opt_table[i].names, &len); + name; + name = next_name(name, &len)) { + /* Skips over first -, so just need to look for one */ + if (name[0] != '-') + continue; + + if (!config || config == &opt_table[i]) { + add_config_deprecated(cmd->ld, response, &opt_table[i], + name+1, len-1); + } + /* If we have more than one long name, first + * is preferred */ + break; + } + } + +modern: + json_object_start(response, "configs"); + for (size_t i = 0; i < opt_count; i++) { + unsigned int len; + const char *name; + const char **names; + + /* FIXME: Print out comment somehow? */ + if (opt_table[i].type == OPT_SUBTABLE) + continue; + + if (config && config != &opt_table[i]) + continue; + + names = tal_arr(tmpctx, const char *, 0); + for (name = first_name(opt_table[i].names, &len); + name; + name = next_name(name, &len)) { + /* Skips over first -, so just need to look for one */ + if (name[0] != '-') + continue; + tal_arr_expand(&names, + tal_strndup(names, name+1, len-1)); + } + /* We don't usually print dev or deprecated options, unless + * they explicitly ask, or they're set. */ + json_add_config(cmd->ld, response, config != NULL, + &opt_table[i], names); + } + json_object_end(response); + + return command_success(cmd, response); +} + +static const struct json_command listconfigs_command = { + "listconfigs", + "utility", + json_listconfigs, + "List all configuration options, or with [config], just that one.", + .verbose = "listconfigs [config]\n" + "Outputs an object, with each field a config options\n" + "(Option names which start with # are comments)\n" + "With [config], object only has that field" +}; +AUTODATA(json_command, &listconfigs_command); diff --git a/lightningd/options.c b/lightningd/options.c index f81ea1e75ca0..ab4eeb0cd0e3 100644 --- a/lightningd/options.c +++ b/lightningd/options.c @@ -1648,22 +1648,6 @@ void handle_opts(struct lightningd *ld) check_config(ld); } -/* FIXME: This is a hack! Expose somehow in ccan/opt.*/ -/* Returns string after first '-'. */ -static const char *first_name(const char *names, unsigned *len) -{ - *len = strcspn(names + 1, "|= "); - return names + 1; -} - -static const char *next_name(const char *names, unsigned *len) -{ - names += *len; - if (names[0] == ' ' || names[0] == '=' || names[0] == '\0') - return NULL; - return first_name(names + 1, len); -} - static void json_add_opt_addrs(struct json_stream *response, const char *name0, const struct wireaddr_internal *wireaddrs, @@ -1720,19 +1704,10 @@ bool opt_canon_bool(const char *val) return b; } -static void check_literal(const char *name, const char *val) -{ - if (streq(val, "true") || streq(val, "false")) - return; - if (!streq(val, "") && strspn(val, "-0123456789.") == strlen(val)) - return; - errx(1, "Bad literal for %s: %s", name, val); -} - -static void add_config_deprecated(struct lightningd *ld, - struct json_stream *response, - const struct opt_table *opt, - const char *name, size_t len) +void add_config_deprecated(struct lightningd *ld, + struct json_stream *response, + const struct opt_table *opt, + const char *name, size_t len) { char *name0 = tal_strndup(tmpctx, name, len); char *answer = NULL; @@ -1922,294 +1897,31 @@ static void add_config_deprecated(struct lightningd *ld, } } -static void json_add_source(struct json_stream *result, - const char *fieldname, - const struct configvar *cv) -{ - const char *source; - - if (!cv) { - source = "default"; - } else { - source = NULL; - switch (cv->src) { - case CONFIGVAR_CMDLINE: - case CONFIGVAR_CMDLINE_SHORT: - source = "cmdline"; - break; - case CONFIGVAR_EXPLICIT_CONF: - case CONFIGVAR_BASE_CONF: - case CONFIGVAR_NETWORK_CONF: - source = tal_fmt(tmpctx, "%s:%u", cv->file, cv->linenum); - break; - case CONFIGVAR_PLUGIN_START: - source = "pluginstart"; - break; - } - } - json_add_string(result, fieldname, source); -} - -static const char *configval_fieldname(const struct opt_table *ot) -{ - bool multi = (ot->type & OPT_MULTI); - if (ot->type & OPT_SHOWBOOL) - return multi ? "values_bool" : "value_bool"; - if (ot->type & OPT_SHOWINT) - return multi ? "values_int" : "value_int"; - if (ot->type & OPT_SHOWMSATS) - return multi ? "values_msat" : "value_msat"; - return multi ? "values_str" : "value_str"; -} - -#define CONFIG_SHOW_BUFSIZE 4096 - -static const char *get_opt_val(const struct opt_table *ot, - char buf[], - const struct configvar *cv) -{ - if (ot->show == (void *)opt_show_charp) { - /* Don't truncate or quote! */ - return *(char **)ot->u.carg; - } - if (ot->show) { - /* Plugins options' show only shows defaults, so show val if - * we have it */ - if (is_plugin_opt(ot) && cv) - return cv->optarg; - strcpy(buf + CONFIG_SHOW_BUFSIZE, "..."); - if (ot->show(buf, CONFIG_SHOW_BUFSIZE, ot->u.carg)) - return buf; - return NULL; - } - - /* For everything else we only display if it's set, - * BUT we check here to make sure you've handled - * everything! */ - if (ot->cb_arg == (void *)opt_set_talstr - || ot->cb_arg == (void *)opt_add_proxy_addr - || ot->cb_arg == (void *)opt_force_feerates - || ot->cb_arg == (void *)opt_set_accept_extra_tlv_types - || ot->cb_arg == (void *)opt_set_websocket_port - || ot->cb_arg == (void *)opt_add_plugin - || ot->cb_arg == (void *)opt_add_plugin_dir - || ot->cb_arg == (void *)opt_important_plugin - || ot->cb_arg == (void *)opt_disable_plugin - || ot->cb_arg == (void *)opt_add_addr - || ot->cb_arg == (void *)opt_add_bind_addr - || ot->cb_arg == (void *)opt_add_announce_addr - || ot->cb_arg == (void *)opt_subdaemon - || ot->cb_arg == (void *)opt_set_db_upgrade - || ot->cb_arg == (void *)arg_log_to_file - || ot->cb_arg == (void *)opt_add_accept_htlc_tlv +bool is_known_opt_cb_arg(char *(*cb_arg)(const char *, void *)) +{ + return cb_arg == (void *)opt_set_talstr + || cb_arg == (void *)opt_add_proxy_addr + || cb_arg == (void *)opt_force_feerates + || cb_arg == (void *)opt_set_accept_extra_tlv_types + || cb_arg == (void *)opt_set_websocket_port + || cb_arg == (void *)opt_add_plugin + || cb_arg == (void *)opt_add_plugin_dir + || cb_arg == (void *)opt_important_plugin + || cb_arg == (void *)opt_disable_plugin + || cb_arg == (void *)opt_add_addr + || cb_arg == (void *)opt_add_bind_addr + || cb_arg == (void *)opt_add_announce_addr + || cb_arg == (void *)opt_subdaemon + || cb_arg == (void *)opt_set_db_upgrade + || cb_arg == (void *)arg_log_to_file + || cb_arg == (void *)opt_add_accept_htlc_tlv #if DEVELOPER - || ot->cb_arg == (void *)opt_subd_dev_disconnect - || ot->cb_arg == (void *)opt_force_featureset - || ot->cb_arg == (void *)opt_force_privkey - || ot->cb_arg == (void *)opt_force_bip32_seed - || ot->cb_arg == (void *)opt_force_channel_secrets - || ot->cb_arg == (void *)opt_force_tmp_channel_id + || cb_arg == (void *)opt_subd_dev_disconnect + || cb_arg == (void *)opt_force_featureset + || cb_arg == (void *)opt_force_privkey + || cb_arg == (void *)opt_force_bip32_seed + || cb_arg == (void *)opt_force_channel_secrets + || cb_arg == (void *)opt_force_tmp_channel_id #endif - || is_restricted_print_if_nonnull(ot->cb_arg)) { - /* Only if set! */ - if (cv) - return cv->optarg; - else - return NULL; - } - - /* Insert more decodes here! */ - errx(1, "Unknown decode for %s", ot->names); -} - -static void json_add_configval(struct json_stream *result, - const char *fieldname, - const struct opt_table *ot, - const char *str) -{ - if (ot->type & OPT_SHOWBOOL) { - json_add_bool(result, fieldname, opt_canon_bool(str)); - } else if (ot->type & (OPT_SHOWMSATS|OPT_SHOWINT)) { - check_literal(ot->names, str); - json_add_primitive(result, fieldname, str); - } else - json_add_string(result, fieldname, str); -} - -/* Config vars can have multiple names ("--large-channels|--wumbo"), but first - * is preferred */ -static void json_add_config(struct lightningd *ld, - struct json_stream *response, - bool always_include, - const struct opt_table *ot, - const char **names) -{ - char buf[CONFIG_SHOW_BUFSIZE + sizeof("...")]; - const char *val; - struct configvar *cv; - - /* This tells us if they actually set the option */ - cv = configvar_first(ld->configvars, names); - - /* Ignore dev/hidden options (deprecated) unless they actually used it */ - if (!cv - && (ot->desc == opt_hidden || (ot->type & OPT_DEV)) - && !always_include) { - return; - } - - /* Ignore options which simply exit */ - if (ot->type & OPT_EXITS) - return; - - if (ot->type & OPT_NOARG) { - json_object_start(response, names[0]); - json_add_bool(response, "set", cv != NULL); - json_add_source(response, "source", cv); - json_add_config_plugin(response, ld->plugins, "plugin", ot); - json_object_end(response); - return; - } - - assert(ot->type & OPT_HASARG); - if (ot->type & OPT_MULTI) { - json_object_start(response, names[0]); - json_array_start(response, configval_fieldname(ot)); - while (cv) { - val = get_opt_val(ot, buf, cv); - json_add_configval(response, NULL, ot, val); - cv = configvar_next(ld->configvars, cv, names); - } - json_array_end(response); - - /* Iterate again, for sources */ - json_array_start(response, "sources"); - for (cv = configvar_first(ld->configvars, names); - cv; - cv = configvar_next(ld->configvars, cv, names)) { - json_add_source(response, NULL, cv); - } - json_array_end(response); - json_add_config_plugin(response, ld->plugins, "plugin", ot); - json_object_end(response); - return; - } - - /* Returns NULL if we don't want to print it */ - val = get_opt_val(ot, buf, cv); - if (!val) - return; - - json_object_start(response, names[0]); - json_add_configval(response, configval_fieldname(ot), ot, val); - json_add_source(response, "source", cv); - json_add_config_plugin(response, ld->plugins, "plugin", ot); - json_object_end(response); -} - -static struct command_result *param_opt_config(struct command *cmd, - const char *name, - const char *buffer, - const jsmntok_t *tok, - const struct opt_table **config) -{ - const char *name0 = json_strdup(tmpctx, buffer, tok); - *config = opt_find_long(name0, NULL); - if (*config) - return NULL; - - return command_fail_badparam(cmd, name, buffer, tok, - "Unknown config option"); -} - -static struct command_result *json_listconfigs(struct command *cmd, - const char *buffer, - const jsmntok_t *obj UNNEEDED, - const jsmntok_t *params) -{ - struct json_stream *response = NULL; - const struct opt_table *config; - - if (!param(cmd, buffer, params, - p_opt("config", param_opt_config, &config), - NULL)) - return command_param_failed(); - - response = json_stream_success(cmd); - - if (!deprecated_apis) - goto modern; - - if (!config) - json_add_string(response, "# version", version()); - - for (size_t i = 0; i < opt_count; i++) { - unsigned int len; - const char *name; - - /* FIXME: Print out comment somehow? */ - if (opt_table[i].type == OPT_SUBTABLE) - continue; - - for (name = first_name(opt_table[i].names, &len); - name; - name = next_name(name, &len)) { - /* Skips over first -, so just need to look for one */ - if (name[0] != '-') - continue; - - if (!config || config == &opt_table[i]) { - add_config_deprecated(cmd->ld, response, &opt_table[i], - name+1, len-1); - } - /* If we have more than one long name, first - * is preferred */ - break; - } - } - -modern: - json_object_start(response, "configs"); - for (size_t i = 0; i < opt_count; i++) { - unsigned int len; - const char *name; - const char **names; - - /* FIXME: Print out comment somehow? */ - if (opt_table[i].type == OPT_SUBTABLE) - continue; - - if (config && config != &opt_table[i]) - continue; - - names = tal_arr(tmpctx, const char *, 0); - for (name = first_name(opt_table[i].names, &len); - name; - name = next_name(name, &len)) { - /* Skips over first -, so just need to look for one */ - if (name[0] != '-') - continue; - tal_arr_expand(&names, - tal_strndup(names, name+1, len-1)); - } - /* We don't usually print dev or deprecated options, unless - * they explicitly ask, or they're set. */ - json_add_config(cmd->ld, response, config != NULL, - &opt_table[i], names); - } - json_object_end(response); - - return command_success(cmd, response); + ; } - -static const struct json_command listconfigs_command = { - "listconfigs", - "utility", - json_listconfigs, - "List all configuration options, or with [config], just that one.", - .verbose = "listconfigs [config]\n" - "Outputs an object, with each field a config options\n" - "(Option names which start with # are comments)\n" - "With [config], object only has that field" -}; -AUTODATA(json_command, &listconfigs_command); diff --git a/lightningd/options.h b/lightningd/options.h index a885dde3abca..0c99a3a7bbe8 100644 --- a/lightningd/options.h +++ b/lightningd/options.h @@ -3,6 +3,7 @@ #include "config.h" #include +struct json_stream; struct lightningd; /* After this, early config file and cmdline options parsed. */ @@ -24,4 +25,10 @@ bool opt_show_autobool(char *buf, size_t len, const enum opt_autobool *b); /* opt_bool is quite loose; you should use this if wanting to add it to JSON */ bool opt_canon_bool(const char *val); + +void add_config_deprecated(struct lightningd *ld, + struct json_stream *response, + const struct opt_table *opt, + const char *name, size_t len); +bool is_known_opt_cb_arg(char *(*cb_arg)(const char *, void *)); #endif /* LIGHTNING_LIGHTNINGD_OPTIONS_H */ From 9455f9fa42e3410f4c7adac63e43df83f07be19c Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Mon, 5 Jun 2023 23:48:21 +0930 Subject: [PATCH 02/12] common: allow configvars to be marked dynamic. To test, we do min-capacity-sat which is simple. We also update the listconfigs man page which contained some obsolete information. Signed-off-by: Rusty Russell --- common/configvar.h | 2 ++ doc/lightning-listconfigs.7.md | 24 ++++++++++++++++++++---- doc/schemas/listconfigs.schema.json | 7 +++++++ lightningd/configs.c | 6 ++++++ lightningd/options.c | 2 +- 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/common/configvar.h b/common/configvar.h index dcfe5d3ace09..81ffda172960 100644 --- a/common/configvar.h +++ b/common/configvar.h @@ -58,6 +58,8 @@ struct configvar { #define OPT_SHOWMSATS (1 << (OPT_USER_START+4)) /* listconfigs should treat as a literal boolean `true` or `false` */ #define OPT_SHOWBOOL (1 << (OPT_USER_START+5)) +/* Can be changed at runtime */ +#define OPT_DYNAMIC (1 << (OPT_USER_START+6)) /* Use this instead of opt_register_*_arg if you want OPT_* from above */ #define clnopt_witharg(names, type, cb, show, arg, desc) \ diff --git a/doc/lightning-listconfigs.7.md b/doc/lightning-listconfigs.7.md index acb1b41e8c86..aa8ec475b6a7 100644 --- a/doc/lightning-listconfigs.7.md +++ b/doc/lightning-listconfigs.7.md @@ -9,12 +9,27 @@ SYNOPSIS DESCRIPTION ----------- -*config* (optional) is a configuration option name, or "plugin" to show plugin options +*config* (optional) is a configuration option name to restrict return. -The **listconfigs** RPC command to list all configuration options, or with *config* only a selection. +The **listconfigs** RPC command to list all configuration options, or with *config* only one. The returned values reflect the current configuration, including -showing default values (`dev-` options are not shown). +showing default values (`dev-` options are not shown unless specified as *config* explicitly). + +Note: as plugins can add options, not all configuration settings are +listed here! The format of each entry is as follows: + +- **source** (string): source of configuration setting (`file`:`linenum`) +- **dynamic** (boolean, optional): true if this option is settable via setconfig +- **plugin** (string, optional): set if this is from a plugin + +Depending on the option type, exactly one of the following is present: + +- **set** (boolean, optional): for simple flag options +- **value\_str** (string, optional): for string options +- **value\_msat** (msat, optional): for msat options +- **value\_int** (integer, optional): for integer options +- **value\_bool** (boolean, optional): for boolean options EXAMPLE JSON REQUEST -------------------- @@ -177,6 +192,7 @@ On success, an object is returned, containing: - **min-capacity-sat** (object, optional): - **value\_int** (u64): field from config or cmdline, or default - **source** (string): source of configuration setting + - **dynamic** (boolean, optional): Can this be set by setconfig() (always *true*) - **addr** (object, optional): - **values\_str** (array of strings): - field from config or cmdline @@ -447,4 +463,4 @@ RESOURCES Main web site: -[comment]: # ( SHA256STAMP:2b3588b395919162c122cd386f0f4b320d906d0190e706bfa1b68db4126e7ee2) +[comment]: # ( SHA256STAMP:0440e4634e4a28681323f891307c7bb61143aacad4824f952f24f027a7543835) diff --git a/doc/schemas/listconfigs.schema.json b/doc/schemas/listconfigs.schema.json index 900dbbcf306c..cac3ecbdf852 100644 --- a/doc/schemas/listconfigs.schema.json +++ b/doc/schemas/listconfigs.schema.json @@ -834,6 +834,13 @@ "source": { "type": "string", "description": "source of configuration setting" + }, + "dynamic": { + "type": "boolean", + "enum": [ + true + ], + "description": "Can this be set by setconfig()" } } }, diff --git a/lightningd/configs.c b/lightningd/configs.c index ad1f50e9e887..0d1565a2bb40 100644 --- a/lightningd/configs.c +++ b/lightningd/configs.c @@ -143,6 +143,8 @@ static void json_add_config(struct lightningd *ld, json_add_bool(response, "set", cv != NULL); json_add_source(response, "source", cv); json_add_config_plugin(response, ld->plugins, "plugin", ot); + if (ot->type & OPT_DYNAMIC) + json_add_bool(response, "dynamic", true); json_object_end(response); return; } @@ -167,6 +169,8 @@ static void json_add_config(struct lightningd *ld, } json_array_end(response); json_add_config_plugin(response, ld->plugins, "plugin", ot); + if (ot->type & OPT_DYNAMIC) + json_add_bool(response, "dynamic", true); json_object_end(response); return; } @@ -180,6 +184,8 @@ static void json_add_config(struct lightningd *ld, json_add_configval(response, configval_fieldname(ot), ot, val); json_add_source(response, "source", cv); json_add_config_plugin(response, ld->plugins, "plugin", ot); + if (ot->type & OPT_DYNAMIC) + json_add_bool(response, "dynamic", true); json_object_end(response); } diff --git a/lightningd/options.c b/lightningd/options.c index ab4eeb0cd0e3..97bcd9ac98d3 100644 --- a/lightningd/options.c +++ b/lightningd/options.c @@ -1352,7 +1352,7 @@ static void register_opts(struct lightningd *ld) opt_set_msat, opt_show_msat, &ld->config.max_dust_htlc_exposure_msat, "Max HTLC amount that can be trimmed"); - clnopt_witharg("--min-capacity-sat", OPT_SHOWINT, opt_set_u64, opt_show_u64, + clnopt_witharg("--min-capacity-sat", OPT_SHOWINT|OPT_DYNAMIC, opt_set_u64, opt_show_u64, &ld->config.min_capacity_sat, "Minimum capacity in satoshis for accepting channels"); clnopt_witharg("--addr", OPT_MULTI, opt_add_addr, NULL, From 8e0d38ed3c2647aab7ed291edc51f2482ab1bfb1 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 6 Jun 2023 10:08:47 +0930 Subject: [PATCH 03/12] lightningd, libplugins: allocate opt strings from tmpctx, not NULL. Previously, if these failed we always exited; once we have dymamic configs this would be a (tiny) memory leak, so use tmpctx. Signed-off-by: Rusty Russell --- lightningd/log.c | 4 +-- lightningd/options.c | 64 ++++++++++++++++++++--------------------- lightningd/plugin.c | 4 +-- plugins/funder.c | 10 +++---- plugins/funder_policy.c | 2 +- plugins/libplugin.c | 20 ++++++------- 6 files changed, 52 insertions(+), 52 deletions(-) diff --git a/lightningd/log.c b/lightningd/log.c index 58ec4fa2277a..44ae23d50b16 100644 --- a/lightningd/log.c +++ b/lightningd/log.c @@ -581,7 +581,7 @@ char *opt_log_level(const char *arg, struct log *log) len = strcspn(arg, ":"); if (!log_level_parse(arg, len, &level)) - return tal_fmt(NULL, "unknown log level %.*s", len, arg); + return tal_fmt(tmpctx, "unknown log level %.*s", len, arg); if (arg[len]) { struct print_filter *f = tal(log->lr, struct print_filter); @@ -717,7 +717,7 @@ char *arg_log_to_file(const char *arg, struct lightningd *ld) else { outf = fopen(arg, "a"); if (!outf) - return tal_fmt(NULL, "Failed to open: %s", strerror(errno)); + return tal_fmt(tmpctx, "Failed to open: %s", strerror(errno)); } tal_arr_expand(&ld->logfiles, tal_strdup(ld->logfiles, arg)); diff --git a/lightningd/options.c b/lightningd/options.c index 97bcd9ac98d3..10495ae18d7e 100644 --- a/lightningd/options.c +++ b/lightningd/options.c @@ -43,10 +43,10 @@ static char *opt_set_u64(const char *arg, u64 *u) errno = 0; l = strtoull(arg, &endp, 0); if (*endp || !arg[0]) - return tal_fmt(NULL, "'%s' is not a number", arg); + return tal_fmt(tmpctx, "'%s' is not a number", arg); *u = l; if (errno || *u != l) - return tal_fmt(NULL, "'%s' is out of range", arg); + return tal_fmt(tmpctx, "'%s' is out of range", arg); return NULL; } static char *opt_set_u32(const char *arg, u32 *u) @@ -60,10 +60,10 @@ static char *opt_set_u32(const char *arg, u32 *u) errno = 0; l = strtoul(arg, &endp, 0); if (*endp || !arg[0]) - return tal_fmt(NULL, "'%s' is not a number", arg); + return tal_fmt(tmpctx, "'%s' is not a number", arg); *u = l; if (errno || *u != l) - return tal_fmt(NULL, "'%s' is out of range", arg); + return tal_fmt(tmpctx, "'%s' is out of range", arg); return NULL; } @@ -78,10 +78,10 @@ static char *opt_set_s32(const char *arg, s32 *u) errno = 0; l = strtol(arg, &endp, 0); if (*endp || !arg[0]) - return tal_fmt(NULL, "'%s' is not a number", arg); + return tal_fmt(tmpctx, "'%s' is not a number", arg); *u = l; if (errno || *u != l) - return tal_fmt(NULL, "'%s' is out of range", arg); + return tal_fmt(tmpctx, "'%s' is out of range", arg); return NULL; } @@ -130,13 +130,13 @@ static char *opt_set_mode(const char *arg, mode_t *m) /* Ensure length, and starts with 0. */ if (strlen(arg) != 4 || arg[0] != '0') - return tal_fmt(NULL, "'%s' is not a file mode", arg); + return tal_fmt(tmpctx, "'%s' is not a file mode", arg); /* strtol, manpage, yech. */ errno = 0; l = strtol(arg, &endp, 8); /* Octal. */ if (errno || *endp) - return tal_fmt(NULL, "'%s' is not a file mode", arg); + return tal_fmt(tmpctx, "'%s' is not a file mode", arg); *m = l; /* Range check not needed, previous strlen checks ensures only * 9-bit, which fits mode_t (unless your Unix is seriously borked). @@ -254,7 +254,7 @@ static char *opt_add_addr_withtype(const char *arg, err_msg = parse_wireaddr_internal(tmpctx, arg, ld->portnum, dns_lookup_ok, &wi); if (err_msg) - return tal_fmt(NULL, "Unable to parse address '%s': %s", arg, err_msg); + return tal_fmt(tmpctx, "Unable to parse address '%s': %s", arg, err_msg); /* Check they didn't specify some weird type! */ switch (wi.itype) { @@ -263,7 +263,7 @@ static char *opt_add_addr_withtype(const char *arg, case ADDR_TYPE_IPV4: case ADDR_TYPE_IPV6: if ((ala & ADDR_ANNOUNCE) && wi.u.allproto.is_websocket) - return tal_fmt(NULL, + return tal_fmt(tmpctx, "Cannot announce websocket address, use --bind-addr=%s", arg); /* These can be either bind or announce */ break; @@ -274,7 +274,7 @@ static char *opt_add_addr_withtype(const char *arg, switch (ala) { case ADDR_LISTEN: if (!deprecated_apis) - return tal_fmt(NULL, + return tal_fmt(tmpctx, "Don't use --bind-addr=%s, use --announce-addr=%s", arg, arg); log_unusual(ld->log, @@ -286,7 +286,7 @@ static char *opt_add_addr_withtype(const char *arg, return NULL; case ADDR_LISTEN_AND_ANNOUNCE: if (!deprecated_apis) - return tal_fmt(NULL, + return tal_fmt(tmpctx, "Don't use --addr=%s, use --announce-addr=%s", arg, arg); log_unusual(ld->log, @@ -306,10 +306,10 @@ static char *opt_add_addr_withtype(const char *arg, case ADDR_ANNOUNCE: break; case ADDR_LISTEN: - return tal_fmt(NULL, + return tal_fmt(tmpctx, "Cannot use dns: prefix with --bind-addr, use --bind-addr=%s", arg + strlen("dns:")); case ADDR_LISTEN_AND_ANNOUNCE: - return tal_fmt(NULL, + return tal_fmt(tmpctx, "Cannot use dns: prefix with --addr, use --bind-addr=%s and --addr=%s", arg + strlen("dns:"), arg); @@ -320,18 +320,18 @@ static char *opt_add_addr_withtype(const char *arg, * - MUST NOT announce more than one `type 5` DNS hostname. */ if (num_announced_types(ADDR_TYPE_DNS, ld) > 0) - return tal_fmt(NULL, "Only one DNS can be announced"); + return tal_fmt(tmpctx, "Only one DNS can be announced"); break; } break; case ADDR_INTERNAL_SOCKNAME: switch (ala) { case ADDR_ANNOUNCE: - return tal_fmt(NULL, + return tal_fmt(tmpctx, "Cannot announce sockets, try --bind-addr=%s", arg); case ADDR_LISTEN_AND_ANNOUNCE: if (!deprecated_apis) - return tal_fmt(NULL, "Don't use --addr=%s, use --bind-addr=%s", + return tal_fmt(tmpctx, "Don't use --addr=%s, use --bind-addr=%s", arg, arg); ala = ADDR_LISTEN; /* Fall thru */ @@ -355,10 +355,10 @@ static char *opt_add_addr_withtype(const char *arg, /* You can only bind to wildcard, and optionally announce */ switch (ala) { case ADDR_ANNOUNCE: - return tal_fmt(NULL, "Cannot use wildcard address '%s'", arg); + return tal_fmt(tmpctx, "Cannot use wildcard address '%s'", arg); case ADDR_LISTEN_AND_ANNOUNCE: if (wi.u.allproto.is_websocket) - return tal_fmt(NULL, + return tal_fmt(tmpctx, "Cannot announce websocket address, use --bind-addr=%s", arg); /* fall thru */ case ADDR_LISTEN: @@ -368,7 +368,7 @@ static char *opt_add_addr_withtype(const char *arg, case ADDR_INTERNAL_FORPROXY: /* You can't use these addresses here at all: this means we've * suppressed DNS and given a string-style name */ - return tal_fmt(NULL, "Cannot resolve address '%s' (not using DNS!)", arg); + return tal_fmt(tmpctx, "Cannot resolve address '%s' (not using DNS!)", arg); } /* Sanity check for exact duplicates. */ @@ -378,7 +378,7 @@ static char *opt_add_addr_withtype(const char *arg, continue; if (wireaddr_internal_eq(&ld->proposed_wireaddr[i], &wi)) - return tal_fmt(NULL, "Duplicate %s address %s", + return tal_fmt(tmpctx, "Duplicate %s address %s", ala & ADDR_ANNOUNCE ? "announce" : "listen", type_to_string(tmpctx, struct wireaddr_internal, &wi)); } @@ -413,11 +413,11 @@ static char *opt_subdaemon(const char *arg, struct lightningd *ld) size_t colonoff = strcspn(arg, ":"); if (!arg[colonoff]) - return tal_fmt(NULL, "argument must contain ':'"); + return tal_fmt(tmpctx, "argument must contain ':'"); subdaemon = tal_strndup(ld, arg, colonoff); if (!is_subdaemon(subdaemon)) - return tal_fmt(NULL, "\"%s\" is not a subdaemon", subdaemon); + return tal_fmt(tmpctx, "\"%s\" is not a subdaemon", subdaemon); /* Make the value a tal-child of the subdaemon */ sdpath = tal_strdup(subdaemon, arg + colonoff + 1); @@ -476,7 +476,7 @@ static char *opt_set_rgb(const char *arg, struct lightningd *ld) */ ld->rgb = tal_hexdata(ld, arg, strlen(arg)); if (!ld->rgb || tal_count(ld->rgb) != 3) - return tal_fmt(NULL, "rgb '%s' is not six hex digits", arg); + return tal_fmt(tmpctx, "rgb '%s' is not six hex digits", arg); return NULL; } @@ -503,7 +503,7 @@ static char *opt_set_alias(const char *arg, struct lightningd *ld) * `alias` trailing-bytes equal to 0. */ if (strlen(arg) > 32) - return tal_fmt(NULL, "Alias '%s' is over 32 characters", arg); + return tal_fmt(tmpctx, "Alias '%s' is over 32 characters", arg); ld->alias = tal_arrz(ld, u8, 33); strncpy((char*)ld->alias, arg, 32); return NULL; @@ -542,7 +542,7 @@ static char *opt_add_plugin(const char *arg, struct lightningd *ld) } p = plugin_register(ld->plugins, arg, NULL, false, NULL, NULL); if (!p) - return tal_fmt(NULL, "Failed to register %s: %s", arg, strerror(errno)); + return tal_fmt(tmpctx, "Failed to register %s: %s", arg, strerror(errno)); return NULL; } @@ -579,7 +579,7 @@ static char *opt_important_plugin(const char *arg, struct lightningd *ld) } p = plugin_register(ld->plugins, arg, NULL, true, NULL, NULL); if (!p) - return tal_fmt(NULL, "Failed to register %s: %s", arg, strerror(errno)); + return tal_fmt(tmpctx, "Failed to register %s: %s", arg, strerror(errno)); return NULL; } @@ -656,7 +656,7 @@ static char *opt_force_privkey(const char *optarg, struct lightningd *ld) ld->dev_force_privkey = tal(ld, struct privkey); if (!hex_decode(optarg, strlen(optarg), ld->dev_force_privkey, sizeof(*ld->dev_force_privkey))) - return tal_fmt(NULL, "Unable to parse privkey '%s'", optarg); + return tal_fmt(tmpctx, "Unable to parse privkey '%s'", optarg); return NULL; } @@ -667,7 +667,7 @@ static char *opt_force_bip32_seed(const char *optarg, struct lightningd *ld) if (!hex_decode(optarg, strlen(optarg), ld->dev_force_bip32_seed, sizeof(*ld->dev_force_bip32_seed))) - return tal_fmt(NULL, "Unable to parse secret '%s'", optarg); + return tal_fmt(tmpctx, "Unable to parse secret '%s'", optarg); return NULL; } @@ -678,7 +678,7 @@ static char *opt_force_tmp_channel_id(const char *optarg, struct lightningd *ld) if (!hex_decode(optarg, strlen(optarg), ld->dev_force_tmp_channel_id, sizeof(*ld->dev_force_tmp_channel_id))) - return tal_fmt(NULL, "Unable to parse channel id '%s'", optarg); + return tal_fmt(tmpctx, "Unable to parse channel id '%s'", optarg); return NULL; } @@ -1109,7 +1109,7 @@ static bool opt_show_msat(char *buf, size_t len, const struct amount_msat *msat) static char *opt_set_msat(const char *arg, struct amount_msat *amt) { if (!parse_amount_msat(amt, arg, strlen(arg))) - return tal_fmt(NULL, "Unable to parse millisatoshi '%s'", arg); + return tal_fmt(tmpctx, "Unable to parse millisatoshi '%s'", arg); return NULL; } @@ -1136,7 +1136,7 @@ static char *opt_set_websocket_port(const char *arg, struct lightningd *ld) ld->websocket_port = port; if (ld->websocket_port != port) - return tal_fmt(NULL, "'%s' is out of range", arg); + return tal_fmt(tmpctx, "'%s' is out of range", arg); return NULL; } diff --git a/lightningd/plugin.c b/lightningd/plugin.c index d7ad4707e795..721e777569a3 100644 --- a/lightningd/plugin.c +++ b/lightningd/plugin.c @@ -1685,7 +1685,7 @@ char *add_plugin_dir(struct plugins *plugins, const char *dir, bool error_ok) if (!d) { if (!error_ok && errno == ENOENT) return NULL; - return tal_fmt(NULL, "Failed to open plugin-dir %s: %s", + return tal_fmt(tmpctx, "Failed to open plugin-dir %s: %s", dir, strerror(errno)); } @@ -1705,7 +1705,7 @@ char *add_plugin_dir(struct plugins *plugins, const char *dir, bool error_ok) NULL, NULL); if (!p && !error_ok) { closedir(d); - return tal_fmt(NULL, "Failed to register %s: %s", + return tal_fmt(tmpctx, "Failed to register %s: %s", fullpath, strerror(errno)); } } diff --git a/plugins/funder.c b/plugins/funder.c index dc09e310f37a..3f684abf5815 100644 --- a/plugins/funder.c +++ b/plugins/funder.c @@ -1523,14 +1523,14 @@ static char *option_channel_base(const char *arg, struct funder_policy *policy) struct amount_msat amt; if (!parse_amount_msat(&amt, arg, strlen(arg))) - return tal_fmt(NULL, "Unable to parse amount '%s'", arg); + return tal_fmt(tmpctx, "Unable to parse amount '%s'", arg); if (!policy->rates) policy->rates = default_lease_rates(policy); if (!assign_overflow_u32(&policy->rates->channel_fee_max_base_msat, amt.millisatoshis)) /* Raw: conversion */ - return tal_fmt(NULL, "channel_fee_max_base_msat overflowed"); + return tal_fmt(tmpctx, "channel_fee_max_base_msat overflowed"); return NULL; } @@ -1547,7 +1547,7 @@ option_channel_fee_proportional_thousandths_max(const char *arg, static char *amount_option(const char *arg, struct amount_sat *amt) { if (!parse_amount_sat(amt, arg, strlen(arg))) - return tal_fmt(NULL, "Unable to parse amount '%s'", arg); + return tal_fmt(tmpctx, "Unable to parse amount '%s'", arg); return NULL; } @@ -1566,7 +1566,7 @@ static char *option_lease_fee_base(const char *arg, if (!assign_overflow_u32(&policy->rates->lease_fee_base_sat, amt.satoshis)) /* Raw: conversion */ - return tal_fmt(NULL, "lease_fee_base_sat overflowed"); + return tal_fmt(tmpctx, "lease_fee_base_sat overflowed"); return NULL; } @@ -1596,7 +1596,7 @@ static char *amount_sat_or_u64_option(const char *arg, u64 *amt) if (err) { tal_free(err); if (!parse_amount_sat(&sats, arg, strlen(arg))) - return tal_fmt(NULL, + return tal_fmt(tmpctx, "Unable to parse option '%s'", arg); diff --git a/plugins/funder_policy.c b/plugins/funder_policy.c index 59f1744c25f7..c7fdffbe4872 100644 --- a/plugins/funder_policy.c +++ b/plugins/funder_policy.c @@ -30,7 +30,7 @@ char *funding_option(const char *arg, enum funder_opt *opt) else if (streq(arg, "fixed")) *opt = FIXED; else - return tal_fmt(NULL, "'%s' is not a valid option" + return tal_fmt(tmpctx, "'%s' is not a valid option" " (match, available, fixed)", arg); return NULL; diff --git a/plugins/libplugin.c b/plugins/libplugin.c index 3f607e89e60c..916c4313fe33 100644 --- a/plugins/libplugin.c +++ b/plugins/libplugin.c @@ -1233,9 +1233,9 @@ char *u64_option(const char *arg, u64 *i) errno = 0; *i = strtol(arg, &endp, 0); if (*endp || !arg[0]) - return tal_fmt(NULL, "'%s' is not a number", arg); + return tal_fmt(tmpctx, "'%s' is not a number", arg); if (errno) - return tal_fmt(NULL, "'%s' is out of range", arg); + return tal_fmt(tmpctx, "'%s' is out of range", arg); return NULL; } @@ -1247,13 +1247,13 @@ char *u32_option(const char *arg, u32 *i) errno = 0; n = strtoul(arg, &endp, 0); if (*endp || !arg[0]) - return tal_fmt(NULL, "'%s' is not a number", arg); + return tal_fmt(tmpctx, "'%s' is not a number", arg); if (errno) - return tal_fmt(NULL, "'%s' is out of range", arg); + return tal_fmt(tmpctx, "'%s' is out of range", arg); *i = n; if (*i != n) - return tal_fmt(NULL, "'%s' is too large (overflow)", arg); + return tal_fmt(tmpctx, "'%s' is too large (overflow)", arg); return NULL; } @@ -1266,13 +1266,13 @@ char *u16_option(const char *arg, u16 *i) errno = 0; n = strtoul(arg, &endp, 0); if (*endp || !arg[0]) - return tal_fmt(NULL, "'%s' is not a number", arg); + return tal_fmt(tmpctx, "'%s' is not a number", arg); if (errno) - return tal_fmt(NULL, "'%s' is out of range", arg); + return tal_fmt(tmpctx, "'%s' is out of range", arg); *i = n; if (*i != n) - return tal_fmt(NULL, "'%s' is too large (overflow)", arg); + return tal_fmt(tmpctx, "'%s' is too large (overflow)", arg); return NULL; } @@ -1280,7 +1280,7 @@ char *u16_option(const char *arg, u16 *i) char *bool_option(const char *arg, bool *i) { if (!streq(arg, "true") && !streq(arg, "false")) - return tal_fmt(NULL, "'%s' is not a bool, must be \"true\" or \"false\"", arg); + return tal_fmt(tmpctx, "'%s' is not a bool, must be \"true\" or \"false\"", arg); *i = streq(arg, "true"); return NULL; @@ -1292,7 +1292,7 @@ char *flag_option(const char *arg, bool *i) * by default */ assert(*i == false); if (!streq(arg, "true")) - return tal_fmt(NULL, "Invalid argument '%s' passed to a flag", arg); + return tal_fmt(tmpctx, "Invalid argument '%s' passed to a flag", arg); *i = true; return NULL; From a5151a663f02abb57375fca9b67eb9de85077e7f Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 6 Jun 2023 10:08:52 +0930 Subject: [PATCH 04/12] lightningd: setconfig command. Currently only implemented for min-capacity-sat. Signed-off-by: Rusty Russell Changelog-Added: JSON-RPC: new command `setconfig` allows a limited number of configuration settings to be changed without restart. --- doc/Makefile | 1 + doc/index.rst | 1 + doc/lightning-setconfig.7.md | 59 +++++++++ doc/lightningd-config.5.md | 4 +- doc/schemas/setconfig.request.json | 15 +++ doc/schemas/setconfig.schema.json | 62 +++++++++ lightningd/configs.c | 200 +++++++++++++++++++++++++++-- tests/test_misc.py | 54 ++++++++ 8 files changed, 387 insertions(+), 9 deletions(-) create mode 100644 doc/lightning-setconfig.7.md create mode 100644 doc/schemas/setconfig.request.json create mode 100644 doc/schemas/setconfig.schema.json diff --git a/doc/Makefile b/doc/Makefile index 84bb63c3b3cc..ceea02655b40 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -89,6 +89,7 @@ MANPAGES := doc/lightning-cli.1 \ doc/lightning-sendonionmessage.7 \ doc/lightning-sendpay.7 \ doc/lightning-setchannel.7 \ + doc/lightning-setconfig.7 \ doc/lightning-setpsbtversion.7 \ doc/lightning-sendcustommsg.7 \ doc/lightning-signinvoice.7 \ diff --git a/doc/index.rst b/doc/index.rst index 65f568e23f21..2cbf78563f27 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -122,6 +122,7 @@ Core Lightning Documentation lightning-sendpay lightning-sendpsbt lightning-setchannel + lightning-setconfig lightning-setpsbtversion lightning-signinvoice lightning-signmessage diff --git a/doc/lightning-setconfig.7.md b/doc/lightning-setconfig.7.md new file mode 100644 index 000000000000..9925a0d40c46 --- /dev/null +++ b/doc/lightning-setconfig.7.md @@ -0,0 +1,59 @@ +lightning-setconfig -- Dynamically change some config options +============================================================= + +SYNOPSIS +-------- + +**setconfig** *config* [*val*] + +DESCRIPTION +----------- + +The **setconfig** RPC command allows you set the (dynamic) configuration option named by `config`: options which take a value (as separate from simple flag options) also need a `val` parameter. + +This new value will *also* be written at the end of the config file, for persistence across restarts. + +You can see what options are dynamically adjustable using lightning-listconfigs(7). Note that you can also adjust existing options for stopped plugins; they will have an effect when the plugin is restarted. + + +RETURN VALUE +------------ + +[comment]: # (GENERATE-FROM-SCHEMA-START) +On success, an object containing **config** is returned. It is an object containing: + +- **config** (string): name of the config variable which was set +- **source** (string): source of configuration setting (`file`:`linenum`) +- **dynamic** (boolean): whether this option is settable via setconfig (always *true*) +- **plugin** (string, optional): the plugin this configuration setting is for +- **set** (boolean, optional): for simple flag options +- **value\_str** (string, optional): for string options +- **value\_msat** (msat, optional): for msat options +- **value\_int** (integer, optional): for integer options +- **value\_bool** (boolean, optional): for boolean options + +[comment]: # (GENERATE-FROM-SCHEMA-END) + +ERRORS +------ + +The following error codes may occur: +- -32602: JSONRPC2\_INVALID\_PARAMS, i.e. the parameter is not dynamic, or the val was invalid. + +AUTHOR +------ + +Rusty Russell <> is mainly responsible for this +feature. + +SEE ALSO +-------- + +lightningd-config(5), lightning-listconfigs(7) + +RESOURCES +--------- + +Main web site: + +[comment]: # ( SHA256STAMP:d61e4e6eea7b8c214644334ee194b273aef2a8a26465adfcd685be0d70653966) diff --git a/doc/lightningd-config.5.md b/doc/lightningd-config.5.md index f64d4c73ad61..59226584c49f 100644 --- a/doc/lightningd-config.5.md +++ b/doc/lightningd-config.5.md @@ -14,6 +14,8 @@ file (default: **$HOME/.lightning/config**) then a network-specific configuration file (default: **$HOME/.lightning/testnet/config**). This can be changed: see *--conf* and *--lightning-dir*. +Note that some configuration options, marked *dynamic*m can be changed at runtime: see lightning-setconfig(7). + General configuration files are processed first, then network-specific ones, then command line options: later options override earlier ones except *addr* options and *log-level* with subsystems, which @@ -316,7 +318,7 @@ millionths, so 10000 is 1%, 1000 is 0.1%. Changing this value will only affect new channels and not existing ones. If you want to change fees for existing channels, use the RPC call lightning-setchannel(7). -* **min-capacity-sat**=*SATOSHI* +* **min-capacity-sat**=*SATOSHI* [*dynamic*] Default: 10000. This value defines the minimal effective channel capacity in satoshi to accept for channel opening requests. This will diff --git a/doc/schemas/setconfig.request.json b/doc/schemas/setconfig.request.json new file mode 100644 index 000000000000..7dd24fc1b151 --- /dev/null +++ b/doc/schemas/setconfig.request.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "config" + ], + "added": "v23.08", + "properties": { + "config": { + "type": "string" + }, + "val": {} + } +} diff --git a/doc/schemas/setconfig.schema.json b/doc/schemas/setconfig.schema.json new file mode 100644 index 000000000000..a4c09e32cb61 --- /dev/null +++ b/doc/schemas/setconfig.schema.json @@ -0,0 +1,62 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "added": "v23.08", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "description": "config settings after completion", + "additionalProperties": false, + "required": [ + "config", + "source", + "dynamic" + ], + "properties": { + "config": { + "type": "string", + "description": "name of the config variable which was set" + }, + "source": { + "type": "string", + "description": "source of configuration setting (`file`:`linenum`)" + }, + "plugin": { + "type": "string", + "description": "the plugin this configuration setting is for" + }, + "dynamic": { + "type": "boolean", + "enum": [ + true + ], + "description": "whether this option is settable via setconfig" + }, + "set": { + "type": "boolean", + "description": "for simple flag options" + }, + "value_str": { + "type": "string", + "description": "for string options" + }, + "value_msat": { + "type": "msat", + "description": "for msat options" + }, + "value_int": { + "type": "integer", + "description": "for integer options" + }, + "value_bool": { + "type": "boolean", + "description": "for boolean options" + } + } + } + } +} diff --git a/lightningd/configs.c b/lightningd/configs.c index 0d1565a2bb40..179740e3e200 100644 --- a/lightningd/configs.c +++ b/lightningd/configs.c @@ -2,15 +2,20 @@ #include #include #include +#include +#include #include #include #include #include #include #include +#include +#include #include #include #include +#include static void json_add_source(struct json_stream *result, const char *fieldname, @@ -113,10 +118,14 @@ static void json_add_configval(struct json_stream *result, } /* Config vars can have multiple names ("--large-channels|--wumbo"), but first - * is preferred */ + * is preferred. + * wrap_object means we wrap json in an object of that name, otherwise outputs + * raw fields. + */ static void json_add_config(struct lightningd *ld, struct json_stream *response, bool always_include, + bool wrap_object, const struct opt_table *ot, const char **names) { @@ -139,19 +148,22 @@ static void json_add_config(struct lightningd *ld, return; if (ot->type & OPT_NOARG) { - json_object_start(response, names[0]); + if (wrap_object) + json_object_start(response, names[0]); json_add_bool(response, "set", cv != NULL); json_add_source(response, "source", cv); json_add_config_plugin(response, ld->plugins, "plugin", ot); if (ot->type & OPT_DYNAMIC) json_add_bool(response, "dynamic", true); - json_object_end(response); + if (wrap_object) + json_object_end(response); return; } assert(ot->type & OPT_HASARG); if (ot->type & OPT_MULTI) { - json_object_start(response, names[0]); + if (wrap_object) + json_object_start(response, names[0]); json_array_start(response, configval_fieldname(ot)); while (cv) { val = get_opt_val(ot, buf, cv); @@ -171,7 +183,8 @@ static void json_add_config(struct lightningd *ld, json_add_config_plugin(response, ld->plugins, "plugin", ot); if (ot->type & OPT_DYNAMIC) json_add_bool(response, "dynamic", true); - json_object_end(response); + if (wrap_object) + json_object_end(response); return; } @@ -180,13 +193,15 @@ static void json_add_config(struct lightningd *ld, if (!val) return; - json_object_start(response, names[0]); + if (wrap_object) + json_object_start(response, names[0]); json_add_configval(response, configval_fieldname(ot), ot, val); json_add_source(response, "source", cv); json_add_config_plugin(response, ld->plugins, "plugin", ot); if (ot->type & OPT_DYNAMIC) json_add_bool(response, "dynamic", true); - json_object_end(response); + if (wrap_object) + json_object_end(response); } static struct command_result *param_opt_config(struct command *cmd, @@ -292,7 +307,7 @@ static struct command_result *json_listconfigs(struct command *cmd, } /* We don't usually print dev or deprecated options, unless * they explicitly ask, or they're set. */ - json_add_config(cmd->ld, response, config != NULL, + json_add_config(cmd->ld, response, config != NULL, true, &opt_table[i], names); } json_object_end(response); @@ -311,3 +326,172 @@ static const struct json_command listconfigs_command = { "With [config], object only has that field" }; AUTODATA(json_command, &listconfigs_command); + +static struct command_result *param_opt_dynamic_config(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + const struct opt_table **config) +{ + struct command_result *ret; + + ret = param_opt_config(cmd, name, buffer, tok, config); + if (ret) + return ret; + + if (!((*config)->type & OPT_DYNAMIC)) + return command_fail_badparam(cmd, name, buffer, tok, + "Not a dynamic config option"); + return NULL; +} + +/* FIXME: put in ccan/mem! */ +static size_t memcount(const void *mem, size_t len, char c) +{ + size_t count = 0; + for (size_t i = 0; i < len; i++) { + if (((char *)mem)[i] == c) + count++; + } + return count; +} + +static void configvar_append_file(struct lightningd *ld, + const char *fname, + enum configvar_src src, + const char *confline, + bool must_exist) +{ + int fd; + size_t num_lines; + const char *buffer, *insert; + bool needs_term; + struct configvar *cv; + time_t now = time(NULL); + + fd = open(fname, O_RDWR|O_APPEND); + if (fd < 0) { + if (errno != ENOENT || must_exist) + fatal("Could not write to config %s: %s", + fname, strerror(errno)); + fd = open(fname, O_RDWR|O_APPEND|O_CREAT, 0644); + if (fd < 0) + fatal("Could not create config file %s: %s", + fname, strerror(errno)); + } + + /* Note: always nul terminates */ + buffer = grab_fd(tmpctx, fd); + if (!buffer) + fatal("Error reading %s: %s", fname, strerror(errno)); + + num_lines = memcount(buffer, tal_bytelen(buffer)-1, '\n'); + + /* If there's a last character and it's not \n, add one */ + if (tal_bytelen(buffer) == 1) + needs_term = false; + else + needs_term = (buffer[tal_bytelen(buffer)-2] != '\n'); + + /* Note: ctime() contains a \n! */ + insert = tal_fmt(tmpctx, "%s# Inserted by setconfig %s%s\n", + needs_term ? "\n": "", + ctime(&now), confline); + if (write(fd, insert, strlen(insert)) != strlen(insert)) + fatal("Could not write to config file %s: %s", + fname, strerror(errno)); + + cv = configvar_new(ld->configvars, src, fname, num_lines+2, confline); + configvar_unparsed(cv); + + log_info(ld->log, "setconfig: %s %s (updated %s:%u)", + cv->optvar, cv->optarg ? cv->optarg : "SET", + cv->file, cv->linenum); + + tal_arr_expand(&ld->configvars, cv); + configvar_finalize_overrides(ld->configvars); +} + +static void configvar_save(struct lightningd *ld, const char *confline) +{ + /* If they used --conf then append to that */ + if (ld->config_filename) + configvar_append_file(ld, + ld->config_filename, + CONFIGVAR_EXPLICIT_CONF, + confline, true); + else { + const char *fname; + + fname = path_join(tmpctx, ld->config_netdir, "config"); + configvar_append_file(ld, + fname, + CONFIGVAR_NETWORK_CONF, + confline, + false); + } +} + +static struct command_result *json_setconfig(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) +{ + struct json_stream *response; + const struct opt_table *ot; + const char *val, **names, *confline; + unsigned int len; + char *err; + + if (!param(cmd, buffer, params, + p_req("config", param_opt_dynamic_config, &ot), + p_opt("val", param_string, &val), + NULL)) + return command_param_failed(); + + /* We don't handle DYNAMIC MULTI, at least yet! */ + assert(!(ot->type & OPT_MULTI)); + + names = tal_arr(tmpctx, const char *, 1); + /* This includes leading -! */ + names[0] = first_name(ot->names, &len) + 1; + + if (ot->type & OPT_NOARG) { + if (val) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%s does not take a value", + ot->names + 2); + confline = tal_strdup(tmpctx, names[0]); + err = ot->cb(ot->u.arg); + } else { + assert(ot->type & OPT_HASARG); + if (!val) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%s requires a value", + ot->names + 2); + confline = tal_fmt(tmpctx, "%s=%s", names[0], val); + err = ot->cb_arg(val, ot->u.arg); + } + + if (err) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Error setting %s: %s", ot->names + 2, err); + } + + configvar_save(cmd->ld, confline); + + response = json_stream_success(cmd); + json_object_start(response, "config"); + json_add_string(response, "config", names[0]); + json_add_config(cmd->ld, response, true, false, ot, names); + json_object_end(response); + return command_success(cmd, response); +} + +static const struct json_command setconfig_command = { + "setconfig", + "utility", + json_setconfig, + "Set a dynamically-adjustable config." +}; +AUTODATA(json_command, &setconfig_command); diff --git a/tests/test_misc.py b/tests/test_misc.py index 759340985e09..c62ffefb2529 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -3396,3 +3396,57 @@ def test_fast_shutdown(node_factory): except ConnectionRefusedError: continue break + + +def test_setconfig(node_factory): + l1, l2 = node_factory.line_graph(2, fundchannel=False) + configfile = os.path.join(l2.daemon.opts.get("lightning-dir"), TEST_NETWORK, 'config') + + assert (l2.rpc.listconfigs('min-capacity-sat')['configs'] + == {'min-capacity-sat': + {'source': 'default', + 'value_int': 10000, + 'dynamic': True}}) + + with pytest.raises(RpcError, match='requires a value'): + l2.rpc.setconfig('min-capacity-sat') + + with pytest.raises(RpcError, match='requires a value'): + l2.rpc.setconfig(config='min-capacity-sat') + + with pytest.raises(RpcError, match='is not a number'): + l2.rpc.setconfig(config='min-capacity-sat', val="abcd") + + ret = l2.rpc.setconfig(config='min-capacity-sat', val=500000) + assert ret == {'config': + {'config': 'min-capacity-sat', + 'source': '{}:2'.format(configfile), + 'value_int': 500000, + 'dynamic': True}} + + with open(configfile, 'r') as f: + lines = f.read().splitlines() + assert lines[0].startswith('# Inserted by setconfig ') + assert lines[1] == 'min-capacity-sat=500000' + assert len(lines) == 2 + + # Now we need to meet minumum + with pytest.raises(RpcError, match='which is below 500000sat'): + l1.fundchannel(l2, 400000) + + l1.fundchannel(l2, 10**6) + l1.rpc.close(l2.info['id']) + + # It's persistent! + l2.restart() + + assert (l2.rpc.listconfigs('min-capacity-sat')['configs'] + == {'min-capacity-sat': + {'source': '{}:2'.format(configfile), + 'value_int': 500000, + 'dynamic': True}}) + + # Still need to meet minumum + l1.connect(l2) + with pytest.raises(RpcError, match='which is below 500000sat'): + l1.fundchannel(l2, 400000) From 1e9c82d0981992a7aa74b87af02ab627db18c8fb Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 6 Jun 2023 10:08:53 +0930 Subject: [PATCH 05/12] setconfig: comment out now-overridden lines. Do it slightly intelligently, so if we had set previously using setconfig we don't keep appending new ones, but replace it in-place. Signed-off-by: Rusty Russell --- doc/lightning-setconfig.7.md | 2 +- lightningd/configs.c | 169 +++++++++++++++++++++++++++++------ tests/test_misc.py | 38 ++++++++ 3 files changed, 181 insertions(+), 28 deletions(-) diff --git a/doc/lightning-setconfig.7.md b/doc/lightning-setconfig.7.md index 9925a0d40c46..93bf3ca94409 100644 --- a/doc/lightning-setconfig.7.md +++ b/doc/lightning-setconfig.7.md @@ -11,7 +11,7 @@ DESCRIPTION The **setconfig** RPC command allows you set the (dynamic) configuration option named by `config`: options which take a value (as separate from simple flag options) also need a `val` parameter. -This new value will *also* be written at the end of the config file, for persistence across restarts. +This new value will *also* be written at the end of the config file, for persistence across restarts (and any old value commented out). You can see what options are dynamically adjustable using lightning-listconfigs(7). Note that you can also adjust existing options for stopped plugins; they will have an effect when the plugin is restarted. diff --git a/lightningd/configs.c b/lightningd/configs.c index 179740e3e200..4258b05092de 100644 --- a/lightningd/configs.c +++ b/lightningd/configs.c @@ -1,7 +1,9 @@ #include "config.h" +#include #include #include #include +#include #include #include #include @@ -235,6 +237,25 @@ static const char *next_name(const char *names, unsigned *len) return first_name(names + 1, len); } +static const char **opt_names_arr(const tal_t *ctx, + const struct opt_table *ot) +{ + const char **names = tal_arr(ctx, const char *, 0); + const char *name; + unsigned len; + + for (name = first_name(ot->names, &len); + name; + name = next_name(name, &len)) { + /* Skips over first -, so just need to look for one */ + if (name[0] != '-') + continue; + tal_arr_expand(&names, + tal_strndup(names, name+1, len-1)); + } + return names; +} + static struct command_result *json_listconfigs(struct command *cmd, const char *buffer, const jsmntok_t *obj UNNEEDED, @@ -284,8 +305,6 @@ static struct command_result *json_listconfigs(struct command *cmd, modern: json_object_start(response, "configs"); for (size_t i = 0; i < opt_count; i++) { - unsigned int len; - const char *name; const char **names; /* FIXME: Print out comment somehow? */ @@ -295,16 +314,7 @@ static struct command_result *json_listconfigs(struct command *cmd, if (config && config != &opt_table[i]) continue; - names = tal_arr(tmpctx, const char *, 0); - for (name = first_name(opt_table[i].names, &len); - name; - name = next_name(name, &len)) { - /* Skips over first -, so just need to look for one */ - if (name[0] != '-') - continue; - tal_arr_expand(&names, - tal_strndup(names, name+1, len-1)); - } + names = opt_names_arr(tmpctx, &opt_table[i]); /* We don't usually print dev or deprecated options, unless * they explicitly ask, or they're set. */ json_add_config(cmd->ld, response, config != NULL, true, @@ -356,6 +366,28 @@ static size_t memcount(const void *mem, size_t len, char c) return count; } +static void configvar_updated(struct lightningd *ld, + enum configvar_src src, + const char *fname, + size_t linenum, + const char *confline) +{ + struct configvar *cv; + + cv = configvar_new(ld->configvars, src, fname, linenum, confline); + configvar_unparsed(cv); + + log_info(ld->log, "setconfig: %s %s (updated %s:%u)", + cv->optvar, cv->optarg ? cv->optarg : "SET", + cv->file, cv->linenum); + + tal_arr_expand(&ld->configvars, cv); + configvar_finalize_overrides(ld->configvars); +} + +/* Marker for our own insertions */ +#define INSERTED_BY_SETCONFIG "# Inserted by setconfig " + static void configvar_append_file(struct lightningd *ld, const char *fname, enum configvar_src src, @@ -366,7 +398,6 @@ static void configvar_append_file(struct lightningd *ld, size_t num_lines; const char *buffer, *insert; bool needs_term; - struct configvar *cv; time_t now = time(NULL); fd = open(fname, O_RDWR|O_APPEND); @@ -394,26 +425,113 @@ static void configvar_append_file(struct lightningd *ld, needs_term = (buffer[tal_bytelen(buffer)-2] != '\n'); /* Note: ctime() contains a \n! */ - insert = tal_fmt(tmpctx, "%s# Inserted by setconfig %s%s\n", + insert = tal_fmt(tmpctx, "%s"INSERTED_BY_SETCONFIG"%s%s\n", needs_term ? "\n": "", ctime(&now), confline); if (write(fd, insert, strlen(insert)) != strlen(insert)) fatal("Could not write to config file %s: %s", fname, strerror(errno)); - cv = configvar_new(ld->configvars, src, fname, num_lines+2, confline); - configvar_unparsed(cv); + configvar_updated(ld, src, fname, num_lines+2, confline); +} - log_info(ld->log, "setconfig: %s %s (updated %s:%u)", - cv->optvar, cv->optarg ? cv->optarg : "SET", - cv->file, cv->linenum); +/* Returns true if it rewrote in place, otherwise it just comments out + * if necessary */ +static bool configfile_replace_var(struct lightningd *ld, + const struct configvar *cv, + const char *confline) +{ + char *contents, **lines, *template; + int outfd; + bool replaced; + + switch (cv->src) { + case CONFIGVAR_CMDLINE: + case CONFIGVAR_CMDLINE_SHORT: + case CONFIGVAR_PLUGIN_START: + /* These can't be commented out */ + return false; + case CONFIGVAR_EXPLICIT_CONF: + case CONFIGVAR_BASE_CONF: + case CONFIGVAR_NETWORK_CONF: + break; + } - tal_arr_expand(&ld->configvars, cv); - configvar_finalize_overrides(ld->configvars); + contents = grab_file(tmpctx, cv->file); + if (!contents) + fatal("Could not load configfile %s: %s", + cv->file, strerror(errno)); + + lines = tal_strsplit(contents, contents, "\r\n", STR_EMPTY_OK); + if (cv->linenum - 1 >= tal_count(lines)) + fatal("Configfile %s no longer has %u lines!", + cv->file, cv->linenum); + + if (!streq(lines[cv->linenum - 1], cv->configline)) + fatal("Configfile %s line %u changed from %s to %s!", + cv->file, cv->linenum, + cv->configline, + lines[cv->linenum - 1]); + + /* If we already have # Inserted by setconfig above, just replace + * those two! */ + if (cv->linenum > 1 + && strstarts(lines[cv->linenum - 2], INSERTED_BY_SETCONFIG)) { + time_t now = time(NULL); + lines[cv->linenum - 2] = tal_fmt(lines, + INSERTED_BY_SETCONFIG"%s", + ctime(&now)); + /* But trim final \n! (thanks ctime!) */ + assert(strends(lines[cv->linenum - 2], "\n")); + lines[cv->linenum - 2][strlen(lines[cv->linenum - 2])-1] = '\0'; + lines[cv->linenum - 1] = cast_const(char *, confline); + replaced = true; + } else { + /* Comment out, in-place */ + lines[cv->linenum - 1] + = tal_fmt(lines, "# setconfig commented out: %s", + lines[cv->linenum - 1]); + log_info(ld->log, "setconfig: commented out line %u of %s (%s)", + cv->linenum, cv->file, cv->configline); + replaced = false; + } + + template = tal_fmt(tmpctx, "%s.setconfig.XXXXXX", cv->file); + outfd = mkstemp(template); + if (outfd < 0) + fatal("Creating %s: %s", template, strerror(errno)); + + contents = tal_strjoin(tmpctx, take(lines), "\n", STR_TRAIL); + if (!write_all(outfd, contents, strlen(contents))) + fatal("Writing %s: %s", template, strerror(errno)); + fdatasync(outfd); + + if (rename(template, cv->file) != 0) + fatal("Renaming %s over %s: %s", + template, cv->file, strerror(errno)); + close(outfd); + + if (replaced) { + configvar_updated(ld, cv->src, cv->file, cv->linenum, confline); + return true; + } + return false; } -static void configvar_save(struct lightningd *ld, const char *confline) +static void configvar_save(struct lightningd *ld, + const char **names, + const char *confline) { + /* Simple case: set in a config file. */ + struct configvar *oldcv; + + oldcv = configvar_first(ld->configvars, names); + if (oldcv) { + /* At least comment out, maybe replace */ + if (configfile_replace_var(ld, oldcv, confline)) + return; + } + /* If they used --conf then append to that */ if (ld->config_filename) configvar_append_file(ld, @@ -440,7 +558,6 @@ static struct command_result *json_setconfig(struct command *cmd, struct json_stream *response; const struct opt_table *ot; const char *val, **names, *confline; - unsigned int len; char *err; if (!param(cmd, buffer, params, @@ -452,9 +569,7 @@ static struct command_result *json_setconfig(struct command *cmd, /* We don't handle DYNAMIC MULTI, at least yet! */ assert(!(ot->type & OPT_MULTI)); - names = tal_arr(tmpctx, const char *, 1); - /* This includes leading -! */ - names[0] = first_name(ot->names, &len) + 1; + names = opt_names_arr(tmpctx, ot); if (ot->type & OPT_NOARG) { if (val) @@ -478,7 +593,7 @@ static struct command_result *json_setconfig(struct command *cmd, "Error setting %s: %s", ot->names + 2, err); } - configvar_save(cmd->ld, confline); + configvar_save(cmd->ld, names, confline); response = json_stream_success(cmd); json_object_start(response, "config"); diff --git a/tests/test_misc.py b/tests/test_misc.py index c62ffefb2529..fb9569010b55 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -3426,6 +3426,7 @@ def test_setconfig(node_factory): with open(configfile, 'r') as f: lines = f.read().splitlines() + timeline = lines[0] assert lines[0].startswith('# Inserted by setconfig ') assert lines[1] == 'min-capacity-sat=500000' assert len(lines) == 2 @@ -3450,3 +3451,40 @@ def test_setconfig(node_factory): l1.connect(l2) with pytest.raises(RpcError, match='which is below 500000sat'): l1.fundchannel(l2, 400000) + + # Now, changing again will comment that one out! + ret = l2.rpc.setconfig(config='min-capacity-sat', val=400000) + assert ret == {'config': + {'config': 'min-capacity-sat', + 'source': '{}:2'.format(configfile), + 'value_int': 400000, + 'dynamic': True}} + + with open(configfile, 'r') as f: + lines = f.read().splitlines() + assert lines[0].startswith('# Inserted by setconfig ') + # It will have changed timestamp since last time! + assert lines[0] != timeline + assert lines[1] == 'min-capacity-sat=400000' + assert len(lines) == 2 + + # If it's not set by setconfig, it will comment it out instead. + l2.stop() + + with open(configfile, 'w') as f: + f.write('min-capacity-sat=500000\n') + + l2.start() + ret = l2.rpc.setconfig(config='min-capacity-sat', val=400000) + assert ret == {'config': + {'config': 'min-capacity-sat', + 'source': '{}:3'.format(configfile), + 'value_int': 400000, + 'dynamic': True}} + + with open(configfile, 'r') as f: + lines = f.read().splitlines() + assert lines[0].startswith('# setconfig commented out: min-capacity-sat=500000') + assert lines[1].startswith('# Inserted by setconfig ') + assert lines[2] == 'min-capacity-sat=400000' + assert len(lines) == 3 From 2e01ea04c29b410c2672e7608a445d693a42479f Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 6 Jun 2023 10:08:53 +0930 Subject: [PATCH 06/12] setconfig: hook into plugin infrastructure for setconfig. If it's a plugin opt, we'll need a callback, so reshuffle logic. Also add infra to map option name to plugin and option. Signed-off-by: Rusty Russell --- lightningd/configs.c | 46 +++++++++++++++++++++++++++------------ lightningd/plugin.c | 51 ++++++++++++++++++++++++++++++++++++-------- lightningd/plugin.h | 8 +++++++ 3 files changed, 82 insertions(+), 23 deletions(-) diff --git a/lightningd/configs.c b/lightningd/configs.c index 4258b05092de..8e3932366d5a 100644 --- a/lightningd/configs.c +++ b/lightningd/configs.c @@ -550,14 +550,37 @@ static void configvar_save(struct lightningd *ld, } } +static struct command_result *setconfig_success(struct command *cmd, + const struct opt_table *ot, + const char *val) +{ + struct json_stream *response; + const char **names, *confline; + + names = opt_names_arr(tmpctx, ot); + + if (val) + confline = tal_fmt(tmpctx, "%s=%s", names[0], val); + else + confline = names[0]; + + configvar_save(cmd->ld, names, confline); + + response = json_stream_success(cmd); + json_object_start(response, "config"); + json_add_string(response, "config", names[0]); + json_add_config(cmd->ld, response, true, false, ot, names); + json_object_end(response); + return command_success(cmd, response); +} + static struct command_result *json_setconfig(struct command *cmd, const char *buffer, const jsmntok_t *obj UNNEEDED, const jsmntok_t *params) { - struct json_stream *response; const struct opt_table *ot; - const char *val, **names, *confline; + const char *val; char *err; if (!param(cmd, buffer, params, @@ -569,14 +592,14 @@ static struct command_result *json_setconfig(struct command *cmd, /* We don't handle DYNAMIC MULTI, at least yet! */ assert(!(ot->type & OPT_MULTI)); - names = opt_names_arr(tmpctx, ot); - if (ot->type & OPT_NOARG) { if (val) return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "%s does not take a value", ot->names + 2); - confline = tal_strdup(tmpctx, names[0]); + if (is_plugin_opt(ot)) + return plugin_set_dynamic_opt(cmd, ot, NULL, + setconfig_success); err = ot->cb(ot->u.arg); } else { assert(ot->type & OPT_HASARG); @@ -584,7 +607,9 @@ static struct command_result *json_setconfig(struct command *cmd, return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "%s requires a value", ot->names + 2); - confline = tal_fmt(tmpctx, "%s=%s", names[0], val); + if (is_plugin_opt(ot)) + return plugin_set_dynamic_opt(cmd, ot, val, + setconfig_success); err = ot->cb_arg(val, ot->u.arg); } @@ -593,14 +618,7 @@ static struct command_result *json_setconfig(struct command *cmd, "Error setting %s: %s", ot->names + 2, err); } - configvar_save(cmd->ld, names, confline); - - response = json_stream_success(cmd); - json_object_start(response, "config"); - json_add_string(response, "config", names[0]); - json_add_config(cmd->ld, response, true, false, ot, names); - json_object_end(response); - return command_success(cmd, response); + return setconfig_success(cmd, ot, val); } static const struct json_command setconfig_command = { diff --git a/lightningd/plugin.c b/lightningd/plugin.c index 721e777569a3..658e409f904a 100644 --- a/lightningd/plugin.c +++ b/lightningd/plugin.c @@ -1366,6 +1366,27 @@ static struct plugin_opt *plugin_opt_find(const struct plugin *plugin, return NULL; } +/* Find the plugin_opt for this ot */ +static struct plugin *plugin_opt_find_any(const struct plugins *plugins, + const struct opt_table *ot, + struct plugin_opt **poptp) +{ + struct plugin *plugin; + + /* Find the plugin that registered this RPC call */ + list_for_each(&plugins->plugins, plugin, list) { + struct plugin_opt *popt = plugin_opt_find(plugin, ot->names+2); + if (popt) { + if (poptp) + *poptp = popt; + return plugin; + } + } + + /* Reaching here is possible, if a plugin was stopped! */ + return NULL; +} + void json_add_config_plugin(struct json_stream *stream, const struct plugins *plugins, const char *fieldname, @@ -1378,15 +1399,9 @@ void json_add_config_plugin(struct json_stream *stream, return; /* Find the plugin that registered this RPC call */ - list_for_each(&plugins->plugins, plugin, list) { - struct plugin_opt *popt = plugin_opt_find(plugin, ot->names+2); - if (popt) { - json_add_string(stream, fieldname, plugin->cmd); - return; - } - } - - /* Reaching here is possible, if a plugin was stopped! */ + plugin = plugin_opt_find_any(plugins, ot, NULL); + if (plugin) + json_add_string(stream, fieldname, plugin->cmd); } /* Start command might have included plugin-specific parameters. @@ -2028,6 +2043,24 @@ bool plugins_config(struct plugins *plugins) return true; } +struct command_result *plugin_set_dynamic_opt(struct command *cmd, + const struct opt_table *ot, + const char *val, + struct command_result *(*success) + (struct command *, + const struct opt_table *, + const char *)) +{ + struct plugin_opt *popt; + struct plugin *plugin; + + plugin = plugin_opt_find_any(cmd->ld->plugins, ot, &popt); + assert(plugin); + + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "FIXME: Implement dynamic"); +} + /** json_add_opt_plugins_array * * @brief add a named array of plugins to the given response, diff --git a/lightningd/plugin.h b/lightningd/plugin.h index 587a214be17f..fc50cd457bd1 100644 --- a/lightningd/plugin.h +++ b/lightningd/plugin.h @@ -365,4 +365,12 @@ void json_add_config_plugin(struct json_stream *stream, const char *fieldname, const struct opt_table *ot); +/* Attempt to setconfig an option in a plugin. Calls success or fail, may be async! */ +struct command_result *plugin_set_dynamic_opt(struct command *cmd, + const struct opt_table *ot, + const char *val, + struct command_result *(*success) + (struct command *, + const struct opt_table *, + const char *)); #endif /* LIGHTNING_LIGHTNINGD_PLUGIN_H */ From d5129e378c639f46c5d07221aee068ec14a55081 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 6 Jun 2023 10:08:53 +0930 Subject: [PATCH 07/12] plugin: allow plugins to set `dynamic` on options. Signed-off-by: Rusty Russell --- lightningd/plugin.c | 72 +++++++++++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/lightningd/plugin.c b/lightningd/plugin.c index 658e409f904a..606d20e1bdce 100644 --- a/lightningd/plugin.c +++ b/lightningd/plugin.c @@ -893,22 +893,43 @@ bool is_plugin_opt(const struct opt_table *ot) || ot->cb_arg == (void *)plugin_opt_bool_check; } +/* Sets *ret to false if it doesn't appear, otherwise, sets to value */ +static char *bool_setting(tal_t *ctx, + const char *optname, + const char *buffer, + const jsmntok_t *opt, + const char *tokname, + bool *ret) +{ + const jsmntok_t *tok = json_get_member(buffer, opt, tokname); + if (!tok) { + *ret = false; + return NULL; + } + if (!json_to_bool(buffer, tok, ret)) + return tal_fmt(ctx, + "%s: invalid \"%s\" field %.*s", + optname, tokname, + tok->end - tok->start, + buffer + tok->start); + return NULL; +} + /* Add a single plugin option to the plugin as well as registering it with the * command line options. */ static const char *plugin_opt_add(struct plugin *plugin, const char *buffer, const jsmntok_t *opt) { - const jsmntok_t *nametok, *typetok, *defaulttok, *desctok, *deptok, *multitok; + const jsmntok_t *nametok, *typetok, *defaulttok, *desctok; struct plugin_opt *popt; - bool multi; - const char *name; + const char *name, *err; + enum opt_type optflags = 0; + bool set; nametok = json_get_member(buffer, opt, "name"); typetok = json_get_member(buffer, opt, "type"); desctok = json_get_member(buffer, opt, "description"); defaulttok = json_get_member(buffer, opt, "default"); - deptok = json_get_member(buffer, opt, "deprecated"); - multitok = json_get_member(buffer, opt, "multi"); if (!typetok || !nametok || !desctok) { return tal_fmt(plugin, @@ -938,25 +959,21 @@ static const char *plugin_opt_add(struct plugin *plugin, const char *buffer, } popt->description = json_strdup(popt, buffer, desctok); - if (deptok) { - if (!json_to_bool(buffer, deptok, &popt->deprecated)) - return tal_fmt(plugin, - "%s: invalid \"deprecated\" field %.*s", - name, - deptok->end - deptok->start, - buffer + deptok->start); - } else - popt->deprecated = false; - - if (multitok) { - if (!json_to_bool(buffer, multitok, &multi)) - return tal_fmt(plugin, - "%s: invalid \"multi\" field %.*s", - name, - multitok->end - multitok->start, - buffer + multitok->start); - } else - multi = false; + err = bool_setting(plugin, popt->name, buffer, opt, "deprecated", &popt->deprecated); + if (err) + return err; + + err = bool_setting(plugin, popt->name, buffer, opt, "multi", &set); + if (err) + return err; + if (set) + optflags |= OPT_MULTI; + + err = bool_setting(plugin, popt->name, buffer, opt, "dynamic", &set); + if (err) + return err; + if (set) + optflags |= OPT_DYNAMIC; if (json_tok_streq(buffer, typetok, "flag")) { if (defaulttok) { @@ -966,16 +983,15 @@ static const char *plugin_opt_add(struct plugin *plugin, const char *buffer, } defaulttok = NULL; } - if (multi) + if (optflags & OPT_MULTI) return tal_fmt(plugin, "flag type cannot be multi"); clnopt_noarg(popt->name, - 0, + optflags, plugin_opt_flag_check, popt, popt->description); } else { /* These all take an arg. */ char *(*cb_arg)(const char *optarg, void *arg); - enum opt_type optflags = multi ? OPT_MULTI : 0; if (json_tok_streq(buffer, typetok, "string")) { cb_arg = (void *)plugin_opt_string_check; @@ -983,7 +999,7 @@ static const char *plugin_opt_add(struct plugin *plugin, const char *buffer, cb_arg = (void *)plugin_opt_long_check; optflags |= OPT_SHOWINT; } else if (json_tok_streq(buffer, typetok, "bool")) { - if (multi) + if (optflags & OPT_MULTI) return tal_fmt(plugin, "bool type cannot be multi"); optflags |= OPT_SHOWBOOL; cb_arg = (void *)plugin_opt_bool_check; From e933b31efb804026d3c5f4a15fc15af55627d4c5 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 6 Jun 2023 10:08:53 +0930 Subject: [PATCH 08/12] lightningd: call setconfig on plugins' dynamic options. Signed-off-by: Rusty Russell --- doc/PLUGINS.md | 10 ++---- lightningd/plugin.c | 84 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 85 insertions(+), 9 deletions(-) diff --git a/doc/PLUGINS.md b/doc/PLUGINS.md index 99c356581600..a7c03d697fca 100644 --- a/doc/PLUGINS.md +++ b/doc/PLUGINS.md @@ -99,7 +99,8 @@ example: "type": "string", "default": "World", "description": "What name should I call you?", - "deprecated": false + "deprecated": false, + "dynamic": false } ], "rpcmethods": [ @@ -143,7 +144,7 @@ example: During startup the `options` will be added to the list of command line options that `lightningd` accepts. If any `options` "name" is already taken startup will abort. The above will add a `--greeting` option with a default value of `World` and the specified description. *Notice that -currently string, integers, bool, and flag options are supported.* +currently string, integers, bool, and flag options are supported.* If an option specifies `dynamic`: `true`, then it should allow a `setvalue` call for that option after initialization. The `rpcmethods` are methods that will be exposed via `lightningd`'s JSON-RPC over Unix-Socket interface, just like the builtin @@ -254,11 +255,6 @@ Here's an example option set, as sent in response to `getmanifest` ], ``` -**Note**: `lightningd` command line options are only parsed during startup and their -values are not remembered when the plugin is stopped or killed. -For dynamic plugins started with `plugin start`, options can be -passed as extra arguments to that [command][lightning-plugin]. - #### Custom notifications diff --git a/lightningd/plugin.c b/lightningd/plugin.c index 606d20e1bdce..edb1241ce9dc 100644 --- a/lightningd/plugin.c +++ b/lightningd/plugin.c @@ -2059,6 +2059,65 @@ bool plugins_config(struct plugins *plugins) return true; } +struct plugin_set_return { + struct command *cmd; + const char *val; + const char *optname; + struct command_result *(*success)(struct command *, + const struct opt_table *, + const char *); +}; + +static void plugin_setconfig_done(const char *buffer, + const jsmntok_t *toks, + const jsmntok_t *idtok UNUSED, + struct plugin_set_return *psr) +{ + const jsmntok_t *t; + const struct opt_table *ot; + + t = json_get_member(buffer, toks, "error"); + if (t) { + const jsmntok_t *e; + int ecode; + + e = json_get_member(buffer, t, "code"); + if (!e || !json_to_int(buffer, e, &ecode)) + goto bad_response; + e = json_get_member(buffer, t, "message"); + if (!e) + goto bad_response; + was_pending(command_fail(psr->cmd, ecode, "%.*s", + e->end - e->start, buffer + e->start)); + return; + } + + /* We have to look this up again, since a new plugin could have added some + * while we were in callback, and moved opt_table! */ + ot = opt_find_long(psr->optname, NULL); + if (!ot) { + log_broken(command_log(psr->cmd), + "Missing opt %s on plugin return?", psr->optname); + was_pending(command_fail(psr->cmd, LIGHTNINGD, + "Missing opt %s on plugin return?", psr->optname)); + return; + } + + t = json_get_member(buffer, toks, "result"); + if (!t) + goto bad_response; + was_pending(psr->success(psr->cmd, ot, psr->val)); + return; + +bad_response: + log_broken(command_log(psr->cmd), + "Invalid setconfig %s response from plugin: %.*s", + psr->optname, + json_tok_full_len(toks), json_tok_full(buffer, toks)); + was_pending(command_fail(psr->cmd, LIGHTNINGD, + "Malformed setvalue %s plugin return", psr->optname)); +} + struct command_result *plugin_set_dynamic_opt(struct command *cmd, const struct opt_table *ot, const char *val, @@ -2069,12 +2128,33 @@ struct command_result *plugin_set_dynamic_opt(struct command *cmd, { struct plugin_opt *popt; struct plugin *plugin; + struct jsonrpc_request *req; + struct plugin_set_return *psr; plugin = plugin_opt_find_any(cmd->ld->plugins, ot, &popt); assert(plugin); - return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "FIXME: Implement dynamic"); + assert(ot->type & OPT_DYNAMIC); + + psr = tal(cmd, struct plugin_set_return); + psr->cmd = cmd; + /* val is a child of cmd, so no copy needed. */ + psr->val = val; + psr->optname = tal_strdup(psr, ot->names + 2); + psr->success = success; + + req = jsonrpc_request_start(cmd, "setconfig", + cmd->id, + plugin->non_numeric_ids, + command_log(cmd), + NULL, plugin_setconfig_done, + psr); + json_add_string(req->stream, "config", psr->optname); + if (psr->val) + json_add_string(req->stream, "val", psr->val); + jsonrpc_request_end(req); + plugin_request_send(plugin, req); + return command_still_pending(cmd); } /** json_add_opt_plugins_array From a78fc10795d17fc7bdc997274fff70cb79b0c4d2 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 6 Jun 2023 10:08:53 +0930 Subject: [PATCH 09/12] libplugin: make set callback for options take plugin ptr, check correct type. I added a plugin arg and was surprised that compile didn't break. This is because typesafe_cb et al are conditional casts: if the type isn't as expected it has no effect, but we're passing plugin_option() through varargs, so everything is accepted! Add a noop inline to check type, and fix up the two cases where we used `const char *` instead of `char *`. Signed-off-by: Rusty Russell --- plugins/funder.c | 30 ++++++++++++++++-------------- plugins/funder_policy.c | 2 +- plugins/funder_policy.h | 3 ++- plugins/libplugin.c | 16 ++++++++-------- plugins/libplugin.h | 26 ++++++++++++++++++-------- plugins/sql.c | 2 +- tests/plugins/test_libplugin.c | 2 +- 7 files changed, 47 insertions(+), 34 deletions(-) diff --git a/plugins/funder.c b/plugins/funder.c index 3f684abf5815..5110a021a883 100644 --- a/plugins/funder.c +++ b/plugins/funder.c @@ -1202,7 +1202,7 @@ param_funder_opt(struct command *cmd, const char *name, opt_str = tal_strndup(cmd, buffer + tok->start, tok->end - tok->start); - err = funding_option(opt_str, *opt); + err = funding_option(cmd->plugin, opt_str, *opt); if (err) return command_fail_badparam(cmd, name, buffer, tok, err); @@ -1222,7 +1222,7 @@ param_policy_mod(struct command *cmd, const char *name, arg_str = tal_strndup(cmd, buffer + tok->start, tok->end - tok->start); - err = u64_option(arg_str, *mod); + err = u64_option(cmd->plugin, arg_str, *mod); if (err) { tal_free(err); if (!parse_amount_sat(&sats, arg_str, strlen(arg_str))) @@ -1518,7 +1518,7 @@ const struct plugin_notification notifs[] = { }, }; -static char *option_channel_base(const char *arg, struct funder_policy *policy) +static char *option_channel_base(struct plugin *plugin, const char *arg, struct funder_policy *policy) { struct amount_msat amt; @@ -1536,15 +1536,16 @@ static char *option_channel_base(const char *arg, struct funder_policy *policy) } static char * -option_channel_fee_proportional_thousandths_max(const char *arg, +option_channel_fee_proportional_thousandths_max(struct plugin *plugin, + const char *arg, struct funder_policy *policy) { if (!policy->rates) policy->rates = default_lease_rates(policy); - return u16_option(arg, &policy->rates->channel_fee_max_proportional_thousandths); + return u16_option(plugin, arg, &policy->rates->channel_fee_max_proportional_thousandths); } -static char *amount_option(const char *arg, struct amount_sat *amt) +static char *amount_option(struct plugin *plugin, const char *arg, struct amount_sat *amt) { if (!parse_amount_sat(amt, arg, strlen(arg))) return tal_fmt(tmpctx, "Unable to parse amount '%s'", arg); @@ -1552,7 +1553,7 @@ static char *amount_option(const char *arg, struct amount_sat *amt) return NULL; } -static char *option_lease_fee_base(const char *arg, +static char *option_lease_fee_base(struct plugin *plugin, const char *arg, struct funder_policy *policy) { struct amount_sat amt; @@ -1560,7 +1561,7 @@ static char *option_lease_fee_base(const char *arg, if (!policy->rates) policy->rates = default_lease_rates(policy); - err = amount_option(arg, &amt); + err = amount_option(plugin, arg, &amt); if (err) return err; @@ -1571,28 +1572,29 @@ static char *option_lease_fee_base(const char *arg, return NULL; } -static char *option_lease_fee_basis(const char *arg, +static char *option_lease_fee_basis(struct plugin *plugin, const char *arg, struct funder_policy *policy) { if (!policy->rates) policy->rates = default_lease_rates(policy); - return u16_option(arg, &policy->rates->lease_fee_basis); + return u16_option(plugin, arg, &policy->rates->lease_fee_basis); } -static char *option_lease_weight_max(const char *arg, +static char *option_lease_weight_max(struct plugin *plugin, const char *arg, struct funder_policy *policy) { if (!policy->rates) policy->rates = default_lease_rates(policy); - return u16_option(arg, &policy->rates->funding_weight); + return u16_option(plugin, arg, &policy->rates->funding_weight); } -static char *amount_sat_or_u64_option(const char *arg, u64 *amt) +static char *amount_sat_or_u64_option(struct plugin *plugin, + const char *arg, u64 *amt) { struct amount_sat sats; char *err; - err = u64_option(arg, amt); + err = u64_option(plugin, arg, amt); if (err) { tal_free(err); if (!parse_amount_sat(&sats, arg, strlen(arg))) diff --git a/plugins/funder_policy.c b/plugins/funder_policy.c index c7fdffbe4872..9ac79f40fba0 100644 --- a/plugins/funder_policy.c +++ b/plugins/funder_policy.c @@ -21,7 +21,7 @@ const char *funder_opt_name(enum funder_opt opt) abort(); } -char *funding_option(const char *arg, enum funder_opt *opt) +char *funding_option(struct plugin *plugin, const char *arg, enum funder_opt *opt) { if (streq(arg, "match")) *opt = MATCH; diff --git a/plugins/funder_policy.h b/plugins/funder_policy.h index c89da3d25800..be55955df002 100644 --- a/plugins/funder_policy.h +++ b/plugins/funder_policy.h @@ -3,6 +3,7 @@ #include "config.h" #include +struct plugin; struct lease_rates; struct node_id; @@ -93,7 +94,7 @@ const char *funder_policy_desc(const tal_t *ctx, const struct funder_policy *policy); /* Convert a cmdline option to a funding_opt */ -char *funding_option(const char *arg, enum funder_opt *opt); +char *funding_option(struct plugin *plugin, const char *arg, enum funder_opt *opt); /* Check policy settings, return error if fails */ char *funder_check_policy(const struct funder_policy *policy); diff --git a/plugins/libplugin.c b/plugins/libplugin.c index 916c4313fe33..2f59057c6984 100644 --- a/plugins/libplugin.c +++ b/plugins/libplugin.c @@ -1202,7 +1202,7 @@ static struct command_result *handle_init(struct command *cmd, char *problem; if (!streq(p->opts[optnum].name, opt)) continue; - problem = p->opts[optnum].handle(json_strdup(opt, buf, t+1), + problem = p->opts[optnum].handle(p, json_strdup(opt, buf, t+1), p->opts[optnum].arg); if (problem) plugin_err(p, "option '%s': %s", @@ -1225,7 +1225,7 @@ static struct command_result *handle_init(struct command *cmd, return command_success(cmd, json_out_obj(cmd, NULL, NULL)); } -char *u64_option(const char *arg, u64 *i) +char *u64_option(struct plugin *plugin, const char *arg, u64 *i) { char *endp; @@ -1239,7 +1239,7 @@ char *u64_option(const char *arg, u64 *i) return NULL; } -char *u32_option(const char *arg, u32 *i) +char *u32_option(struct plugin *plugin, const char *arg, u32 *i) { char *endp; u64 n; @@ -1258,7 +1258,7 @@ char *u32_option(const char *arg, u32 *i) return NULL; } -char *u16_option(const char *arg, u16 *i) +char *u16_option(struct plugin *plugin, const char *arg, u16 *i) { char *endp; u64 n; @@ -1277,7 +1277,7 @@ char *u16_option(const char *arg, u16 *i) return NULL; } -char *bool_option(const char *arg, bool *i) +char *bool_option(struct plugin *plugin, const char *arg, bool *i) { if (!streq(arg, "true") && !streq(arg, "false")) return tal_fmt(tmpctx, "'%s' is not a bool, must be \"true\" or \"false\"", arg); @@ -1286,7 +1286,7 @@ char *bool_option(const char *arg, bool *i) return NULL; } -char *flag_option(const char *arg, bool *i) +char *flag_option(struct plugin *plugin, const char *arg, bool *i) { /* We only get called if the flag was provided, so *i should be false * by default */ @@ -1298,7 +1298,7 @@ char *flag_option(const char *arg, bool *i) return NULL; } -char *charp_option(const char *arg, char **p) +char *charp_option(struct plugin *plugin, const char *arg, char **p) { *p = tal_strdup(NULL, arg); return NULL; @@ -1839,7 +1839,7 @@ static struct plugin *new_plugin(const tal_t *ctx, o.name = optname; o.type = va_arg(ap, const char *); o.description = va_arg(ap, const char *); - o.handle = va_arg(ap, char *(*)(const char *str, void *arg)); + o.handle = va_arg(ap, char *(*)(struct plugin *, const char *str, void *arg)); o.arg = va_arg(ap, void *); o.deprecated = va_arg(ap, int); /* bool gets promoted! */ tal_arr_expand(&p->opts, o); diff --git a/plugins/libplugin.h b/plugins/libplugin.h index 1c09a78004a5..83b03f4bf579 100644 --- a/plugins/libplugin.h +++ b/plugins/libplugin.h @@ -77,7 +77,7 @@ struct plugin_option { const char *name; const char *type; const char *description; - char *(*handle)(const char *str, void *arg); + char *(*handle)(struct plugin *plugin, const char *str, void *arg); void *arg; /* If true, this options *disabled* if allow-deprecated-apis = false */ bool deprecated; @@ -393,12 +393,22 @@ void plugin_notify_progress(struct command *cmd, u32 num_stages, u32 stage, u32 num_progress, u32 progress); +/* Simply exists to check that `set` to plugin_option* is correct type */ +static inline void *plugin_option_cb_check(char *(*set)(struct plugin *plugin, + const char *arg, void *)) +{ + return set; +} + /* Macro to define arguments */ #define plugin_option_(name, type, description, set, arg, deprecated) \ (name), \ (type), \ (description), \ - typesafe_cb_preargs(char *, void *, (set), (arg), const char *), \ + plugin_option_cb_check(typesafe_cb_preargs(char *, void *, \ + (set), (arg), \ + struct plugin *, \ + const char *)), \ (arg), \ (deprecated) @@ -409,12 +419,12 @@ void plugin_notify_progress(struct command *cmd, plugin_option_((name), (type), (description), (set), (arg), true) /* Standard helpers */ -char *u64_option(const char *arg, u64 *i); -char *u32_option(const char *arg, u32 *i); -char *u16_option(const char *arg, u16 *i); -char *bool_option(const char *arg, bool *i); -char *charp_option(const char *arg, char **p); -char *flag_option(const char *arg, bool *i); +char *u64_option(struct plugin *plugin, const char *arg, u64 *i); +char *u32_option(struct plugin *plugin, const char *arg, u32 *i); +char *u16_option(struct plugin *plugin, const char *arg, u16 *i); +char *bool_option(struct plugin *plugin, const char *arg, bool *i); +char *charp_option(struct plugin *plugin, const char *arg, char **p); +char *flag_option(struct plugin *plugin, const char *arg, bool *i); /* The main plugin runner: append with 0 or more plugin_option(), then NULL. */ void NORETURN LAST_ARG_NULL plugin_main(char *argv[], diff --git a/plugins/sql.c b/plugins/sql.c index 6c0036859acb..ba6f9ce1016d 100644 --- a/plugins/sql.c +++ b/plugins/sql.c @@ -114,7 +114,7 @@ struct table_desc { static STRMAP(struct table_desc *) tablemap; static size_t max_dbmem = 500000000; static struct sqlite3 *db; -static const char *dbfilename; +static char *dbfilename; static int gosstore_fd = -1; static size_t gosstore_nodes_off = 0, gosstore_channels_off = 0; static u64 next_rowid = 1; diff --git a/tests/plugins/test_libplugin.c b/tests/plugins/test_libplugin.c index b70f42cb992a..53bf23bda53b 100644 --- a/tests/plugins/test_libplugin.c +++ b/tests/plugins/test_libplugin.c @@ -6,7 +6,7 @@ #include #include -static const char *somearg; +static char *somearg; static bool self_disable = false; static bool dont_shutdown = false; From b868217423784abe1c32ea438afe7d4a4f9fd89c Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 6 Jun 2023 10:08:53 +0930 Subject: [PATCH 10/12] plugins: libplugin support for dynamic config options. Signed-off-by: Rusty Russell --- plugins/libplugin.c | 70 ++++++++++++++++++++++++++++++++++++--------- plugins/libplugin.h | 16 +++++++---- 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/plugins/libplugin.c b/plugins/libplugin.c index 2f59057c6984..109ba95880e9 100644 --- a/plugins/libplugin.c +++ b/plugins/libplugin.c @@ -915,6 +915,7 @@ handle_getmanifest(struct command *getmanifest_cmd, json_add_string(params, "type", p->opts[i].type); json_add_string(params, "description", p->opts[i].description); json_add_bool(params, "deprecated", p->opts[i].deprecated); + json_add_bool(params, "dynamic", p->opts[i].dynamic); json_object_end(params); } json_array_end(params); @@ -1136,6 +1137,15 @@ static struct feature_set *json_to_feature_set(struct plugin *plugin, return fset; } +static struct plugin_option *find_opt(struct plugin *plugin, const char *name) +{ + for (size_t i = 0; i < tal_count(plugin->opts); i++) { + if (streq(plugin->opts[i].name, name)) + return &plugin->opts[i]; + } + return NULL; +} + static struct command_result *handle_init(struct command *cmd, const char *buf, const jsmntok_t *params) @@ -1197,19 +1207,17 @@ static struct command_result *handle_init(struct command *cmd, opttok = json_get_member(buf, params, "options"); json_for_each_obj(i, t, opttok) { - char *opt = json_strdup(NULL, buf, t); - for (size_t optnum = 0; optnum < tal_count(p->opts); optnum++) { - char *problem; - if (!streq(p->opts[optnum].name, opt)) - continue; - problem = p->opts[optnum].handle(p, json_strdup(opt, buf, t+1), - p->opts[optnum].arg); - if (problem) - plugin_err(p, "option '%s': %s", - p->opts[optnum].name, problem); - break; - } - tal_free(opt); + const char *name, *problem; + struct plugin_option *popt; + + name = json_strdup(tmpctx, buf, t); + popt = find_opt(p, name); + if (!popt) + plugin_err(p, "lightningd specified unknown option '%s'?", name); + + problem = popt->handle(p, json_strdup(tmpctx, buf, t+1), popt->arg); + if (problem) + plugin_err(p, "option '%s': %s", popt->name, problem); } if (p->init) { @@ -1638,6 +1646,41 @@ static void ld_command_handle(struct plugin *plugin, } } + /* Dynamic parameters */ + if (streq(cmd->methodname, "setconfig")) { + const jsmntok_t *valtok; + const char *config, *val, *problem; + struct plugin_option *popt; + struct command_result *ret; + config = json_strdup(tmpctx, plugin->buffer, + json_get_member(plugin->buffer, paramstok, "config")); + popt = find_opt(plugin, config); + if (!popt) { + plugin_err(plugin, + "lightningd setconfig unknown option '%s'?", + config); + } + if (!popt->dynamic) { + plugin_err(plugin, + "lightningd setconfig non-dynamic option '%s'?", + config); + } + valtok = json_get_member(plugin->buffer, paramstok, "val"); + if (valtok) + val = json_strdup(tmpctx, plugin->buffer, valtok); + else + val = "true"; + + problem = popt->handle(plugin, val, popt->arg); + if (problem) + ret = command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%s", problem); + else + ret = command_finished(cmd, jsonrpc_stream_success(cmd)); + assert(ret == &complete); + return; + } + plugin_err(plugin, "Unknown command '%s'", cmd->methodname); } @@ -1842,6 +1885,7 @@ static struct plugin *new_plugin(const tal_t *ctx, o.handle = va_arg(ap, char *(*)(struct plugin *, const char *str, void *arg)); o.arg = va_arg(ap, void *); o.deprecated = va_arg(ap, int); /* bool gets promoted! */ + o.dynamic = va_arg(ap, int); /* bool gets promoted! */ tal_arr_expand(&p->opts, o); } diff --git a/plugins/libplugin.h b/plugins/libplugin.h index 83b03f4bf579..efda2f49254e 100644 --- a/plugins/libplugin.h +++ b/plugins/libplugin.h @@ -81,6 +81,8 @@ struct plugin_option { void *arg; /* If true, this options *disabled* if allow-deprecated-apis = false */ bool deprecated; + /* If true, allow setting after plugin has initialized */ + bool dynamic; }; /* Create an array of these, one for each notification you subscribe to. */ @@ -401,7 +403,7 @@ static inline void *plugin_option_cb_check(char *(*set)(struct plugin *plugin, } /* Macro to define arguments */ -#define plugin_option_(name, type, description, set, arg, deprecated) \ +#define plugin_option_(name, type, description, set, arg, deprecated, dynamic) \ (name), \ (type), \ (description), \ @@ -410,13 +412,17 @@ static inline void *plugin_option_cb_check(char *(*set)(struct plugin *plugin, struct plugin *, \ const char *)), \ (arg), \ - (deprecated) + (deprecated), \ + (dynamic) #define plugin_option(name, type, description, set, arg) \ - plugin_option_((name), (type), (description), (set), (arg), false) + plugin_option_((name), (type), (description), (set), (arg), false, false) -#define plugin_option_deprecated(name, type, description, set, arg) \ - plugin_option_((name), (type), (description), (set), (arg), true) +#define plugin_option_dynamic(name, type, description, set, arg) \ + plugin_option_((name), (type), (description), (set), (arg), false, true) + +#define plugin_option_deprecated(name, type, description, set, arg) \ + plugin_option_((name), (type), (description), (set), (arg), true, false) /* Standard helpers */ char *u64_option(struct plugin *plugin, const char *arg, u64 *i); From f0754e83a13dbc87a05c3003f4cb2cece858210a Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 6 Jun 2023 10:08:53 +0930 Subject: [PATCH 11/12] autoclean: various configuration options now dynamic. Signed-off-by: Rusty Russell Changelog-Changed: Plugins: `autoclean` configuration variables now settable with `setconfig`. --- doc/lightningd-config.5.md | 12 +++++----- plugins/autoclean.c | 48 +++++++++++++++++++------------------- tests/test_plugin.py | 36 ++++++++++------------------ 3 files changed, 42 insertions(+), 54 deletions(-) diff --git a/doc/lightningd-config.5.md b/doc/lightningd-config.5.md index 59226584c49f..1532ae12a7cf 100644 --- a/doc/lightningd-config.5.md +++ b/doc/lightningd-config.5.md @@ -447,27 +447,27 @@ accepted, and ignored. Perform search for things to clean every *SECONDS* seconds (default 3600, or 1 hour, which is usually sufficient). -* **autoclean-succeededforwards-age**=*SECONDS* [plugin `autoclean`] +* **autoclean-succeededforwards-age**=*SECONDS* [plugin `autoclean`, *dynamic*] How old successful forwards (`settled` in listforwards `status`) have to be before deletion (default 0, meaning never). -* **autoclean-failedforwards-age**=*SECONDS* [plugin `autoclean`] +* **autoclean-failedforwards-age**=*SECONDS* [plugin `autoclean`, *dynamic*] How old failed forwards (`failed` or `local_failed` in listforwards `status`) have to be before deletion (default 0, meaning never). -* **autoclean-succeededpays-age**=*SECONDS* [plugin `autoclean`] +* **autoclean-succeededpays-age**=*SECONDS* [plugin `autoclean`, *dynamic*] How old successful payments (`complete` in listpays `status`) have to be before deletion (default 0, meaning never). -* **autoclean-failedpays-age**=*SECONDS* [plugin `autoclean`] +* **autoclean-failedpays-age**=*SECONDS* [plugin `autoclean`, *dynamic*] How old failed payment attempts (`failed` in listpays `status`) have to be before deletion (default 0, meaning never). -* **autoclean-paidinvoices-age**=*SECONDS* [plugin `autoclean`] +* **autoclean-paidinvoices-age**=*SECONDS* [plugin `autoclean`, *dynamic*] How old invoices which were paid (`paid` in listinvoices `status`) have to be before deletion (default 0, meaning never). -* **autoclean-expiredinvoices-age**=*SECONDS* [plugin `autoclean`] +* **autoclean-expiredinvoices-age**=*SECONDS* [plugin `autoclean`, *dynamic*] How old invoices which were not paid (and cannot be) (`expired` in listinvoices `status`) before deletion (default 0, meaning never). diff --git a/plugins/autoclean.c b/plugins/autoclean.c index cb123c04d233..a82d34d57a8c 100644 --- a/plugins/autoclean.c +++ b/plugins/autoclean.c @@ -631,29 +631,29 @@ int main(int argc, char *argv[]) "Perform cleanup every" " given seconds", u64_option, &cycle_seconds), - plugin_option("autoclean-succeededforwards-age", - "int", - "How old do successful forwards have to be before deletion (0 = never)", - u64_option, &timer_cinfo.subsystem_age[SUCCEEDEDFORWARDS]), - plugin_option("autoclean-failedforwards-age", - "int", - "How old do failed forwards have to be before deletion (0 = never)", - u64_option, &timer_cinfo.subsystem_age[FAILEDFORWARDS]), - plugin_option("autoclean-succeededpays-age", - "int", - "How old do successful pays have to be before deletion (0 = never)", - u64_option, &timer_cinfo.subsystem_age[SUCCEEDEDPAYS]), - plugin_option("autoclean-failedpays-age", - "int", - "How old do failed pays have to be before deletion (0 = never)", - u64_option, &timer_cinfo.subsystem_age[FAILEDPAYS]), - plugin_option("autoclean-paidinvoices-age", - "int", - "How old do paid invoices have to be before deletion (0 = never)", - u64_option, &timer_cinfo.subsystem_age[PAIDINVOICES]), - plugin_option("autoclean-expiredinvoices-age", - "int", - "How old do expired invoices have to be before deletion (0 = never)", - u64_option, &timer_cinfo.subsystem_age[EXPIREDINVOICES]), + plugin_option_dynamic("autoclean-succeededforwards-age", + "int", + "How old do successful forwards have to be before deletion (0 = never)", + u64_option, &timer_cinfo.subsystem_age[SUCCEEDEDFORWARDS]), + plugin_option_dynamic("autoclean-failedforwards-age", + "int", + "How old do failed forwards have to be before deletion (0 = never)", + u64_option, &timer_cinfo.subsystem_age[FAILEDFORWARDS]), + plugin_option_dynamic("autoclean-succeededpays-age", + "int", + "How old do successful pays have to be before deletion (0 = never)", + u64_option, &timer_cinfo.subsystem_age[SUCCEEDEDPAYS]), + plugin_option_dynamic("autoclean-failedpays-age", + "int", + "How old do failed pays have to be before deletion (0 = never)", + u64_option, &timer_cinfo.subsystem_age[FAILEDPAYS]), + plugin_option_dynamic("autoclean-paidinvoices-age", + "int", + "How old do paid invoices have to be before deletion (0 = never)", + u64_option, &timer_cinfo.subsystem_age[PAIDINVOICES]), + plugin_option_dynamic("autoclean-expiredinvoices-age", + "int", + "How old do expired invoices have to be before deletion (0 = never)", + u64_option, &timer_cinfo.subsystem_age[EXPIREDINVOICES]), NULL); } diff --git a/tests/test_plugin.py b/tests/test_plugin.py index a5c3a85d4d87..0a77a942fd14 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -3148,9 +3148,11 @@ def test_autoclean(node_factory): inv4 = l3.rpc.invoice(amount_msat=12300, label='inv4', description='description4', expiry=2000) inv5 = l3.rpc.invoice(amount_msat=12300, label='inv5', description='description5', expiry=2000) - l3.stop() - l3.daemon.opts['autoclean-expiredinvoices-age'] = 2 - l3.start() + # It must be an integer! + with pytest.raises(RpcError, match=r'is not a number'): + l3.rpc.setconfig('autoclean-expiredinvoices-age', 'xxx') + + l3.rpc.setconfig('autoclean-expiredinvoices-age', 2) assert l3.rpc.autoclean_status()['autoclean']['expiredinvoices']['enabled'] is True assert l3.rpc.autoclean_status()['autoclean']['expiredinvoices']['age'] == 2 @@ -3174,9 +3176,7 @@ def test_autoclean(node_factory): assert l3.rpc.autoclean_status()['autoclean']['expiredinvoices']['cleaned'] == 1 # Disabling works - l3.stop() - l3.daemon.opts['autoclean-expiredinvoices-age'] = 0 - l3.start() + l3.rpc.setconfig('autoclean-expiredinvoices-age', 0) assert l3.rpc.autoclean_status()['autoclean']['expiredinvoices']['enabled'] is False assert 'age' not in l3.rpc.autoclean_status()['autoclean']['expiredinvoices'] @@ -3197,9 +3197,7 @@ def test_autoclean(node_factory): assert 'age' not in l3.rpc.autoclean_status()['autoclean']['expiredinvoices'] # Now enable: they will get autocleaned - l3.stop() - l3.daemon.opts['autoclean-expiredinvoices-age'] = 2 - l3.start() + l3.rpc.setconfig('autoclean-expiredinvoices-age', 2) wait_for(lambda: len(l3.rpc.listinvoices()['invoices']) == 2) assert l3.rpc.autoclean_status()['autoclean']['expiredinvoices']['cleaned'] == 3 @@ -3214,9 +3212,7 @@ def test_autoclean(node_factory): assert l3.rpc.autoclean_status()['autoclean']['paidinvoices']['enabled'] is False assert l3.rpc.autoclean_status()['autoclean']['paidinvoices']['cleaned'] == 0 - l3.stop() - l3.daemon.opts['autoclean-paidinvoices-age'] = 1 - l3.start() + l3.rpc.setconfig('autoclean-paidinvoices-age', 1) assert l3.rpc.autoclean_status()['autoclean']['paidinvoices']['enabled'] is True wait_for(lambda: l3.rpc.listinvoices()['invoices'] == []) @@ -3225,17 +3221,13 @@ def test_autoclean(node_factory): assert only_one(l1.rpc.listpays(inv5['bolt11'])['pays'])['status'] == 'failed' assert only_one(l1.rpc.listpays(inv4['bolt11'])['pays'])['status'] == 'complete' - l1.stop() - l1.daemon.opts['autoclean-failedpays-age'] = 1 - l1.start() + l1.rpc.setconfig('autoclean-failedpays-age', 1) wait_for(lambda: l1.rpc.listpays(inv5['bolt11'])['pays'] == []) assert l1.rpc.autoclean_status()['autoclean']['failedpays']['cleaned'] == 1 assert l1.rpc.autoclean_status()['autoclean']['succeededpays']['cleaned'] == 0 - l1.stop() - l1.daemon.opts['autoclean-succeededpays-age'] = 2 - l1.start() + l1.rpc.setconfig('autoclean-succeededpays-age', 2) wait_for(lambda: l1.rpc.listpays(inv4['bolt11'])['pays'] == []) assert l1.rpc.listsendpays() == {'payments': []} @@ -3245,9 +3237,7 @@ def test_autoclean(node_factory): assert len(l2.rpc.listforwards()['forwards']) == 2 # Clean failed ones. - l2.stop() - l2.daemon.opts['autoclean-failedforwards-age'] = 2 - l2.start() + l2.rpc.setconfig('autoclean-failedforwards-age', 2) wait_for(lambda: l2.rpc.listforwards(status='failed')['forwards'] == []) assert len(l2.rpc.listforwards(status='settled')['forwards']) == 1 @@ -3257,9 +3247,7 @@ def test_autoclean(node_factory): amt_before = l2.rpc.getinfo()['fees_collected_msat'] # Clean succeeded ones - l2.stop() - l2.daemon.opts['autoclean-succeededforwards-age'] = 2 - l2.start() + l2.rpc.setconfig('autoclean-succeededforwards-age', 2) wait_for(lambda: l2.rpc.listforwards(status='settled')['forwards'] == []) assert l2.rpc.listforwards() == {'forwards': []} assert l2.rpc.autoclean_status()['autoclean']['failedforwards']['cleaned'] == 1 From 961fa3f346abafc6684fe578156c8b44e3bc443e Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 6 Jun 2023 10:08:53 +0930 Subject: [PATCH 12/12] autoclean: allow dynamic changes to autoclean-cycle. Slightly less trivial: reset timer unless it's currently running callback. Signed-off-by: Rusty Russell --- doc/lightningd-config.5.md | 2 +- plugins/autoclean.c | 28 +++++++++++++++++++++++----- tests/test_plugin.py | 7 +++++-- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/doc/lightningd-config.5.md b/doc/lightningd-config.5.md index 1532ae12a7cf..deaab2cb29d6 100644 --- a/doc/lightningd-config.5.md +++ b/doc/lightningd-config.5.md @@ -442,7 +442,7 @@ accepted, and ignored. ### Cleanup control options: -* **autoclean-cycle**=*SECONDS* [plugin `autoclean`] +* **autoclean-cycle**=*SECONDS* [plugin `autoclean`, *dynamic*] Perform search for things to clean every *SECONDS* seconds (default 3600, or 1 hour, which is usually sufficient). diff --git a/plugins/autoclean.c b/plugins/autoclean.c index a82d34d57a8c..901b8ab51b49 100644 --- a/plugins/autoclean.c +++ b/plugins/autoclean.c @@ -61,6 +61,7 @@ static u64 cycle_seconds = 3600; static struct clean_info timer_cinfo; static u64 total_cleaned[NUM_SUBSYSTEM]; static struct plugin *plugin; +/* This is NULL if it's running now. */ static struct plugin_timer *cleantimer; static void do_clean_timer(void *unused); @@ -439,6 +440,7 @@ static struct command_result *do_clean(struct clean_info *cinfo) static void do_clean_timer(void *unused) { assert(timer_cinfo.cleanup_reqs_remaining == 0); + cleantimer = NULL; do_clean(&timer_cinfo); } @@ -587,6 +589,22 @@ static const char *init(struct plugin *p, return NULL; } +static char *cycle_seconds_option(struct plugin *plugin, const char *arg, + void *unused) +{ + char *problem = u64_option(plugin, arg, &cycle_seconds); + if (problem) + return problem; + + /* If timer is not running right now, reset it to new cycle_seconds */ + if (cleantimer) { + tal_free(cleantimer); + cleantimer = plugin_timer(plugin, time_from_sec(cycle_seconds), + do_clean_timer, NULL); + } + return NULL; +} + static const struct plugin_command commands[] = { { "autocleaninvoice", "payment", @@ -626,11 +644,11 @@ int main(int argc, char *argv[]) " invoices that have expired for at least" " this given seconds are cleaned", u64_option, &timer_cinfo.subsystem_age[EXPIREDINVOICES]), - plugin_option("autoclean-cycle", - "int", - "Perform cleanup every" - " given seconds", - u64_option, &cycle_seconds), + plugin_option_dynamic("autoclean-cycle", + "int", + "Perform cleanup every" + " given seconds", + cycle_seconds_option, NULL), plugin_option_dynamic("autoclean-succeededforwards-age", "int", "How old do successful forwards have to be before deletion (0 = never)", diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 0a77a942fd14..eb95bf64f44a 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -3128,8 +3128,7 @@ def test_commando_badrune(node_factory): def test_autoclean(node_factory): - l1, l2, l3 = node_factory.line_graph(3, opts={'autoclean-cycle': 10, - 'may_reconnect': True}, + l1, l2, l3 = node_factory.line_graph(3, opts={'may_reconnect': True}, wait_for_announce=True) # Under valgrind in CI, it can 50 seconds between creating invoice @@ -3162,6 +3161,8 @@ def test_autoclean(node_factory): assert len(l3.rpc.listinvoices('inv2')['invoices']) == 1 assert l3.rpc.listinvoices('inv1')['invoices'][0]['description'] == 'description1' + l3.rpc.setconfig('autoclean-cycle', 10) + # First it expires. wait_for(lambda: only_one(l3.rpc.listinvoices('inv1')['invoices'])['status'] == 'expired') # Now will get autocleaned @@ -3222,6 +3223,7 @@ def test_autoclean(node_factory): assert only_one(l1.rpc.listpays(inv5['bolt11'])['pays'])['status'] == 'failed' assert only_one(l1.rpc.listpays(inv4['bolt11'])['pays'])['status'] == 'complete' l1.rpc.setconfig('autoclean-failedpays-age', 1) + l1.rpc.setconfig('autoclean-cycle', 5) wait_for(lambda: l1.rpc.listpays(inv5['bolt11'])['pays'] == []) assert l1.rpc.autoclean_status()['autoclean']['failedpays']['cleaned'] == 1 @@ -3237,6 +3239,7 @@ def test_autoclean(node_factory): assert len(l2.rpc.listforwards()['forwards']) == 2 # Clean failed ones. + l2.rpc.setconfig('autoclean-cycle', 5) l2.rpc.setconfig('autoclean-failedforwards-age', 2) wait_for(lambda: l2.rpc.listforwards(status='failed')['forwards'] == [])