diff --git a/README.md b/README.md index f08e517e72..d2073c831a 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,17 @@ If you want to persist the data directory, you can make the volume to your host docker run -ti --rm -v "/path_on_host:/data" -e "JAVA_OPTS=-Declair.printToConsole" acinq/eclair ``` +## Plugins + +For advanced usage, Eclair supports plugins written in Scala, Java, or any JVM-compatible language. + +A valid plugin is a jar that contains an implementation of the [Plugin](eclair-node/src/main/scala/fr/acinq/eclair/Plugin.scala) interface. + +Here is how to run Eclair with plugins: +```shell +java -jar eclair-node--.jar <...> +``` + ## Mainnet usage Following are the minimum configuration files you need to use for Bitcoin Core and Eclair. diff --git a/eclair-node/pom.xml b/eclair-node/pom.xml index 1cdd3cbbff..7c9c2ffdc1 100644 --- a/eclair-node/pom.xml +++ b/eclair-node/pom.xml @@ -74,6 +74,12 @@ eclair-core_${scala.version.short} ${project.version} + + + org.clapper + classutil_${scala.version.short} + 1.4.0 + ch.qos.logback logback-classic diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala b/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala index 26e0dcf0de..5bbf72dff8 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala @@ -22,6 +22,7 @@ import akka.actor.ActorSystem import grizzled.slf4j.Logging import scala.concurrent.ExecutionContext +import scala.util.{Failure, Success} /** * Created by PM on 25/01/2016. @@ -31,10 +32,15 @@ object Boot extends App with Logging { val datadir = new File(System.getProperty("eclair.datadir", System.getProperty("user.home") + "/.eclair")) try { + val plugins = Plugin.loadPlugins(args.map(new File(_))) + plugins.foreach(plugin => logger.info(s"loaded plugin ${plugin.getClass.getSimpleName}")) implicit val system: ActorSystem = ActorSystem("eclair-node") implicit val ec: ExecutionContext = system.dispatcher - new Setup(datadir).bootstrap onFailure { - case t: Throwable => onError(t) + val setup = new Setup(datadir) + plugins.foreach(_.onSetup(setup)) + setup.bootstrap onComplete { + case Success(kit) => plugins.foreach(_.onKit(kit)) + case Failure(t) => onError(t) } } catch { case t: Throwable => onError(t) @@ -47,4 +53,3 @@ object Boot extends App with Logging { System.exit(1) } } - diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/Plugin.scala b/eclair-node/src/main/scala/fr/acinq/eclair/Plugin.scala new file mode 100644 index 0000000000..491b1623f5 --- /dev/null +++ b/eclair-node/src/main/scala/fr/acinq/eclair/Plugin.scala @@ -0,0 +1,29 @@ +package fr.acinq.eclair + +import java.io.File +import java.net.{URL, URLClassLoader} + +import org.clapper.classutil.ClassFinder + +trait Plugin { + + def onSetup(setup: Setup): Unit + + def onKit(kit: Kit): Unit + +} + +object Plugin { + + def loadPlugins(jars: Seq[File]): Seq[Plugin] = { + val finder = ClassFinder(jars) + val classes = finder.getClasses + val urls = jars.map(f => new URL(s"file:${f.getCanonicalPath}")) + val loader = new URLClassLoader(urls.toArray, ClassLoader.getSystemClassLoader) + classes + .filter(_.isConcrete) + .filter(_.implements(classOf[Plugin].getName)) + .map(c => Class.forName(c.name, true, loader).getDeclaredConstructor().newInstance().asInstanceOf[Plugin]) + .toList + } +} \ No newline at end of file