diff --git a/.travis/build.sh b/.travis/build.sh index 7bb9a0e45173..0537bf80408b 100755 --- a/.travis/build.sh +++ b/.travis/build.sh @@ -10,6 +10,7 @@ export COMPAT=${COMPAT:-1} export PATH=$CWD/dependencies/bin:"$HOME"/.local/bin:"$PATH" export TIMEOUT=180 export PYTEST_PAR=2 +export PYTHONPATH=$PWD/contrib/pylightning:$PYTHONPATH # If we're not in developer mode, tests spend a lot of time waiting for gossip! # But if we're under valgrind, we can run out of memory! if [ "$DEVELOPER" = 0 ] && [ "$VALGRIND" = 0 ]; then diff --git a/CHANGELOG.md b/CHANGELOG.md index fda5a9a26825..800dc56bca0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - build: now requires `python3-mako` to be installed, i.e. `sudo apt-get install python3-mako` - plugins: a new notification type `invoice_payment` (sent when an invoice is paid) has been added - plugins: a new 'channel_opened' notification type is added, which is emitted when a peer succesfully funds a channel to us +- rpc: a new rpc command is added, `plugin`. It allows one to manage plugins without restarting `lightningd`. +- plugins: a new boolean field is added to the `init`'s `configuration`, `startup`. It allows a plugin to know if it has been started on `lightningd` startup. +- plugins: a new boolean field can be added to a plugin manifest, `dynamic`. It allows a plugin to tell if it can be started or stopped "on-the-fly". ### Deprecated diff --git a/contrib/pylightning/lightning/lightning.py b/contrib/pylightning/lightning/lightning.py index 23122728b783..824a4b3468ca 100644 --- a/contrib/pylightning/lightning/lightning.py +++ b/contrib/pylightning/lightning/lightning.py @@ -714,6 +714,51 @@ def ping(self, peer_id, length=128, pongbytes=128): } return self.call("ping", payload) + def plugin_start(self, plugin): + """ + Adds a plugin to lightningd. + """ + payload = { + "subcommand": "start", + "plugin": plugin + } + return self.call("plugin", payload) + + def plugin_startdir(self, directory): + """ + Adds all plugins from a directory to lightningd. + """ + payload = { + "subcommand": "startdir", + "directory": directory + } + return self.call("plugin", payload) + + def plugin_stop(self, plugin): + """ + Stops a lightningd plugin, will fail if plugin is not dynamic. + """ + payload = { + "subcommand": "stop", + "plugin": plugin + } + return self.call("plugin", payload) + + def plugin_list(self): + """ + Lists all plugins lightningd knows about. + """ + payload = { + "subcommand": "list" + } + return self.call("plugin", payload) + + def plugin_rescan(self): + payload = { + "subcommand": "rescan" + } + return self.call("plugin", payload) + def sendpay(self, route, payment_hash, description=None, msatoshi=None): """ Send along {route} in return for preimage of {payment_hash} diff --git a/contrib/pylightning/lightning/plugin.py b/contrib/pylightning/lightning/plugin.py index fa764059feb1..038f9d256044 100644 --- a/contrib/pylightning/lightning/plugin.py +++ b/contrib/pylightning/lightning/plugin.py @@ -99,7 +99,7 @@ class Plugin(object): """ - def __init__(self, stdout=None, stdin=None, autopatch=True): + def __init__(self, stdout=None, stdin=None, autopatch=True, dynamic=True): self.methods = {'init': Method('init', self._init, MethodType.RPCMETHOD)} self.options = {} @@ -118,6 +118,8 @@ def __init__(self, stdout=None, stdin=None, autopatch=True): self.rpc_filename = None self.lightning_dir = None self.rpc = None + self.startup = True + self.dynamic = dynamic self.child_init = None self.write_lock = RLock() @@ -496,6 +498,7 @@ def _getmanifest(self, **kwargs): 'rpcmethods': methods, 'subscriptions': list(self.subscriptions.keys()), 'hooks': hooks, + 'dynamic': self.dynamic } def _init(self, options, configuration, request): @@ -503,6 +506,7 @@ def _init(self, options, configuration, request): self.lightning_dir = configuration['lightning-dir'] path = os.path.join(self.lightning_dir, self.rpc_filename) self.rpc = LightningRpc(path) + self.startup = configuration['startup'] for name, value in options.items(): self.options[name]['value'] = value diff --git a/doc/PLUGINS.md b/doc/PLUGINS.md index 7e8a262875c4..c0484d9a96c3 100644 --- a/doc/PLUGINS.md +++ b/doc/PLUGINS.md @@ -85,7 +85,8 @@ this example: "hooks": [ "openchannel", "htlc_accepted" - ] + ], + "dynamic": true } ``` @@ -102,6 +103,10 @@ are mandatory, while the `long_description` can be omitted (it'll be set to `description` if it was not provided). `usage` should surround optional parameter names in `[]`. +The `dynamic` indicates if the plugin can be managed after `lightningd` +has been started. Critical plugins that should not be stop should set it +to false. + 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 @@ -122,7 +127,8 @@ simple JSON object containing the options: }, "configuration": { "lightning-dir": "/home/user/.lightning", - "rpc-file": "lightning-rpc" + "rpc-file": "lightning-rpc", + "startup": true } } ``` @@ -132,6 +138,9 @@ 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. +The `startup` field allows a plugin to detect if it was started at +`lightningd` startup (true), or at runtime (false). + ## JSON-RPC passthrough Plugins may register their own JSON-RPC methods that are exposed diff --git a/doc/lightning-plugin.7 b/doc/lightning-plugin.7 new file mode 100644 index 000000000000..ff93c021c941 --- /dev/null +++ b/doc/lightning-plugin.7 @@ -0,0 +1,56 @@ +'\" t +.\" Title: lightning-plugin +.\" Author: [see the "AUTHOR" section] +.\" Generator: DocBook XSL Stylesheets v1.79.1 +.\" Date: 07/23/2019 +.\" Manual: \ \& +.\" Source: \ \& +.\" Language: English +.\" +.TH "LIGHTNING\-PLUGIN" "7" "07/23/2019" "\ \&" "\ \&" +.\" ----------------------------------------------------------------- +.\" * Define some portability stuff +.\" ----------------------------------------------------------------- +.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.\" http://bugs.debian.org/507673 +.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html +.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.\" ----------------------------------------------------------------- +.\" * set default formatting +.\" ----------------------------------------------------------------- +.\" disable hyphenation +.nh +.\" disable justification (adjust text to left margin only) +.ad l +.\" ----------------------------------------------------------------- +.\" * MAIN CONTENT STARTS HERE * +.\" ----------------------------------------------------------------- +.SH "NAME" +lightning-plugin \- Manage plugins with RPC +.SH "SYNOPSIS" +.sp +\fBplugin\fR command [parameter] [second_parameter] +.SH "DESCRIPTION" +.sp +The \fBplugin\fR RPC command allows to manage plugins without having to restart lightningd\&. It takes 1 to 3 parameters: a command (start/stop/startdir/rescan/list) which describes the action to take and optionally one or two parameters which describes the plugin on which the action has to be taken\&. +.sp +The \fIstart\fR command takes a path as parameter and will load the plugin available from this path\&. +.sp +The \fIstop\fR command takes a plugin name as parameter and will kill and unload the specified plugin\&. +.sp +The \fIstartdir\fR command takes a directory path as parameter and will load all plugins this directory contains\&. +.sp +The \fIrescan\fR command starts all not\-already\-loaded plugins from the default plugins directory (by default \fI~/\&.lightning/plugins\fR)\&. +.sp +The \fIlist\fR command will return all the active plugins\&. +.SH "RETURN VALUE" +.sp +On success, this returns an array \fIplugins\fR of objects, one by plugin\&. Each object contains the name of the plugin (\fIname\fR field) and its status (\fIactive\fR boolean field)\&. Since plugins are configured asynchronously, a freshly started plugin may not appear immediately\&. +.SH "AUTHOR" +.sp +Antoine Poinsot is mainly responsible\&. +.SH "RESOURCES" +.sp +Main web site: https://github\&.com/ElementsProject/lightning diff --git a/doc/lightning-plugin.7.txt b/doc/lightning-plugin.7.txt new file mode 100644 index 000000000000..1e86b922f68d --- /dev/null +++ b/doc/lightning-plugin.7.txt @@ -0,0 +1,48 @@ +LIGHTNING-PLUGIN(7) +=================== +:doctype: manpage + +NAME +---- +lightning-plugin - Manage plugins with RPC + +SYNOPSIS +-------- +*plugin* command [parameter] [second_parameter] + +DESCRIPTION +----------- + +The *plugin* RPC command allows to manage plugins without having to restart lightningd. +It takes 1 to 3 parameters: a command (start/stop/startdir/rescan/list) which describes the +action to take and optionally one or two parameters which describes the plugin on which +the action has to be taken. + +The 'start' command takes a path as parameter and will load the plugin available from this +path. + +The 'stop' command takes a plugin name as parameter and will kill and unload the specified +plugin. + +The 'startdir' command takes a directory path as parameter and will load all plugins this +directory contains. + +The 'rescan' command starts all not-already-loaded plugins from the default plugins directory +(by default '~/.lightning/plugins'). + +The 'list' command will return all the active plugins. + +RETURN VALUE +------------ + +On success, this returns an array 'plugins' of objects, one by plugin. Each object contains +the name of the plugin ('name' field) and its status ('active' boolean field). +Since plugins are configured asynchronously, a freshly started plugin may not appear immediately. + +AUTHOR +------ +Antoine Poinsot is mainly responsible. + +RESOURCES +--------- +Main web site: https://github.com/ElementsProject/lightning diff --git a/lightningd/Makefile b/lightningd/Makefile index bc7ceff78a56..ede14b7d6ae0 100644 --- a/lightningd/Makefile +++ b/lightningd/Makefile @@ -89,6 +89,7 @@ LIGHTNINGD_SRC := \ lightningd/peer_htlcs.c \ lightningd/ping.c \ lightningd/plugin.c \ + lightningd/plugin_control.c \ lightningd/plugin_hook.c \ lightningd/subd.c \ lightningd/watch.c diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c index f887369eb890..c26ff2414adb 100644 --- a/lightningd/lightningd.c +++ b/lightningd/lightningd.c @@ -221,6 +221,7 @@ static struct lightningd *new_lightningd(const tal_t *ctx) *the plugins. */ ld->plugins = plugins_new(ld, ld->log_book, ld); + ld->plugins->startup = true; /*~ This is set when a JSON RPC command comes in to shut us down. */ ld->stop_conn = NULL; diff --git a/lightningd/plugin.c b/lightningd/plugin.c index 9f43748d7b0e..30a8425c4c06 100644 --- a/lightningd/plugin.c +++ b/lightningd/plugin.c @@ -1,8 +1,4 @@ -#include "lightningd/plugin.h" - #include -#include -#include #include #include #include @@ -21,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -35,70 +32,6 @@ * `getmanifest` call anyway, that's what `init `is for. */ #define PLUGIN_MANIFEST_TIMEOUT 60 -struct plugin { - struct list_node list; - - pid_t pid; - char *cmd; - struct io_conn *stdin_conn, *stdout_conn; - bool stop; - struct plugins *plugins; - const char **plugin_path; - - /* Stuff we read */ - char *buffer; - size_t used, len_read; - - /* Our json_streams. Since multiple streams could start - * returning data at once, we always service these in order, - * freeing once empty. */ - struct json_stream **js_arr; - - struct log *log; - - /* List of options that this plugin registered */ - struct list_head plugin_opts; - - const char **methods; - - /* Timer to add a timeout to some plugin RPC calls. Used to - * guarantee that `getmanifest` doesn't block indefinitely. */ - const struct oneshot *timeout_timer; - - /* An array of subscribed topics */ - char **subscriptions; -}; - -struct plugins { - struct list_head plugins; - size_t pending_manifests; - - /* Currently pending requests by their request ID */ - UINTMAP(struct jsonrpc_request *) pending_requests; - struct log *log; - struct log_book *log_book; - - struct lightningd *ld; -}; - -/* The value of a plugin option, which can have different types. - * The presence of the integer and boolean values will depend of - * the option type, but the string value will always be filled. - */ -struct plugin_opt_value { - char *as_str; - int *as_int; - bool *as_bool; -}; - -struct plugin_opt { - struct list_node list; - const char *name; - const char *type; - const char *description; - struct plugin_opt_value *value; -}; - struct plugins *plugins_new(const tal_t *ctx, struct log_book *log_book, struct lightningd *ld) { @@ -118,13 +51,25 @@ static void destroy_plugin(struct plugin *p) void plugin_register(struct plugins *plugins, const char* path TAKES) { - struct plugin *p; + struct plugin *p, *p_temp; + + /* Don't register an already registered plugin */ + list_for_each(&plugins->plugins, p_temp, list) { + if (streq(path, p_temp->cmd)) { + if (taken(path)) + tal_free(path); + return; + } + } + p = tal(plugins, struct plugin); list_add_tail(&plugins->plugins, &p->list); p->plugins = plugins; p->cmd = tal_strdup(p, path); + p->configured = false; p->js_arr = tal_arr(p, struct json_stream *, 0); p->used = 0; + p->signal_startup = false; p->log = new_log(p, plugins->log_book, "plugin-%s", path_basename(tmpctx, p->cmd)); @@ -133,7 +78,7 @@ void plugin_register(struct plugins *plugins, const char* path TAKES) tal_add_destructor(p, destroy_plugin); } -static bool paths_match(const char *cmd, const char *name) +bool plugin_paths_match(const char *cmd, const char *name) { if (strchr(name, PATH_SEP)) { const char *cmd_canon, *name_canon; @@ -159,7 +104,7 @@ bool plugin_remove(struct plugins *plugins, const char *name) bool removed = false; list_for_each_safe(&plugins->plugins, p, next, list) { - if (paths_match(p->cmd, name)) { + if (plugin_paths_match(p->cmd, name)) { list_del_from(&plugins->plugins, &p->list); tal_free(p); removed = true; @@ -168,10 +113,7 @@ bool plugin_remove(struct plugins *plugins, const char *name) return removed; } -/** - * Kill a plugin process, with an error message. - */ -static void PRINTF_FMT(2,3) plugin_kill(struct plugin *plugin, char *fmt, ...) +void PRINTF_FMT(2,3) plugin_kill(struct plugin *plugin, char *fmt, ...) { char *msg; va_list ap; @@ -180,7 +122,7 @@ static void PRINTF_FMT(2,3) plugin_kill(struct plugin *plugin, char *fmt, ...) msg = tal_vfmt(plugin, fmt, ap); va_end(ap); - log_broken(plugin->log, "Killing plugin: %s", msg); + log_info(plugin->log, "Killing plugin: %s", msg); plugin->stop = true; io_wake(plugin); kill(plugin->pid, SIGKILL); @@ -848,12 +790,13 @@ static void plugin_manifest_cb(const char *buffer, const jsmntok_t *idtok, struct plugin *plugin) { - const jsmntok_t *resulttok; + const jsmntok_t *resulttok, *dynamictok; + bool dynamic_plugin; /* Check if all plugins have replied to getmanifest, and break - * if they are */ + * if they have and this is the startup init */ plugin->plugins->pending_manifests--; - if (plugin->plugins->pending_manifests == 0) + if (plugin->plugins->startup && plugin->plugins->pending_manifests == 0) io_break(plugin->plugins); resulttok = json_get_member(buffer, toks, "result"); @@ -865,6 +808,12 @@ static void plugin_manifest_cb(const char *buffer, return; } + dynamictok = json_get_member(buffer, resulttok, "dynamic"); + if (dynamictok && json_to_bool(buffer, dynamictok, &dynamic_plugin)) { + plugin->signal_startup = true; + plugin->dynamic = dynamic_plugin; + } + if (!plugin_opts_add(plugin, buffer, resulttok) || !plugin_rpcmethods_add(plugin, buffer, resulttok) || !plugin_subscriptions_add(plugin, buffer, resulttok) || @@ -872,6 +821,12 @@ static void plugin_manifest_cb(const char *buffer, plugin_kill( plugin, "Failed to register options, methods, hooks, or subscriptions."); + + /* If all plugins have replied to getmanifest and this is not + * the startup init, configure them */ + if (!plugin->plugins->startup && plugin->plugins->pending_manifests == 0) + plugins_config(plugin->plugins); + /* Reset timer, it'd kill us otherwise. */ tal_free(plugin->timeout_timer); } @@ -963,20 +918,17 @@ void plugins_add_default_dir(struct plugins *plugins, const char *default_dir) } } -void plugins_init(struct plugins *plugins, const char *dev_plugin_debug) +void plugins_start(struct plugins *plugins, const char *dev_plugin_debug) { struct plugin *p; char **cmd; int stdin, stdout; struct jsonrpc_request *req; - plugins->pending_manifests = 0; - uintmap_init(&plugins->pending_requests); - - plugins_add_default_dir(plugins, path_join(tmpctx, plugins->ld->config_dir, "plugins")); - setenv("LIGHTNINGD_PLUGIN", "1", 1); - /* Spawn the plugin processes before entering the io_loop */ list_for_each(&plugins->plugins, p, list) { + if (p->configured) + continue; + bool debug; debug = dev_plugin_debug && strends(p->cmd, dev_plugin_debug); @@ -1015,9 +967,24 @@ void plugins_init(struct plugins *plugins, const char *dev_plugin_debug) } tal_free(cmd); } +} + +void plugins_init(struct plugins *plugins, const char *dev_plugin_debug) +{ + plugins->pending_manifests = 0; + uintmap_init(&plugins->pending_requests); + + plugins_add_default_dir(plugins, + path_join(tmpctx, plugins->ld->config_dir, "plugins")); + + setenv("LIGHTNINGD_PLUGIN", "1", 1); + /* Spawn the plugin processes before entering the io_loop */ + plugins_start(plugins, dev_plugin_debug); if (plugins->pending_manifests > 0) io_loop_with_timers(plugins->ld); + // There won't be io_loop anymore to wait for plugins + plugins->startup = false; } static void plugin_config_cb(const char *buffer, @@ -1025,7 +992,7 @@ static void plugin_config_cb(const char *buffer, const jsmntok_t *idtok, struct plugin *plugin) { - /* Nothing to be done here, this is just a report */ + plugin->configured = true; } /* FIXME(cdecker) This just builds a string for the request because @@ -1055,6 +1022,7 @@ static void plugin_config(struct plugin *plugin) json_object_start(req->stream, "configuration"); json_add_string(req->stream, "lightning-dir", ld->config_dir); json_add_string(req->stream, "rpc-file", ld->rpc_filename); + json_add_bool(req->stream, "startup", plugin->plugins->startup); json_object_end(req->stream); jsonrpc_request_end(req); @@ -1065,7 +1033,8 @@ void plugins_config(struct plugins *plugins) { struct plugin *p; list_for_each(&plugins->plugins, p, list) { - plugin_config(p); + if (!p->configured) + plugin_config(p); } } diff --git a/lightningd/plugin.h b/lightningd/plugin.h index dc0c9e10768a..033ea5966617 100644 --- a/lightningd/plugin.h +++ b/lightningd/plugin.h @@ -1,28 +1,93 @@ #ifndef LIGHTNING_LIGHTNINGD_PLUGIN_H #define LIGHTNING_LIGHTNINGD_PLUGIN_H #include "config.h" +#include +#include #include #include #include #include +/** + * A plugin, exposed as a stub so we can pass it as an argument. + */ +struct plugin { + struct list_node list; + + pid_t pid; + char *cmd; + struct io_conn *stdin_conn, *stdout_conn; + bool stop; + struct plugins *plugins; + const char **plugin_path; + bool configured; + /* If this plugin can be restarted without restarting lightningd */ + bool dynamic; + bool signal_startup; + + /* Stuff we read */ + char *buffer; + size_t used, len_read; + + /* Our json_streams. Since multiple streams could start + * returning data at once, we always service these in order, + * freeing once empty. */ + struct json_stream **js_arr; + + struct log *log; + + /* List of options that this plugin registered */ + struct list_head plugin_opts; + + const char **methods; + + /* Timer to add a timeout to some plugin RPC calls. Used to + * guarantee that `getmanifest` doesn't block indefinitely. */ + const struct oneshot *timeout_timer; + + /* An array of subscribed topics */ + char **subscriptions; +}; + /** * A collection of plugins, and some associated information. * * Mainly used as root context for calls in the plugin subsystem. */ -struct plugins; - -/** - * A plugin, exposed as a stub so we can pass it as an argument. +struct plugins { + struct list_head plugins; + size_t pending_manifests; + bool startup; + + /* Currently pending requests by their request ID */ + UINTMAP(struct jsonrpc_request *) pending_requests; + struct log *log; + struct log_book *log_book; + + struct lightningd *ld; +}; + +/* The value of a plugin option, which can have different types. + * The presence of the integer and boolean values will depend of + * the option type, but the string value will always be filled. */ -struct plugin; +struct plugin_opt_value { + char *as_str; + int *as_int; + bool *as_bool; +}; /** * Simple storage for plugin options inbetween registering them on the * command line and passing them off to the plugin */ -struct plugin_opt; +struct plugin_opt { + struct list_node list; + const char *name; + const char *type; + const char *description; + struct plugin_opt_value *value; +}; /** * Create a new plugins context. @@ -36,6 +101,8 @@ struct plugins *plugins_new(const tal_t *ctx, struct log_book *log_book, */ void plugins_add_default_dir(struct plugins *plugins, const char *default_dir); +void plugins_start(struct plugins *plugins, const char *dev_plugin_debug); + /** * Initialize the registered plugins. * @@ -57,6 +124,10 @@ void plugins_init(struct plugins *plugins, const char *dev_plugin_debug); */ void plugin_register(struct plugins *plugins, const char* path TAKES); +/** + * Returns true if the provided name matches a plugin command + */ +bool plugin_paths_match(const char *cmd, const char *name); /** * Remove a plugin registered for initialization. @@ -66,6 +137,11 @@ void plugin_register(struct plugins *plugins, const char* path TAKES); */ bool plugin_remove(struct plugins *plugins, const char *name); +/** + * Kill a plugin process, with an error message. + */ +void PRINTF_FMT(2,3) plugin_kill(struct plugin *plugin, char *fmt, ...); + /** * Send the configure message to all plugins. * diff --git a/lightningd/plugin_control.c b/lightningd/plugin_control.c new file mode 100644 index 000000000000..89393d557eed --- /dev/null +++ b/lightningd/plugin_control.c @@ -0,0 +1,136 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * A plugin command which permits to control plugins without restarting + * lightningd. It takes a subcommand, and an optional subcommand parameter. + */ +static struct command_result *json_plugin_control(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) +{ + const char *subcmd; + subcmd = param_subcommand(cmd, buffer, params, + "start", "stop", "startdir", "rescan", "list", NULL); + if (!subcmd) + return command_param_failed(); + + struct plugin *p; + struct json_stream *response; + + if (streq(subcmd, "stop")) { + const char *plugin_name; + bool plugin_found; + + if (!param(cmd, buffer, params, + p_req("subcommand", param_ignore, cmd), + p_req("plugin", param_string, &plugin_name), + NULL)) + return command_param_failed(); + + plugin_found = false; + list_for_each(&cmd->ld->plugins->plugins, p, list) { + if (plugin_paths_match(p->cmd, plugin_name)) { + if (!p->dynamic) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%s plugin cannot be managed when lightningd is up", + plugin_name); + plugin_found = true; + plugin_hook_unregister_all(p); + plugin_kill(p, "%s stopped by lightningd via RPC", + plugin_name); + break; + } + } + if (!plugin_found) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Could not find plugin %s", plugin_name); + } else if (streq(subcmd, "start")) { + const char *plugin_path; + + if (!param(cmd, buffer, params, + p_req("subcommand", param_ignore, cmd), + p_req("plugin", param_string, &plugin_path), + NULL)) + return command_param_failed(); + + if (access(plugin_path, X_OK) == 0) + plugin_register(cmd->ld->plugins, plugin_path); + else + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%s is not executable: %s", + plugin_path, strerror(errno)); + } else if (streq(subcmd, "startdir")) { + const char *dir_path; + + if (!param(cmd, buffer, params, + p_req("subcommand", param_ignore, cmd), + p_req("directory", param_string, &dir_path), + NULL)) + return command_param_failed(); + + if (access(dir_path, F_OK) == 0) + add_plugin_dir(cmd->ld->plugins, dir_path, true); + else + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Could not open %s", dir_path); + } else if (streq(subcmd, "rescan")) { + if (!param(cmd, buffer, params, + p_req("subcommand", param_ignore, cmd), + NULL)) + return command_param_failed(); + + plugins_add_default_dir(cmd->ld->plugins, + path_join(tmpctx, cmd->ld->config_dir, "plugins")); + } else if (streq(subcmd, "list")) { + if (!param(cmd, buffer, params, + p_req("subcommand", param_ignore, cmd), + NULL)) + return command_param_failed(); + /* Don't do anything as we return the plugin list anyway */ + } + + /* The config function is called once we got the manifest, + * in 'plugin_manifest_cb'.*/ + plugins_start(cmd->ld->plugins, cmd->ld->dev_debug_subprocess); + + response = json_stream_success(cmd); + json_array_start(response, "plugins"); + list_for_each(&cmd->ld->plugins->plugins, p, list) { + json_object_start(response, NULL); + json_add_string(response, "name", p->cmd); + json_add_bool(response, "active", p->configured); + json_object_end(response); + } + json_array_end(response); + + return command_success(cmd, response); +} + +static const struct json_command plugin_control_command = { + "plugin", + "plugin", + json_plugin_control, + "Control plugins (start, stop, startdir, rescan, list)", + .verbose = "Usage :\n" + "plugin start /path/to/a/plugin\n" + " adds a new plugin to c-lightning\n" + "plugin stop plugin_name\n" + " stops an already registered plugin\n" + "plugin startdir /path/to/a/plugin_dir/\n" + " adds a new plugin directory\n" + "plugin rescan\n" + " loads not-already-loaded plugins from the default plugins dir\n" + "plugin list\n" + " lists all active plugins\n" + "\n" +}; +AUTODATA(json_command, &plugin_control_command); diff --git a/lightningd/plugin_control.h b/lightningd/plugin_control.h new file mode 100644 index 000000000000..bcc74abfd86a --- /dev/null +++ b/lightningd/plugin_control.h @@ -0,0 +1,7 @@ +#ifndef LIGHTNING_LIGHTNINGD_PLUGIN_CONTROL_H +#define LIGHTNING_LIGHTNINGD_PLUGIN_CONTROL_H +#include "config.h" +#include + + +#endif /* LIGHTNING_LIGHTNINGD_PLUGIN_CONTROL_H */ diff --git a/lightningd/plugin_hook.c b/lightningd/plugin_hook.c index 54e7098ee8c2..bd620db705bc 100644 --- a/lightningd/plugin_hook.c +++ b/lightningd/plugin_hook.c @@ -39,6 +39,32 @@ bool plugin_hook_register(struct plugin *plugin, const char *method) return true; } +bool plugin_hook_unregister(struct plugin *plugin, const char *method) +{ + struct plugin_hook *hook = plugin_hook_by_name(method); + if (!hook) { + /* No such hook name registered */ + return false; + } else if (hook->plugin == NULL) { + /* This name is not registered */ + return false; + } + hook->plugin = NULL; + return true; +} + +void plugin_hook_unregister_all(struct plugin *plugin) +{ + static struct plugin_hook **hooks = NULL; + static size_t num_hooks; + if (!hooks) + hooks = autodata_get(hooks, &num_hooks); + + for (size_t i = 0; i < num_hooks; i++) + if (hooks[i]->plugin == plugin) + hooks[i]->plugin = NULL; +} + /** * Callback to be passed to the jsonrpc_request. * diff --git a/lightningd/plugin_hook.h b/lightningd/plugin_hook.h index 76d9b6fea98c..4869d69e9af3 100644 --- a/lightningd/plugin_hook.h +++ b/lightningd/plugin_hook.h @@ -102,6 +102,12 @@ void plugin_hook_call_(struct lightningd *ld, const struct plugin_hook *hook, bool plugin_hook_register(struct plugin *plugin, const char *method); +/* Unregister a hook a plugin has registered for */ +bool plugin_hook_unregister(struct plugin *plugin, const char *method); + +/* Unregister all hooks a plugin has registered for */ +void plugin_hook_unregister_all(struct plugin *plugin); + /* Special sync plugin hook for db: changes[] are SQL statements, with optional * final command appended. */ void plugin_hook_db_sync(struct db *db, const char **changes, const char *final); diff --git a/tests/plugins/static.py b/tests/plugins/static.py new file mode 100755 index 000000000000..ff89eeffc106 --- /dev/null +++ b/tests/plugins/static.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +"""Simple plugin to test the dynamic behavior. + +A plugin started with dynamic to False cannot be controlled after lightningd +has been started. +""" + +from lightning import Plugin + +plugin = Plugin(dynamic=False) + + +@plugin.init() +def init(configuration, options, plugin): + plugin.log("Static plugin initialized.") + + +@plugin.method('hello') +def reject(plugin): + """Mark a given node_id as reject for future connections. + """ + return "Hello, you cannot stop me without stopping lightningd" + + +plugin.run() diff --git a/tests/test_plugin.py b/tests/test_plugin.py index ed5ee19b747f..42023bd4402a 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -90,6 +90,54 @@ def test_plugin_dir(node_factory): node_factory.get_node(options={'plugin-dir': plugin_dir, 'greeting': 'Mars'}) +def test_plugin_command(node_factory): + """Tests the 'plugin' RPC command""" + n = node_factory.get_node() + + # Make sure that the 'hello' command from the helloworld.py plugin + # is not available. + cmd = [hlp for hlp in n.rpc.help()["help"] if "hello" in hlp["command"]] + assert(len(cmd) == 0) + + # Add the 'contrib/plugins' test dir + time.sleep(2) + n.rpc.plugin_startdir(directory=os.path.join(os.getcwd(), "contrib/plugins")) + n.daemon.wait_for_log(r"Plugin helloworld.py initialized") + # Make sure that the 'hello' command from the helloworld.py plugin + # is now available. + cmd = [hlp for hlp in n.rpc.help()["help"] if "hello" in hlp["command"]] + assert(len(cmd) == 1) + + # Make sure 'rescan' and 'list' controls dont crash + n.rpc.plugin_rescan() + n.rpc.plugin_list() + time.sleep(1) + + # Make sure the plugin behaves normally after stop and restart + n.rpc.plugin_stop(plugin="helloworld.py") + n.daemon.wait_for_log(r"Killing plugin: helloworld.py") + time.sleep(1) + n.rpc.plugin_start(plugin=os.path.join(os.getcwd(), "contrib/plugins/helloworld.py")) + n.daemon.wait_for_log(r"Plugin helloworld.py initialized") + assert("Hello world" == n.rpc.call(method="hello")) + + # Now stop the helloworld plugin + n.rpc.plugin_stop(plugin="helloworld.py") + n.daemon.wait_for_log(r"Killing plugin: helloworld.py") + time.sleep(1) + # Make sure that the 'hello' command from the helloworld.py plugin + # is not available anymore. + cmd = [hlp for hlp in n.rpc.help()["help"] if "hello" in hlp["command"]] + assert(len(cmd) == 0) + + # Test that we cannot stop a plugin with 'dynamic' set to False in + # getmanifest + n.rpc.plugin_start(plugin=os.path.join(os.getcwd(), "tests/plugins/static.py")) + n.daemon.wait_for_log(r"Static plugin initialized.") + with pytest.raises(RpcError, match=r"plugin cannot be managed when lightningd is up"): + n.rpc.plugin_stop(plugin="static.py") + + def test_plugin_disable(node_factory): """--disable-plugin works""" plugin_dir = 'contrib/plugins'