From d971e3de982d6982910f8fb5a971d8d77dd36e9a Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Mon, 14 Dec 2020 15:28:35 +1030 Subject: [PATCH] Plugin: support extra args to "start". Signed-off-by: Rusty Russell Changelog-Added: Plugins: `start` command can now take plugin-specific parameters. --- contrib/pyln-client/pyln/client/lightning.py | 5 +- doc/lightning-plugin.7 | 9 ++-- doc/lightning-plugin.7.md | 7 +-- lightningd/options.c | 4 +- lightningd/plugin.c | 57 ++++++++++++++++++-- lightningd/plugin.h | 11 +++- lightningd/plugin_control.c | 27 ++++++++-- tests/test_plugin.py | 15 ++++++ 8 files changed, 117 insertions(+), 18 deletions(-) diff --git a/contrib/pyln-client/pyln/client/lightning.py b/contrib/pyln-client/pyln/client/lightning.py index 22f559120..19af7e85b 100644 --- a/contrib/pyln-client/pyln/client/lightning.py +++ b/contrib/pyln-client/pyln/client/lightning.py @@ -1024,14 +1024,15 @@ class LightningRpc(UnixDomainSocketRpc): } return self.call("ping", payload) - def plugin_start(self, plugin): + def plugin_start(self, plugin, **kwargs): """ Adds a plugin to lightningd. """ payload = { "subcommand": "start", - "plugin": plugin + "plugin": plugin, } + payload.update({k: v for k, v in kwargs.items()}) return self.call("plugin", payload) def plugin_startdir(self, directory): diff --git a/doc/lightning-plugin.7 b/doc/lightning-plugin.7 index 6ffcde101..de27fddee 100644 --- a/doc/lightning-plugin.7 +++ b/doc/lightning-plugin.7 @@ -14,9 +14,10 @@ optionally one or two parameters which describes the plugin on which the action has to be taken\. -The \fIstart\fR command takes a path as the first parameter and will load the -plugin available from this path\. It will wait for the plugin to complete -the handshake with \fBlightningd\fR for 20 seconds at the most\. +The \fIstart\fR command takes a path as the first parameter and will load +the plugin available from this path\. Any additional parameters are +passed to the plugin\. It will wait for the plugin to complete the +handshake with \fBlightningd\fR for 20 seconds at the most\. The \fIstop\fR command takes a plugin name as parameter\. It will kill and @@ -54,4 +55,4 @@ Antoine Poinsot \fI is mainly responsible\. Main web site: \fIhttps://github.com/ElementsProject/lightning\fR -\" SHA256STAMP:a8d7488cb0a731e794aeed1d2d30448cef27204d9ec0374610faf2a84a401762 +\" SHA256STAMP:f4b405aa5a5bb8dde3b8947b7d6152ba5ac88108cde43c1375121e5c24555d30 diff --git a/doc/lightning-plugin.7.md b/doc/lightning-plugin.7.md index e1b518a6d..e0b91426e 100644 --- a/doc/lightning-plugin.7.md +++ b/doc/lightning-plugin.7.md @@ -15,9 +15,10 @@ restart lightningd. It takes 1 to 3 parameters: a command optionally one or two parameters which describes the plugin on which the action has to be taken. -The *start* command takes a path as the first parameter and will load the -plugin available from this path. It will wait for the plugin to complete -the handshake with `lightningd` for 20 seconds at the most. +The *start* command takes a path as the first parameter and will load +the plugin available from this path. Any additional parameters are +passed to the plugin. It will wait for the plugin to complete the +handshake with `lightningd` for 20 seconds at the most. The *stop* command takes a plugin name as parameter. It will kill and unload the specified plugin. diff --git a/lightningd/options.c b/lightningd/options.c index 264a32484..119569f01 100644 --- a/lightningd/options.c +++ b/lightningd/options.c @@ -348,7 +348,7 @@ static char *opt_add_plugin(const char *arg, struct lightningd *ld) log_info(ld->log, "%s: disabled via disable-plugin", arg); return NULL; } - plugin_register(ld->plugins, arg, NULL, false); + plugin_register(ld->plugins, arg, NULL, false, NULL, NULL); return NULL; } @@ -375,7 +375,7 @@ static char *opt_important_plugin(const char *arg, struct lightningd *ld) log_info(ld->log, "%s: disabled via disable-plugin", arg); return NULL; } - plugin_register(ld->plugins, arg, NULL, true); + plugin_register(ld->plugins, arg, NULL, true, NULL, NULL); return NULL; } diff --git a/lightningd/plugin.c b/lightningd/plugin.c index f649b9770..b6d5ea869 100644 --- a/lightningd/plugin.c +++ b/lightningd/plugin.c @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -174,7 +175,9 @@ static void destroy_plugin(struct plugin *p) } struct plugin *plugin_register(struct plugins *plugins, const char* path TAKES, - struct command *start_cmd, bool important) + struct command *start_cmd, bool important, + const char *parambuf STEALS, + const jsmntok_t *params STEALS) { struct plugin *p, *p_temp; @@ -212,6 +215,8 @@ struct plugin *plugin_register(struct plugins *plugins, const char* path TAKES, list_head_init(&p->pending_rpccalls); p->important = important; + p->parambuf = tal_steal(p, parambuf); + p->params = tal_steal(p, params); return p; } @@ -1154,6 +1159,48 @@ static const char *plugin_hooks_add(struct plugin *plugin, const char *buffer, return NULL; } +static struct plugin_opt *plugin_opt_find(struct plugin *plugin, + const char *name, size_t namelen) +{ + struct plugin_opt *opt; + + list_for_each(&plugin->plugin_opts, opt, list) { + /* Trim the `--` that we added before */ + if (memeqstr(name, namelen, opt->name + 2)) + return opt; + } + return NULL; +} + +/* start command might have included plugin-specific parameters */ +static const char *plugin_add_params(struct plugin *plugin) +{ + size_t i; + const jsmntok_t *t; + + if (!plugin->params) + return NULL; + + json_for_each_obj(i, t, plugin->params) { + struct plugin_opt *popt; + char *err; + + popt = plugin_opt_find(plugin, + plugin->parambuf + t->start, + t->end - t->start); + if (!popt) { + return tal_fmt(plugin, "unknown parameter %.*s", + json_tok_full_len(t), + json_tok_full(plugin->parambuf, t)); + } + err = plugin_opt_set(json_strdup(tmpctx, plugin->parambuf, + t + 1), popt); + if (err) + return err; + } + return NULL; +} + static void plugin_manifest_timeout(struct plugin *plugin) { bool startup = plugin->plugins->startup; @@ -1243,6 +1290,8 @@ static const char *plugin_parse_getmanifest_response(const char *buffer, err = plugin_subscriptions_add(plugin, buffer, resulttok); if (!err) err = plugin_hooks_add(plugin, buffer, resulttok); + if (!err) + err = plugin_add_params(plugin); plugin->plugin_state = NEEDS_INIT; return err; @@ -1360,7 +1409,8 @@ char *add_plugin_dir(struct plugins *plugins, const char *dir, bool error_ok) log_info(plugins->log, "%s: disabled via disable-plugin", fullpath); } else { - p = plugin_register(plugins, fullpath, NULL, false); + p = plugin_register(plugins, fullpath, NULL, false, + NULL, NULL); if (!p && !error_ok) return tal_fmt(NULL, "Failed to register %s: %s", fullpath, strerror(errno)); @@ -1807,7 +1857,8 @@ void plugins_set_builtin_plugins_dir(struct plugins *plugins, take(path_join(NULL, dir, list_of_builtin_plugins[i])), NULL, - /* important = */ true); + /* important = */ true, + NULL, NULL); } struct plugin_destroyed { diff --git a/lightningd/plugin.h b/lightningd/plugin.h index 14d405e32..ccba56f0b 100644 --- a/lightningd/plugin.h +++ b/lightningd/plugin.h @@ -88,6 +88,10 @@ struct plugin { /* If set, the plugin is so important that if it terminates early, * C-lightning should terminate as well. */ bool important; + + /* Parameters for dynamically-started plugins. */ + const char *parambuf; + const jsmntok_t *params; }; /** @@ -200,6 +204,8 @@ void plugins_free(struct plugins *plugins); * @param path: The path of the executable for this plugin * @param start_cmd: The optional JSON command which caused this. * @param important: The plugin is important. + * @param parambuf: NULL, or the JSON buffer for extra parameters. + * @param params: NULL, or the tokens for extra parameters. * * If @start_cmd, then plugin_cmd_killed or plugin_cmd_succeeded will be called * on it eventually. @@ -207,7 +213,10 @@ void plugins_free(struct plugins *plugins); struct plugin *plugin_register(struct plugins *plugins, const char* path TAKES, struct command *start_cmd, - bool important); + bool important, + const char *parambuf STEALS, + const jsmntok_t *params STEALS); + /** * Returns true if the provided name matches a plugin command diff --git a/lightningd/plugin_control.c b/lightningd/plugin_control.c index 107be1339..d5eadfad4 100644 --- a/lightningd/plugin_control.c +++ b/lightningd/plugin_control.c @@ -56,9 +56,10 @@ struct command_result *plugin_cmd_all_complete(struct plugins *plugins, * will give a result 60 seconds later at the most (once init completes). */ static struct command_result * -plugin_dynamic_start(struct command *cmd, const char *plugin_path) +plugin_dynamic_start(struct command *cmd, const char *plugin_path, + const char *buffer, const jsmntok_t *params) { - struct plugin *p = plugin_register(cmd->ld->plugins, plugin_path, cmd, false); + struct plugin *p = plugin_register(cmd->ld->plugins, plugin_path, cmd, false, buffer, params); const char *err; if (!p) @@ -173,15 +174,35 @@ static struct command_result *json_plugin_control(struct command *cmd, return plugin_dynamic_stop(cmd, plugin_name); } else if (streq(subcmd, "start")) { const char *plugin_path; + jsmntok_t *mod_params; if (!param(cmd, buffer, params, p_req("subcommand", param_ignore, cmd), p_req("plugin", param_string, &plugin_path), + p_opt_any(), NULL)) return command_param_failed(); + /* Manually parse any remaining options (only for objects, + * since plugin options must be explicitly named!). */ + if (params->type == JSMN_ARRAY) { + if (params->size != 2) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Extra parameters must be in object"); + mod_params = NULL; + } else { + mod_params = json_tok_copy(cmd, params); + + json_tok_remove(&mod_params, mod_params, + json_get_member(buffer, mod_params, + "subcommand") - 1, 1); + json_tok_remove(&mod_params, mod_params, + json_get_member(buffer, mod_params, + "plugin") - 1, 1); + } if (access(plugin_path, X_OK) == 0) - return plugin_dynamic_start(cmd, plugin_path); + return plugin_dynamic_start(cmd, plugin_path, + buffer, mod_params); else return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "%s is not executable: %s", diff --git a/tests/test_plugin.py b/tests/test_plugin.py index b3e72d1fb..4f3d95e07 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -2182,3 +2182,18 @@ def test_htlc_accepted_hook_failonion(node_factory): inv = l2.rpc.invoice(42, 'failonion000', '')['bolt11'] with pytest.raises(RpcError): l1.rpc.pay(inv) + + +def test_dynamic_args(node_factory): + plugin_path = os.path.join(os.getcwd(), 'contrib/plugins/helloworld.py') + + l1 = node_factory.get_node() + l1.rpc.plugin_start(plugin_path, greeting='Test arg parsing') + + assert l1.rpc.call("hello") == "Test arg parsing world" + plugin = only_one([p for p in l1.rpc.listconfigs()['plugins'] if p['path'] == plugin_path]) + assert plugin['options']['greeting'] == 'Test arg parsing' + + l1.rpc.plugin_stop(plugin_path) + + assert [p for p in l1.rpc.listconfigs()['plugins'] if p['path'] == plugin_path] == []