From 0fe3d9cd59f8194589f5c8b805d9531ead853889 Mon Sep 17 00:00:00 2001 From: Erik De Smedt Date: Fri, 23 Feb 2024 09:29:03 +0100 Subject: [PATCH 1/3] cln_plugin: Example package subscribing to "*" Creates an example package that subscribes to all notifications and logs them. This is useful for testing the behavior of subscribing to "*". I've also edited the Makefile to ensure that `make` builds the example and that `make clean` removes the example --- plugins/Makefile | 6 +++- plugins/examples/cln-subscribe-wildcard.rs | 35 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 plugins/examples/cln-subscribe-wildcard.rs diff --git a/plugins/Makefile b/plugins/Makefile index 6396be75d05f..10e8d2471365 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -253,10 +253,14 @@ PLUGIN_BASES := $(PLUGINS:plugins/%=%) $(PY_PLUGINS:plugins/%=%) plugins/list_of_builtin_plugins_gen.h: plugins/Makefile Makefile config.vars @$(call VERBOSE,GEN $@,echo "static const char *list_of_builtin_plugins[] = { $(PLUGIN_BASES:%=\"%\",) NULL };" > $@) +target/${RUST_PROFILE}/examples/cln-subscribe-wildcard: ${CLN_PLUGIN_SRC} plugins/examples/cln-subscribe-wildcard.rs + cargo build ${CARGO_OPTS} --example cln-subscribe-wildcard + CLN_PLUGIN_EXAMPLES := \ target/${RUST_PROFILE}/examples/cln-plugin-startup \ target/${RUST_PROFILE}/examples/cln-plugin-reentrant \ - target/${RUST_PROFILE}/examples/cln-rpc-getinfo + target/${RUST_PROFILE}/examples/cln-rpc-getinfo \ + target/${RUST_PROFILE}/examples/cln-subscribe-wildcard CLN_PLUGIN_SRC = $(shell find plugins/src -name "*.rs") diff --git a/plugins/examples/cln-subscribe-wildcard.rs b/plugins/examples/cln-subscribe-wildcard.rs new file mode 100644 index 000000000000..4942f35333f4 --- /dev/null +++ b/plugins/examples/cln-subscribe-wildcard.rs @@ -0,0 +1,35 @@ +/// This plug-in subscribes to the wildcard-notifications +/// and creates a corresponding log-entry + +use anyhow::Result; +use cln_plugin::{Builder, Plugin}; + +#[tokio::main] +async fn main() -> Result<()> { + let state = (); + + let configured = Builder::new(tokio::io::stdin(), tokio::io::stdout()) + .subscribe("*", handle_wildcard_notification) + .start(state) + .await?; + + match configured { + Some(p) => p.join().await?, + None => return Ok(()) // cln was started with --help + }; + + Ok(()) +} + +async fn handle_wildcard_notification(_plugin: Plugin<()>, value : serde_json::Value) -> Result<()> { + let notification_type : String = value + .as_object() + .unwrap() + .keys() + .next() + .unwrap() + .into(); + + log::info!("Received notification {}", notification_type); + Ok(()) +} From c2e71f2a5030f67e9b6ec5daa812c3af8838571c Mon Sep 17 00:00:00 2001 From: Erik De Smedt Date: Fri, 23 Feb 2024 10:13:53 +0100 Subject: [PATCH 2/3] Repro: cln-plugin cannot subscribe to wildcard "*" This test reproduces a bug in the `cln-plugin`-crate. Core Lightning supports a wildcard `*` that plugins can use to subscribe to all notifications. However, `cln-plugin` does not support this case. It allows the developer to subscribe `*`. But the plug-in crashes when the first notification is received --- tests/test_cln_rs.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_cln_rs.py b/tests/test_cln_rs.py index de97ec717223..d87eafa93d39 100644 --- a/tests/test_cln_rs.py +++ b/tests/test_cln_rs.py @@ -382,3 +382,16 @@ def test_grpc_decode(node_factory): string=inv.bolt11 )) print(res) + + +@pytest.mark.xfail(strict=True) +def test_rust_plugin_subscribe_wildcard(node_factory): + """ Creates a plugin that loads the subscribe_wildcard plugin + """ + bin_path = Path.cwd() / "target" / RUST_PROFILE / "examples" / "cln-subscribe-wildcard" + l1 = node_factory.get_node(options={"plugin": bin_path}) + l2 = node_factory.get_node() + + l2.connect(l1) + + l1.daemon.wait_for_log("Received notification connect") From e32e654e1c76a2149a617dea43fe38c6d977e077 Mon Sep 17 00:00:00 2001 From: Erik De Smedt Date: Fri, 23 Feb 2024 11:01:30 +0100 Subject: [PATCH 3/3] cln_plugin: Support wildcard subscriptions Adapts `cln_plugin` to make it support wildcard `*`-subscriptions. --- plugins/src/lib.rs | 68 ++++++++++++++++++++++++++++++++------------ tests/test_cln_rs.py | 1 - 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/plugins/src/lib.rs b/plugins/src/lib.rs index ee50ccbe419e..474615468291 100644 --- a/plugins/src/lib.rs +++ b/plugins/src/lib.rs @@ -47,6 +47,8 @@ where option_values: HashMap>, rpcmethods: HashMap>, subscriptions: HashMap>, + // Contains a Subscription if the user subscribed to "*" + wildcard_subscription : Option>, notifications: Vec, custommessages: Vec, featurebits: FeatureBits, @@ -72,6 +74,7 @@ where rpcmethods: HashMap>, hooks: HashMap>, subscriptions: HashMap>, + wildcard_subscription : Option>, #[allow(dead_code)] // unsure why rust thinks this field isn't used notifications: Vec, } @@ -91,6 +94,7 @@ where #[allow(dead_code)] // Unused until we fill in the Hook structs. hooks: HashMap>, subscriptions: HashMap>, + wildcard_subscription : Option> } #[derive(Clone)] @@ -123,6 +127,7 @@ where output: Some(output), hooks: HashMap::new(), subscriptions: HashMap::new(), + wildcard_subscription: None, options: HashMap::new(), // Should not be configured by user. // This values are set when parsing the init-call @@ -173,12 +178,16 @@ where C: Fn(Plugin, Request) -> F + 'static, F: Future> + Send + 'static, { - self.subscriptions.insert( - topic.to_string(), - Subscription { - callback: Box::new(move |p, r| Box::pin(callback(p, r))), - }, - ); + let subscription = Subscription { + callback : Box::new(move |p, r| Box::pin(callback(p, r))) + }; + + if topic == "*" { + self.wildcard_subscription = Some(subscription); + } + else { + self.subscriptions.insert(topic.to_string(), subscription); + }; self } @@ -328,6 +337,7 @@ where let subscriptions = HashMap::from_iter(self.subscriptions.drain().map(|(k, v)| (k, v.callback))); + let all_subscription = self.wildcard_subscription.map(|s| s.callback); // Leave the `init` reply pending, so we can disable based on // the options if required. @@ -339,6 +349,7 @@ where rpcmethods, notifications: self.notifications, subscriptions, + wildcard_subscription: all_subscription, options: self.options, option_values: self.option_values, configuration, @@ -378,9 +389,13 @@ where }) .collect(); + let subscriptions = self.subscriptions.keys() + .map(|s| s.clone()) + .chain(self.wildcard_subscription.iter().map(|_| String::from("*"))).collect(); + messages::GetManifestResponse { options: self.options.values().cloned().collect(), - subscriptions: self.subscriptions.keys().map(|s| s.clone()).collect(), + subscriptions, hooks: self.hooks.keys().map(|s| s.clone()).collect(), rpcmethods, notifications: self.notifications.clone(), @@ -553,6 +568,7 @@ where rpcmethods: self.rpcmethods, hooks: self.hooks, subscriptions: self.subscriptions, + wildcard_subscription : self.wildcard_subscription }; output @@ -724,25 +740,41 @@ where Ok(()) } messages::JsonRpc::CustomNotification(request) => { + // This code handles notifications trace!("Dispatching custom notification {:?}", request); let method = request .get("method") .context("Missing 'method' in request")? .as_str() .context("'method' is not a string")?; - let callback = self.subscriptions.get(method).with_context(|| { - anyhow!("No handler for notification '{}' registered", method) - })?; + let params = request .get("params") - .context("Missing 'params' field in request")? - .clone(); - - let plugin = plugin.clone(); - let call = callback(plugin.clone(), params); - - tokio::spawn(async move { call.await.unwrap() }); - Ok(()) + .context("Missing 'params' field in request")?; + + // Send to notification to the wildcard + // subscription "*" it it exists + match &self.wildcard_subscription { + Some(cb) => { + let call = cb(plugin.clone(), params.clone()); + tokio::spawn(async move {call.await.unwrap()});} + None => {} + }; + + // Find the appropriate callback and process it + // We'll log a warning if no handler is defined + match self.subscriptions.get(method) { + Some(cb) => { + let call = cb(plugin.clone(), params.clone()); + tokio::spawn(async move {call.await.unwrap()}); + }, + None => { + if self.wildcard_subscription.is_none() { + log::warn!("No handler for notification '{}' registered", method); + } + } + }; + Ok(()) } } } diff --git a/tests/test_cln_rs.py b/tests/test_cln_rs.py index d87eafa93d39..fa4a8196248a 100644 --- a/tests/test_cln_rs.py +++ b/tests/test_cln_rs.py @@ -384,7 +384,6 @@ def test_grpc_decode(node_factory): print(res) -@pytest.mark.xfail(strict=True) def test_rust_plugin_subscribe_wildcard(node_factory): """ Creates a plugin that loads the subscribe_wildcard plugin """