From 74c58e9f254886deec33f0f599cb17dae9602c02 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Tue, 6 Nov 2018 04:12:43 +0100 Subject: [PATCH] docs: Add an initial draft of the plugin documentation Signed-off-by: Christian Decker <@cdecker> --- doc/plugins.md | 113 ++++++++++++++++++++++++++++++++++++++++++++ lightningd/plugin.c | 30 ++++++------ 2 files changed, 129 insertions(+), 14 deletions(-) create mode 100644 doc/plugins.md diff --git a/doc/plugins.md b/doc/plugins.md new file mode 100644 index 000000000..114c09f2e --- /dev/null +++ b/doc/plugins.md @@ -0,0 +1,113 @@ +# Plugins + +Plugins are a simple yet powerful way to extend the functionality +provided by c-lightning. They are subprocesses that are started by the +main `lightningd` daemon and can interact with `lightningd` in a +variety of ways: + + - **Command line option passthrough** allows plugins to register their + own command line options that are exposed through `lightningd` so + that only the main process needs to be configured. + - **JSON-RPC command passthrough** adds a way for plugins to add their + own commands to the JSON-RPC interface. + - **Event stream subscriptions** provide plugins with a push-based + notification mechanism about events from the `lightningd`. + - **Hooks** are a primitive that allows plugins to be notified about + internal events in `lightningd` and alter its behavior or inject + custom behaviors. + +*Notice: at the time of writing only command line option passthrough +is implemented, the other features are under active development.* + +A plugin may be written in any language, and communicates with +`lightningd` through the plugin's `stdin` and `stdout`. JSON-RPCv2 is +used as protocol on top of the two streams, with the plugin acting as +server and `lightningd` acting as client. + +## A day in the life of a plugin + +During startup of `lightningd` you can use the `--plugin=` option to +register one or more plugins that should be started. `lightningd` will +write JSON-RPC requests to the plugin's `stdin` and will read replies +from its `stdout`. To initialize the plugin two RPC methods are +required: + + - `getmanifest` asks the plugin for command line options and JSON-RPC + commands that should be passed through + - `init` is called after the command line options have been + parsed and passes them through with the real values. This is also + the signal that `lightningd`'s JSON-RPC over Unix Socket is now up + and ready to receive incoming requests from the plugin. + +Once those two methods were called `lightningd` will start passing +through incoming JSON-RPC commands that were registered and the plugin +may interact with `lightningd` using the JSON-RPC over Unix-Socket +interface. + +### The `getmanifest` method + +The `getmanifest` method is required for all plugins and will be called on +startup without any params. It MUST return a JSON object similar to +this example: + +```json +{ + "options": [ + { + "name": "greeting", + "type": "string", + "default": "World", + "description": "What name should I call you?" + } + ], + "rpcmethods": [ + { + "name": "hello", + "description": "Returns a personalized greeting for {greeting} (set via options)." + }, + { + "name": "gettime", + "description": "Returns the current time in {timezone}", + "params": ["timezone"] + } + ] +} +``` + +The `options` will be added to the list of command line options that +`lightningd` accepts. The above will add a `--greeting` option with a +default value of `World` and the specified description. *Notice that +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. + +### The `init` method + +The `init` method is required so that `lightningd` can pass back the +filled command line options and notify the plugin that `lightningd` is +now ready to receive JSON-RPC commands. The `params` of the call are a +simple JSON object containing the options: + +```json +{ + "objects": { + "greeting": "World" + } +} +``` + +The plugin must respond to `init` calls, however the response can be +arbitrary and will currently be discarded by `lightningd`. JSON-RPC +commands were chosen over notifications in order not to force plugins +to implement notifications which are not that well supported. + +## Event stream subscriptions + +*TBD* + +## Hooks + +*TBD* diff --git a/lightningd/plugin.c b/lightningd/plugin.c index b074e7049..c4bea8a27 100644 --- a/lightningd/plugin.c +++ b/lightningd/plugin.c @@ -51,7 +51,7 @@ struct plugin_request { struct plugins { struct list_head plugins; - size_t pending_init; + size_t pending_manifests; /* Currently pending requests by their request ID */ UINTMAP(struct plugin_request *) pending_requests; @@ -335,8 +335,8 @@ static bool plugin_opt_add(struct plugin *plugin, const char *buffer, return true; } -/* Iterate through the options in the init response, and add them to - * the plugin and the command line options */ +/* Iterate through the options in the manifest response, and add them + * to the plugin and the command line options */ static bool plugin_opts_add(const struct plugin_request *req) { const char *buffer = req->plugin->buffer; @@ -364,17 +364,19 @@ static bool plugin_opts_add(const struct plugin_request *req) } /** - * Callback for the plugin_init request. + * Callback for the plugin_manifest request. */ -static void plugin_init_cb(const struct plugin_request *req, struct plugin *plugin) +static void plugin_manifest_cb(const struct plugin_request *req, struct plugin *plugin) { - /* Check if all plugins are initialized, and break if they are */ - plugin->plugins->pending_init--; - if (plugin->plugins->pending_init == 0) + /* Check if all plugins have replied to getmanifest, and break + * if they are */ + plugin->plugins->pending_manifests--; + if (plugin->plugins->pending_manifests == 0) io_break(plugin->plugins); if (req->resulttok->type != JSMN_OBJECT) { - plugin_kill(plugin, "\"init\" response is not an object"); + plugin_kill(plugin, + "\"getmanifest\" response is not an object"); return; } @@ -387,7 +389,7 @@ void plugins_init(struct plugins *plugins) struct plugin *p; char **cmd; int stdin, stdout; - plugins->pending_init = 0; + plugins->pending_manifests = 0; uintmap_init(&plugins->pending_requests); /* Spawn the plugin processes before entering the io_loop */ @@ -406,10 +408,10 @@ void plugins_init(struct plugins *plugins) * write-only on p->stdout */ io_new_conn(p, stdout, plugin_stdout_conn_init, p); io_new_conn(p, stdin, plugin_stdin_conn_init, p); - plugin_request_send(p, "init", "[]", plugin_init_cb, p); - plugins->pending_init++; + plugin_request_send(p, "getmanifest", "[]", plugin_manifest_cb, p); + plugins->pending_manifests++; } - if (plugins->pending_init > 0) + if (plugins->pending_manifests > 0) io_loop(NULL, NULL); } @@ -437,7 +439,7 @@ static void plugin_config(struct plugin *plugin) tal_append_fmt(&conf, "%s\n \"%s\": \"%s\"", sep, name, opt->value); } tal_append_fmt(&conf, "\n }\n}"); - plugin_request_send(plugin, "configure", conf, plugin_config_cb, plugin); + plugin_request_send(plugin, "init", conf, plugin_config_cb, plugin); } void plugins_config(struct plugins *plugins)