Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contrib/msggen/msggen/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -32987,7 +32987,7 @@
],
"description": [
"Determines what action is taken:",
" - *subcommand* **start** takes a *path* to an executable as argument and starts it as plugin. *path* may be an absolute path or a path relative to the plugins directory (default *~/.lightning/plugins*). If the plugin is already running and the executable (checksum) has changed, the plugin is killed and restarted except if its an important (or builtin) plugin. If the plugin doesn't complete the 'getmanifest' and 'init' handshakes within 60 seconds, the command will timeout and kill the plugin. Additional *options* may be passed to the plugin, but requires all parameters to be passed as keyword=value pairs using the `-k|--keyword` option which is recommended. For example the following command starts the plugin helloworld.py (present in the plugin directory) with the option greeting set to 'A crazy':",
" - *subcommand* **start** takes a *path* to an executable as argument and starts it as plugin. *path* may be an absolute path or a path relative to the plugins directory (default *~/.lightning/plugins*). If the plugin is already running and the executable (checksum) has changed, the plugin is killed and restarted except if its an important (or builtin) plugin. If the plugin doesn't complete the 'getmanifest' and 'init' handshakes within 60 seconds, the command will timeout and kill the plugin. Additional *options* may be passed to the plugin, either as an *options* array of *keyword=value* strings, or as flattened keyword=value parameters using the `-k|--keyword` option which is recommended. For example the following command starts the plugin helloworld.py (present in the plugin directory) with the option greeting set to 'A crazy':",
" ```shell",
" lightning-cli -k plugin subcommand=start plugin=helloworld.py greeting='A crazy'",
" ```",
Expand Down
2 changes: 1 addition & 1 deletion doc/schemas/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
],
"description": [
"Determines what action is taken:",
" - *subcommand* **start** takes a *path* to an executable as argument and starts it as plugin. *path* may be an absolute path or a path relative to the plugins directory (default *~/.lightning/plugins*). If the plugin is already running and the executable (checksum) has changed, the plugin is killed and restarted except if its an important (or builtin) plugin. If the plugin doesn't complete the 'getmanifest' and 'init' handshakes within 60 seconds, the command will timeout and kill the plugin. Additional *options* may be passed to the plugin, but requires all parameters to be passed as keyword=value pairs using the `-k|--keyword` option which is recommended. For example the following command starts the plugin helloworld.py (present in the plugin directory) with the option greeting set to 'A crazy':",
" - *subcommand* **start** takes a *path* to an executable as argument and starts it as plugin. *path* may be an absolute path or a path relative to the plugins directory (default *~/.lightning/plugins*). If the plugin is already running and the executable (checksum) has changed, the plugin is killed and restarted except if its an important (or builtin) plugin. If the plugin doesn't complete the 'getmanifest' and 'init' handshakes within 60 seconds, the command will timeout and kill the plugin. Additional *options* may be passed to the plugin, either as an *options* array of *keyword=value* strings, or as flattened keyword=value parameters using the `-k|--keyword` option which is recommended. For example the following command starts the plugin helloworld.py (present in the plugin directory) with the option greeting set to 'A crazy':",
" ```shell",
" lightning-cli -k plugin subcommand=start plugin=helloworld.py greeting='A crazy'",
" ```",
Expand Down
84 changes: 81 additions & 3 deletions lightningd/plugin_control.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "config.h"
#include <ccan/json_escape/json_escape.h>
#include <ccan/tal/path/path.h>
#include <ccan/tal/str/str.h>
#include <common/json_command.h>
Expand Down Expand Up @@ -86,6 +87,68 @@ plugin_dynamic_start(struct plugin_command *pcmd, const char *plugin_path,
return command_still_pending(pcmd->cmd);
}

/* Plugin options can be given either flattened as a JSON object (e.g. via the
* CLI, with "subcommand" and "plugin" keys removed), or as an "options" array
* of "keyword=value" strings. This merges both forms into a single JSON
* object of name/value pairs, suitable for plugin_add_params(). An element
* without an '=' is treated as a boolean flag. */
static jsmntok_t *plugin_start_params(const tal_t *ctx, const char *buffer,
const jsmntok_t *params,
const jsmntok_t *options, char **parambuf)
{
size_t i;
const jsmntok_t *t;
char *newbuf = tal_strdup(ctx, "{");
bool first = true;

/* Copy any remaining flat params (object form only). */
if (params->type == JSMN_OBJECT) {
json_for_each_obj(i, t, params) {
if (json_tok_streq(buffer, t, "subcommand")
|| json_tok_streq(buffer, t, "plugin")
|| json_tok_streq(buffer, t, "options"))
continue;
if (!first)
tal_append_fmt(&newbuf, ",");
first = false;
/* Raw keys and values are already valid JSON. */
tal_append_fmt(&newbuf, "%.*s:%.*s",
json_tok_full_len(t),
json_tok_full(buffer, t),
json_tok_full_len(t+1),
json_tok_full(buffer, t+1));
}
}

/* Add any explicit "options" array entries. */
if (options) {
json_for_each_arr(i, t, options) {
const char *opt = json_strdup(tmpctx, buffer, t);
const char *eq = strchr(opt, '=');
struct json_escape *esc;

if (!first)
tal_append_fmt(&newbuf, ",");
first = false;
if (eq) {
esc = json_escape(tmpctx,
take(tal_strndup(tmpctx, opt, eq - opt)));
tal_append_fmt(&newbuf, "\"%s\":", esc->s);
esc = json_escape(tmpctx, eq + 1);
tal_append_fmt(&newbuf, "\"%s\"", esc->s);
} else {
/* Boolean flags are given without a value. */
esc = json_escape(tmpctx, opt);
tal_append_fmt(&newbuf, "\"%s\":true", esc->s);
}
}
}

tal_append_fmt(&newbuf, "}");
*parambuf = newbuf;
return json_parse_simple(ctx, newbuf, strlen(newbuf));
}

/**
* Called when trying to start a plugin directory through RPC, it registers
* all contained plugins recursively and then starts them.
Expand Down Expand Up @@ -235,23 +298,36 @@ 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;
const jsmntok_t *options = NULL;
char *mod_buffer;
jsmntok_t *mod_params;

if (!param_check(cmd, buffer, params,
p_req("subcommand", param_ignore, cmd),
p_req("plugin", param_string, &plugin_path),
p_opt("options", param_array, &options),
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)
if (params->size > 2 && !options)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Extra parameters must be in object");
mod_params = NULL;
if (options) {
mod_params = plugin_start_params(cmd, buffer, params,
options, &mod_buffer);
} else {
mod_buffer = NULL;
mod_params = NULL;
}
} else if (options) {
mod_params = plugin_start_params(cmd, buffer, params,
options, &mod_buffer);
} else {
mod_buffer = NULL;
mod_params = json_tok_copy(cmd, params);

json_tok_remove(&mod_params, mod_params,
Expand All @@ -271,7 +347,9 @@ static struct command_result *json_plugin_control(struct command *cmd,
if (command_check_only(cmd))
return command_check_done(cmd);

return plugin_dynamic_start(pcmd, plugin_path, buffer, mod_params);
return plugin_dynamic_start(pcmd, plugin_path,
mod_buffer ? mod_buffer : buffer,
mod_params);
} else if (streq(subcmd, "startdir")) {
const char *dir_path;

Expand Down
15 changes: 15 additions & 0 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3060,6 +3060,21 @@ def test_dynamic_args(node_factory):
assert 'greeting' not in l1.rpc.listconfigs()['configs']


def test_dynamic_args_options_array(node_factory):
"""plugin start accepts options as an explicit array, matching the schema."""
plugin_path = os.path.join(os.getcwd(), 'tests/plugins/dynamic_option.py')

l1 = node_factory.get_node()
l1.rpc.plugin_start(plugin_path, options=["test-dynamic-config=Test options array"])

assert l1.rpc.dynamic_option_report() == {'test-dynamic-config': 'Test options array'}
assert l1.rpc.listconfigs('test-dynamic-config')['configs']['test-dynamic-config']['value_str'] == 'Test options array'
assert l1.rpc.listconfigs('test-dynamic-config')['configs']['test-dynamic-config']['plugin'] == plugin_path

l1.rpc.plugin_stop(plugin_path)
assert 'test-dynamic-config' not in l1.rpc.listconfigs()['configs']


def test_pyln_request_notify(node_factory):
"""Test that pyln-client plugins can send notifications.
"""
Expand Down
Loading