Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugin event notification subscription wildcard support #6347

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions contrib/pyln-client/pyln/client/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -659,10 +659,13 @@ def _dispatch_request(self, request: Request) -> None:
self.log(traceback.format_exc())

def _dispatch_notification(self, request: Request) -> None:
if request.method not in self.subscriptions:
raise ValueError("No subscription for {name} found.".format(
name=request.method))
func = self.subscriptions[request.method]
if request.method in self.subscriptions:
func = self.subscriptions[request.method]
# Wildcard 'all' subscriptions using asterisk
elif '*' in self.subscriptions:
func = self.subscriptions['*']
else:
raise ValueError(f"No subscription for {request.method} found.")

try:
self._exec_func(func, request)
Expand Down
13 changes: 13 additions & 0 deletions doc/PLUGINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,13 @@ above for example subscribes to the two topics `connect` and
corresponding payloads are listed below.


### `*`

This is a way of specifying that you want to subscribe to all possible
event notifications. It is not recommended, but is useful for plugins
which want to provide generic infrastructure for others (in future, we
may add the ability to dynamically subscribe/unsubscribe).

### `channel_opened`

A notification for topic `channel_opened` is sent if a peer successfully
Expand Down Expand Up @@ -922,6 +929,12 @@ logging or notifications. New rpc calls will fail with error code -5 and (plugin
responses will be ignored. Because lightningd can crash or be killed, a plugin cannot
rely on the shutdown notification always been send.

```json
{
"shutdown": {
}
}
```

## Hooks

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ Event notifications allow a plugin to subscribe to events in `lightningd`. `ligh

Plugins subscribe by returning an array of subscriptions as part of the `getmanifest` response. The result for the `getmanifest` call above for example subscribes to the two topics `connect` and `disconnect`. The topics that are currently defined and the corresponding payloads are listed below.

### `*`

This is a way of specifying that you want to subscribe to all possible
event notifications. It is not recommended, but is useful for plugins
which want to provide generic infrastructure for others (in future, we
may add the ability to dynamically subscribe/unsubscribe).

### `channel_opened`

A notification for topic `channel_opened` is sent if a peer successfully funded a channel with us. It contains the peer id, the funding amount (in millisatoshis), the funding transaction id, and a boolean indicating if the funding transaction has been included into a block.
Expand Down
3 changes: 3 additions & 0 deletions lightningd/notification.c
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,9 @@ bool notify_plugin_shutdown(struct lightningd *ld, struct plugin *p)
struct jsonrpc_notification *n =
jsonrpc_notification_start(NULL, "shutdown");

/* Even shutdown should follow the same "object inside notification" pattern */
json_object_start(n->stream, "shutdown");
json_object_end(n->stream);
jsonrpc_notification_end(n);
return plugin_single_notify(p, take(n));
}
19 changes: 12 additions & 7 deletions lightningd/plugin.c
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ static void plugin_check_subscriptions(struct plugins *plugins,
{
for (size_t i = 0; i < tal_count(plugin->subscriptions); i++) {
const char *topic = plugin->subscriptions[i];
if (!notifications_have_topic(plugins, topic))
if (!streq(topic, "*")
&& !notifications_have_topic(plugins, topic))
log_unusual(
plugin->log,
"topic '%s' is not a known notification topic",
Expand Down Expand Up @@ -1306,7 +1307,8 @@ static const char *plugin_subscriptions_add(struct plugin *plugin,
const char *buffer,
const jsmntok_t *resulttok)
{
const jsmntok_t *subscriptions =
size_t i;
const jsmntok_t *s, *subscriptions =
json_get_member(buffer, resulttok, "subscriptions");

if (!subscriptions) {
Expand All @@ -1318,12 +1320,11 @@ static const char *plugin_subscriptions_add(struct plugin *plugin,
return tal_fmt(plugin, "\"result.subscriptions\" is not an array");
}

for (int i = 0; i < subscriptions->size; i++) {
json_for_each_arr(i, s, subscriptions) {
char *topic;
const jsmntok_t *s = json_get_arr(subscriptions, i);
if (s->type != JSMN_STRING) {
return tal_fmt(plugin,
"result.subscriptions[%d] is not a string: '%.*s'", i,
"result.subscriptions[%zu] is not a string: '%.*s'", i,
json_tok_full_len(s),
json_tok_full(buffer, s));
}
Expand Down Expand Up @@ -2236,9 +2237,13 @@ void json_add_opt_disable_plugins(struct json_stream *response,
static bool plugin_subscriptions_contains(struct plugin *plugin,
const char *method)
{
for (size_t i = 0; i < tal_count(plugin->subscriptions); i++)
if (streq(method, plugin->subscriptions[i]))
for (size_t i = 0; i < tal_count(plugin->subscriptions); i++) {
if (streq(method, plugin->subscriptions[i])
/* Asterisk is magic "all" */
|| streq(plugin->subscriptions[i], "*")) {
return true;
}
}

return false;
}
Expand Down
3 changes: 2 additions & 1 deletion plugins/libplugin.c
Original file line number Diff line number Diff line change
Expand Up @@ -1607,7 +1607,8 @@ static void ld_command_handle(struct plugin *plugin,
#endif
for (size_t i = 0; i < plugin->num_notif_subs; i++) {
if (streq(cmd->methodname,
plugin->notif_subs[i].name)) {
plugin->notif_subs[i].name)
|| streq(plugin->notif_subs[i].name, "*")) {
plugin->notif_subs[i].handle(cmd,
plugin->buffer,
paramstok);
Expand Down
1 change: 1 addition & 0 deletions plugins/libplugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ struct plugin_option {

/* Create an array of these, one for each notification you subscribe to. */
struct plugin_notification {
/* "*" means wildcard: notify me on everything (should be last!) */
const char *name;
/* The handler must eventually trigger a `notification_handled`
* call. */
Expand Down
17 changes: 17 additions & 0 deletions tests/plugins/all_notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env python3
from pyln.client import Plugin
import sys


plugin = Plugin()


@plugin.subscribe("*")
def on_any_notification(request, **kwargs):
plugin.log("notification {}: {}".format(request.method, kwargs))
if request.method == 'shutdown':
# A plugin which subscribes to shutdown is expected to exit itself.
sys.exit(0)


plugin.run()
14 changes: 14 additions & 0 deletions tests/plugins/test_libplugin.c
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,17 @@ static struct command_result *json_shutdown(struct command *cmd,
plugin_exit(cmd->plugin, 0);
}

static struct command_result *json_all_notifs(struct command *cmd,
const char *buf,
const jsmntok_t *params)
{
plugin_log(cmd->plugin, LOG_DBG, "all: %s: %.*s",
cmd->methodname,
json_tok_full_len(params),
json_tok_full(buf, params));
return notification_handled(cmd);
}

static struct command_result *testrpc_cb(struct command *cmd,
const char *buf,
const jsmntok_t *params,
Expand Down Expand Up @@ -209,6 +220,9 @@ static const struct plugin_notification notifs[] = { {
}, {
"shutdown",
json_shutdown
}, {
"*",
json_all_notifs
}
};

Expand Down
29 changes: 29 additions & 0 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4248,3 +4248,32 @@ def test_plugin_persist_option(node_factory):
assert c['value_str'] == "Static option"
assert c['plugin'] == plugin_path
assert l1.rpc.call("hello") == "Static option world"


def test_all_subscription(node_factory, directory):
"""Ensure that registering for all notifications works."""
plugin1 = os.path.join(os.getcwd(), 'tests/plugins/all_notifications.py')
plugin2 = os.path.join(os.getcwd(), "tests/plugins/test_libplugin")

l1, l2 = node_factory.line_graph(2, opts=[{"plugin": plugin1},
{"plugin": plugin2}])

l1.stop()
l2.stop()

# There will be a lot of these!
for notstr in ("block_added: {'block_added': {'hash': ",
"balance_snapshot: {'balance_snapshot': {'node_id': ",
"connect: {'connect': {'id': ",
"channel_state_changed: {'channel_state_changed': {'peer_id': ",
"shutdown: {'shutdown': {}"):
assert l1.daemon.is_in_log(f".*plugin-all_notifications.py: notification {notstr}.*")

for notstr in ('block_added: ',
'balance_snapshot: ',
'channel_state_changed: {'):
assert l2.daemon.is_in_log(f'.*test_libplugin: all: {notstr}.*')

# shutdown and connect are subscribed before the wildcard, so is handled by that handler
assert not l2.daemon.is_in_log(f'.*test_libplugin: all: shutdown.*')
assert not l2.daemon.is_in_log(f'.*test_libplugin: all: connect.*')