From 76c0fff86568157a3dd7e3d3bfbf9a076a39352b Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 12 Apr 2022 11:40:37 +0200 Subject: [PATCH 1/4] Add a "stop" API method This API call was added for certain uses cases where killing the process was impractical but internally it just calls `sys.exit()`. Eclair is designed to shutdown cleanly when its process is killed and this is still the recommended way of stopping it. --- .../src/main/scala/fr/acinq/eclair/Eclair.scala | 10 ++++++++++ .../scala/fr/acinq/eclair/api/handlers/Node.scala | 10 +++++++++- .../scala/fr/acinq/eclair/api/ApiServiceSpec.scala | 14 ++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) 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..0add9a009f 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(exitCode: Int): 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(exitCode: Int): 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(s"stopping eclair with exit code $exitCode") + sys.exit(exitCode) + 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..de37910677 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,13 @@ trait Node { } } - val nodeRoutes: Route = getInfo ~ connect ~ disconnect ~ peers ~ audit + val stop: Route = postRequest("stop") { implicit t => + formFields("exitCode".as[Int].?) { exitCode_opt => + val exitCode = exitCode_opt.getOrElse(0) + eclairApi.stop(exitCode) + complete("ok") + } + } + + 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..96264cd33c 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(0) 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 { From 974a73ce24972f6caac7d9dc31e3d2a4b0da24af Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 12 Apr 2022 16:32:18 +0200 Subject: [PATCH 2/4] Address review comments --- eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala | 8 ++++---- .../main/scala/fr/acinq/eclair/api/handlers/Node.scala | 6 +----- .../test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala | 1 - 3 files changed, 5 insertions(+), 10 deletions(-) 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 0add9a009f..0d6be5469e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -160,7 +160,7 @@ trait Eclair { def sendOnionMessage(intermediateNodes: Seq[PublicKey], destination: Either[PublicKey, Sphinx.RouteBlinding.BlindedRoute], replyPath: Option[Seq[PublicKey]], userCustomContent: ByteVector)(implicit timeout: Timeout): Future[SendOnionMessageResponse] - def stop(exitCode: Int): Future[Unit] + def stop(): Future[Unit] } class EclairImpl(appKit: Kit) extends Eclair with Logging { @@ -562,11 +562,11 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - override def stop(exitCode: Int): Future[Unit] = { + 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(s"stopping eclair with exit code $exitCode") - sys.exit(exitCode) + 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 de37910677..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 @@ -63,11 +63,7 @@ trait Node { } val stop: Route = postRequest("stop") { implicit t => - formFields("exitCode".as[Int].?) { exitCode_opt => - val exitCode = exitCode_opt.getOrElse(0) - eclairApi.stop(exitCode) - complete("ok") - } + 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 96264cd33c..63e0e50b1c 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 @@ -1188,7 +1188,6 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("stop eclair") { val eclair = mock[Eclair] val mockService = new MockService(eclair) - eclair.stop(0) returns Future.successful(()) Post("/stop") ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> From 8642313629a93c59b67c8bc2d630b4a653f8ab8e Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 12 Apr 2022 17:30:04 +0200 Subject: [PATCH 3/4] Update release notes and eclair-cli --- docs/release-notes/eclair-vnext.md | 1 + eclair-core/eclair-cli | 1 + 2 files changed, 2 insertions(+) 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 From 9823db00ae6556dd0ec6639316a883414b86beb9 Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 12 Apr 2022 18:22:55 +0200 Subject: [PATCH 4/4] fixup: fix API test --- .../src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala | 1 + 1 file changed, 1 insertion(+) 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 63e0e50b1c..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 @@ -1188,6 +1188,7 @@ 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)) ~>