Plugin: support extra args to "start".

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
Changelog-Added: Plugins: `start` command can now take plugin-specific parameters.
This commit is contained in:
Rusty Russell 2020-12-14 15:28:35 +10:30
parent 8a9976c4c1
commit d971e3de98
8 changed files with 117 additions and 18 deletions

View File

@ -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):

View File

@ -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<darosior@protonmail.com\fR> is mainly responsible\.
Main web site: \fIhttps://github.com/ElementsProject/lightning\fR
\" SHA256STAMP:a8d7488cb0a731e794aeed1d2d30448cef27204d9ec0374610faf2a84a401762
\" SHA256STAMP:f4b405aa5a5bb8dde3b8947b7d6152ba5ac88108cde43c1375121e5c24555d30

View File

@ -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.

View File

@ -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;
}

View File

@ -1,5 +1,6 @@
#include <ccan/array_size/array_size.h>
#include <ccan/list/list.h>
#include <ccan/mem/mem.h>
#include <ccan/opt/opt.h>
#include <ccan/tal/str/str.h>
#include <ccan/utf8/utf8.h>
@ -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 {

View File

@ -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

View File

@ -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",

View File

@ -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] == []