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'