Compare commits
16 Commits
btcpaymast
...
plugin-3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01e555a9a3 | ||
|
|
eafe32eba7 | ||
|
|
7c3c63a9ee | ||
|
|
f2b2919564 | ||
|
|
d21025bf07 | ||
|
|
6f43b6ee26 | ||
|
|
50d64e1564 | ||
|
|
a33bb774d7 | ||
|
|
6df8046dc4 | ||
|
|
93dcf8954d | ||
|
|
bfdab9438c | ||
|
|
6c0a4db8e9 | ||
|
|
7bcf3e2ee5 | ||
|
|
cc04d7eaa2 | ||
|
|
ec592e05b9 | ||
|
|
2899bbcf00 |
@ -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 = ""
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user