mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-02-21 22:31:48 +01:00
cln-plugin: Add dynamic configs and a callback for changes
Changelog-Added: cln-plugin: Add dynamic configs and a callback for changes
This commit is contained in:
parent
dab9605e1f
commit
b69609b9c3
4 changed files with 182 additions and 46 deletions
|
@ -2,7 +2,9 @@
|
|||
//! plugins using the Rust API against Core Lightning.
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
use cln_plugin::options::{DefaultIntegerConfigOption, IntegerConfigOption};
|
||||
use cln_plugin::options::{
|
||||
self, BooleanConfigOption, DefaultIntegerConfigOption, IntegerConfigOption,
|
||||
};
|
||||
use cln_plugin::{messages, Builder, Error, Plugin};
|
||||
use tokio;
|
||||
|
||||
|
@ -21,9 +23,17 @@ const TEST_OPTION_NO_DEFAULT: IntegerConfigOption =
|
|||
async fn main() -> Result<(), anyhow::Error> {
|
||||
let state = ();
|
||||
|
||||
let test_dynamic_option: BooleanConfigOption = BooleanConfigOption::new_bool_no_default(
|
||||
"test-dynamic-option",
|
||||
"A option that can be changed dynamically",
|
||||
)
|
||||
.dynamic();
|
||||
|
||||
if let Some(plugin) = Builder::new(tokio::io::stdin(), tokio::io::stdout())
|
||||
.option(TEST_OPTION)
|
||||
.option(TEST_OPTION_NO_DEFAULT)
|
||||
.option(test_dynamic_option)
|
||||
.setconfig_callback(setconfig_callback)
|
||||
.rpcmethod("testmethod", "This is a test", testmethod)
|
||||
.rpcmethod(
|
||||
"testoptions",
|
||||
|
@ -48,10 +58,27 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||
}
|
||||
}
|
||||
|
||||
async fn setconfig_callback(
|
||||
plugin: Plugin<()>,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, Error> {
|
||||
let name = args.get("config").unwrap().as_str().unwrap();
|
||||
let value = args.get("val").unwrap();
|
||||
|
||||
let opt_value = options::Value::String(value.to_string());
|
||||
|
||||
plugin.set_option_str(name, opt_value)?;
|
||||
log::info!(
|
||||
"cln-plugin-startup: Got dynamic option change: {} {}",
|
||||
name,
|
||||
plugin.option_str(name).unwrap().unwrap().as_str().unwrap()
|
||||
);
|
||||
Ok(json!({}))
|
||||
}
|
||||
|
||||
async fn testoptions(p: Plugin<()>, _v: serde_json::Value) -> Result<serde_json::Value, Error> {
|
||||
let test_option = p.option(&TEST_OPTION)?;
|
||||
let test_option_no_default = p
|
||||
.option(&TEST_OPTION_NO_DEFAULT)?;
|
||||
let test_option_no_default = p.option(&TEST_OPTION_NO_DEFAULT)?;
|
||||
|
||||
Ok(json!({
|
||||
"test-option": test_option,
|
||||
|
|
|
@ -46,9 +46,10 @@ where
|
|||
options: HashMap<String, UntypedConfigOption>,
|
||||
option_values: HashMap<String, Option<options::Value>>,
|
||||
rpcmethods: HashMap<String, RpcMethod<S>>,
|
||||
setconfig_callback: Option<AsyncCallback<S>>,
|
||||
subscriptions: HashMap<String, Subscription<S>>,
|
||||
// Contains a Subscription if the user subscribed to "*"
|
||||
wildcard_subscription : Option<Subscription<S>>,
|
||||
wildcard_subscription: Option<Subscription<S>>,
|
||||
notifications: Vec<NotificationTopic>,
|
||||
custommessages: Vec<u16>,
|
||||
featurebits: FeatureBits,
|
||||
|
@ -72,9 +73,10 @@ where
|
|||
option_values: HashMap<String, Option<options::Value>>,
|
||||
configuration: Configuration,
|
||||
rpcmethods: HashMap<String, AsyncCallback<S>>,
|
||||
setconfig_callback: Option<AsyncCallback<S>>,
|
||||
hooks: HashMap<String, AsyncCallback<S>>,
|
||||
subscriptions: HashMap<String, AsyncNotificationCallback<S>>,
|
||||
wildcard_subscription : Option<AsyncNotificationCallback<S>>,
|
||||
wildcard_subscription: Option<AsyncNotificationCallback<S>>,
|
||||
#[allow(dead_code)] // unsure why rust thinks this field isn't used
|
||||
notifications: Vec<NotificationTopic>,
|
||||
}
|
||||
|
@ -90,11 +92,12 @@ where
|
|||
{
|
||||
plugin: Plugin<S>,
|
||||
rpcmethods: HashMap<String, AsyncCallback<S>>,
|
||||
setconfig_callback: Option<AsyncCallback<S>>,
|
||||
|
||||
#[allow(dead_code)] // Unused until we fill in the Hook structs.
|
||||
hooks: HashMap<String, AsyncCallback<S>>,
|
||||
subscriptions: HashMap<String, AsyncNotificationCallback<S>>,
|
||||
wildcard_subscription : Option<AsyncNotificationCallback<S>>
|
||||
wildcard_subscription: Option<AsyncNotificationCallback<S>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -106,7 +109,7 @@ where
|
|||
state: S,
|
||||
/// "options" field of "init" message sent by cln
|
||||
options: HashMap<String, UntypedConfigOption>,
|
||||
option_values: HashMap<String, Option<options::Value>>,
|
||||
option_values: Arc<std::sync::Mutex<HashMap<String, Option<options::Value>>>>,
|
||||
/// "configuration" field of "init" message sent by cln
|
||||
configuration: Configuration,
|
||||
/// A signal that allows us to wait on the plugin's shutdown.
|
||||
|
@ -133,6 +136,7 @@ where
|
|||
// This values are set when parsing the init-call
|
||||
option_values: HashMap::new(),
|
||||
rpcmethods: HashMap::new(),
|
||||
setconfig_callback: None,
|
||||
notifications: vec![],
|
||||
featurebits: FeatureBits::default(),
|
||||
dynamic: false,
|
||||
|
@ -179,13 +183,12 @@ where
|
|||
F: Future<Output = Result<(), Error>> + Send + 'static,
|
||||
{
|
||||
let subscription = Subscription {
|
||||
callback : Box::new(move |p, r| Box::pin(callback(p, r)))
|
||||
callback: Box::new(move |p, r| Box::pin(callback(p, r))),
|
||||
};
|
||||
|
||||
if topic == "*" {
|
||||
self.wildcard_subscription = Some(subscription);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
self.subscriptions.insert(topic.to_string(), subscription);
|
||||
};
|
||||
self
|
||||
|
@ -233,6 +236,17 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Register a callback for setconfig to accept changes for dynamic options
|
||||
pub fn setconfig_callback<C, F>(mut self, setconfig_callback: C) -> Builder<S, I, O>
|
||||
where
|
||||
C: Send + Sync + 'static,
|
||||
C: Fn(Plugin<S>, Request) -> F + 'static,
|
||||
F: Future<Output = Response> + Send + 'static,
|
||||
{
|
||||
self.setconfig_callback = Some(Box::new(move |p, r| Box::pin(setconfig_callback(p, r))));
|
||||
self
|
||||
}
|
||||
|
||||
/// Send true value for "dynamic" field in "getmanifest" response
|
||||
pub fn dynamic(mut self) -> Builder<S, I, O> {
|
||||
self.dynamic = true;
|
||||
|
@ -337,7 +351,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);
|
||||
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.
|
||||
|
@ -347,9 +361,10 @@ where
|
|||
input,
|
||||
output,
|
||||
rpcmethods,
|
||||
setconfig_callback: self.setconfig_callback,
|
||||
notifications: self.notifications,
|
||||
subscriptions,
|
||||
wildcard_subscription: all_subscription,
|
||||
wildcard_subscription: all_subscription,
|
||||
options: self.options,
|
||||
option_values: self.option_values,
|
||||
configuration,
|
||||
|
@ -389,9 +404,12 @@ where
|
|||
})
|
||||
.collect();
|
||||
|
||||
let subscriptions = self.subscriptions.keys()
|
||||
let subscriptions = self
|
||||
.subscriptions
|
||||
.keys()
|
||||
.map(|s| s.clone())
|
||||
.chain(self.wildcard_subscription.iter().map(|_| String::from("*"))).collect();
|
||||
.chain(self.wildcard_subscription.iter().map(|_| String::from("*")))
|
||||
.collect();
|
||||
|
||||
messages::GetManifestResponse {
|
||||
options: self.options.values().cloned().collect(),
|
||||
|
@ -524,9 +542,11 @@ where
|
|||
{
|
||||
pub fn option_str(&self, name: &str) -> Result<Option<options::Value>> {
|
||||
self.option_values
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(name)
|
||||
.ok_or(anyhow!("No option named {}", name))
|
||||
.map(|c| c.clone())
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn option<'a, OV: OptionType<'a>>(
|
||||
|
@ -536,6 +556,25 @@ where
|
|||
let value = self.option_str(config_option.name())?;
|
||||
Ok(OV::from_value(&value))
|
||||
}
|
||||
|
||||
pub fn set_option_str(&self, name: &str, value: options::Value) -> Result<()> {
|
||||
*self
|
||||
.option_values
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_mut(name)
|
||||
.ok_or(anyhow!("No option named {}", name))? = Some(value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_option<'a, OV: OptionType<'a>>(
|
||||
&self,
|
||||
config_option: &options::ConfigOption<'a, OV>,
|
||||
value: options::Value,
|
||||
) -> Result<()> {
|
||||
self.set_option_str(config_option.name(), value)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, I, O> ConfiguredPlugin<S, I, O>
|
||||
|
@ -557,7 +596,7 @@ where
|
|||
let plugin = Plugin {
|
||||
state,
|
||||
options: self.options,
|
||||
option_values: self.option_values,
|
||||
option_values: Arc::new(std::sync::Mutex::new(self.option_values)),
|
||||
configuration: self.configuration,
|
||||
wait_handle,
|
||||
sender,
|
||||
|
@ -566,9 +605,10 @@ where
|
|||
let driver = PluginDriver {
|
||||
plugin: plugin.clone(),
|
||||
rpcmethods: self.rpcmethods,
|
||||
setconfig_callback: self.setconfig_callback,
|
||||
hooks: self.hooks,
|
||||
subscriptions: self.subscriptions,
|
||||
wildcard_subscription : self.wildcard_subscription
|
||||
wildcard_subscription: self.wildcard_subscription,
|
||||
};
|
||||
|
||||
output
|
||||
|
@ -704,9 +744,16 @@ where
|
|||
.context("Missing 'method' in request")?
|
||||
.as_str()
|
||||
.context("'method' is not a string")?;
|
||||
let callback = self.rpcmethods.get(method).with_context(|| {
|
||||
anyhow!("No handler for method '{}' registered", method)
|
||||
})?;
|
||||
let callback = match method {
|
||||
name if name.eq("setconfig") => {
|
||||
self.setconfig_callback.as_ref().ok_or_else(|| {
|
||||
anyhow!("No handler for method '{}' registered", method)
|
||||
})?
|
||||
}
|
||||
_ => self.rpcmethods.get(method).with_context(|| {
|
||||
anyhow!("No handler for method '{}' registered", method)
|
||||
})?,
|
||||
};
|
||||
let params = request
|
||||
.get("params")
|
||||
.context("Missing 'params' field in request")?
|
||||
|
@ -740,7 +787,7 @@ where
|
|||
Ok(())
|
||||
}
|
||||
messages::JsonRpc::CustomNotification(request) => {
|
||||
// This code handles notifications
|
||||
// This code handles notifications
|
||||
trace!("Dispatching custom notification {:?}", request);
|
||||
let method = request
|
||||
.get("method")
|
||||
|
@ -751,30 +798,34 @@ where
|
|||
let params = request
|
||||
.get("params")
|
||||
.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(())
|
||||
// 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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,6 +76,7 @@
|
|||
//! default : (), // We provide no default here
|
||||
//! description : "A config option of type string that takes no default",
|
||||
//! deprecated : false, // Option is not deprecated
|
||||
//! dynamic: false, //Option is not dynamic
|
||||
//! };
|
||||
//! ```
|
||||
//!
|
||||
|
@ -420,6 +421,7 @@ pub struct ConfigOption<'a, V: OptionType<'a>> {
|
|||
pub default: V::DefaultValue,
|
||||
pub description: &'a str,
|
||||
pub deprecated: bool,
|
||||
pub dynamic: bool,
|
||||
}
|
||||
|
||||
impl<'a, V: OptionType<'a>> ConfigOption<'a, V> {
|
||||
|
@ -430,6 +432,7 @@ impl<'a, V: OptionType<'a>> ConfigOption<'a, V> {
|
|||
default: <V as OptionType>::convert_default(&self.default),
|
||||
description: self.description.to_string(),
|
||||
deprecated: self.deprecated,
|
||||
dynamic: self.dynamic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -445,8 +448,13 @@ impl<'a> DefaultStringConfigOption<'a> {
|
|||
default: default,
|
||||
description: description,
|
||||
deprecated: false,
|
||||
dynamic: false,
|
||||
}
|
||||
}
|
||||
pub fn dynamic(mut self) -> Self {
|
||||
self.dynamic = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> StringConfigOption<'a> {
|
||||
|
@ -456,8 +464,13 @@ impl<'a> StringConfigOption<'a> {
|
|||
default: (),
|
||||
description: description,
|
||||
deprecated: false,
|
||||
dynamic: false,
|
||||
}
|
||||
}
|
||||
pub fn dynamic(mut self) -> Self {
|
||||
self.dynamic = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DefaultIntegerConfigOption<'a> {
|
||||
|
@ -467,8 +480,13 @@ impl<'a> DefaultIntegerConfigOption<'a> {
|
|||
default: default,
|
||||
description: description,
|
||||
deprecated: false,
|
||||
dynamic: false,
|
||||
}
|
||||
}
|
||||
pub fn dynamic(mut self) -> Self {
|
||||
self.dynamic = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntegerConfigOption<'a> {
|
||||
|
@ -478,8 +496,13 @@ impl<'a> IntegerConfigOption<'a> {
|
|||
default: (),
|
||||
description: description,
|
||||
deprecated: false,
|
||||
dynamic: false,
|
||||
}
|
||||
}
|
||||
pub fn dynamic(mut self) -> Self {
|
||||
self.dynamic = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> BooleanConfigOption<'a> {
|
||||
|
@ -489,8 +512,13 @@ impl<'a> BooleanConfigOption<'a> {
|
|||
description,
|
||||
default: (),
|
||||
deprecated: false,
|
||||
dynamic: false,
|
||||
}
|
||||
}
|
||||
pub fn dynamic(mut self) -> Self {
|
||||
self.dynamic = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DefaultBooleanConfigOption<'a> {
|
||||
|
@ -500,8 +528,13 @@ impl<'a> DefaultBooleanConfigOption<'a> {
|
|||
description,
|
||||
default: default,
|
||||
deprecated: false,
|
||||
dynamic: false,
|
||||
}
|
||||
}
|
||||
pub fn dynamic(mut self) -> Self {
|
||||
self.dynamic = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FlagConfigOption<'a> {
|
||||
|
@ -511,8 +544,13 @@ impl<'a> FlagConfigOption<'a> {
|
|||
description,
|
||||
default: (),
|
||||
deprecated: false,
|
||||
dynamic: false,
|
||||
}
|
||||
}
|
||||
pub fn dynamic(mut self) -> Self {
|
||||
self.dynamic = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn is_false(b: &bool) -> bool {
|
||||
|
@ -530,6 +568,7 @@ pub struct UntypedConfigOption {
|
|||
description: String,
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
deprecated: bool,
|
||||
dynamic: bool,
|
||||
}
|
||||
|
||||
impl UntypedConfigOption {
|
||||
|
@ -539,6 +578,10 @@ impl UntypedConfigOption {
|
|||
pub fn default(&self) -> &Option<Value> {
|
||||
&self.default
|
||||
}
|
||||
pub fn dynamic(mut self) -> Self {
|
||||
self.dynamic = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, V> ConfigOption<'a, V>
|
||||
|
@ -569,6 +612,7 @@ mod test {
|
|||
"description":"description",
|
||||
"default": "default",
|
||||
"type": "string",
|
||||
"dynamic": false,
|
||||
}),
|
||||
),
|
||||
(
|
||||
|
@ -578,15 +622,21 @@ mod test {
|
|||
"description":"description",
|
||||
"default": 42,
|
||||
"type": "int",
|
||||
"dynamic": false,
|
||||
}),
|
||||
),
|
||||
(
|
||||
ConfigOption::new_bool_with_default("name", true, "description").build(),
|
||||
{
|
||||
ConfigOption::new_bool_with_default("name", true, "description")
|
||||
.build()
|
||||
.dynamic()
|
||||
},
|
||||
json!({
|
||||
"name": "name",
|
||||
"description":"description",
|
||||
"default": true,
|
||||
"type": "bool",
|
||||
"dynamic": true,
|
||||
}),
|
||||
),
|
||||
(
|
||||
|
@ -595,7 +645,8 @@ mod test {
|
|||
"name" : "name",
|
||||
"description": "description",
|
||||
"type" : "flag",
|
||||
"default" : false
|
||||
"default" : false,
|
||||
"dynamic": false,
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
|
|
@ -66,6 +66,13 @@ def test_plugin_start(node_factory):
|
|||
l1.daemon.wait_for_log(r'Got a connect hook call')
|
||||
l1.daemon.wait_for_log(r'Got a connect notification')
|
||||
|
||||
l1.rpc.setconfig("test-dynamic-option", True)
|
||||
assert l1.rpc.listconfigs("test-dynamic-option")["configs"]["test-dynamic-option"]["value_bool"]
|
||||
wait_for(lambda: l1.daemon.is_in_log(r'cln-plugin-startup: Got dynamic option change: test-dynamic-option \\"true\\"'))
|
||||
l1.rpc.setconfig("test-dynamic-option", False)
|
||||
assert not l1.rpc.listconfigs("test-dynamic-option")["configs"]["test-dynamic-option"]["value_bool"]
|
||||
wait_for(lambda: l1.daemon.is_in_log(r'cln-plugin-startup: Got dynamic option change: test-dynamic-option \\"false\\"'))
|
||||
|
||||
|
||||
def test_plugin_options_handle_defaults(node_factory):
|
||||
"""Start a minimal plugin and ensure it is well-behaved
|
||||
|
|
Loading…
Add table
Reference in a new issue