This is an automated email from the ASF dual-hosted git repository. toulmean pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/incubator-tuweni.git
The following commit(s) were added to refs/heads/main by this push: new 776bd698 expose information from the rlpx service in the UI new 260b9e00 Merge pull request #426 from atoulme/new_ui_improvements 776bd698 is described below commit 776bd6981f1b23f39c38807b0ad435923121fb74 Author: Antoine Toulme <anto...@lunar-ocean.com> AuthorDate: Sat Jul 16 10:01:05 2022 -0700 expose information from the rlpx service in the UI --- eth-client-ui/build.gradle | 4 ++ .../apache/tuweni/ethclientui/UIIntegrationTest.kt | 32 +++++++++++-- .../org/apache/tuweni/ethclientui/JSONProvider.kt | 56 ++++++++++++++++++++++ .../org/apache/tuweni/ethclientui/StateService.kt | 19 ++++++-- .../kotlin/org/apache/tuweni/ethclientui/UI.kt | 2 +- .../org/apache/tuweni/ethclient/EthereumClient.kt | 14 +++--- 6 files changed, 114 insertions(+), 13 deletions(-) diff --git a/eth-client-ui/build.gradle b/eth-client-ui/build.gradle index 6f468ef6..64a0bea6 100644 --- a/eth-client-ui/build.gradle +++ b/eth-client-ui/build.gradle @@ -26,12 +26,16 @@ dependencies { implementation 'javax.servlet:javax.servlet-api' implementation 'javax.ws.rs:javax.ws.rs-api' + implementation project(':bytes') implementation project(':concurrent') implementation project(':config') implementation project(':concurrent-coroutines') implementation project(':crypto') + implementation project(':eth') implementation project(':eth-client') + implementation project(':eth-repository') implementation project(':peer-repository') + implementation project(':units') testImplementation project(':junit') testImplementation 'org.bouncycastle:bcprov-jdk15on' diff --git a/eth-client-ui/src/integrationTest/kotlin/org/apache/tuweni/ethclientui/UIIntegrationTest.kt b/eth-client-ui/src/integrationTest/kotlin/org/apache/tuweni/ethclientui/UIIntegrationTest.kt index 353fa3e3..19f0e0ba 100644 --- a/eth-client-ui/src/integrationTest/kotlin/org/apache/tuweni/ethclientui/UIIntegrationTest.kt +++ b/eth-client-ui/src/integrationTest/kotlin/org/apache/tuweni/ethclientui/UIIntegrationTest.kt @@ -17,22 +17,42 @@ package org.apache.tuweni.ethclientui import io.vertx.core.Vertx +import kotlinx.coroutines.runBlocking import org.apache.tuweni.ethclient.EthereumClient import org.apache.tuweni.ethclient.EthereumClientConfig +import org.apache.tuweni.junit.BouncyCastleExtension +import org.apache.tuweni.junit.TempDirectory +import org.apache.tuweni.junit.TempDirectoryExtension import org.apache.tuweni.junit.VertxExtension import org.apache.tuweni.junit.VertxInstance +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import java.net.HttpURLConnection import java.net.URL +import java.nio.file.Path -@ExtendWith(VertxExtension::class) +@ExtendWith(VertxExtension::class, BouncyCastleExtension::class, TempDirectoryExtension::class) class UIIntegrationTest { @Test - fun testServerComesUp(@VertxInstance vertx: Vertx) { - val ui = UI(client = EthereumClient(vertx, EthereumClientConfig.fromString("[storage.forui]\npath=\"data\""))) + fun testServerComesUp(@VertxInstance vertx: Vertx, @TempDirectory tempDir: Path) = runBlocking { + val ui = UI( + client = EthereumClient( + vertx, + EthereumClientConfig.fromString( + """[storage.default] +path="${tempDir.toAbsolutePath()}" +genesis="default" +[genesis.default] +path="classpath:/genesis/dev.json" +[peerRepository.default] +type="memory"""" + ) + ) + ) + ui.client.start() ui.start() val url = URL("http://localhost:" + ui.actualPort) val con = url.openConnection() as HttpURLConnection @@ -45,6 +65,12 @@ class UIIntegrationTest { con2.requestMethod = "GET" val response2 = con2.inputStream.readAllBytes() assertTrue(response2.isNotEmpty()) + val url3 = URL("http://localhost:" + ui.actualPort + "/rest/state") + val con3 = url3.openConnection() as HttpURLConnection + con3.requestMethod = "GET" + val response3 = con3.inputStream.readAllBytes() + assertTrue(response3.isNotEmpty()) + assertEquals("""{"peerCounts":{"default":0},"bestBlocks":{"default":{"hash":"0xa08d1edb37ba1c62db764ef7c2566cbe368b850f5b3762c6c24114a3fd97b87f","number":"0x0000000000000000000000000000000000000000000000000000000000000000"}}}""", String(response3)) ui.stop() } } diff --git a/eth-client-ui/src/main/kotlin/org/apache/tuweni/ethclientui/JSONProvider.kt b/eth-client-ui/src/main/kotlin/org/apache/tuweni/ethclientui/JSONProvider.kt new file mode 100644 index 00000000..365beb51 --- /dev/null +++ b/eth-client-ui/src/main/kotlin/org/apache/tuweni/ethclientui/JSONProvider.kt @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tuweni.ethclientui + +import com.fasterxml.jackson.databind.ObjectMapper +import jakarta.ws.rs.Produces +import jakarta.ws.rs.core.Feature +import jakarta.ws.rs.core.FeatureContext +import jakarta.ws.rs.core.MediaType +import jakarta.ws.rs.ext.MessageBodyReader +import jakarta.ws.rs.ext.MessageBodyWriter +import jakarta.ws.rs.ext.Provider +import org.apache.tuweni.eth.EthJsonModule +import org.glassfish.jersey.jackson.internal.jackson.jaxrs.cfg.Annotations +import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider + +@Provider +@Produces(MediaType.APPLICATION_JSON) +class JSONProvider : JacksonJaxbJsonProvider { + + constructor() : super() + constructor(vararg annotationsToUse: Annotations?) : super(mapper, annotationsToUse) + + companion object { + val mapper = ObjectMapper() + + init { + mapper.registerModule(EthJsonModule()) + } + } + + init { + setMapper(mapper) + } +} + +class MarshallingFeature : Feature { + override fun configure(context: FeatureContext): Boolean { + context.register(JSONProvider::class.java, MessageBodyReader::class.java, MessageBodyWriter::class.java) + return true + } +} diff --git a/eth-client-ui/src/main/kotlin/org/apache/tuweni/ethclientui/StateService.kt b/eth-client-ui/src/main/kotlin/org/apache/tuweni/ethclientui/StateService.kt index f37461e6..9eee076f 100644 --- a/eth-client-ui/src/main/kotlin/org/apache/tuweni/ethclientui/StateService.kt +++ b/eth-client-ui/src/main/kotlin/org/apache/tuweni/ethclientui/StateService.kt @@ -23,6 +23,13 @@ import jakarta.ws.rs.Path import jakarta.ws.rs.Produces import jakarta.ws.rs.core.Context import jakarta.ws.rs.core.MediaType +import kotlinx.coroutines.runBlocking +import org.apache.tuweni.bytes.Bytes32 +import org.apache.tuweni.units.bigints.UInt256 + +data class BlockHashAndNumber(val hash: Bytes32, val number: UInt256) + +data class State(val peerCounts: Map<String, Long>, val bestBlocks: Map<String, BlockHashAndNumber>) @Path("state") class StateService { @@ -32,12 +39,18 @@ class StateService { @GET @Produces(MediaType.APPLICATION_JSON) - fun get(): Map<String, Long> { + fun get(): State { val client = context!!.getAttribute("ethclient") as EthereumClient - val pairs = client.peerRepositories.entries.map { + val peerCounts = client.peerRepositories.entries.map { Pair(it.key, it.value.activeConnections().count()) } - return mapOf(*pairs.toTypedArray()) + val bestBlocks = client.storageRepositories.entries.map { + runBlocking { + val bestBlock = it.value.retrieveChainHeadHeader() + Pair(it.key, BlockHashAndNumber(bestBlock.hash, bestBlock.number)) + } + } + return State(mapOf(*peerCounts.toTypedArray()), mapOf(*bestBlocks.toTypedArray())) } } diff --git a/eth-client-ui/src/main/kotlin/org/apache/tuweni/ethclientui/UI.kt b/eth-client-ui/src/main/kotlin/org/apache/tuweni/ethclientui/UI.kt index 41570903..fad640c8 100644 --- a/eth-client-ui/src/main/kotlin/org/apache/tuweni/ethclientui/UI.kt +++ b/eth-client-ui/src/main/kotlin/org/apache/tuweni/ethclientui/UI.kt @@ -45,7 +45,7 @@ class UI( ctx.contextPath = path newServer.handler = ctx - val config = ResourceConfig().packages(true, "org.apache.tuweni.ethclientui") + val config = ResourceConfig().packages(true, "org.apache.tuweni.ethclientui").register(MarshallingFeature::class.java) val holder = ServletHolder(ServletContainer(config)) holder.initOrder = 1 ctx.addServlet(holder, "/rest/*") diff --git a/eth-client/src/main/kotlin/org/apache/tuweni/ethclient/EthereumClient.kt b/eth-client/src/main/kotlin/org/apache/tuweni/ethclient/EthereumClient.kt index 15bfbedf..061b1028 100644 --- a/eth-client/src/main/kotlin/org/apache/tuweni/ethclient/EthereumClient.kt +++ b/eth-client/src/main/kotlin/org/apache/tuweni/ethclient/EthereumClient.kt @@ -83,8 +83,8 @@ class EthereumClient( private var metricsService: MetricsService? = null private val genesisFiles = mutableMapOf<String, GenesisFile>() - private val services = mutableMapOf<String, RLPxService>() - private val storageRepositories = mutableMapOf<String, BlockchainRepository>() + private val rlpxServices = mutableMapOf<String, RLPxService>() + val storageRepositories = mutableMapOf<String, BlockchainRepository>() val peerRepositories = mutableMapOf<String, EthereumPeerRepository>() private val dnsClients = mutableMapOf<String, DNSClient>() private val discoveryServices = mutableMapOf<String, DiscoveryService>() @@ -198,6 +198,7 @@ class EthereumClient( discoveryServices[it.getName()] = discoveryService logger.info("Started discovery service ${it.getName()}") } + val adapters = mutableMapOf<String, WireConnectionPeerRepositoryAdapter>() AsyncCompletion.allOf( config.rlpxServices().map { rlpxConfig -> @@ -252,7 +253,8 @@ class EthereumClient( meter, adapter ) - services[rlpxConfig.getName()] = service + adapters[rlpxConfig.getName()] = adapter + rlpxServices[rlpxConfig.getName()] = service peerRepository.addIdentityListener { service.connectTo( it.publicKey(), @@ -287,9 +289,9 @@ class EthereumClient( for (sync in config.synchronizers()) { val syncRepository = storageRepositories[sync.getRepository()] ?: throw IllegalArgumentException("Repository ${sync.getRepository()} missing for synchronizer ${sync.getName()}") - val syncService = services[sync.getRlpxService()] ?: throw IllegalArgumentException("Service ${sync.getRlpxService()} missing for synchronizer ${sync.getName()}") + val syncService = rlpxServices[sync.getRlpxService()] ?: throw IllegalArgumentException("Service ${sync.getRlpxService()} missing for synchronizer ${sync.getName()}") val syncPeerRepository = peerRepositories[sync.getPeerRepository()] ?: throw IllegalArgumentException("Peer repository ${sync.getPeerRepository()} missing for synchronizer ${sync.getName()}") - val adapter = WireConnectionPeerRepositoryAdapter(syncPeerRepository) + val adapter = adapters[sync.getRlpxService()] ?: throw IllegalArgumentException("Service ${sync.getRlpxService()} missing for synchronizer ${sync.getName()}") when (sync.getType()) { SynchronizerType.best -> { @@ -397,7 +399,7 @@ class EthereumClient( managerHandler.forEach { it.stop() } - AsyncCompletion.allOf(services.values.map(RLPxService::stop)).await() + AsyncCompletion.allOf(rlpxServices.values.map(RLPxService::stop)).await() storageRepositories.values.forEach(BlockchainRepository::close) metricsService?.close() Unit --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@tuweni.apache.org For additional commands, e-mail: commits-h...@tuweni.apache.org