Compare commits

...

16 Commits

Author SHA1 Message Date
Christian Decker
01e555a9a3
plugin: Iterate over the options from a plugin using the tok->size
I had this really contorted way of iterating over options that could
cause valgrind to choke. This is the much more intuitive way to
iterate.

Signed-off-by: Christian Decker <decker.christian@gmail.com>
2018-11-30 11:15:44 +01:00
Christian Decker
eafe32eba7
plugin: Make plugin_kill a printf-like function
Suggested-by: Rusty Russell <@rustyrussell>
Signed-off-by: Christian Decker <@cdecker>
2018-11-30 11:15:44 +01:00
Christian Decker
7c3c63a9ee
plugin: Update documentation of the rpcmethods
Suggested-by: Lisa Neigut <@niftynei>
Signed-off-by: Christian Decker <decker.christian@gmail.com>
2018-11-30 11:15:43 +01:00
Christian Decker
f2b2919564
pytest: Add a test for the JSON-RPC passthrough
Tests JSON-RPC registration, as well as success and failures.

Signed-off-by: Christian Decker <decker.christian@gmail.com>
2018-11-30 11:15:42 +01:00
Christian Decker
d21025bf07
plugin: Map results back to the incoming JSON-RPC request
The final step in the JSON-RPC passthrough: map the result we got from
the plugin back to the original request we got from the client.

Signed-off-by: Christian Decker <decker.christian@gmail.com>
2018-11-30 11:15:42 +01:00
Christian Decker
6f43b6ee26
plugin: Dispatch incoming RPC calls to appropriate plugin
Signed-off-by: Christian Decker <@cdecker>
2018-11-30 11:15:40 +01:00
Christian Decker
50d64e1564
plugin: Make memleak happy
List element structs must have the list_node as their first element.

Signed-off-by: Christian Decker <@cdecker>
2018-11-30 11:15:38 +01:00
Christian Decker
a33bb774d7
plugin: Plugins need a list of methods they registered
This will be used in the next commit to dispatch calls to the correct
plugin.

Signed-off-by: Christian Decker <@cdecker>
2018-11-30 11:15:36 +01:00
Christian Decker
6df8046dc4
plugin: Remove added JSON-RPC methods if a plugin gets killed
Removes the method from the dispatch table, leaving a NULL entry
currently.

Signed-off-by: Christian Decker <@cdecker>
2018-11-30 11:15:19 +01:00
Christian Decker
93dcf8954d
plugin: Add plugin rpcmethods to the JSON-RPC interface
Signed-off-by: Christian Decker <@cdecker>
2018-11-30 11:14:14 +01:00
Christian Decker
bfdab9438c
plugin: Add pointer to jsonrpc so we can add new methods
Signed-off-by: Christian Decker <@cdecker>
2018-11-30 11:13:00 +01:00
Christian Decker
6c0a4db8e9
jsonrpc: Split the jsonrpc object creation from starting to listen
This is needed in order to be able to add methods while initializing
the plugins, but before actually moving to the config dir and starting
to listen.

Signed-off-by: Christian Decker <decker.christian@gmail.com>
2018-11-30 11:12:59 +01:00
Christian Decker
7bcf3e2ee5
jsonrpc: Make an explicit jsonrpc struct
This wraps the listener, a separate log and the registered
commands. This is mainly needed once we dynamically add
sjson_command`s to the JSON-RPC.
2018-11-30 11:12:57 +01:00
Christian Decker
cc04d7eaa2
plugin: The example plugin can now return errors
Signed-off-by: Christian Decker <decker.christian@gmail.com>
2018-11-30 09:50:19 +01:00
Christian Decker
ec592e05b9
plugin: Allow both array as well as object params in example plugin
Signed-off-by: Christian Decker <decker.christian@gmail.com>
2018-11-30 09:50:18 +01:00
Christian Decker
2899bbcf00
plugin: Fix memory leak when requests are done
We weren't cleaning the requests we fulfilled, so this does that :-)
2018-11-30 09:50:13 +01:00
11 changed files with 443 additions and 73 deletions

View File

@ -1,10 +1,10 @@
#!/usr/bin/env python3
"""Simple plugin to show how to build new plugins for c-lightning
It demonstrates how a plugin communicates with c-lightning, how it registers
command line arguments that should be passed through and how it can register
JSON-RPC commands. We communicate with the main daemon through STDIN and STDOUT,
reading and writing JSON-RPC requests.
It demonstrates how a plugin communicates with c-lightning, how it
registers command line arguments that should be passed through and how
it can register JSON-RPC commands. We communicate with the main daemon
through STDIN and STDOUT, reading and writing JSON-RPC requests.
"""
import json
@ -14,11 +14,15 @@ import sys
greeting = "World"
def json_hello(request):
greeting = "Hello {}".format(request['params']['name'])
def json_hello(request, name):
greeting = "Hello {}".format(name)
return greeting
def json_fail(request):
raise ValueError("This will fail")
def json_getmanifest(request):
global greeting
return {
@ -33,11 +37,15 @@ def json_getmanifest(request):
"name": "hello",
"description": "Returns a personalized greeting for {name}",
},
{
"name": "fail",
"description": "Always returns a failure for testing",
},
]
}
def json_init(request):
def json_init(request, options):
"""The main daemon is telling us the relevant cli options
"""
global greeting
@ -48,23 +56,41 @@ def json_init(request):
methods = {
'hello': json_hello,
'fail': json_fail,
'getmanifest': json_getmanifest,
'init': json_init,
}
partial = ""
for l in sys.stdin:
partial += l
try:
partial += l
request = json.loads(partial)
except Exception:
continue
result = None
method = methods[request['method']]
params = request['params']
try:
if isinstance(params, dict):
result = method(request, **params)
else:
result = method(request, *params)
result = {
"jsonrpc": "2.0",
"result": methods[request['method']](request),
"result": result,
"id": request['id']
}
json.dump(result, fp=sys.stdout)
sys.stdout.write('\n')
sys.stdout.flush()
partial = ""
except Exception:
pass
except Exception as e:
result = {
"jsonrpc": "2.0",
"error": "Error while processing {}".format(request['method']),
"id": request['id']
}
json.dump(result, fp=sys.stdout)
sys.stdout.write('\n')
sys.stdout.flush()
partial = ""

View File

@ -82,7 +82,15 @@ currently only string options are supported.*
The `rpcmethods` are methods that will be exposed via `lightningd`'s
JSON-RPC over Unix-Socket interface, just like the builtin
commands. Any parameters given to the JSON-RPC calls will be passed
through verbatim.
through verbatim. Notice that the `name` and the `description` fields
are mandatory, while the `long_description` can be omitted (it'll be
set to `description` if it was not provided).
Plugins are free to register any `name` for their `rpcmethod` as long
as the name was not previously registered. This includes both built-in
methods, such as `help` and `getinfo`, as well as methods registered
by other plugins. If there is a conflict then `lightningd` will report
an error and exit.
### The `init` method

View File

@ -77,6 +77,18 @@ struct json_connection {
struct json_stream **js_arr;
};
/**
* `jsonrpc` encapsulates the entire state of the JSON-RPC interface,
* including a list of methods that the interface supports (can be
* appended dynamically, e.g., for plugins, and logs. It also serves
* as a convenient `tal`-parent for all JSON-RPC related allocations.
*/
struct jsonrpc {
struct io_listener *rpc_listener;
struct json_command **commands;
struct log *log;
};
/* The command itself usually owns the stream, because jcon may get closed.
* The command transfers ownership once it's done though. */
static struct json_stream *jcon_new_json_stream(const tal_t *ctx,
@ -292,10 +304,9 @@ static void json_add_help_command(struct command *cmd,
static void json_help(struct command *cmd,
const char *buffer, const jsmntok_t *params)
{
unsigned int i;
struct json_stream *response;
struct json_command **cmdlist = get_cmdlist();
const jsmntok_t *cmdtok;
struct json_command **commands = cmd->ld->jsonrpc->commands;
if (!param(cmd, buffer, params,
p_opt("command", json_tok_tok, &cmdtok),
@ -303,10 +314,10 @@ static void json_help(struct command *cmd,
return;
if (cmdtok) {
for (i = 0; i < num_cmdlist; i++) {
if (json_tok_streq(buffer, cmdtok, cmdlist[i]->name)) {
for (size_t i = 0; i < tal_count(commands); i++) {
if (json_tok_streq(buffer, cmdtok, commands[i]->name)) {
response = json_stream_success(cmd);
json_add_help_command(cmd, response, cmdlist[i]);
json_add_help_command(cmd, response, commands[i]);
goto done;
}
}
@ -320,8 +331,8 @@ static void json_help(struct command *cmd,
response = json_stream_success(cmd);
json_object_start(response, NULL);
json_array_start(response, "help");
for (i = 0; i < num_cmdlist; i++) {
json_add_help_command(cmd, response, cmdlist[i]);
for (size_t i=0; i<tal_count(commands); i++) {
json_add_help_command(cmd, response, commands[i]);
}
json_array_end(response);
json_object_end(response);
@ -330,17 +341,18 @@ done:
command_success(cmd, response);
}
static const struct json_command *find_cmd(const char *buffer,
static const struct json_command *find_cmd(const struct jsonrpc *rpc,
const char *buffer,
const jsmntok_t *tok)
{
unsigned int i;
struct json_command **cmdlist = get_cmdlist();
struct json_command **commands = rpc->commands;
/* cmdlist[i]->name can be NULL in test code. */
for (i = 0; i < num_cmdlist; i++)
if (cmdlist[i]->name
&& json_tok_streq(buffer, tok, cmdlist[i]->name))
return cmdlist[i];
/* commands[i] can be NULL if the plugin that registered it
* was killed, commands[i]->name can be NULL in test code. */
for (size_t i = 0; i < tal_count(commands); i++)
if (commands[i] && commands[i]->name &&
json_tok_streq(buffer, tok, commands[i]->name))
return commands[i];
return NULL;
}
@ -516,9 +528,9 @@ static void parse_request(struct json_connection *jcon, const jsmntok_t tok[])
return;
}
/* This is a convenient tal parent for duration of command
* (which may outlive the conn!). */
c = tal(jcon->ld->rpc_listener, struct command);
/* Allocate the command off of the `jsonrpc` object and not
* the connection since the command may outlive `conn`. */
c = tal(jcon->ld->jsonrpc, struct command);
c->jcon = jcon;
c->ld = jcon->ld;
c->pending = false;
@ -543,8 +555,8 @@ static void parse_request(struct json_connection *jcon, const jsmntok_t tok[])
return;
}
c->json_cmd = find_cmd(jcon->buffer, method);
if (!c->json_cmd) {
c->json_cmd = find_cmd(jcon->ld->jsonrpc, jcon->buffer, method);
if (!c->json_cmd) {
command_fail(c, JSONRPC2_METHOD_NOT_FOUND,
"Unknown command '%.*s'",
method->end - method->start,
@ -713,10 +725,54 @@ static struct io_plan *incoming_jcon_connected(struct io_conn *conn,
return jcon_connected(notleak(conn), ld);
}
void setup_jsonrpc(struct lightningd *ld, const char *rpc_filename)
bool jsonrpc_command_add(struct jsonrpc *rpc, struct json_command *command)
{
size_t count = tal_count(rpc->commands);
/* Check that we don't clobber a method */
for (size_t i = 0; i < count; i++)
if (rpc->commands[i] != NULL &&
streq(rpc->commands[i]->name, command->name))
return false;
*tal_arr_expand(&rpc->commands) = command;
return true;
}
void jsonrpc_command_remove(struct jsonrpc *rpc, const char *method)
{
// FIXME: Currently leaves NULL entries in the table, if we
// restart plugins we should shift them out.
for (size_t i=0; i<tal_count(rpc->commands); i++) {
struct json_command *cmd = rpc->commands[i];
if (cmd && streq(cmd->name, method)) {
rpc->commands[i] = tal_free(cmd);
}
}
}
struct jsonrpc *jsonrpc_new(const tal_t *ctx, struct lightningd *ld)
{
struct jsonrpc *jsonrpc = tal(ctx, struct jsonrpc);
struct json_command **commands = get_cmdlist();
jsonrpc->commands = tal_arr(jsonrpc, struct json_command *, 0);
jsonrpc->log = new_log(jsonrpc, ld->log_book, "jsonrpc");
for (size_t i=0; i<num_cmdlist; i++) {
jsonrpc_command_add(jsonrpc, commands[i]);
}
jsonrpc->rpc_listener = NULL;
return jsonrpc;
}
void jsonrpc_listen(struct jsonrpc *jsonrpc, struct lightningd *ld)
{
struct sockaddr_un addr;
int fd, old_umask;
const char *rpc_filename = ld->rpc_filename;
/* Should not initialize it twice. */
assert(!jsonrpc->rpc_listener);
if (streq(rpc_filename, "/dev/tty")) {
fd = open(rpc_filename, O_RDWR);
@ -749,9 +805,9 @@ void setup_jsonrpc(struct lightningd *ld, const char *rpc_filename)
if (listen(fd, 1) != 0)
err(1, "Listening on '%s'", rpc_filename);
log_debug(ld->log, "Listening on '%s'", rpc_filename);
ld->rpc_listener = io_new_listener(ld->rpc_filename, fd, incoming_jcon_connected, ld);
jsonrpc->rpc_listener = io_new_listener(
ld->rpc_filename, fd, incoming_jcon_connected, ld);
log_debug(jsonrpc->log, "Listening on '%s'", ld->rpc_filename);
}
/**

View File

@ -92,8 +92,38 @@ void PRINTF_FMT(3, 4) command_fail(struct command *cmd, int code,
/* Mainly for documentation, that we plan to close this later. */
void command_still_pending(struct command *cmd);
/* For initialization */
void setup_jsonrpc(struct lightningd *ld, const char *rpc_filename);
/**
* Create a new jsonrpc to wrap all related information.
*
* This doesn't setup the listener yet, see `jsonrpc_listen` for
* that. This just creates the container for all jsonrpc-related
* information so we can start gathering it before actually starting.
*/
struct jsonrpc *jsonrpc_new(const tal_t *ctx, struct lightningd *ld);
/**
* Start listeing on ld->rpc_filename.
*
* Sets up the listener effectively starting the RPC interface.
*/
void jsonrpc_listen(struct jsonrpc *rpc, struct lightningd *ld);
/**
* Add a new command/method to the JSON-RPC interface.
*
* Returns true if the command was added correctly, false if adding
* this would clobber a command name.
*/
bool jsonrpc_command_add(struct jsonrpc *rpc, struct json_command *command);
/**
* Remove a command/method from the JSON-RPC.
*
* Used to dynamically remove a `struct json_command` from the
* JSON-RPC dispatch table by its name.
*/
void jsonrpc_command_remove(struct jsonrpc *rpc, const char *method);
AUTODATA_TYPE(json_command, struct json_command);
#endif /* LIGHTNING_LIGHTNINGD_JSONRPC_H */

View File

@ -19,6 +19,9 @@
/* Developer error in the parameters to param() call */
#define PARAM_DEV_ERROR -2
/* Plugin returned an error */
#define PLUGIN_ERROR -3
/* Errors from `pay`, `sendpay`, or `waitsendpay` commands */
#define PAY_IN_PROGRESS 200
#define PAY_RHASH_ALREADY_USED 201

View File

@ -201,13 +201,20 @@ static struct lightningd *new_lightningd(const tal_t *ctx)
ld->tor_service_password = NULL;
ld->max_funding_unconfirmed = 2016;
/*~ In the next step we will initialize the plugins. This will
* also populate the JSON-RPC with passthrough methods, hence
* lightningd needs to have something to put those in. This
* is that :-)
*/
ld->jsonrpc = jsonrpc_new(ld, ld);
/*~ We run a number of plugins (subprocesses that we talk JSON-RPC with)
*alongside this process. This allows us to have an easy way for users
*to add their own tools without having to modify the c-lightning source
*code. Here we initialize the context that will keep track and control
*the plugins.
*/
ld->plugins = plugins_new(ld, ld->log_book);
ld->plugins = plugins_new(ld, ld->log_book, ld->jsonrpc);
return ld;
}
@ -695,7 +702,7 @@ int main(int argc, char *argv[])
/*~ Create RPC socket: now lightning-cli can send us JSON RPC commands
* over a UNIX domain socket specified by `ld->rpc_filename`. */
setup_jsonrpc(ld, ld->rpc_filename);
jsonrpc_listen(ld->jsonrpc, ld);
/*~ We defer --daemon until we've completed most initialization: that
* way we'll exit with an error rather than silently exiting 0, then
@ -774,11 +781,13 @@ int main(int argc, char *argv[])
shutdown_subdaemons(ld);
tal_free(ld->plugins);
/* Clean up the JSON-RPC. This needs to happen in a DB transaction since
* it might actually be touching the DB in some destructors, e.g.,
* unreserving UTXOs (see #1737) */
db_begin_transaction(ld->wallet->db);
tal_free(ld->rpc_listener);
tal_free(ld->jsonrpc);
db_commit_transaction(ld->wallet->db);
remove(ld->pidfile);

View File

@ -84,10 +84,11 @@ struct lightningd {
/* Location of the RPC socket. */
char *rpc_filename;
/* The listener for the RPC socket. Can be shut down separately from the
* rest of the daemon to allow a clean shutdown, which frees all pending
* cmds in a DB transaction. */
struct io_listener *rpc_listener;
/* The root of the jsonrpc interface. Can be shut down
* separately from the rest of the daemon to allow a clean
* shutdown, which frees all pending cmds in a DB
* transaction. */
struct jsonrpc *jsonrpc;
/* Configuration file name */
char *config_filename;

View File

@ -6,12 +6,17 @@
#include <ccan/opt/opt.h>
#include <ccan/pipecmd/pipecmd.h>
#include <ccan/tal/str/str.h>
#include <common/memleak.h>
#include <errno.h>
#include <lightningd/json.h>
#include <lightningd/jsonrpc_errors.h>
#include <lightningd/lightningd.h>
#include <signal.h>
#include <unistd.h>
struct plugin {
struct list_node list;
pid_t pid;
char *cmd;
struct io_conn *stdin_conn, *stdout_conn;
@ -31,7 +36,7 @@ struct plugin {
/* List of options that this plugin registered */
struct list_head plugin_opts;
struct list_node list;
const char **methods;
};
struct plugin_request {
@ -59,6 +64,9 @@ struct plugins {
UINTMAP(struct plugin_request *) pending_requests;
struct log *log;
struct log_book *log_book;
/* RPC interface to bind JSON-RPC methods to */
struct jsonrpc *rpc;
};
struct json_output {
@ -66,6 +74,20 @@ struct json_output {
const char *json;
};
/* Represents a pending JSON-RPC request that was forwarded to a
* plugin and is currently waiting for it to return the result. */
struct plugin_rpc_request {
/* The json-serialized ID as it was passed to us by the
* client, will be used to return the result */
const char *id;
const char *method;
const char *params;
struct plugin *plugin;
struct command *cmd;
};
/* Simple storage for plugin options inbetween registering them on the
* command line and passing them off to the plugin */
struct plugin_opt {
@ -75,12 +97,15 @@ struct plugin_opt {
char *value;
};
struct plugins *plugins_new(const tal_t *ctx, struct log_book *log_book){
struct plugins *plugins_new(const tal_t *ctx, struct log_book *log_book,
struct jsonrpc *rpc)
{
struct plugins *p;
p = tal(ctx, struct plugins);
list_head_init(&p->plugins);
p->log_book = log_book;
p->log = new_log(p, log_book, "plugin-manager");
p->rpc = rpc;
return p;
}
@ -92,25 +117,35 @@ void plugin_register(struct plugins *plugins, const char* path TAKES)
list_add_tail(&plugins->plugins, &p->list);
p->plugins = plugins;
p->cmd = tal_strdup(p, path);
p->outbuf = NULL;
/* FIXME(cdecker): Referring to plugin by their registration
number might not be that useful, come up with a naming scheme
that makes more sense. */
plugin_count++;
p->log = new_log(p, plugins->log_book, "plugin-%zu", plugin_count);
p->log = plugins->log;
p->methods = tal_arr(p, const char *, 0);
list_head_init(&p->plugin_opts);
}
/**
* Kill a plugin process, with an error message.
*/
static void plugin_kill(struct plugin *plugin, char *msg)
static void PRINTF_FMT(2,3) plugin_kill(struct plugin *plugin, char *fmt, ...)
{
char *msg;
va_list ap;
va_start(ap, fmt);
msg = tal_vfmt(plugin, fmt, ap);
va_end(ap);
log_broken(plugin->log, "Killing plugin: %s", msg);
plugin->stop = true;
io_wake(plugin);
kill(plugin->pid, SIGKILL);
list_del(&plugin->list);
tal_free(plugin);
}
/**
@ -178,6 +213,9 @@ static bool plugin_read_json_one(struct plugin *plugin)
request->toks = toks;
request->cb(request, request->arg);
tal_free(request);
uintmap_del(&plugin->plugins->pending_requests, id);
/* Move this object out of the buffer */
memmove(plugin->buffer, plugin->buffer + toks[0].end,
tal_count(plugin->buffer) - toks[0].end);
@ -213,6 +251,9 @@ static struct io_plan *plugin_write_json(struct io_conn *conn UNUSED,
struct plugin *plugin)
{
struct json_output *out;
if (plugin->outbuf)
plugin->outbuf = tal_free(plugin->outbuf);
out = list_pop(&plugin->output, struct json_output, list);
if (!out) {
if (plugin->stop) {
@ -294,6 +335,7 @@ static struct io_plan *plugin_stdout_conn_init(struct io_conn *conn,
* the plugin_opt */
static char *plugin_opt_set(const char *arg, struct plugin_opt *popt)
{
tal_free(popt->value);
popt->value = tal_strdup(popt, arg);
return NULL;
}
@ -329,13 +371,13 @@ static bool plugin_opt_add(struct plugin *plugin, const char *buffer,
buffer + nametok->start);
popt->value = NULL;
if (defaulttok) {
popt->value = tal_strndup(plugin, buffer + defaulttok->start,
popt->value = tal_strndup(popt, buffer + defaulttok->start,
defaulttok->end - defaulttok->start);
popt->description = tal_fmt(
plugin, "%.*s (default: %s)", desctok->end - desctok->start,
popt, "%.*s (default: %s)", desctok->end - desctok->start,
buffer + desctok->start, popt->value);
} else {
popt->description = tal_strndup(plugin, buffer + desctok->start,
popt->description = tal_strndup(popt, buffer + desctok->start,
desctok->end - desctok->start);
}
@ -351,29 +393,194 @@ static bool plugin_opt_add(struct plugin *plugin, const char *buffer,
static bool plugin_opts_add(const struct plugin_request *req)
{
const char *buffer = req->plugin->buffer;
const jsmntok_t *cur, *options;
/* This is the parent for all elements in the "options" array */
int optpos;
options =
const jsmntok_t *options =
json_get_member(req->plugin->buffer, req->resulttok, "options");
if (!options)
return false;
optpos = options - req->toks;
if (!options) {
plugin_kill(req->plugin,
"\"result.options\" was not found in the manifest");
return false;
}
if (options->type != JSMN_ARRAY) {
plugin_kill(req->plugin, "\"result.options\" is not an array");
return false;
}
for (cur = options + 1; cur->parent == optpos; cur = json_next(cur))
if (!plugin_opt_add(req->plugin, buffer, cur))
for (size_t i = 0; i < options->size; i++)
if (!plugin_opt_add(req->plugin, buffer, json_get_arr(options, i)))
return false;
return true;
}
static void plugin_rpcmethod_destroy(struct json_command *cmd,
struct jsonrpc *rpc)
{
jsonrpc_command_remove(rpc, cmd->name);
}
static void plugin_rpcmethod_cb(const struct plugin_request *req,
struct plugin_rpc_request *rpc_req)
{
struct json_stream *response;
const jsmntok_t *res;
assert(req->resulttok || req->errortok);
if (req->errortok) {
res = req->errortok;
command_fail(rpc_req->cmd, PLUGIN_ERROR, "%.*s",
res->end - res->start, req->response + res->start);
tal_free(rpc_req);
return;
}
res = req->resulttok;
response = json_stream_success(rpc_req->cmd);
json_add_member(response, NULL, "%.*s", json_tok_len(res),
json_tok_contents(req->response, res));
command_success(rpc_req->cmd, response);
tal_free(rpc_req);
}
static void plugin_rpcmethod_dispatch(struct command *cmd, const char *buffer,
const jsmntok_t *params)
{
const jsmntok_t *toks = params, *methtok, *idtok;
struct plugin_rpc_request *request;
struct plugins *plugins = cmd->ld->plugins;
struct plugin *plugin;
if (cmd->mode == CMD_USAGE) {
cmd->usage = "[params]";
return;
}
/* We're given the params, but we need to walk back to the
* root object, so just walk backwards until the current
* element has no parents, that's going to be the root
* element. */
while (toks->parent != -1)
toks--;
methtok = json_get_member(buffer, toks, "method");
idtok = json_get_member(buffer, toks, "id");
/* We've parsed them before, these should not fail! */
assert(idtok != NULL && methtok != NULL);
request = tal(NULL, struct plugin_rpc_request);
request->method = tal_strndup(request, buffer + methtok->start,
methtok->end - methtok->start);
request->id = tal_strndup(request, buffer + idtok->start,
idtok->end - idtok->start);
request->params = tal_strndup(request, buffer + params->start,
params->end - params->start);
request->plugin = NULL;
request->cmd = cmd;
/* Find the plugin that registered this RPC call */
list_for_each(&plugins->plugins, plugin, list) {
for (size_t i=0; i<tal_count(plugin->methods); i++) {
if (streq(request->method, plugin->methods[i])) {
request->plugin = plugin;
goto found;
}
}
}
found:
/* This should never happen, it'd mean that a plugin didn't
* cleanup after dying */
assert(request->plugin);
tal_steal(request->plugin, request);
plugin_request_send(request->plugin, request->method, request->params, plugin_rpcmethod_cb, request);
command_still_pending(cmd);
}
static bool plugin_rpcmethod_add(struct plugin *plugin, const char *buffer,
const jsmntok_t *meth)
{
const jsmntok_t *nametok, *desctok, *longdesctok;
struct json_command *cmd;
nametok = json_get_member(buffer, meth, "name");
desctok = json_get_member(buffer, meth, "description");
longdesctok = json_get_member(buffer, meth, "long_description");
if (!nametok || nametok->type != JSMN_STRING) {
plugin_kill(plugin,
"rpcmethod does not have a string \"name\": %.*s",
meth->end - meth->start, buffer + meth->start);
return false;
}
if (!desctok || desctok->type != JSMN_STRING) {
plugin_kill(plugin,
"rpcmethod does not have a string "
"\"description\": %.*s",
meth->end - meth->start, buffer + meth->start);
return false;
}
if (longdesctok && longdesctok->type != JSMN_STRING) {
plugin_kill(plugin,
"\"long_description\" is not a string: %.*s",
meth->end - meth->start, buffer + meth->start);
return false;
}
cmd = notleak(tal(plugin, struct json_command));
cmd->name = tal_strndup(cmd, buffer + nametok->start,
nametok->end - nametok->start);
cmd->description = tal_strndup(cmd, buffer + desctok->start,
desctok->end - desctok->start);
if (longdesctok)
cmd->verbose =
tal_strndup(cmd, buffer + longdesctok->start,
longdesctok->end - longdesctok->start);
else
cmd->verbose = cmd->description;
cmd->deprecated = false;
cmd->dispatch = plugin_rpcmethod_dispatch;
tal_add_destructor2(cmd, plugin_rpcmethod_destroy, plugin->plugins->rpc);
if (!jsonrpc_command_add(plugin->plugins->rpc, cmd)) {
log_broken(plugin->log,
"Could not register method \"%s\", a method with "
"that name is already registered",
cmd->name);
return false;
}
*tal_arr_expand(&plugin->methods) = cmd->name;
return true;
}
static bool plugin_rpcmethods_add(const struct plugin_request *req)
{
const char *buffer = req->plugin->buffer;
const jsmntok_t *methods =
json_get_member(req->plugin->buffer, req->resulttok, "rpcmethods");
if (!methods)
return false;
if (methods->type != JSMN_ARRAY) {
plugin_kill(req->plugin,
"\"result.rpcmethods\" is not an array");
return false;
}
for (size_t i = 0; i < methods->size; i++)
if (!plugin_rpcmethod_add(req->plugin, buffer,
json_get_arr(methods, i)))
return false;
return true;
}
/**
* Callback for the plugin_manifest request.
*/
@ -391,8 +598,8 @@ static void plugin_manifest_cb(const struct plugin_request *req, struct plugin *
return;
}
if (!plugin_opts_add(req))
return;
if (!plugin_opts_add(req) || !plugin_rpcmethods_add(req))
plugin_kill(plugin, "Failed to register options or methods");
}
void plugins_init(struct plugins *plugins)
@ -425,6 +632,7 @@ void plugins_init(struct plugins *plugins)
io_new_conn(p, stdin, plugin_stdin_conn_init, p);
plugin_request_send(p, "getmanifest", "[]", plugin_manifest_cb, p);
plugins->pending_manifests++;
tal_free(cmd);
}
if (plugins->pending_manifests > 0)
io_loop(NULL, NULL);

View File

@ -16,7 +16,8 @@ struct plugins;
/**
* Create a new plugins context.
*/
struct plugins *plugins_new(const tal_t *ctx, struct log_book *log_book);
struct plugins *plugins_new(const tal_t *ctx, struct log_book *log_book,
struct jsonrpc *rpc);
/**
* Initialize the registered plugins.

View File

@ -90,6 +90,12 @@ void htlcs_notify_new_block(struct lightningd *ld UNNEEDED, u32 height UNNEEDED)
/* Generated stub for json_escape */
struct json_escaped *json_escape(const tal_t *ctx UNNEEDED, const char *str TAKES UNNEEDED)
{ fprintf(stderr, "json_escape called!\n"); abort(); }
/* Generated stub for jsonrpc_listen */
void jsonrpc_listen(struct jsonrpc *rpc UNNEEDED, struct lightningd *ld UNNEEDED)
{ fprintf(stderr, "jsonrpc_listen called!\n"); abort(); }
/* Generated stub for jsonrpc_new */
struct jsonrpc *jsonrpc_new(const tal_t *ctx UNNEEDED, struct lightningd *ld UNNEEDED)
{ fprintf(stderr, "jsonrpc_new called!\n"); abort(); }
/* Generated stub for load_channels_from_wallet */
void load_channels_from_wallet(struct lightningd *ld UNNEEDED)
{ fprintf(stderr, "load_channels_from_wallet called!\n"); abort(); }
@ -129,7 +135,8 @@ void plugins_config(struct plugins *plugins UNNEEDED)
void plugins_init(struct plugins *plugins UNNEEDED)
{ fprintf(stderr, "plugins_init called!\n"); abort(); }
/* Generated stub for plugins_new */
struct plugins *plugins_new(const tal_t *ctx UNNEEDED, struct log_book *log_book UNNEEDED)
struct plugins *plugins_new(const tal_t *ctx UNNEEDED, struct log_book *log_book UNNEEDED,
struct jsonrpc *rpc UNNEEDED)
{ fprintf(stderr, "plugins_new called!\n"); abort(); }
/* Generated stub for register_opts */
void register_opts(struct lightningd *ld UNNEEDED)
@ -137,9 +144,6 @@ void register_opts(struct lightningd *ld UNNEEDED)
/* Generated stub for setup_color_and_alias */
void setup_color_and_alias(struct lightningd *ld UNNEEDED)
{ fprintf(stderr, "setup_color_and_alias called!\n"); abort(); }
/* Generated stub for setup_jsonrpc */
void setup_jsonrpc(struct lightningd *ld UNNEEDED, const char *rpc_filename UNNEEDED)
{ fprintf(stderr, "setup_jsonrpc called!\n"); abort(); }
/* Generated stub for setup_topology */
void setup_topology(struct chain_topology *topology UNNEEDED, struct timers *timers UNNEEDED,
u32 min_blockheight UNNEEDED, u32 max_blockheight UNNEEDED)

View File

@ -1,5 +1,7 @@
from fixtures import * # noqa: F401,F403
from lightning import RpcError
import pytest
import subprocess
@ -27,3 +29,25 @@ def test_option_passthrough(node_factory):
# option didn't exist
n = node_factory.get_node(options={'plugin': plugin_path, 'greeting': 'Mars'})
n.stop()
def test_rpc_passthrough(node_factory):
"""Starting with a plugin exposes its RPC methods.
First check that the RPC method appears in the help output and
then try to call it.
"""
plugin_path = 'contrib/plugins/helloworld.py'
n = node_factory.get_node(options={'plugin': plugin_path, 'greeting': 'Mars'})
# Make sure that the 'hello' command that the helloworld.py plugin
# has registered is available.
cmd = [hlp for hlp in n.rpc.help()['help'] if 'hello' in hlp['command']]
assert(len(cmd) == 1)
# Now try to call it and see what it returns:
greet = n.rpc.hello(name='Sun')
assert(greet == "Hello Sun")
with pytest.raises(RpcError):
n.rpc.fail()