mirror of
https://github.com/aljazceru/lightning.git
synced 2025-12-22 00:24:19 +01:00
553 lines
18 KiB
Rust
553 lines
18 KiB
Rust
use crate::codec::{JsonCodec, JsonRpcCodec};
|
|
pub use anyhow::{anyhow, Context};
|
|
use futures::sink::SinkExt;
|
|
extern crate log;
|
|
use log::trace;
|
|
use std::collections::HashMap;
|
|
use std::future::Future;
|
|
use std::pin::Pin;
|
|
use std::sync::Arc;
|
|
use tokio::io::{AsyncRead, AsyncWrite};
|
|
use tokio::sync::Mutex;
|
|
use tokio_stream::StreamExt;
|
|
use tokio_util::codec::FramedRead;
|
|
use tokio_util::codec::FramedWrite;
|
|
|
|
pub mod codec;
|
|
pub mod logging;
|
|
mod messages;
|
|
|
|
#[macro_use]
|
|
extern crate serde_json;
|
|
|
|
pub mod options;
|
|
|
|
use options::ConfigOption;
|
|
|
|
/// Need to tell us about something that went wrong? Use this error
|
|
/// type to do that. Use this alias to be safe from future changes in
|
|
/// our internal error handling, since we'll implement any necessary
|
|
/// conversions for you :-)
|
|
pub type Error = anyhow::Error;
|
|
|
|
/// Builder for a new plugin.
|
|
pub struct Builder<S, I, O>
|
|
where
|
|
I: AsyncRead + Unpin,
|
|
O: Send + AsyncWrite + Unpin,
|
|
S: Clone + Send,
|
|
{
|
|
state: S,
|
|
|
|
input: Option<I>,
|
|
output: Option<O>,
|
|
|
|
hooks: HashMap<String, Hook<S>>,
|
|
options: Vec<ConfigOption>,
|
|
rpcmethods: HashMap<String, RpcMethod<S>>,
|
|
subscriptions: HashMap<String, Subscription<S>>,
|
|
}
|
|
|
|
impl<S, I, O> Builder<S, I, O>
|
|
where
|
|
O: Send + AsyncWrite + Unpin + 'static,
|
|
S: Clone + Sync + Send + Clone + 'static,
|
|
I: AsyncRead + Send + Unpin + 'static,
|
|
{
|
|
pub fn new(state: S, input: I, output: O) -> Self {
|
|
Self {
|
|
state,
|
|
input: Some(input),
|
|
output: Some(output),
|
|
hooks: HashMap::new(),
|
|
subscriptions: HashMap::new(),
|
|
options: vec![],
|
|
rpcmethods: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
pub fn option(mut self, opt: options::ConfigOption) -> Builder<S, I, O> {
|
|
self.options.push(opt);
|
|
self
|
|
}
|
|
|
|
/// Subscribe to notifications for the given `topic`. The handler
|
|
/// is an async function that takes a `Plugin<S>` and the
|
|
/// notification as a `serde_json::Value` as inputs. Since
|
|
/// notifications do not expect a result the handler should only
|
|
/// report errors while processing. Any error reported while
|
|
/// processing the notification will be logged in the cln logs.
|
|
///
|
|
/// ```
|
|
/// use cln_plugin::{options, Builder, Error, Plugin};
|
|
///
|
|
/// async fn connect_handler(_p: Plugin<()>, v: serde_json::Value) -> Result<(), Error> {
|
|
/// println!("Got a connect notification: {}", v);
|
|
/// Ok(())
|
|
/// }
|
|
///
|
|
/// let b = Builder::new((), tokio::io::stdin(), tokio::io::stdout())
|
|
/// .subscribe("connect", connect_handler);
|
|
/// ```
|
|
pub fn subscribe<C, F>(mut self, topic: &str, callback: C) -> Builder<S, I, O>
|
|
where
|
|
C: Send + Sync + 'static,
|
|
C: Fn(Plugin<S>, Request) -> F + 'static,
|
|
F: Future<Output = Result<(), Error>> + Send + Sync + 'static,
|
|
{
|
|
self.subscriptions.insert(
|
|
topic.to_string(),
|
|
Subscription {
|
|
callback: Box::new(move |p, r| Box::pin(callback(p, r))),
|
|
},
|
|
);
|
|
self
|
|
}
|
|
|
|
/// Add a subscription to a given `hookname`
|
|
pub fn hook<C, F>(mut self, hookname: &str, callback: C) -> Self
|
|
where
|
|
C: Send + Sync + 'static,
|
|
C: Fn(Plugin<S>, Request) -> F + 'static,
|
|
F: Future<Output = Response> + Send + Sync + 'static,
|
|
{
|
|
self.hooks.insert(
|
|
hookname.to_string(),
|
|
Hook {
|
|
callback: Box::new(move |p, r| Box::pin(callback(p, r))),
|
|
},
|
|
);
|
|
self
|
|
}
|
|
|
|
/// Register a custom RPC method for the RPC passthrough from the
|
|
/// main daemon
|
|
pub fn rpcmethod<C, F>(mut self, name: &str, description: &str, callback: C) -> Builder<S, I, O>
|
|
where
|
|
C: Send + Sync + 'static,
|
|
C: Fn(Plugin<S>, Request) -> F + 'static,
|
|
F: Future<Output = Response> + Send + Sync + 'static,
|
|
{
|
|
self.rpcmethods.insert(
|
|
name.to_string(),
|
|
RpcMethod {
|
|
name: name.to_string(),
|
|
description: description.to_string(),
|
|
callback: Box::new(move |p, r| Box::pin(callback(p, r))),
|
|
},
|
|
);
|
|
self
|
|
}
|
|
|
|
/// Build and start the plugin loop. This performs the handshake
|
|
/// and spawns a new task that accepts incoming messages from
|
|
/// c-lightning and dispatches them to the handlers. It only
|
|
/// returns after completing the handshake to ensure that the
|
|
/// configuration and initialization was successfull.
|
|
pub async fn start(mut self) -> Result<Plugin<S>, anyhow::Error> {
|
|
let mut input = FramedRead::new(self.input.take().unwrap(), JsonRpcCodec::default());
|
|
|
|
// Sadly we need to wrap the output in a mutex in order to
|
|
// enable early logging, i.e., logging that is done before the
|
|
// PluginDriver is processing events during the
|
|
// handshake. Otherwise we could just write the log events to
|
|
// the event queue and have the PluginDriver be the sole owner
|
|
// of `Stdout`.
|
|
let output = Arc::new(Mutex::new(FramedWrite::new(
|
|
self.output.take().unwrap(),
|
|
JsonCodec::default(),
|
|
)));
|
|
|
|
// Now configure the logging, so any `log` call is wrapped
|
|
// in a JSON-RPC notification and sent to c-lightning
|
|
crate::logging::init(output.clone()).await?;
|
|
trace!("Plugin logging initialized");
|
|
|
|
// Read the `getmanifest` message:
|
|
match input.next().await {
|
|
Some(Ok(messages::JsonRpc::Request(id, messages::Request::Getmanifest(m)))) => {
|
|
output
|
|
.lock()
|
|
.await
|
|
.send(json!({
|
|
"jsonrpc": "2.0",
|
|
"result": self.handle_get_manifest(m),
|
|
"id": id,
|
|
}))
|
|
.await?
|
|
}
|
|
o => return Err(anyhow!("Got unexpected message {:?} from lightningd", o)),
|
|
};
|
|
|
|
match input.next().await {
|
|
Some(Ok(messages::JsonRpc::Request(id, messages::Request::Init(m)))) => {
|
|
output
|
|
.lock()
|
|
.await
|
|
.send(json!({
|
|
"jsonrpc": "2.0",
|
|
"result": self.handle_init(m)?,
|
|
"id": id,
|
|
}))
|
|
.await?
|
|
}
|
|
|
|
o => return Err(anyhow!("Got unexpected message {:?} from lightningd", o)),
|
|
};
|
|
|
|
let (wait_handle, _) = tokio::sync::broadcast::channel(1);
|
|
|
|
// An MPSC pair used by anything that needs to send messages
|
|
// to the main daemon.
|
|
let (sender, receiver) = tokio::sync::mpsc::channel(4);
|
|
let plugin = Plugin {
|
|
state: self.state,
|
|
options: self.options,
|
|
wait_handle,
|
|
sender,
|
|
};
|
|
|
|
// TODO Split the two hashmaps once we fill in the hook
|
|
// payload structs in messages.rs
|
|
let mut rpcmethods: HashMap<String, AsyncCallback<S>> =
|
|
HashMap::from_iter(self.rpcmethods.drain().map(|(k, v)| (k, v.callback)));
|
|
rpcmethods.extend(self.hooks.drain().map(|(k, v)| (k, v.callback)));
|
|
|
|
// Start the PluginDriver to handle plugin IO
|
|
tokio::spawn(
|
|
PluginDriver {
|
|
plugin: plugin.clone(),
|
|
rpcmethods,
|
|
hooks: HashMap::new(),
|
|
subscriptions: HashMap::from_iter(
|
|
self.subscriptions.drain().map(|(k, v)| (k, v.callback)),
|
|
),
|
|
}
|
|
.run(receiver, input, output),
|
|
// TODO Use the broadcast to distribute any error that we
|
|
// might receive here to anyone listening. (Shutdown
|
|
// signal)
|
|
);
|
|
|
|
Ok(plugin)
|
|
}
|
|
|
|
fn handle_get_manifest(
|
|
&mut self,
|
|
_call: messages::GetManifestCall,
|
|
) -> messages::GetManifestResponse {
|
|
let rpcmethods: Vec<_> = self
|
|
.rpcmethods
|
|
.values()
|
|
.map(|v| messages::RpcMethod {
|
|
name: v.name.clone(),
|
|
description: v.description.clone(),
|
|
usage: String::new(),
|
|
})
|
|
.collect();
|
|
|
|
messages::GetManifestResponse {
|
|
options: self.options.clone(),
|
|
subscriptions: self.subscriptions.keys().map(|s| s.clone()).collect(),
|
|
hooks: self.hooks.keys().map(|s| s.clone()).collect(),
|
|
rpcmethods,
|
|
}
|
|
}
|
|
|
|
fn handle_init(&mut self, call: messages::InitCall) -> Result<messages::InitResponse, Error> {
|
|
use options::Value as OValue;
|
|
use serde_json::Value as JValue;
|
|
|
|
// Match up the ConfigOptions and fill in their values if we
|
|
// have a matching entry.
|
|
for opt in self.options.iter_mut() {
|
|
if let Some(val) = call.options.get(opt.name()) {
|
|
opt.value = Some(match (opt.default(), &val) {
|
|
(OValue::String(_), JValue::String(s)) => OValue::String(s.clone()),
|
|
(OValue::Integer(_), JValue::Number(n)) => OValue::Integer(n.as_i64().unwrap()),
|
|
(OValue::Boolean(_), JValue::Bool(n)) => OValue::Boolean(*n),
|
|
|
|
// It's ok to panic, if we get here c-lightning
|
|
// has not enforced the option type.
|
|
(_, _) => panic!("Mismatching types in options: {:?} != {:?}", opt, val),
|
|
});
|
|
}
|
|
}
|
|
|
|
Ok(messages::InitResponse::default())
|
|
}
|
|
}
|
|
|
|
// Just some type aliases so we don't get confused in a lisp-like sea
|
|
// of parentheses.
|
|
type Request = serde_json::Value;
|
|
type Response = Result<serde_json::Value, Error>;
|
|
type AsyncCallback<S> =
|
|
Box<dyn Fn(Plugin<S>, Request) -> Pin<Box<dyn Future<Output = Response> + Send>> + Send + Sync>;
|
|
type AsyncNotificationCallback<S> = Box<
|
|
dyn Fn(Plugin<S>, Request) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send>>
|
|
+ Send
|
|
+ Sync,
|
|
>;
|
|
|
|
/// A struct collecting the metadata required to register a custom
|
|
/// rpcmethod with the main daemon upon init. It'll get deconstructed
|
|
/// into just the callback after the init.
|
|
struct RpcMethod<S>
|
|
where
|
|
S: Clone + Send,
|
|
{
|
|
callback: AsyncCallback<S>,
|
|
description: String,
|
|
name: String,
|
|
}
|
|
|
|
struct Subscription<S>
|
|
where
|
|
S: Clone + Send,
|
|
{
|
|
callback: AsyncNotificationCallback<S>,
|
|
}
|
|
|
|
struct Hook<S>
|
|
where
|
|
S: Clone + Send,
|
|
{
|
|
callback: AsyncCallback<S>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct Plugin<S>
|
|
where
|
|
S: Clone + Send,
|
|
{
|
|
/// The state gets cloned for each request
|
|
state: S,
|
|
options: Vec<ConfigOption>,
|
|
|
|
/// A signal that allows us to wait on the plugin's shutdown.
|
|
wait_handle: tokio::sync::broadcast::Sender<()>,
|
|
|
|
sender: tokio::sync::mpsc::Sender<serde_json::Value>,
|
|
}
|
|
|
|
/// The [PluginDriver] is used to run the IO loop, reading messages
|
|
/// from the Lightning daemon, dispatching calls and notifications to
|
|
/// the plugin, and returning responses to the the daemon. We also use
|
|
/// it to handle spontaneous messages like Notifications and logging
|
|
/// events.
|
|
struct PluginDriver<S>
|
|
where
|
|
S: Send + Clone,
|
|
{
|
|
plugin: Plugin<S>,
|
|
rpcmethods: HashMap<String, AsyncCallback<S>>,
|
|
|
|
#[allow(dead_code)] // Unused until we fill in the Hook structs.
|
|
hooks: HashMap<String, AsyncCallback<S>>,
|
|
subscriptions: HashMap<String, AsyncNotificationCallback<S>>,
|
|
}
|
|
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
impl<S> PluginDriver<S>
|
|
where
|
|
S: Send + Clone,
|
|
{
|
|
/// Run the plugin until we get a shutdown command.
|
|
async fn run<I, O>(
|
|
self,
|
|
mut receiver: tokio::sync::mpsc::Receiver<serde_json::Value>,
|
|
mut input: FramedRead<I, JsonRpcCodec>,
|
|
output: Arc<Mutex<FramedWrite<O, JsonCodec>>>,
|
|
) -> Result<(), Error>
|
|
where
|
|
I: Send + AsyncReadExt + Unpin,
|
|
O: Send + AsyncWriteExt + Unpin,
|
|
{
|
|
loop {
|
|
// If we encounter any error reading or writing from/to
|
|
// the master we hand them up, so we can return control to
|
|
// the user-code, which may require some cleanups or
|
|
// similar.
|
|
tokio::select! {
|
|
e = self.dispatch_one(&mut input, &self.plugin) => {
|
|
//Hand any error up.
|
|
e?;
|
|
},
|
|
v = receiver.recv() => {
|
|
output.lock().await.send(
|
|
v.context("internal communication error")?
|
|
).await?;
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Dispatch one server-side event and then return. Just so we
|
|
/// have a nicer looking `select` statement in `run` :-)
|
|
async fn dispatch_one<I>(
|
|
&self,
|
|
input: &mut FramedRead<I, JsonRpcCodec>,
|
|
plugin: &Plugin<S>,
|
|
) -> Result<(), Error>
|
|
where
|
|
I: Send + AsyncReadExt + Unpin,
|
|
{
|
|
match input.next().await {
|
|
Some(Ok(msg)) => {
|
|
trace!("Received a message: {:?}", msg);
|
|
match msg {
|
|
messages::JsonRpc::Request(id, p) => {
|
|
PluginDriver::<S>::dispatch_request(id, p, plugin).await
|
|
}
|
|
messages::JsonRpc::Notification(n) => {
|
|
self.dispatch_notification(n, plugin).await
|
|
}
|
|
messages::JsonRpc::CustomRequest(id, p) => {
|
|
match self.dispatch_custom_request(id, p, plugin).await {
|
|
Ok(v) => plugin
|
|
.sender
|
|
.send(json!({
|
|
"jsonrpc": "2.0",
|
|
"id": id,
|
|
"result": v
|
|
}))
|
|
.await
|
|
.context("returning custom result"),
|
|
Err(e) => plugin
|
|
.sender
|
|
.send(json!({
|
|
"jsonrpc": "2.0",
|
|
"id": id,
|
|
"error": e.to_string(),
|
|
}))
|
|
.await
|
|
.context("returning custom error"),
|
|
}
|
|
}
|
|
messages::JsonRpc::CustomNotification(n) => {
|
|
self.dispatch_custom_notification(n, plugin).await
|
|
}
|
|
}
|
|
}
|
|
Some(Err(e)) => Err(anyhow!("Error reading command: {}", e)),
|
|
None => Err(anyhow!("Error reading from master")),
|
|
}
|
|
}
|
|
|
|
async fn dispatch_request(
|
|
_id: usize,
|
|
_request: messages::Request,
|
|
_plugin: &Plugin<S>,
|
|
) -> Result<(), Error> {
|
|
todo!("This is unreachable until we start filling in messages:Request. Until then the custom dispatcher below is used exclusively.")
|
|
}
|
|
|
|
async fn dispatch_notification(
|
|
&self,
|
|
_notification: messages::Notification,
|
|
_plugin: &Plugin<S>,
|
|
) -> Result<(), Error>
|
|
where
|
|
S: Send + Clone,
|
|
{
|
|
todo!("As soon as we define the full structure of the messages::Notification we'll get here. Until then the custom dispatcher below is used.")
|
|
}
|
|
|
|
async fn dispatch_custom_request(
|
|
&self,
|
|
_id: usize,
|
|
request: serde_json::Value,
|
|
plugin: &Plugin<S>,
|
|
) -> Result<serde_json::Value, Error> {
|
|
let method = request
|
|
.get("method")
|
|
.context("Missing 'method' in request")?
|
|
.as_str()
|
|
.context("'method' is not a string")?;
|
|
|
|
let params = request
|
|
.get("params")
|
|
.context("Missing 'params' field in request")?;
|
|
let callback = self
|
|
.rpcmethods
|
|
.get(method)
|
|
.with_context(|| anyhow!("No handler for method '{}' registered", method))?;
|
|
|
|
trace!(
|
|
"Dispatching custom request: method={}, params={}",
|
|
method,
|
|
params
|
|
);
|
|
callback(plugin.clone(), params.clone()).await
|
|
}
|
|
|
|
async fn dispatch_custom_notification(
|
|
&self,
|
|
notification: serde_json::Value,
|
|
plugin: &Plugin<S>,
|
|
) -> Result<(), Error>
|
|
where
|
|
S: Send + Clone,
|
|
{
|
|
trace!("Dispatching custom notification {:?}", notification);
|
|
let method = notification
|
|
.get("method")
|
|
.context("Missing 'method' in notification")?
|
|
.as_str()
|
|
.context("'method' is not a string")?;
|
|
let params = notification
|
|
.get("params")
|
|
.context("Missing 'params' field in notification")?;
|
|
let callback = self
|
|
.subscriptions
|
|
.get(method)
|
|
.with_context(|| anyhow!("No handler for method '{}' registered", method))?;
|
|
trace!(
|
|
"Dispatching custom request: method={}, params={}",
|
|
method,
|
|
params
|
|
);
|
|
if let Err(e) = callback(plugin.clone(), params.clone()).await {
|
|
log::error!("Error in notification handler '{}': {}", method, e);
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl<S> Plugin<S>
|
|
where
|
|
S: Clone + Send,
|
|
{
|
|
pub fn options(&self) -> Vec<ConfigOption> {
|
|
self.options.clone()
|
|
}
|
|
pub fn state(&self) -> &S {
|
|
&self.state
|
|
}
|
|
}
|
|
|
|
impl<S> Plugin<S>
|
|
where
|
|
S: Send + Clone,
|
|
{
|
|
pub async fn join(&self) -> Result<(), Error> {
|
|
self.wait_handle
|
|
.subscribe()
|
|
.recv()
|
|
.await
|
|
.context("error waiting for shutdown")
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn init() {
|
|
let builder = Builder::new((), tokio::io::stdin(), tokio::io::stdout());
|
|
let _ = builder.start();
|
|
}
|
|
}
|