diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 286ac71557..97bd4cc0e1 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -9,6 +9,7 @@ ### API changes - `channelbalances` Retrieves information about the balances of all local channels. (#2196) +- `stop` Stops eclair. Please note that the recommended way of stopping eclair is simply to kill its process (#2233) ### Miscellaneous improvements and bug fixes diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 553df5a977..22be3fe5f5 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -34,6 +34,7 @@ and COMMAND is one of the available commands: - disconnect - peers - audit + - stop === Channel === - open diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 6874f8c357..0d6be5469e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -159,6 +159,8 @@ trait Eclair { def verifyMessage(message: ByteVector, recoverableSignature: ByteVector): VerifiedMessage def sendOnionMessage(intermediateNodes: Seq[PublicKey], destination: Either[PublicKey, Sphinx.RouteBlinding.BlindedRoute], replyPath: Option[Seq[PublicKey]], userCustomContent: ByteVector)(implicit timeout: Timeout): Future[SendOnionMessageResponse] + + def stop(): Future[Unit] } class EclairImpl(appKit: Kit) extends Eclair with Logging { @@ -559,4 +561,12 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { case Attempt.Failure(cause) => Future.successful(SendOnionMessageResponse(sent = false, failureMessage = Some(s"the `content` field is invalid, it must contain encoded tlvs: ${cause.message}"), response = None)) } } + + override def stop(): Future[Unit] = { + // README: do not make this smarter or more complex ! + // eclair can simply and cleanly be stopped by killing its process without fear of losing data, payments, ... and it should remain this way. + logger.info("stopping eclair") + sys.exit(0) + Future.successful(()) + } } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Node.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Node.scala index 28cf82ed24..a3a835015b 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Node.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Node.scala @@ -62,5 +62,9 @@ trait Node { } } - val nodeRoutes: Route = getInfo ~ connect ~ disconnect ~ peers ~ audit + val stop: Route = postRequest("stop") { implicit t => + complete(eclairApi.stop()) + } + + val nodeRoutes: Route = getInfo ~ connect ~ disconnect ~ peers ~ audit ~ stop } diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index f79260a986..fcef0b94e9 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -1185,6 +1185,20 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } } + test("stop eclair") { + val eclair = mock[Eclair] + val mockService = new MockService(eclair) + eclair.stop() returns Future.successful(()) + + Post("/stop") ~> + addCredentials(BasicHttpCredentials("", mockApi().password)) ~> + Route.seal(mockService.stop) ~> + check { + assert(handled) + assert(status == OK) + } + } + private def matchTestJson(apiName: String, response: String) = { val resource = getClass.getResourceAsStream(s"/api/$apiName") val expectedResponse = Try(Source.fromInputStream(resource).mkString).getOrElse {