mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-03-03 18:57:06 +01:00
cln-plugin: Rework the plugin library using a Builder
This commit is contained in:
parent
4aba119733
commit
22618a2f94
3 changed files with 201 additions and 139 deletions
|
@ -6,17 +6,13 @@ use tokio;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), anyhow::Error> {
|
async fn main() -> Result<(), anyhow::Error> {
|
||||||
let (plugin, stdin) = Builder::new((), tokio::io::stdin(), tokio::io::stdout())
|
let plugin = Builder::new((), tokio::io::stdin(), tokio::io::stdout())
|
||||||
.option(options::ConfigOption::new(
|
.option(options::ConfigOption::new(
|
||||||
"test-option",
|
"test-option",
|
||||||
options::Value::Integer(42),
|
options::Value::Integer(42),
|
||||||
"a test-option with default 42",
|
"a test-option with default 42",
|
||||||
))
|
))
|
||||||
.build();
|
.start()
|
||||||
|
.await?;
|
||||||
tokio::spawn(async {
|
plugin.join().await
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
|
|
||||||
log::info!("Hello world");
|
|
||||||
});
|
|
||||||
plugin.run(stdin).await
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
use crate::codec::{JsonCodec, JsonRpcCodec};
|
use crate::codec::{JsonCodec, JsonRpcCodec};
|
||||||
pub use anyhow::Error;
|
pub use anyhow::{anyhow, Context, Error};
|
||||||
use futures::sink::SinkExt;
|
use futures::sink::SinkExt;
|
||||||
extern crate log;
|
extern crate log;
|
||||||
use log::{trace, warn};
|
use log::trace;
|
||||||
use std::marker::PhantomData;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::io::{AsyncRead, AsyncWrite};
|
use tokio::io::{AsyncRead, AsyncWrite};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
@ -25,14 +24,13 @@ use options::ConfigOption;
|
||||||
/// Builder for a new plugin.
|
/// Builder for a new plugin.
|
||||||
pub struct Builder<S, I, O>
|
pub struct Builder<S, I, O>
|
||||||
where
|
where
|
||||||
S: Clone + Send,
|
|
||||||
I: AsyncRead + Unpin,
|
I: AsyncRead + Unpin,
|
||||||
O: Send + AsyncWrite + Unpin,
|
O: Send + AsyncWrite + Unpin,
|
||||||
{
|
{
|
||||||
state: S,
|
state: S,
|
||||||
|
|
||||||
input: I,
|
input: Option<I>,
|
||||||
output: O,
|
output: Option<O>,
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
hooks: Hooks,
|
hooks: Hooks,
|
||||||
|
@ -46,14 +44,14 @@ where
|
||||||
impl<S, I, O> Builder<S, I, O>
|
impl<S, I, O> Builder<S, I, O>
|
||||||
where
|
where
|
||||||
O: Send + AsyncWrite + Unpin + 'static,
|
O: Send + AsyncWrite + Unpin + 'static,
|
||||||
S: Clone + Send + 'static,
|
S: Clone + Sync + Send + Clone + 'static,
|
||||||
I: AsyncRead + Send + Unpin + 'static,
|
I: AsyncRead + Send + Unpin + 'static,
|
||||||
{
|
{
|
||||||
pub fn new(state: S, input: I, output: O) -> Self {
|
pub fn new(state: S, input: I, output: O) -> Self {
|
||||||
Self {
|
Self {
|
||||||
state,
|
state,
|
||||||
input,
|
input: Some(input),
|
||||||
output,
|
output: Some(output),
|
||||||
hooks: Hooks::default(),
|
hooks: Hooks::default(),
|
||||||
subscriptions: Subscriptions::default(),
|
subscriptions: Subscriptions::default(),
|
||||||
options: vec![],
|
options: vec![],
|
||||||
|
@ -65,147 +63,96 @@ where
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(self) -> (Plugin<S, I, O>, I) {
|
/// 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(
|
let output = Arc::new(Mutex::new(FramedWrite::new(
|
||||||
self.output,
|
self.output.take().unwrap(),
|
||||||
JsonCodec::default(),
|
JsonCodec::default(),
|
||||||
)));
|
)));
|
||||||
|
|
||||||
// Now configure the logging, so any `log` call is wrapped
|
// Now configure the logging, so any `log` call is wrapped
|
||||||
// in a JSON-RPC notification and sent to c-lightning
|
// in a JSON-RPC notification and sent to c-lightning
|
||||||
tokio::spawn(async move {});
|
crate::logging::init(output.clone()).await?;
|
||||||
(
|
|
||||||
Plugin {
|
|
||||||
state: Arc::new(Mutex::new(self.state)),
|
|
||||||
output,
|
|
||||||
input_type: PhantomData,
|
|
||||||
options: self.options,
|
|
||||||
},
|
|
||||||
self.input,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Plugin<S, I, O>
|
|
||||||
where
|
|
||||||
S: Clone + Send,
|
|
||||||
I: AsyncRead,
|
|
||||||
O: Send + AsyncWrite + 'static,
|
|
||||||
{
|
|
||||||
//input: FramedRead<Stdin, JsonCodec>,
|
|
||||||
output: Arc<Mutex<FramedWrite<O, JsonCodec>>>,
|
|
||||||
|
|
||||||
/// The state gets cloned for each request
|
|
||||||
state: Arc<Mutex<S>>,
|
|
||||||
input_type: PhantomData<I>,
|
|
||||||
options: Vec<ConfigOption>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S, I, O> Plugin<S, I, O>
|
|
||||||
where
|
|
||||||
S: Clone + Send,
|
|
||||||
I: AsyncRead + Send + Unpin,
|
|
||||||
O: Send + AsyncWrite + Unpin,
|
|
||||||
{
|
|
||||||
pub fn options(&self) -> Vec<ConfigOption> {
|
|
||||||
self.options.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S, I, O> Plugin<S, I, O>
|
|
||||||
where
|
|
||||||
S: Clone + Send,
|
|
||||||
I: AsyncRead + Send + Unpin,
|
|
||||||
O: Send + AsyncWrite + Unpin + 'static,
|
|
||||||
{
|
|
||||||
/// Read incoming requests from `c-lightning and dispatch their handling.
|
|
||||||
#[allow(unused_mut)]
|
|
||||||
pub async fn run(mut self, input: I) -> Result<(), Error> {
|
|
||||||
crate::logging::init(self.output.clone()).await?;
|
|
||||||
trace!("Plugin logging initialized");
|
trace!("Plugin logging initialized");
|
||||||
|
|
||||||
let mut input = FramedRead::new(input, JsonRpcCodec::default());
|
// Read the `getmanifest` message:
|
||||||
loop {
|
|
||||||
match input.next().await {
|
match input.next().await {
|
||||||
Some(Ok(msg)) => {
|
Some(Ok(messages::JsonRpc::Request(id, messages::Request::Getmanifest(m)))) => {
|
||||||
trace!("Received a message: {:?}", msg);
|
output
|
||||||
match msg {
|
.lock()
|
||||||
messages::JsonRpc::Request(id, p) => {
|
.await
|
||||||
self.dispatch_request(id, p).await?
|
.send(json!({
|
||||||
// Use a match to detect Ok / Error and return an error if we failed.
|
|
||||||
}
|
|
||||||
messages::JsonRpc::Notification(n) => self.dispatch_notification(n).await?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(Err(e)) => {
|
|
||||||
warn!("Error reading command: {}", e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
None => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn dispatch_request(
|
|
||||||
&mut self,
|
|
||||||
id: usize,
|
|
||||||
request: messages::Request,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
trace!("Dispatching request {:?}", request);
|
|
||||||
let state = self.state.clone();
|
|
||||||
let res: serde_json::Value = match request {
|
|
||||||
messages::Request::Getmanifest(c) => {
|
|
||||||
serde_json::to_value(self.handle_get_manifest(c, state).await?).unwrap()
|
|
||||||
}
|
|
||||||
messages::Request::Init(c) => {
|
|
||||||
serde_json::to_value(self.handle_init(c, state).await?).unwrap()
|
|
||||||
}
|
|
||||||
o => panic!("Request {:?} is currently unhandled", o),
|
|
||||||
};
|
|
||||||
trace!("Sending respone {:?}", res);
|
|
||||||
|
|
||||||
let mut out = self.output.lock().await;
|
|
||||||
out.send(json!({
|
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"result": res,
|
"result": self.handle_get_manifest(m),
|
||||||
"id": id,
|
"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
|
.await
|
||||||
.unwrap();
|
.send(json!({
|
||||||
Ok(())
|
"jsonrpc": "2.0",
|
||||||
|
"result": self.handle_init(m)?,
|
||||||
|
"id": id,
|
||||||
|
}))
|
||||||
|
.await?
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn dispatch_notification(
|
o => return Err(anyhow!("Got unexpected message {:?} from lightningd", o)),
|
||||||
&mut self,
|
};
|
||||||
notification: messages::Notification,
|
|
||||||
) -> Result<(), Error> {
|
let (tx, _) = tokio::sync::broadcast::channel(1);
|
||||||
trace!("Dispatching notification {:?}", notification);
|
let plugin = Plugin {
|
||||||
unimplemented!()
|
state: self.state,
|
||||||
|
options: self.options,
|
||||||
|
wait_handle: tx,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start the PluginDriver to handle plugin IO
|
||||||
|
tokio::spawn(
|
||||||
|
PluginDriver {
|
||||||
|
plugin: plugin.clone(),
|
||||||
|
}
|
||||||
|
.run(input, output),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(plugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_get_manifest(
|
fn handle_get_manifest(
|
||||||
&mut self,
|
&mut self,
|
||||||
_call: messages::GetManifestCall,
|
_call: messages::GetManifestCall,
|
||||||
_state: Arc<Mutex<S>>,
|
) -> messages::GetManifestResponse {
|
||||||
) -> Result<messages::GetManifestResponse, Error> {
|
messages::GetManifestResponse {
|
||||||
Ok(messages::GetManifestResponse {
|
|
||||||
options: self.options.clone(),
|
options: self.options.clone(),
|
||||||
rpcmethods: vec![],
|
rpcmethods: vec![],
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_init(
|
fn handle_init(&mut self, call: messages::InitCall) -> Result<messages::InitResponse, Error> {
|
||||||
&mut self,
|
|
||||||
call: messages::InitCall,
|
|
||||||
_state: Arc<Mutex<S>>,
|
|
||||||
) -> Result<messages::InitResponse, Error> {
|
|
||||||
use options::Value as OValue;
|
use options::Value as OValue;
|
||||||
use serde_json::Value as JValue;
|
use serde_json::Value as JValue;
|
||||||
|
|
||||||
// Match up the ConfigOptions and fill in their values if we
|
// Match up the ConfigOptions and fill in their values if we
|
||||||
// have a matching entry.
|
// have a matching entry.
|
||||||
|
|
||||||
for opt in self.options.iter_mut() {
|
for opt in self.options.iter_mut() {
|
||||||
if let Some(val) = call.options.get(opt.name()) {
|
if let Some(val) = call.options.get(opt.name()) {
|
||||||
opt.value = Some(match (opt.default(), &val) {
|
opt.value = Some(match (opt.default(), &val) {
|
||||||
|
@ -224,6 +171,125 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
{
|
||||||
|
#[allow(dead_code)]
|
||||||
|
plugin: Plugin<S>,
|
||||||
|
}
|
||||||
|
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
impl<S> PluginDriver<S>
|
||||||
|
where
|
||||||
|
S: Send + Clone,
|
||||||
|
{
|
||||||
|
/// Run the plugin until we get a shutdown command.
|
||||||
|
async fn run<I, O>(
|
||||||
|
self,
|
||||||
|
mut input: FramedRead<I, JsonRpcCodec>,
|
||||||
|
_output: Arc<Mutex<FramedWrite<O, JsonCodec>>>,
|
||||||
|
) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
I: Send + AsyncReadExt + Unpin,
|
||||||
|
O: Send,
|
||||||
|
{
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = PluginDriver::dispatch_one(&mut input, &self.plugin) => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispatch one server-side event and then return. Just so we
|
||||||
|
/// have a nicer looking `select` statement in `run` :-)
|
||||||
|
async fn dispatch_one<I>(
|
||||||
|
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) => {
|
||||||
|
PluginDriver::<S>::dispatch_notification(n, plugin).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Err(e)) => Err(anyhow!("Error reading command: {}", e)),
|
||||||
|
None => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn dispatch_request(
|
||||||
|
id: usize,
|
||||||
|
request: messages::Request,
|
||||||
|
_plugin: &Plugin<S>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
panic!("Unexpected request {:?} with id {}", request, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn dispatch_notification(
|
||||||
|
notification: messages::Notification,
|
||||||
|
_plugin: &Plugin<S>,
|
||||||
|
) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
S: Send + Clone,
|
||||||
|
{
|
||||||
|
trace!("Dispatching notification {:?}", notification);
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A container for all the configure hooks. It is just a collection
|
/// A container for all the configure hooks. It is just a collection
|
||||||
/// of callbacks that can be registered by the users of the
|
/// of callbacks that can be registered by the users of the
|
||||||
/// library. Based on this configuration we can then generate the
|
/// library. Based on this configuration we can then generate the
|
||||||
|
@ -239,9 +305,9 @@ struct Subscriptions {}
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn init() {
|
async fn init() {
|
||||||
let builder = Builder::new((), tokio::io::stdin(), tokio::io::stdout());
|
let builder = Builder::new((), tokio::io::stdin(), tokio::io::stdout());
|
||||||
builder.build();
|
let _ = builder.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,14 +104,14 @@ mod test {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
ConfigOption::new("name", Value::Boolean(true), "description"
|
ConfigOption::new("name", Value::Boolean(true), "description"),
|
||||||
json!({
|
json!({
|
||||||
"name": "name",
|
"name": "name",
|
||||||
"description":"description",
|
"description":"description",
|
||||||
"default": true,
|
"default": true,
|
||||||
"type": "booltes",
|
"type": "booltes",
|
||||||
}),
|
}),
|
||||||
)),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (input, expected) in tests.iter() {
|
for (input, expected) in tests.iter() {
|
||||||
|
|
Loading…
Add table
Reference in a new issue