Cleanup: move node startup code into a class that can be sub-classed to customise behaviour.

This commit is contained in:
Mike Hearn 2017-06-13 15:43:06 +02:00
parent 0cb41c55a5
commit 28b850ec85
12 changed files with 375 additions and 338 deletions

View File

@ -8,6 +8,7 @@ import net.corda.core.getOrThrow
import net.corda.core.messaging.startFlow import net.corda.core.messaging.startFlow
import net.corda.core.utilities.ALICE import net.corda.core.utilities.ALICE
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
import net.corda.node.internal.NodeStartup
import net.corda.node.services.startFlowPermission import net.corda.node.services.startFlowPermission
import net.corda.nodeapi.User import net.corda.nodeapi.User
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
@ -43,7 +44,7 @@ class BootTests {
startNode(ALICE.name).getOrThrow() startNode(ALICE.name).getOrThrow()
} }
// We count the number of nodes that wrote into the logfile by counting "Logs can be found in" // We count the number of nodes that wrote into the logfile by counting "Logs can be found in"
val numberOfNodesThatLogged = Files.lines(logFile.toPath()).filter { it.contains(LOGS_CAN_BE_FOUND_IN_STRING) }.count() val numberOfNodesThatLogged = Files.lines(logFile.toPath()).filter { NodeStartup.LOGS_CAN_BE_FOUND_IN_STRING in it }.count()
assertEquals(1, numberOfNodesThatLogged) assertEquals(1, numberOfNodesThatLogged)
} }
} }

View File

@ -2,281 +2,11 @@
package net.corda.node package net.corda.node
import com.jcabi.manifests.Manifests import net.corda.node.internal.NodeStartup
import com.typesafe.config.ConfigException
import joptsimple.OptionException
import net.corda.core.*
import net.corda.core.crypto.commonName
import net.corda.core.crypto.orgName
import net.corda.core.node.VersionInfo
import net.corda.core.utilities.Emoji
import net.corda.node.internal.Node
import net.corda.node.internal.enforceSingleNodeIsRunning
import net.corda.node.services.config.FullNodeConfiguration
import net.corda.node.services.transactions.bftSMaRtSerialFilter
import net.corda.node.shell.InteractiveShell
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NetworkRegistrationHelper
import org.fusesource.jansi.Ansi
import org.fusesource.jansi.AnsiConsole
import org.slf4j.LoggerFactory
import org.slf4j.bridge.SLF4JBridgeHandler
import java.lang.management.ManagementFactory
import java.net.InetAddress
import java.nio.file.Paths
import java.util.*
import kotlin.system.exitProcess
private var renderBasicInfoToConsole = true
/** Used for useful info that we always want to show, even when not logging to the console */
fun printBasicNodeInfo(description: String, info: String? = null) {
val msg = if (info == null) description else "${description.padEnd(40)}: $info"
val loggerName = if (renderBasicInfoToConsole) "BasicInfo" else "Main"
LoggerFactory.getLogger(loggerName).info(msg)
}
val LOGS_DIRECTORY_NAME = "logs"
val LOGS_CAN_BE_FOUND_IN_STRING = "Logs can be found in"
private val log by lazy { LoggerFactory.getLogger("Main") }
private fun initLogging(cmdlineOptions: CmdLineOptions) {
val loggingLevel = cmdlineOptions.loggingLevel.name.toLowerCase(Locale.ENGLISH)
System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file.
if (cmdlineOptions.logToConsole) {
System.setProperty("consoleLogLevel", loggingLevel)
renderBasicInfoToConsole = false
}
System.setProperty("log-path", (cmdlineOptions.baseDirectory / LOGS_DIRECTORY_NAME).toString())
SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler.
SLF4JBridgeHandler.install()
}
fun main(args: Array<String>) { fun main(args: Array<String>) {
val startTime = System.currentTimeMillis() // Pass the arguments to the Node factory. In the Enterprise edition, this line is modified to point to a subclass.
assertCanNormalizeEmptyPath() // It will exit the process in case of startup failure and is not intended to be used by embedders. If you want
// to embed Node in your own container, instantiate it directly and set up the configuration objects yourself.
val argsParser = ArgsParser() NodeStartup(args).run()
}
val cmdlineOptions = try {
argsParser.parse(*args)
} catch (ex: OptionException) {
println("Invalid command line arguments: ${ex.message}")
argsParser.printHelp(System.out)
exitProcess(1)
}
// We do the single node check before we initialise logging so that in case of a double-node start it doesn't mess
// with the running node's logs.
enforceSingleNodeIsRunning(cmdlineOptions.baseDirectory)
initLogging(cmdlineOptions)
// Manifest properties are only available if running from the corda jar
fun manifestValue(name: String): String? = if (Manifests.exists(name)) Manifests.read(name) else null
val versionInfo = VersionInfo(
manifestValue("Corda-Platform-Version")?.toInt() ?: 1,
manifestValue("Corda-Release-Version") ?: "Unknown",
manifestValue("Corda-Revision") ?: "Unknown",
manifestValue("Corda-Vendor") ?: "Unknown"
)
if (cmdlineOptions.isVersion) {
println("${versionInfo.vendor} ${versionInfo.releaseVersion}")
println("Revision ${versionInfo.revision}")
println("Platform Version ${versionInfo.platformVersion}")
exitProcess(0)
}
// Maybe render command line help.
if (cmdlineOptions.help) {
argsParser.printHelp(System.out)
exitProcess(0)
}
drawBanner(versionInfo)
printBasicNodeInfo(LOGS_CAN_BE_FOUND_IN_STRING, System.getProperty("log-path"))
val conf = try {
cmdlineOptions.loadConfig()
} catch (e: ConfigException) {
println("Unable to load the configuration file: ${e.rootCause.message}")
exitProcess(2)
}
// TODO: Replace with "::funcName" notation when Dokka is upgraded to not crash when analysing the notation
val filterFunc = if (conf.bftReplicaId != null) { it: Class<*> -> bftSMaRtSerialFilter(it) } else { it: Class<*> -> defaultSerialFilter(it) }
SerialFilter.install(filterFunc)
if (cmdlineOptions.isRegistration) {
println()
println("******************************************************************")
println("* *")
println("* Registering as a new participant with Corda network *")
println("* *")
println("******************************************************************")
NetworkRegistrationHelper(conf, HTTPNetworkRegistrationService(conf.certificateSigningService)).buildKeystore()
exitProcess(0)
}
log.info("Vendor: ${versionInfo.vendor}")
log.info("Release: ${versionInfo.releaseVersion}")
log.info("Platform Version: ${versionInfo.platformVersion}")
log.info("Revision: ${versionInfo.revision}")
val info = ManagementFactory.getRuntimeMXBean()
log.info("PID: ${info.name.split("@").firstOrNull()}") // TODO Java 9 has better support for this
log.info("Main class: ${FullNodeConfiguration::class.java.protectionDomain.codeSource.location.toURI().path}")
log.info("CommandLine Args: ${info.inputArguments.joinToString(" ")}")
log.info("Application Args: ${args.joinToString(" ")}")
log.info("bootclasspath: ${info.bootClassPath}")
log.info("classpath: ${info.classPath}")
log.info("VM ${info.vmName} ${info.vmVendor} ${info.vmVersion}")
log.info("Machine: ${lookupMachineNameAndMaybeWarn()}")
log.info("Working Directory: ${cmdlineOptions.baseDirectory}")
val agentProperties = sun.misc.VMSupport.getAgentProperties()
if (agentProperties.containsKey("sun.jdwp.listenerAddress")) {
log.info("Debug port: ${agentProperties.getProperty("sun.jdwp.listenerAddress")}")
}
log.info("Starting as node on ${conf.p2pAddress}")
try {
cmdlineOptions.baseDirectory.createDirectories()
val node = conf.createNode(versionInfo)
node.start()
printPluginsAndServices(node)
node.networkMapRegistrationFuture.success {
val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0
// TODO: Replace this with a standard function to get an unambiguous rendering of the X.500 name.
val name = node.info.legalIdentity.name.orgName ?: node.info.legalIdentity.name.commonName
printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec")
// Don't start the shell if there's no console attached.
val runShell = !cmdlineOptions.noLocalShell && System.console() != null
node.startupComplete then {
try {
InteractiveShell.startShell(cmdlineOptions.baseDirectory, runShell, cmdlineOptions.sshdServer, node)
} catch(e: Throwable) {
log.error("Shell failed to start", e)
}
}
} failure {
log.error("Error during network map registration", it)
exitProcess(1)
}
node.run()
} catch (e: Exception) {
if (e.message?.startsWith("Unknown named curve:") ?: false) {
log.error("Exception during node startup - ${e.message}. " +
"This is a known OpenJDK issue on some Linux distributions, please use OpenJDK from zulu.org or Oracle JDK.")
} else
log.error("Exception during node startup", e)
exitProcess(1)
}
exitProcess(0)
}
private fun lookupMachineNameAndMaybeWarn(): String {
val start = System.currentTimeMillis()
val hostName: String = InetAddress.getLocalHost().hostName
val elapsed = System.currentTimeMillis() - start
if (elapsed > 1000 && hostName.endsWith(".local")) {
// User is probably on macOS and experiencing this problem: http://stackoverflow.com/questions/10064581/how-can-i-eliminate-slow-resolving-loading-of-localhost-virtualhost-a-2-3-secon
//
// Also see https://bugs.openjdk.java.net/browse/JDK-8143378
val messages = listOf(
"Your computer took over a second to resolve localhost due an incorrect configuration. Corda will work but start very slowly until this is fixed. ",
"Please see https://docs.corda.net/troubleshooting.html#slow-localhost-resolution for information on how to fix this. ",
"It will only take a few seconds for you to resolve."
)
log.warn(messages.joinToString(""))
Emoji.renderIfSupported {
print(Ansi.ansi().fgBrightRed())
messages.forEach {
println("${Emoji.sleepingFace}$it")
}
print(Ansi.ansi().reset())
}
}
return hostName
}
private fun assertCanNormalizeEmptyPath() {
// Check we're not running a version of Java with a known bug: https://github.com/corda/corda/issues/83
try {
Paths.get("").normalize()
} catch (e: ArrayIndexOutOfBoundsException) {
failStartUp("You are using a version of Java that is not supported (${System.getProperty("java.version")}). Please upgrade to the latest version.")
}
}
internal fun failStartUp(message: String): Nothing {
println(message)
println("Corda will now exit...")
exitProcess(1)
}
private fun printPluginsAndServices(node: Node) {
node.configuration.extraAdvertisedServiceIds.let {
if (it.isNotEmpty()) printBasicNodeInfo("Providing network services", it.joinToString())
}
val plugins = node.pluginRegistries
.map { it.javaClass.name }
.filterNot { it.startsWith("net.corda.node.") || it.startsWith("net.corda.core.") || it.startsWith("net.corda.nodeapi.") }
.map { it.substringBefore('$') }
if (plugins.isNotEmpty())
printBasicNodeInfo("Loaded plugins", plugins.joinToString())
}
private fun messageOfTheDay(): Pair<String, String> {
val messages = arrayListOf(
"The only distributed ledger that pays\nhomage to Pac Man in its logo.",
"You know, I was a banker\nonce ... but I lost interest. ${Emoji.bagOfCash}",
"It's not who you know, it's who you know\nknows what you know you know.",
"It runs on the JVM because QuickBasic\nis apparently not 'professional' enough.",
"\"It's OK computer, I go to sleep after\ntwenty minutes of inactivity too!\"",
"It's kind of like a block chain but\ncords sounded healthier than chains.",
"Computer science and finance together.\nYou should see our crazy Christmas parties!",
"I met my bank manager yesterday and asked\nto check my balance ... he pushed me over!",
"A banker with nobody around may find\nthemselves .... a-loan! <applause>",
"Whenever I go near my bank I get\nwithdrawal symptoms ${Emoji.coolGuy}",
"There was an earthquake in California,\na local bank went into de-fault.",
"I asked for insurance if the nearby\nvolcano erupted. They said I'd be covered.",
"I had an account with a bank in the\nNorth Pole, but they froze all my assets ${Emoji.santaClaus}",
"Check your contracts carefully. The fine print\nis usually a clause for suspicion ${Emoji.santaClaus}",
"Some bankers are generous ...\nto a vault! ${Emoji.bagOfCash} ${Emoji.coolGuy}",
"What you can buy for a dollar these\ndays is absolute non-cents! ${Emoji.bagOfCash}",
"Old bankers never die, they\njust... pass the buck",
"I won $3M on the lottery so I donated a quarter\nof it to charity. Now I have $2,999,999.75.",
"There are two rules for financial success:\n1) Don't tell everything you know.",
"Top tip: never say \"oops\", instead\nalways say \"Ah, Interesting!\"",
"Computers are useless. They can only\ngive you answers. -- Picasso"
)
if (Emoji.hasEmojiTerminal)
messages += "Kind of like a regular database but\nwith emojis, colours and ascii art. ${Emoji.coolGuy}"
val (a, b) = messages.randomOrNull()!!.split('\n')
return Pair(a, b)
}
private fun drawBanner(versionInfo: VersionInfo) {
// This line makes sure ANSI escapes work on Windows, where they aren't supported out of the box.
AnsiConsole.systemInstall()
Emoji.renderIfSupported {
val (msg1, msg2) = messageOfTheDay()
println(Ansi.ansi().fgBrightRed().a("""
______ __
/ ____/ _________/ /___ _
/ / __ / ___/ __ / __ `/ """).fgBrightBlue().a(msg1).newline().fgBrightRed().a(
"/ /___ /_/ / / / /_/ / /_/ / ").fgBrightBlue().a(msg2).newline().fgBrightRed().a(
"""\____/ /_/ \__,_/\__,_/""").reset().newline().newline().fgBrightDefault().bold().
a("--- ${versionInfo.vendor} ${versionInfo.releaseVersion} (${versionInfo.revision.take(7)}) -----------------------------------------------").
newline().
newline().
a("${Emoji.books}New! ").reset().a("Training now available worldwide, see https://corda.net/corda-training/").
newline().
reset())
}
}

View File

@ -1,5 +1,6 @@
package net.corda.node package net.corda.node
import net.corda.node.internal.Node
import java.lang.reflect.Field import java.lang.reflect.Field
import java.lang.reflect.Method import java.lang.reflect.Method
import java.lang.reflect.Proxy import java.lang.reflect.Proxy
@ -24,7 +25,7 @@ internal object SerialFilter {
// JDK 8u121 is the earliest JDK8 JVM that supports this functionality. // JDK 8u121 is the earliest JDK8 JVM that supports this functionality.
filterInterface = getFilterInterface("java.io") filterInterface = getFilterInterface("java.io")
?: getFilterInterface("sun.misc") ?: getFilterInterface("sun.misc")
?: failStartUp("Corda forbids Java deserialisation. Please upgrade to at least JDK 8u121.") ?: Node.failStartUp("Corda forbids Java deserialisation. Please upgrade to at least JDK 8u121.")
serialClassGetter = Class.forName("${filterInterface.name}\$FilterInfo").getMethod("serialClass") serialClassGetter = Class.forName("${filterInterface.name}\$FilterInfo").getMethod("serialClass")
val statusEnum = Class.forName("${filterInterface.name}\$Status") val statusEnum = Class.forName("${filterInterface.name}\$Status")
undecided = statusEnum.getField("UNDECIDED").get(null) undecided = statusEnum.getField("UNDECIDED").get(null)

View File

@ -1,35 +0,0 @@
package net.corda.node.internal
import net.corda.nodeapi.internal.addShutdownHook
import net.corda.core.div
import java.io.RandomAccessFile
import java.lang.management.ManagementFactory
import java.nio.file.Path
/**
* This function enforces that only a single node is running using the given [baseDirectory] by using a file lock.
*/
fun enforceSingleNodeIsRunning(baseDirectory: Path) {
// Write out our process ID (which may or may not resemble a UNIX process id - to us it's just a string) to a
// file that we'll do our best to delete on exit. But if we don't, it'll be overwritten next time. If it already
// exists, we try to take the file lock first before replacing it and if that fails it means we're being started
// twice with the same directory: that's a user error and we should bail out.
val pidFile = (baseDirectory / "process-id").toFile()
pidFile.createNewFile()
pidFile.deleteOnExit()
val pidFileRw = RandomAccessFile(pidFile, "rw")
val pidFileLock = pidFileRw.channel.tryLock()
if (pidFileLock == null) {
println("It appears there is already a node running with the specified data directory $baseDirectory")
println("Shut that other node down and try again. It may have process ID ${pidFile.readText()}")
System.exit(1)
}
// Avoid the lock being garbage collected. We don't really need to release it as the OS will do so for us
// when our process shuts down, but we try in stop() anyway just to be nice.
addShutdownHook {
pidFileLock.release()
}
val ourProcessID: String = ManagementFactory.getRuntimeMXBean().name.split("@")[0]
pidFileRw.setLength(0)
pidFileRw.write(ourProcessID.toByteArray())
}

View File

@ -5,7 +5,6 @@ import com.google.common.net.HostAndPort
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.SettableFuture
import net.corda.nodeapi.internal.ShutdownHook
import net.corda.core.flatMap import net.corda.core.flatMap
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.core.minutes import net.corda.core.minutes
@ -18,7 +17,6 @@ import net.corda.core.seconds
import net.corda.core.success import net.corda.core.success
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.trace import net.corda.core.utilities.trace
import net.corda.node.printBasicNodeInfo
import net.corda.node.serialization.NodeClock import net.corda.node.serialization.NodeClock
import net.corda.node.services.RPCUserService import net.corda.node.services.RPCUserService
import net.corda.node.services.RPCUserServiceImpl import net.corda.node.services.RPCUserServiceImpl
@ -39,16 +37,19 @@ import net.corda.nodeapi.ArtemisMessagingComponent.Companion.PEER_USER
import net.corda.nodeapi.ArtemisMessagingComponent.NetworkMapAddress import net.corda.nodeapi.ArtemisMessagingComponent.NetworkMapAddress
import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ArtemisTcpTransport
import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.internal.ShutdownHook
import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.addShutdownHook
import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException
import org.apache.activemq.artemis.api.core.client.ActiveMQClient import org.apache.activemq.artemis.api.core.client.ActiveMQClient
import org.apache.activemq.artemis.api.core.client.ClientMessage import org.apache.activemq.artemis.api.core.client.ClientMessage
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.IOException import java.io.IOException
import java.time.Clock import java.time.Clock
import java.util.* import java.util.*
import javax.management.ObjectName import javax.management.ObjectName
import kotlin.system.exitProcess
/** /**
* A Node manages a standalone server that takes part in the P2P network. It creates the services found in [ServiceHub], * A Node manages a standalone server that takes part in the P2P network. It creates the services found in [ServiceHub],
@ -59,12 +60,26 @@ import javax.management.ObjectName
* but nodes are not required to advertise services they run (hence subset). * but nodes are not required to advertise services they run (hence subset).
* @param clock The clock used within the node and by all flows etc. * @param clock The clock used within the node and by all flows etc.
*/ */
class Node(override val configuration: FullNodeConfiguration, open class Node(override val configuration: FullNodeConfiguration,
advertisedServices: Set<ServiceInfo>, advertisedServices: Set<ServiceInfo>,
val versionInfo: VersionInfo, val versionInfo: VersionInfo,
clock: Clock = NodeClock()) : AbstractNode(configuration, advertisedServices, clock) { clock: Clock = NodeClock()) : AbstractNode(configuration, advertisedServices, clock) {
companion object { companion object {
private val logger = loggerFor<Node>() private val logger = loggerFor<Node>()
var renderBasicInfoToConsole = true
/** Used for useful info that we always want to show, even when not logging to the console */
fun printBasicNodeInfo(description: String, info: String? = null) {
val msg = if (info == null) description else "${description.padEnd(40)}: $info"
val loggerName = if (renderBasicInfoToConsole) "BasicInfo" else "Main"
LoggerFactory.getLogger(loggerName).info(msg)
}
internal fun failStartUp(message: String): Nothing {
println(message)
println("Corda will now exit...")
exitProcess(1)
}
} }
override val log: Logger get() = logger override val log: Logger get() = logger

View File

@ -0,0 +1,330 @@
package net.corda.node.internal
import com.jcabi.manifests.Manifests
import com.typesafe.config.ConfigException
import joptsimple.OptionException
import net.corda.core.*
import net.corda.core.crypto.commonName
import net.corda.core.crypto.orgName
import net.corda.core.node.VersionInfo
import net.corda.core.node.services.ServiceInfo
import net.corda.core.utilities.Emoji
import net.corda.core.utilities.loggerFor
import net.corda.node.ArgsParser
import net.corda.node.CmdLineOptions
import net.corda.node.SerialFilter
import net.corda.node.defaultSerialFilter
import net.corda.node.serialization.NodeClock
import net.corda.node.services.config.FullNodeConfiguration
import net.corda.node.services.transactions.bftSMaRtSerialFilter
import net.corda.node.shell.InteractiveShell
import net.corda.node.utilities.TestClock
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NetworkRegistrationHelper
import net.corda.nodeapi.internal.addShutdownHook
import org.fusesource.jansi.Ansi
import org.fusesource.jansi.AnsiConsole
import org.slf4j.bridge.SLF4JBridgeHandler
import sun.misc.VMSupport
import java.io.RandomAccessFile
import java.lang.management.ManagementFactory
import java.net.InetAddress
import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
import kotlin.system.exitProcess
/** This class is responsible for starting a Node from command line arguments. */
open class NodeStartup(val args: Array<String>) {
companion object {
private val logger by lazy { loggerFor<Node>() }
val LOGS_DIRECTORY_NAME = "logs"
val LOGS_CAN_BE_FOUND_IN_STRING = "Logs can be found in"
}
open fun run() {
val startTime = System.currentTimeMillis()
assertCanNormalizeEmptyPath()
val (argsParser, cmdlineOptions) = parseArguments()
// We do the single node check before we initialise logging so that in case of a double-node start it
// doesn't mess with the running node's logs.
enforceSingleNodeIsRunning(cmdlineOptions.baseDirectory)
initLogging(cmdlineOptions)
val versionInfo = getVersionInfo()
if (cmdlineOptions.isVersion) {
println("${versionInfo.vendor} ${versionInfo.releaseVersion}")
println("Revision ${versionInfo.revision}")
println("Platform Version ${versionInfo.platformVersion}")
exitProcess(0)
}
// Maybe render command line help.
if (cmdlineOptions.help) {
argsParser.printHelp(System.out)
exitProcess(0)
}
drawBanner(versionInfo)
Node.printBasicNodeInfo(LOGS_CAN_BE_FOUND_IN_STRING, System.getProperty("log-path"))
val conf = loadConfigFile(cmdlineOptions)
banJavaSerialisation(conf)
preNetworkRegistration()
maybeRegisterWithNetworkAndExit(cmdlineOptions, conf)
logStartupInfo(versionInfo, cmdlineOptions, conf)
try {
cmdlineOptions.baseDirectory.createDirectories()
startNode(conf, versionInfo, startTime, cmdlineOptions)
} catch (e: Exception) {
if (e.message?.startsWith("Unknown named curve:") ?: false) {
logger.error("Exception during node startup - ${e.message}. " +
"This is a known OpenJDK issue on some Linux distributions, please use OpenJDK from zulu.org or Oracle JDK.")
} else
logger.error("Exception during node startup", e)
exitProcess(1)
}
exitProcess(0)
}
open protected fun preNetworkRegistration() = Unit
open protected fun createNode(conf: FullNodeConfiguration, versionInfo: VersionInfo, services: Set<ServiceInfo>): Node {
return Node(conf, services, versionInfo, if (conf.useTestClock) TestClock() else NodeClock())
}
open protected fun startNode(conf: FullNodeConfiguration, versionInfo: VersionInfo, startTime: Long, cmdlineOptions: CmdLineOptions) {
val advertisedServices = conf.calculateServices()
val node = createNode(conf, versionInfo, advertisedServices)
node.start()
printPluginsAndServices(node)
node.networkMapRegistrationFuture.success {
val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0
// TODO: Replace this with a standard function to get an unambiguous rendering of the X.500 name.
val name = node.info.legalIdentity.name.orgName ?: node.info.legalIdentity.name.commonName
Node.printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec")
// Don't start the shell if there's no console attached.
val runShell = !cmdlineOptions.noLocalShell && System.console() != null
node.startupComplete then {
try {
InteractiveShell.startShell(cmdlineOptions.baseDirectory, runShell, cmdlineOptions.sshdServer, node)
} catch(e: Throwable) {
logger.error("Shell failed to start", e)
}
}
} failure {
logger.error("Error during network map registration", it)
exitProcess(1)
}
node.run()
}
open protected fun logStartupInfo(versionInfo: VersionInfo, cmdlineOptions: CmdLineOptions, conf: FullNodeConfiguration) {
logger.info("Vendor: ${versionInfo.vendor}")
logger.info("Release: ${versionInfo.releaseVersion}")
logger.info("Platform Version: ${versionInfo.platformVersion}")
logger.info("Revision: ${versionInfo.revision}")
val info = ManagementFactory.getRuntimeMXBean()
logger.info("PID: ${info.name.split("@").firstOrNull()}") // TODO Java 9 has better support for this
logger.info("Main class: ${FullNodeConfiguration::class.java.protectionDomain.codeSource.location.toURI().path}")
logger.info("CommandLine Args: ${info.inputArguments.joinToString(" ")}")
logger.info("Application Args: ${args.joinToString(" ")}")
logger.info("bootclasspath: ${info.bootClassPath}")
logger.info("classpath: ${info.classPath}")
logger.info("VM ${info.vmName} ${info.vmVendor} ${info.vmVersion}")
logger.info("Machine: ${lookupMachineNameAndMaybeWarn()}")
logger.info("Working Directory: ${cmdlineOptions.baseDirectory}")
val agentProperties = VMSupport.getAgentProperties()
if (agentProperties.containsKey("sun.jdwp.listenerAddress")) {
logger.info("Debug port: ${agentProperties.getProperty("sun.jdwp.listenerAddress")}")
}
logger.info("Starting as node on ${conf.p2pAddress}")
}
open protected fun maybeRegisterWithNetworkAndExit(cmdlineOptions: CmdLineOptions, conf: FullNodeConfiguration) {
if (!cmdlineOptions.isRegistration) return
println()
println("******************************************************************")
println("* *")
println("* Registering as a new participant with Corda network *")
println("* *")
println("******************************************************************")
NetworkRegistrationHelper(conf, HTTPNetworkRegistrationService(conf.certificateSigningService)).buildKeystore()
exitProcess(0)
}
open protected fun loadConfigFile(cmdlineOptions: CmdLineOptions): FullNodeConfiguration {
val conf = try {
cmdlineOptions.loadConfig()
} catch (e: ConfigException) {
println("Unable to load the configuration file: ${e.rootCause.message}")
exitProcess(2)
}
return conf
}
open protected fun banJavaSerialisation(conf: FullNodeConfiguration) {
SerialFilter.install(if (conf.bftReplicaId != null) ::bftSMaRtSerialFilter else ::defaultSerialFilter)
}
open protected fun getVersionInfo(): VersionInfo {
// Manifest properties are only available if running from the corda jar
fun manifestValue(name: String): String? = if (Manifests.exists(name)) Manifests.read(name) else null
val versionInfo = VersionInfo(
manifestValue("Corda-Platform-Version")?.toInt() ?: 1,
manifestValue("Corda-Release-Version") ?: "Unknown",
manifestValue("Corda-Revision") ?: "Unknown",
manifestValue("Corda-Vendor") ?: "Unknown"
)
return versionInfo
}
private fun enforceSingleNodeIsRunning(baseDirectory: Path) {
// Write out our process ID (which may or may not resemble a UNIX process id - to us it's just a string) to a
// file that we'll do our best to delete on exit. But if we don't, it'll be overwritten next time. If it already
// exists, we try to take the file lock first before replacing it and if that fails it means we're being started
// twice with the same directory: that's a user error and we should bail out.
val pidFile = (baseDirectory / "process-id").toFile()
pidFile.createNewFile()
pidFile.deleteOnExit()
val pidFileRw = RandomAccessFile(pidFile, "rw")
val pidFileLock = pidFileRw.channel.tryLock()
if (pidFileLock == null) {
println("It appears there is already a node running with the specified data directory $baseDirectory")
println("Shut that other node down and try again. It may have process ID ${pidFile.readText()}")
System.exit(1)
}
// Avoid the lock being garbage collected. We don't really need to release it as the OS will do so for us
// when our process shuts down, but we try in stop() anyway just to be nice.
addShutdownHook {
pidFileLock.release()
}
val ourProcessID: String = ManagementFactory.getRuntimeMXBean().name.split("@")[0]
pidFileRw.setLength(0)
pidFileRw.write(ourProcessID.toByteArray())
}
private fun parseArguments(): Pair<ArgsParser, CmdLineOptions> {
val argsParser = ArgsParser()
val cmdlineOptions = try {
argsParser.parse(*args)
} catch (ex: OptionException) {
println("Invalid command line arguments: ${ex.message}")
argsParser.printHelp(System.out)
exitProcess(1)
}
return Pair(argsParser, cmdlineOptions)
}
open protected fun initLogging(cmdlineOptions: CmdLineOptions) {
val loggingLevel = cmdlineOptions.loggingLevel.name.toLowerCase(Locale.ENGLISH)
System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file.
if (cmdlineOptions.logToConsole) {
System.setProperty("consoleLogLevel", loggingLevel)
Node.renderBasicInfoToConsole = false
}
System.setProperty("log-path", (cmdlineOptions.baseDirectory / LOGS_DIRECTORY_NAME).toString())
SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler.
SLF4JBridgeHandler.install()
}
private fun lookupMachineNameAndMaybeWarn(): String {
val start = System.currentTimeMillis()
val hostName: String = InetAddress.getLocalHost().hostName
val elapsed = System.currentTimeMillis() - start
if (elapsed > 1000 && hostName.endsWith(".local")) {
// User is probably on macOS and experiencing this problem: http://stackoverflow.com/questions/10064581/how-can-i-eliminate-slow-resolving-loading-of-localhost-virtualhost-a-2-3-secon
//
// Also see https://bugs.openjdk.java.net/browse/JDK-8143378
val messages = listOf(
"Your computer took over a second to resolve localhost due an incorrect configuration. Corda will work but start very slowly until this is fixed. ",
"Please see https://docs.corda.net/troubleshooting.html#slow-localhost-resolution for information on how to fix this. ",
"It will only take a few seconds for you to resolve."
)
logger.warn(messages.joinToString(""))
Emoji.renderIfSupported {
print(Ansi.ansi().fgBrightRed())
messages.forEach {
println("${Emoji.sleepingFace}$it")
}
print(Ansi.ansi().reset())
}
}
return hostName
}
private fun assertCanNormalizeEmptyPath() {
// Check we're not running a version of Java with a known bug: https://github.com/corda/corda/issues/83
try {
Paths.get("").normalize()
} catch (e: ArrayIndexOutOfBoundsException) {
Node.failStartUp("You are using a version of Java that is not supported (${System.getProperty("java.version")}). Please upgrade to the latest version.")
}
}
private fun printPluginsAndServices(node: Node) {
node.configuration.extraAdvertisedServiceIds.let {
if (it.isNotEmpty()) Node.printBasicNodeInfo("Providing network services", it.joinToString())
}
val plugins = node.pluginRegistries
.map { it.javaClass.name }
.filterNot { it.startsWith("net.corda.node.") || it.startsWith("net.corda.core.") || it.startsWith("net.corda.nodeapi.") }
.map { it.substringBefore('$') }
if (plugins.isNotEmpty())
Node.printBasicNodeInfo("Loaded plugins", plugins.joinToString())
}
open fun drawBanner(versionInfo: VersionInfo) {
// This line makes sure ANSI escapes work on Windows, where they aren't supported out of the box.
AnsiConsole.systemInstall()
Emoji.renderIfSupported {
val messages = arrayListOf(
"The only distributed ledger that pays\nhomage to Pac Man in its logo.",
"You know, I was a banker\nonce ... but I lost interest. ${Emoji.bagOfCash}",
"It's not who you know, it's who you know\nknows what you know you know.",
"It runs on the JVM because QuickBasic\nis apparently not 'professional' enough.",
"\"It's OK computer, I go to sleep after\ntwenty minutes of inactivity too!\"",
"It's kind of like a block chain but\ncords sounded healthier than chains.",
"Computer science and finance together.\nYou should see our crazy Christmas parties!",
"I met my bank manager yesterday and asked\nto check my balance ... he pushed me over!",
"A banker with nobody around may find\nthemselves .... a-loan! <applause>",
"Whenever I go near my bank I get\nwithdrawal symptoms ${Emoji.coolGuy}",
"There was an earthquake in California,\na local bank went into de-fault.",
"I asked for insurance if the nearby\nvolcano erupted. They said I'd be covered.",
"I had an account with a bank in the\nNorth Pole, but they froze all my assets ${Emoji.santaClaus}",
"Check your contracts carefully. The fine print\nis usually a clause for suspicion ${Emoji.santaClaus}",
"Some bankers are generous ...\nto a vault! ${Emoji.bagOfCash} ${Emoji.coolGuy}",
"What you can buy for a dollar these\ndays is absolute non-cents! ${Emoji.bagOfCash}",
"Old bankers never die, they\njust... pass the buck",
"I won $3M on the lottery so I donated a quarter\nof it to charity. Now I have $2,999,999.75.",
"There are two rules for financial success:\n1) Don't tell everything you know.",
"Top tip: never say \"oops\", instead\nalways say \"Ah, Interesting!\"",
"Computers are useless. They can only\ngive you answers. -- Picasso"
)
if (Emoji.hasEmojiTerminal)
messages += "Kind of like a regular database but\nwith emojis, colours and ascii art. ${Emoji.coolGuy}"
val (msg1, msg2) = messages.randomOrNull()!!.split('\n')
println(Ansi.ansi().fgBrightRed().a("""
______ __
/ ____/ _________/ /___ _
/ / __ / ___/ __ / __ `/ """).fgBrightBlue().a(msg1).newline().fgBrightRed().a(
"/ /___ /_/ / / / /_/ / /_/ / ").fgBrightBlue().a(msg2).newline().fgBrightRed().a(
"""\____/ /_/ \__,_/\__,_/""").reset().newline().newline().fgBrightDefault().bold().
a("--- ${versionInfo.vendor} ${versionInfo.releaseVersion} (${versionInfo.revision.take(7)}) -----------------------------------------------").
newline().
newline().
a("${Emoji.books}New! ").reset().a("Training now available worldwide, see https://corda.net/corda-training/").
newline().
reset())
}
}
}

View File

@ -1,14 +1,10 @@
package net.corda.node.services.config package net.corda.node.services.config
import com.google.common.net.HostAndPort import com.google.common.net.HostAndPort
import net.corda.core.node.VersionInfo
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.node.internal.NetworkMapInfo import net.corda.node.internal.NetworkMapInfo
import net.corda.node.internal.Node
import net.corda.node.serialization.NodeClock
import net.corda.node.services.messaging.CertificateChainCheckPolicy import net.corda.node.services.messaging.CertificateChainCheckPolicy
import net.corda.node.services.network.NetworkMapService import net.corda.node.services.network.NetworkMapService
import net.corda.node.utilities.TestClock
import net.corda.nodeapi.User import net.corda.nodeapi.User
import net.corda.nodeapi.config.NodeSSLConfiguration import net.corda.nodeapi.config.NodeSSLConfiguration
import net.corda.nodeapi.config.OldConfig import net.corda.nodeapi.config.OldConfig
@ -78,14 +74,13 @@ data class FullNodeConfiguration(
} }
} }
fun createNode(versionInfo: VersionInfo): Node { fun calculateServices(): Set<ServiceInfo> {
val advertisedServices = extraAdvertisedServiceIds val advertisedServices = extraAdvertisedServiceIds
.filter(String::isNotBlank) .filter(String::isNotBlank)
.map { ServiceInfo.parse(it) } .map { ServiceInfo.parse(it) }
.toMutableSet() .toMutableSet()
if (networkMapService == null) advertisedServices += ServiceInfo(NetworkMapService.type) if (networkMapService == null) advertisedServices += ServiceInfo(NetworkMapService.type)
return advertisedServices
return Node(this, advertisedServices, versionInfo, if (useTestClock) TestClock() else NodeClock())
} }
} }

View File

@ -13,7 +13,7 @@ import net.corda.core.node.services.NetworkMapCache
import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.node.services.NetworkMapCache.MapChange
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.node.printBasicNodeInfo import net.corda.node.internal.Node
import net.corda.node.services.RPCUserService import net.corda.node.services.RPCUserService
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.messaging.NodeLoginModule.Companion.NODE_ROLE import net.corda.node.services.messaging.NodeLoginModule.Companion.NODE_ROLE
@ -47,7 +47,6 @@ import org.apache.activemq.artemis.utils.ConfigurationHelper
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.X509CertificateHolder
import rx.Subscription import rx.Subscription
import sun.security.x509.X509CertImpl
import java.io.IOException import java.io.IOException
import java.math.BigInteger import java.math.BigInteger
import java.security.KeyStore import java.security.KeyStore
@ -156,9 +155,9 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
registerPostQueueDeletionCallback { address, qName -> log.debug { "Queue deleted: $qName for $address" } } registerPostQueueDeletionCallback { address, qName -> log.debug { "Queue deleted: $qName for $address" } }
} }
activeMQServer.start() activeMQServer.start()
printBasicNodeInfo("Listening on port", p2pPort.toString()) Node.printBasicNodeInfo("Listening on port", p2pPort.toString())
if (rpcPort != null) { if (rpcPort != null) {
printBasicNodeInfo("RPC service listening on port", rpcPort.toString()) Node.printBasicNodeInfo("RPC service listening on port", rpcPort.toString())
} }
} }

View File

@ -14,14 +14,12 @@ import net.corda.core.flows.FlowInitiator
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowStateMachine import net.corda.core.flows.FlowStateMachine
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.StateMachineInfo
import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.utilities.Emoji import net.corda.core.utilities.Emoji
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.jackson.JacksonSupport import net.corda.jackson.JacksonSupport
import net.corda.jackson.StringToMethodCallParser import net.corda.jackson.StringToMethodCallParser
import net.corda.node.internal.Node import net.corda.node.internal.Node
import net.corda.node.printBasicNodeInfo
import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT
import net.corda.node.services.messaging.RpcContext import net.corda.node.services.messaging.RpcContext
import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.FlowStateMachineImpl
@ -93,7 +91,7 @@ object InteractiveShell {
// to that local copy, as CRaSH is no longer well maintained by the upstream and the SSH plugin // to that local copy, as CRaSH is no longer well maintained by the upstream and the SSH plugin
// that it comes with is based on a very old version of Apache SSHD which can't handle connections // that it comes with is based on a very old version of Apache SSHD which can't handle connections
// from newer SSH clients. It also means hooking things up to the authentication system. // from newer SSH clients. It also means hooking things up to the authentication system.
printBasicNodeInfo("SSH server access is not fully implemented, sorry.") Node.printBasicNodeInfo("SSH server access is not fully implemented, sorry.")
runSSH = false runSSH = false
} }

View File

@ -9,7 +9,7 @@ import net.corda.core.readLines
import net.corda.core.utilities.DUMMY_BANK_A import net.corda.core.utilities.DUMMY_BANK_A
import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.core.utilities.DUMMY_REGULATOR import net.corda.core.utilities.DUMMY_REGULATOR
import net.corda.node.LOGS_DIRECTORY_NAME import net.corda.node.internal.NodeStartup
import net.corda.node.services.api.RegulatorService import net.corda.node.services.api.RegulatorService
import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.nodeapi.ArtemisMessagingComponent import net.corda.nodeapi.ArtemisMessagingComponent
@ -74,7 +74,7 @@ class DriverTests {
assertThat(logConfigFile).isRegularFile() assertThat(logConfigFile).isRegularFile()
driver(isDebug = true, systemProperties = mapOf("log4j.configurationFile" to logConfigFile.toString())) { driver(isDebug = true, systemProperties = mapOf("log4j.configurationFile" to logConfigFile.toString())) {
val baseDirectory = startNode(DUMMY_BANK_A.name).getOrThrow().configuration.baseDirectory val baseDirectory = startNode(DUMMY_BANK_A.name).getOrThrow().configuration.baseDirectory
val logFile = (baseDirectory / LOGS_DIRECTORY_NAME).list { it.sorted().findFirst().get() } val logFile = (baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).list { it.sorted().findFirst().get() }
val debugLinesPresent = logFile.readLines { lines -> lines.anyMatch { line -> line.startsWith("[DEBUG]") } } val debugLinesPresent = logFile.readLines { lines -> lines.anyMatch { line -> line.startsWith("[DEBUG]") } }
assertThat(debugLinesPresent).isTrue() assertThat(debugLinesPresent).isTrue()
} }

View File

@ -19,7 +19,7 @@ import net.corda.core.node.NodeInfo
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.ServiceType import net.corda.core.node.services.ServiceType
import net.corda.core.utilities.* import net.corda.core.utilities.*
import net.corda.node.LOGS_DIRECTORY_NAME import net.corda.node.internal.NodeStartup
import net.corda.node.services.config.* import net.corda.node.services.config.*
import net.corda.node.services.network.NetworkMapService import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.services.transactions.RaftValidatingNotaryService
@ -698,7 +698,7 @@ class DriverDSL(
), ),
jdwpPort = debugPort, jdwpPort = debugPort,
extraJvmArguments = extraJvmArguments, extraJvmArguments = extraJvmArguments,
errorLogPath = nodeConf.baseDirectory / LOGS_DIRECTORY_NAME / "error.log", errorLogPath = nodeConf.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME / "error.log",
workingDirectory = nodeConf.baseDirectory workingDirectory = nodeConf.baseDirectory
) )
} }

View File

@ -12,6 +12,7 @@ import net.corda.core.utilities.DUMMY_CA
import net.corda.core.utilities.DUMMY_MAP import net.corda.core.utilities.DUMMY_MAP
import net.corda.core.utilities.WHITESPACE import net.corda.core.utilities.WHITESPACE
import net.corda.node.internal.Node import net.corda.node.internal.Node
import net.corda.node.serialization.NodeClock
import net.corda.node.services.config.ConfigHelper import net.corda.node.services.config.ConfigHelper
import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.config.FullNodeConfiguration
import net.corda.node.services.config.configOf import net.corda.node.services.config.configOf
@ -158,7 +159,9 @@ abstract class NodeBasedTest {
) + configOverrides ) + configOverrides
) )
val node = config.parseAs<FullNodeConfiguration>().createNode(MOCK_VERSION_INFO.copy(platformVersion = platformVersion)) val parsedConfig = config.parseAs<FullNodeConfiguration>()
val node = Node(parsedConfig, parsedConfig.calculateServices(), MOCK_VERSION_INFO.copy(platformVersion = platformVersion),
if (parsedConfig.useTestClock) TestClock() else NodeClock())
node.start() node.start()
nodes += node nodes += node
thread(name = legalName.commonName) { thread(name = legalName.commonName) {