CORDA-1838: Add subcommands to node (#4091)

* Tidy up

* Add install-shell-extensions command

* Make cli tests use same version of picocli as everything else

* Remove initLogging from NodeStartup, it is ran earlier by CordaCLIWrapper

* Use picocli snapshot for testing

* Use RunLast() parser to invoke correct subcommands

* Deprecate old clear-network-map-cache parameter

* Restructure NodeStartup for commands

* Get rid of -c option since the flag method has been deprecated and that didn't exist in last release

* Update documentation

* Update backwards compatibility test

* Get all subcommands working

* Refactor sub commands into seperate classes

* Update docs and fix some tests

* Docs changes

* Fix merge conflicts with master

* Fix renamed parameters

* Fix test failure

* Fix compatibility tests

* Add missing compatibility test for blob inspector

* Remove blob inspector compatibility test as there are import conflicts

* Assorted doc fixes

* Addressing review comments

* More review comments

* Couple more bits

* Fix broken tests

* Fix compilation error

* More merge conflicts

* Make startup logging function a bit more sensible

* Fix broken shell extensions

* Make shell extensions work with subcommands

* Make sure parameters for deprecated options are carried through

* More review comments

* Adding some s's

* One last go

* Fix compilation error on Windows

* Revert logging changes

* Revert docs back to their original imperatively moody state
This commit is contained in:
Anthony Keenan 2018-10-24 13:58:19 +01:00 committed by Tommy Lillehagen
parent 7e3aa7f30c
commit 0ab644783e
36 changed files with 716 additions and 500 deletions

View File

@ -70,7 +70,7 @@ buildscript {
ext.snappy_version = '0.4'
ext.class_graph_version = '4.2.12'
ext.jcabi_manifests_version = '1.1'
ext.picocli_version = '3.5.2'
ext.picocli_version = '3.6.1'
// Name of the IntelliJ SDK created for the deterministic Java rt.jar.
// ext.deterministic_idea_sdk = '1.8 (Deterministic)'

View File

@ -98,9 +98,9 @@ The blob inspector can be started with the following command-line options:
.. code-block:: shell
blob-inspector [-hvV] [--full-parties] [--install-shell-extensions] [--schema]
[--format=type] [--input-format=type]
[--logging-level=<loggingLevel>] [SOURCE]
blob-inspector [-hvV] [--full-parties] [--schema] [--format=type]
[--input-format=type] [--logging-level=<loggingLevel>] SOURCE
[COMMAND]
* ``--format=type``: Output format. Possible values: [YAML, JSON]. Default: YAML.
* ``--input-format=type``: Input format. If the file can't be decoded with the given value it's auto-detected, so you should
@ -109,6 +109,10 @@ The blob inspector can be started with the following command-line options:
* ``--schema``: Print the blob's schema first.
* ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file.
* ``--logging-level=<loggingLevel>``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO.
* ``--install-shell-extensions``: Install ``blob-inspector`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
* ``--help``, ``-h``: Show this help message and exit.
* ``--version``, ``-V``: Print version information and exit.
* ``--version``, ``-V``: Print version information and exit.
Sub-commands
^^^^^^^^^^^^
``install-shell-extensions``: Install ``blob-inspector`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.

View File

@ -1548,9 +1548,9 @@ New features in this release:
* Testnet
* Permissioning infrastructure phase one is built out. The node now has a notion of developer mode vs normal
mode. In developer mode it works like M3 and the SSL certificates used by nodes running on your local
machine all self-sign using a developer key included in the source tree. When developer mode is not active,
* Permissioning infrastructure phase one is built out. The node now has a notion of development mode vs normal
mode. In development mode it works like M3 and the SSL certificates used by nodes running on your local
machine all self-sign using a developer key included in the source tree. When development mode is not active,
the node won't start until it has a signed certificate. Such a certificate can be obtained by simply running
an included command line utility which generates a CSR and submits it to a permissioning service, then waits
for the signed certificate to be returned. Note that currently there is no public Corda testnet, so we are

View File

@ -10,7 +10,7 @@ Users of ``bash`` or ``zsh`` can install an alias and auto-completion for Corda
.. code-block:: shell
java -jar <name-of-JAR>.jar --install-shell-extensions
java -jar <name-of-JAR>.jar install-shell-extensions
Then, either restart your shell, or for ``bash`` users run:
@ -34,7 +34,7 @@ For example, for the Corda node, install the shell extensions using
.. code-block:: shell
java -jar corda-<version>.jar --install-shell-extensions
java -jar corda-<version>.jar install-shell-extensions
And then run the node by running:

View File

@ -50,8 +50,11 @@ Standard options
* A ``--logging-level`` option should be provided which specifies the logging level to be used in any logging files. Acceptable values should be ``DEBUG``, ``TRACE``, ``INFO``, ``WARN`` and ``ERROR``.
* ``--verbose`` and ``--log-to-console`` options should be provided (both equivalent) which specifies that logging output should be displayed in the console.
A ``-v`` short option should also be provided.
* A ``--install-shell-extensions`` option should be provided that creates and installs a bash completion file.
Standard subcommands
~~~~~~~~~~~~~~~~~~~~
* An ``install-shell-extensions`` subcommand should be provided that creates and installs a bash completion file.
Defaults
~~~~~~~~
@ -94,7 +97,7 @@ In order to use it, create a class containing your command line options using th
}
class UsefulUtility : CordaCliWrapper(
"useful-utility", // the alias to be used for this utility in bash. When --install-shell-extensions is run
"useful-utility", // the alias to be used for this utility in bash. When install-shell-extensions is run
// you will be able to invoke this command by running <useful-utility --opts> from the command line
"A command line utility that is super useful!" // A description of this utility to be displayed when --help is run
) {

View File

@ -125,7 +125,7 @@ absolute path to the node's base directory.
.. note:: The RPC SSL certificate is used by RPC clients to authenticate the connection.
The Node operator must provide RPC clients with a truststore containing the certificate they can trust.
We advise Node operators to not use the P2P keystore for RPC.
The node ships with a command line argument "--just-generate-rpc-ssl-settings", which generates a secure keystore
The node can be run with the "generate-rpc-ssl-settings" command, which generates a secure keystore
and truststore that can be used to secure the RPC connection. You can use this if you have no special requirements.

View File

@ -255,14 +255,19 @@ The network bootstrapper can be started with the following command-line options:
.. code-block:: shell
bootstrapper [-hvV] [--install-shell-extensions] [--no-copy] [--dir=<dir>]
[--logging-level=<loggingLevel>]
bootstrapper [-hvV] [--no-copy] [--dir=<dir>] [--logging-level=<loggingLevel>]
[--minimum-platform-version=<minimumPlatformVersion>] [COMMAND]
* ``--dir=<dir>``: Root directory containing the node configuration files and CorDapp JARs that will form the test network.
It may also contain existing node directories. Defaults to the current directory.
* ``--no-copy``: Don't copy the CorDapp JARs into the nodes' "cordapps" directories.
* ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file.
* ``--logging-level=<loggingLevel>``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO.
* ``--install-shell-extensions``: Install ``bootstrapper`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
* ``--help``, ``-h``: Show this help message and exit.
* ``--version``, ``-V``: Print version information and exit.
* ``--version``, ``-V``: Print version information and exit.
* ``--minimum-platform-version``: The minimum platform version to use in the generated network-parameters.
Sub-commands
^^^^^^^^^^^^
``install-shell-extensions``: Install ``bootstrapper`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.

View File

@ -63,7 +63,7 @@ be used to supplement or replace the HTTP network map. If the same node is adver
latest one is taken.
On startup the node generates its own signed node info file, filename of the format ``nodeInfo-${hash}``. It can also be
generated using the ``--just-generate-node-info`` command line flag without starting the node. To create a simple network
generated using the ``generate-node-info`` sub-command without starting the node. To create a simple network
without the HTTP network map service simply place this file in the ``additional-node-infos`` directory of every node that's
part of this network. For example, a simple way to do this is to use rsync.
@ -192,7 +192,7 @@ you either need to run from the command line:
.. code-block:: shell
java -jar corda.jar --clear-network-map-cache
java -jar corda.jar clear-network-cache
or call RPC method `clearNetworkMapCache` (it can be invoked through the node's shell as `run clearNetworkMapCache`, for more information on
how to log into node's shell see :doc:`shell`). As we are testing and hardening the implementation this step shouldn't be required.

View File

@ -48,25 +48,38 @@ Command-line options
The node can optionally be started with the following command-line options:
* ``--base-directory``, ``-b``: The node working directory where all the files are kept (default: ``.``).
* ``--clear-network-map-cache``, ``-c``: Clears local copy of network map, on node startup it will be restored from server or file system.
* ``--config-file``, ``-f``: The path to the config file. Defaults to ``node.conf``.
* ``--dev-mode``, ``-d``: Runs the node in developer mode. Unsafe in production. Defaults to true on MacOS and desktop versions of Windows. False otherwise.
* ``--initial-registration``: Start initial node registration with the compatibility zone to obtain a certificate from the Doorman.
* ``--just-generate-node-info``: Perform the node start-up task necessary to generate its nodeInfo, save it to disk, then
quit.
* ``--just-generate-rpc-ssl-settings``: Generate the ssl keystore and truststore for a secure RPC connection.
* ``--network-root-truststore``, ``-t``: Network root trust store obtained from network operator.
* ``--network-root-truststore-password``, ``-p``: Network root trust store password obtained from network operator.
* ``--dev-mode``, ``-d``: Runs the node in development mode. Unsafe in production. Defaults to true on MacOS and desktop versions of Windows. False otherwise.
* ``--no-local-shell``, ``-n``: Do not start the embedded shell locally.
* ``--on-unknown-config-keys <[FAIL,WARN,INFO]>``: How to behave on unknown node configuration. Defaults to FAIL.
* ``--sshd``: Enables SSH server for node administration.
* ``--sshd-port``: Sets the port for the SSH server. If not supplied and SSH server is enabled, the port defaults to 2222.
* ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file.
* ``--logging-level=<loggingLevel>``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO.
* ``--install-shell-extensions``: Install ``corda`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
* ``--help``, ``-h``: Show this help message and exit.
* ``--version``, ``-V``: Print version information and exit.
Sub-commands
^^^^^^^^^^^^
``bootstrap-raft-cluster``: Bootstraps Raft cluster. The node forms a single node cluster (ignoring otherwise configured peer
addresses), acting as a seed for other nodes to join the cluster.
``clear-network-cache``: Clears local copy of network map, on node startup it will be restored from server or file system.
``initial-registration``: Starts initial node registration with the compatibility zone to obtain a certificate from the Doorman.
Parameters:
* ``--network-root-truststore``, ``-t`` **required**: Network root trust store obtained from network operator.
* ``--network-root-truststore-password``, ``-p``: Network root trust store password obtained from network operator.
``generate-node-info``: Performs the node start-up tasks necessary to generate the nodeInfo file, saves it to disk, then exits.
``generate-rpc-ssl-settings``: Generates the SSL keystore and truststore for a secure RPC connection.
``install-shell-extensions``: Install ``corda`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
.. _enabling-remote-debugging:
Enabling remote debugging

View File

@ -105,28 +105,15 @@ Starting the standalone shell
Run the following command from the terminal:
Linux and MacOS
^^^^^^^^^^^^^^^
.. code:: bash
java -jar corda-tools-shell-cli-VERSION_NUMBER.jar [--config-file PATH | --cordpass-directory PATH --commands-directory PATH --host HOST --port PORT
--user USER --password PASSWORD --sshd-port PORT --sshd-hostkey-directory PATH --keystore-password PASSWORD
--keystore-file FILE --truststore-password PASSWORD --truststore-file FILE | --help]
Windows
^^^^^^^
.. code:: bash
corda-shell [-hvV] [--install-shell-extensions]
[--logging-level=<loggingLevel>] [--password=<password>]
corda-shell [-hvV] [--logging-level=<loggingLevel>] [--password=<password>]
[--sshd-hostkey-directory=<sshdHostKeyDirectory>]
[--sshd-port=<sshdPort>] [--truststore-file=<trustStoreFile>]
[--truststore-password=<trustStorePassword>]
[--truststore-type=<trustStoreType>] [--user=<user>] [-a=<host>]
[-c=<cordappDirectory>] [-f=<configFile>] [-o=<commandsDirectory>]
[-p=<port>]
[-p=<port>] [COMMAND]
Where:
@ -144,10 +131,11 @@ Where:
* ``--truststore-type=<trustStoreType>``: The type of the TrustStore (e.g. JKS).
* ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file.
* ``--logging-level=<loggingLevel>``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO.
* ``--install-shell-extensions``: Install ``corda-shell`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
* ``--help``, ``-h``: Show this help message and exit.
* ``--version``, ``-V``: Print version information and exit.
Additionally, the ``install-shell-extensions`` subcommand can be used to install the ``corda-shell`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
The format of ``config-file``:
.. code:: bash

View File

@ -65,7 +65,7 @@ internal constructor(private val initSerEnv: Boolean,
"java",
"-jar",
"corda.jar",
"--just-generate-node-info"
"generate-node-info"
)
private const val LOGS_DIR_NAME = "logs"

View File

@ -64,7 +64,7 @@ class FlowOverrideTests {
private val nodeBClasses = setOf(Ping::class.java, Pong::class.java)
@Test
fun `should use the most "specific" implementation of a responding flow`() {
fun `should use the most specific implementation of a responding flow`() {
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = emptySet())) {
val nodeA = startNode(additionalCordapps = setOf(cordappForClasses(*nodeAClasses.toTypedArray()))).getOrThrow()
val nodeB = startNode(additionalCordapps = setOf(cordappForClasses(*nodeBClasses.toTypedArray()))).getOrThrow()

View File

@ -4,11 +4,11 @@
package net.corda.node
import net.corda.cliutils.start
import net.corda.node.internal.NodeStartup
import net.corda.node.internal.NodeStartupCli
fun main(args: Array<String>) {
// Pass the arguments to the Node factory. In the Enterprise edition, this line is modified to point to a subclass.
// 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.
NodeStartup().start(args)
NodeStartupCli().start(args)
}

View File

@ -2,18 +2,18 @@ package net.corda.node
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions
import net.corda.core.internal.div
import net.corda.core.internal.exists
import net.corda.core.utilities.Try
import net.corda.node.services.config.ConfigHelper
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.NodeConfigurationImpl
import net.corda.node.services.config.parseAsNodeConfiguration
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
import picocli.CommandLine.Option
import java.nio.file.Path
import java.nio.file.Paths
class NodeCmdLineOptions {
open class SharedNodeCmdLineOptions {
@Option(
names = ["-b", "--base-directory"],
description = ["The node working directory where all the files are kept."]
@ -27,6 +27,53 @@ class NodeCmdLineOptions {
private var _configFile: Path? = null
val configFile: Path get() = _configFile ?: (baseDirectory / "node.conf")
@Option(
names = ["--on-unknown-config-keys"],
description = ["How to behave on unknown node configuration. \${COMPLETION-CANDIDATES}"]
)
var unknownConfigKeysPolicy: UnknownConfigKeysPolicy = UnknownConfigKeysPolicy.FAIL
@Option(
names = ["-d", "--dev-mode"],
description = ["Runs the node in development mode. Unsafe for production."]
)
var devMode: Boolean? = null
open fun loadConfig(): NodeConfiguration {
return getRawConfig().parseAsNodeConfiguration(unknownConfigKeysPolicy::handle)
}
protected fun getRawConfig(): Config {
val rawConfig = ConfigHelper.loadConfig(
baseDirectory,
configFile
)
if (devMode == true) {
println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}")
}
return rawConfig
}
fun copyFrom(other: SharedNodeCmdLineOptions) {
baseDirectory = other.baseDirectory
_configFile = other._configFile
unknownConfigKeysPolicy= other.unknownConfigKeysPolicy
devMode = other.devMode
}
}
class InitialRegistrationCmdLineOptions : SharedNodeCmdLineOptions() {
override fun loadConfig(): NodeConfiguration {
return getRawConfig().parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config ->
require(!config.devMode) { "Registration cannot occur in development mode" }
require(config.compatibilityZoneURL != null || config.networkServices != null) {
"compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode."
}
}
}
}
open class NodeCmdLineOptions : SharedNodeCmdLineOptions() {
@Option(
names = ["--sshd"],
description = ["If set, enables SSH server for node administration."]
@ -45,84 +92,66 @@ class NodeCmdLineOptions {
)
var noLocalShell: Boolean = false
@Option(
names = ["--initial-registration"],
description = ["Start initial node registration with Corda network to obtain certificate from the permissioning server."]
)
var isRegistration: Boolean = false
@Option(
names = ["-t", "--network-root-truststore"],
description = ["Network root trust store obtained from network operator."]
)
private var _networkRootTrustStorePath: Path? = null
val networkRootTrustStorePath: Path get() = _networkRootTrustStorePath ?: baseDirectory / "certificates" / "network-root-truststore.jks"
@Option(
names = ["-p", "--network-root-truststore-password"],
description = ["Network root trust store password obtained from network operator."]
)
var networkRootTrustStorePassword: String? = null
@Option(
names = ["--on-unknown-config-keys"],
description = ["How to behave on unknown node configuration. \${COMPLETION-CANDIDATES}"]
)
var unknownConfigKeysPolicy: UnknownConfigKeysPolicy = UnknownConfigKeysPolicy.FAIL
@Option(
names = ["-d", "--dev-mode"],
description = ["Run the node in developer mode. Unsafe for production."]
)
var devMode: Boolean? = null
@Option(
names = ["--just-generate-node-info"],
description = ["Perform the node start-up task necessary to generate its node info, save it to disk, then quit"]
description = ["DEPRECATED. Performs the node start-up tasks necessary to generate the nodeInfo file, saves it to disk, then exits."],
hidden = true
)
var justGenerateNodeInfo: Boolean = false
@Option(
names = ["--just-generate-rpc-ssl-settings"],
description = ["Generate the SSL key and trust stores for a secure RPC connection."]
description = ["DEPRECATED. Generates the SSL key and trust stores for a secure RPC connection."],
hidden = true
)
var justGenerateRpcSslCerts: Boolean = false
@Option(
names = ["-c", "--clear-network-map-cache"],
description = ["Clears local copy of network map, on node startup it will be restored from server or file system."]
names = ["--clear-network-map-cache"],
description = ["DEPRECATED. Clears local copy of network map, on node startup it will be restored from server or file system."],
hidden = true
)
var clearNetworkMapCache: Boolean = false
val nodeRegistrationOption: NodeRegistrationOption? by lazy {
if (isRegistration) {
requireNotNull(networkRootTrustStorePassword) { "Network root trust store password must be provided in registration mode using --network-root-truststore-password." }
require(networkRootTrustStorePath.exists()) { "Network root trust store path: '$networkRootTrustStorePath' doesn't exist" }
NodeRegistrationOption(networkRootTrustStorePath, networkRootTrustStorePassword!!)
} else {
null
}
}
@Option(
names = ["--initial-registration"],
description = ["DEPRECATED. Starts initial node registration with Corda network to obtain certificate from the permissioning server."],
hidden = true
)
var isRegistration: Boolean = false
fun loadConfig(): Pair<Config, Try<NodeConfiguration>> {
@Option(
names = ["-t", "--network-root-truststore"],
description = ["DEPRECATED. Network root trust store obtained from network operator."],
hidden = true
)
var networkRootTrustStorePathParameter: Path? = null
@Option(
names = ["-p", "--network-root-truststore-password"],
description = ["DEPRECATED. Network root trust store password obtained from network operator."],
hidden = true
)
var networkRootTrustStorePassword: String? = null
override fun loadConfig(): NodeConfiguration {
val rawConfig = ConfigHelper.loadConfig(
baseDirectory,
configFile,
configOverrides = ConfigFactory.parseMap(mapOf("noLocalShell" to this.noLocalShell) +
if (sshdServer) mapOf("sshd" to mapOf("port" to sshdServerPort.toString())) else emptyMap<String, Any>() +
if (devMode != null) mapOf("devMode" to this.devMode) else emptyMap())
if (devMode != null) mapOf("devMode" to this.devMode) else emptyMap())
)
return rawConfig to Try.on {
rawConfig.parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config ->
if (nodeRegistrationOption != null) {
require(!config.devMode) { "Registration cannot occur in devMode" }
require(config.compatibilityZoneURL != null || config.networkServices != null) {
"compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode."
}
return rawConfig.parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config ->
if (isRegistration) {
require(!config.devMode) { "Registration cannot occur in development mode" }
require(config.compatibilityZoneURL != null || config.networkServices != null) {
"compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode."
}
}
}
}
}
data class NodeRegistrationOption(val networkRootTrustStorePath: Path, val networkRootTrustStorePassword: String)

View File

@ -661,7 +661,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
requireNotNull(getCertificateStores()) {
"One or more keyStores (identity or TLS) or trustStore not found. " +
"Please either copy your existing keys and certificates from another node, " +
"or if you don't have one yet, fill out the config file and run corda.jar --initial-registration. " +
"or if you don't have one yet, fill out the config file and run corda.jar initial-registration. " +
"Read more at: https://docs.corda.net/permissioning.html"
}
} catch (e: KeyStoreException) {

View File

@ -6,6 +6,7 @@ import com.codahale.metrics.MetricRegistry
import com.palominolabs.metrics.newrelic.AllEnabledMetricAttributeFilter
import com.palominolabs.metrics.newrelic.NewRelicReporter
import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme
import net.corda.cliutils.ShellConstants
import net.corda.core.concurrent.CordaFuture
import net.corda.core.flows.FlowLogic
import net.corda.core.identity.CordaX500Name
@ -110,9 +111,13 @@ open class Node(configuration: NodeConfiguration,
LoggerFactory.getLogger(loggerName).info(msg)
}
fun printInRed(message: String) {
println("${ShellConstants.RED}$message${ShellConstants.RESET}")
}
fun printWarning(message: String) {
Emoji.renderIfSupported {
println("${Emoji.warningSign} ATTENTION: $message")
printInRed("${Emoji.warningSign} ATTENTION: $message")
}
staticLog.warn(message)
}
@ -132,13 +137,13 @@ open class Node(configuration: NodeConfiguration,
// TODO: make this configurable.
const val MAX_RPC_MESSAGE_SIZE = 10485760
fun isValidJavaVersion(): Boolean {
fun isInvalidJavaVersion(): Boolean {
if (!hasMinimumJavaVersion()) {
println("You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8.")
println("Corda will now exit...")
return false
return true
}
return true
return false
}
private fun hasMinimumJavaVersion(): Boolean {

View File

@ -1,31 +1,24 @@
package net.corda.node.internal
import com.typesafe.config.Config
import com.typesafe.config.ConfigException
import com.typesafe.config.ConfigRenderOptions
import io.netty.channel.unix.Errors
import net.corda.cliutils.CordaCliWrapper
import net.corda.cliutils.CordaVersionProvider
import net.corda.cliutils.ExitCodes
import net.corda.cliutils.*
import net.corda.core.crypto.Crypto
import net.corda.core.internal.*
import net.corda.core.internal.concurrent.thenMatch
import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.internal.errors.AddressBindingException
import net.corda.core.utilities.Try
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.loggerFor
import net.corda.node.*
import net.corda.node.internal.Node.Companion.isValidJavaVersion
import net.corda.node.internal.Node.Companion.isInvalidJavaVersion
import net.corda.node.internal.cordapp.MultipleCordappsForFlowException
import net.corda.node.internal.subcommands.*
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.shouldStartLocalShell
import net.corda.node.services.config.shouldStartSSHDaemon
import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NodeRegistrationException
import net.corda.node.utilities.registration.NodeRegistrationHelper
import net.corda.node.utilities.saveToKeyStore
import net.corda.node.utilities.saveToTrustStore
import net.corda.nodeapi.internal.addShutdownHook
import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException
import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
@ -33,10 +26,8 @@ import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException
import net.corda.tools.shell.InteractiveShell
import org.fusesource.jansi.Ansi
import org.slf4j.bridge.SLF4JBridgeHandler
import picocli.CommandLine.Mixin
import picocli.CommandLine.*
import sun.misc.VMSupport
import java.io.Console
import java.io.File
import java.io.IOException
import java.io.RandomAccessFile
import java.lang.management.ManagementFactory
@ -45,201 +36,145 @@ import java.nio.file.Path
import java.time.DayOfWeek
import java.time.ZonedDateTime
import java.util.*
import kotlin.system.exitProcess
/** This class is responsible for starting a Node from command line arguments. */
open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
/** An interface that can be implemented to tell the node what to do once it's intitiated. */
interface RunAfterNodeInitialisation {
fun run(node: Node)
}
/** Base class for subcommands to derive from that initialises the logs and provides standard options. */
abstract class NodeCliCommand(alias: String, description: String, val startup: NodeStartup) : CliWrapperBase(alias, description), NodeStartupLogging {
companion object {
private val logger by lazy { loggerFor<Node>() } // I guess this is lazy to allow for logging init, but why Node?
const val LOGS_DIRECTORY_NAME = "logs"
const val LOGS_CAN_BE_FOUND_IN_STRING = "Logs can be found in"
private const val INITIAL_REGISTRATION_MARKER = ".initialregistration"
}
override fun initLogging() = this.initLogging(cmdLineOptions.baseDirectory)
@Mixin
val cmdLineOptions = SharedNodeCmdLineOptions()
}
/** Main corda entry point. */
open class NodeStartupCli : CordaCliWrapper("corda", "Runs a Corda Node") {
val startup = NodeStartup()
private val networkCacheCli = ClearNetworkCacheCli(startup)
private val justGenerateNodeInfoCli = GenerateNodeInfoCli(startup)
private val justGenerateRpcSslCertsCli = GenerateRpcSslCertsCli(startup)
private val initialRegistrationCli = InitialRegistrationCli(startup)
override fun initLogging() = this.initLogging(cmdLineOptions.baseDirectory)
override fun additionalSubCommands() = setOf(networkCacheCli, justGenerateNodeInfoCli, justGenerateRpcSslCertsCli, initialRegistrationCli)
override fun runProgram(): Int {
return when {
InitialRegistration.checkRegistrationMode(cmdLineOptions.baseDirectory) -> {
println("Node was started before in `initial-registration` mode, but the registration was not completed.\nResuming registration.")
initialRegistrationCli.cmdLineOptions.copyFrom(cmdLineOptions)
initialRegistrationCli.runProgram()
}
//deal with legacy flags and redirect to subcommands
cmdLineOptions.isRegistration -> {
Node.printWarning("The --initial-registration flag has been deprecated and will be removed in a future version. Use the initial-registration command instead.")
requireNotNull(cmdLineOptions.networkRootTrustStorePassword) { "Network root trust store password must be provided in registration mode using --network-root-truststore-password." }
initialRegistrationCli.networkRootTrustStorePassword = cmdLineOptions.networkRootTrustStorePassword!!
initialRegistrationCli.networkRootTrustStorePathParameter = cmdLineOptions.networkRootTrustStorePathParameter
initialRegistrationCli.cmdLineOptions.copyFrom(cmdLineOptions)
initialRegistrationCli.runProgram()
}
cmdLineOptions.clearNetworkMapCache -> {
Node.printWarning("The --clear-network-map-cache flag has been deprecated and will be removed in a future version. Use the clear-network-cache command instead.")
networkCacheCli.cmdLineOptions.copyFrom(cmdLineOptions)
networkCacheCli.runProgram()
}
cmdLineOptions.justGenerateNodeInfo -> {
Node.printWarning("The --just-generate-node-info flag has been deprecated and will be removed in a future version. Use the generate-node-info command instead.")
justGenerateNodeInfoCli.cmdLineOptions.copyFrom(cmdLineOptions)
justGenerateNodeInfoCli.runProgram()
}
cmdLineOptions.justGenerateRpcSslCerts -> {
Node.printWarning("The --just-generate-rpc-ssl-settings flag has been deprecated and will be removed in a future version. Use the generate-rpc-ssl-settings command instead.")
justGenerateRpcSslCertsCli.cmdLineOptions.copyFrom(cmdLineOptions)
justGenerateRpcSslCertsCli.runProgram()
}
else -> startup.initialiseAndRun(cmdLineOptions, object : RunAfterNodeInitialisation {
val startupTime = System.currentTimeMillis()
override fun run(node: Node) = startup.startNode(node, startupTime)
})
}
}
@Mixin
val cmdLineOptions = NodeCmdLineOptions()
}
/** This class provides a common set of functionality for starting a Node from command line arguments. */
open class NodeStartup : NodeStartupLogging {
companion object {
private val logger by lazy { loggerFor<Node>() } // I guess this is lazy to allow for logging init, but why Node?
const val LOGS_DIRECTORY_NAME = "logs"
const val LOGS_CAN_BE_FOUND_IN_STRING = "Logs can be found in"
}
lateinit var cmdLineOptions: SharedNodeCmdLineOptions
fun initialiseAndRun(cmdLineOptions: SharedNodeCmdLineOptions, afterNodeInitialisation: RunAfterNodeInitialisation): Int {
this.cmdLineOptions = cmdLineOptions
/**
* @return exit code based on the success of the node startup. This value is intended to be the exit code of the process.
*/
override fun runProgram(): Int {
val startTime = System.currentTimeMillis()
// Step 1. Check for supported Java version.
if (!isValidJavaVersion()) return ExitCodes.FAILURE
if (isInvalidJavaVersion()) return ExitCodes.FAILURE
// Step 2. 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)
// Step 3. Initialise logging.
initLogging()
// Step 4. Register all cryptography [Provider]s.
// Step 3. Register all cryptography [Provider]s.
// Required to install our [SecureRandom] before e.g., UUID asks for one.
// This needs to go after initLogging(netty clashes with our logging).
// This needs to go after initLogging(netty clashes with our logging)
Crypto.registerProviders()
// Step 5. Print banner and basic node info.
// Step 4. Print banner and basic node info.
val versionInfo = getVersionInfo()
drawBanner(versionInfo)
Node.printBasicNodeInfo(LOGS_CAN_BE_FOUND_IN_STRING, System.getProperty("log-path"))
// Step 6. Load and validate node configuration.
val configuration = (attempt { loadConfiguration() }.doOnException(handleConfigurationLoadingError(cmdLineOptions.configFile)) as? Try.Success)?.let(Try.Success<NodeConfiguration>::value) ?: return ExitCodes.FAILURE
// Step 5. Load and validate node configuration.
val configuration = (attempt { cmdLineOptions.loadConfig() }.doOnException(handleConfigurationLoadingError(cmdLineOptions.configFile)) as? Try.Success)?.let(Try.Success<NodeConfiguration>::value)
?: return ExitCodes.FAILURE
val errors = configuration.validate()
if (errors.isNotEmpty()) {
logger.error("Invalid node configuration. Errors were:${System.lineSeparator()}${errors.joinToString(System.lineSeparator())}")
return ExitCodes.FAILURE
}
// Step 7. Configuring special serialisation requirements, i.e., bft-smart relies on Java serialization.
attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success ?: return ExitCodes.FAILURE
// Step 6. Configuring special serialisation requirements, i.e., bft-smart relies on Java serialization.
attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success
?: return ExitCodes.FAILURE
// Step 8. Any actions required before starting up the Corda network layer.
attempt { preNetworkRegistration(configuration) }.doOnException(handleRegistrationError) as? Try.Success ?: return ExitCodes.FAILURE
// Step 7. Any actions required before starting up the Corda network layer.
attempt { preNetworkRegistration(configuration) }.doOnException(::handleRegistrationError) as? Try.Success
?: return ExitCodes.FAILURE
// Step 9. Check if in registration mode.
checkAndRunRegistrationMode(configuration, versionInfo)?.let {
return if (it) ExitCodes.SUCCESS
else ExitCodes.FAILURE
}
// Step 10. Log startup info.
// Step 8. Log startup info.
logStartupInfo(versionInfo, configuration)
// Step 11. Start node: create the node, check for other command-line options, add extra logging etc.
attempt { startNode(configuration, versionInfo, startTime) }.doOnSuccess { logger.info("Node exiting successfully") }.doOnException(handleStartError) as? Try.Success ?: return ExitCodes.FAILURE
// Step 9. Start node: create the node, check for other command-line options, add extra logging etc.
attempt {
cmdLineOptions.baseDirectory.createDirectories()
afterNodeInitialisation.run(createNode(configuration, versionInfo))
}.doOnException(::handleStartError) as? Try.Success ?: return ExitCodes.FAILURE
return ExitCodes.SUCCESS
}
private fun checkAndRunRegistrationMode(configuration: NodeConfiguration, versionInfo: VersionInfo): Boolean? {
checkUnfinishedRegistration()
cmdLineOptions.nodeRegistrationOption?.let {
// Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig]
attempt { registerWithNetwork(configuration, versionInfo, it) }.doOnException(handleRegistrationError) as? Try.Success
?: return false
// At this point the node registration was successful. We can delete the marker file.
deleteNodeRegistrationMarker(cmdLineOptions.baseDirectory)
return true
}
return null
}
// TODO: Reconsider if automatic re-registration should be applied when something failed during initial registration.
// There might be cases where the node user should investigate what went wrong before registering again.
private fun checkUnfinishedRegistration() {
if (checkRegistrationMode() && !cmdLineOptions.isRegistration) {
println("Node was started before with `--initial-registration`, but the registration was not completed.\nResuming registration.")
// Pretend that the node was started with `--initial-registration` to help prevent user error.
cmdLineOptions.isRegistration = true
}
}
private fun <RESULT> attempt(action: () -> RESULT): Try<RESULT> = Try.on(action)
private fun Exception.isExpectedWhenStartingNode() = startNodeExpectedErrors.any { error -> error.isInstance(this) }
private val startNodeExpectedErrors = setOf(MultipleCordappsForFlowException::class, CheckpointIncompatibleException::class, AddressBindingException::class, NetworkParametersReader::class, DatabaseIncompatibleException::class)
private fun Exception.logAsExpected(message: String? = this.message, print: (String?) -> Unit = logger::error) = print(message)
private fun Exception.logAsUnexpected(message: String? = this.message, error: Exception = this, print: (String?, Throwable) -> Unit = logger::error) = print("$message${this.message?.let { ": $it" } ?: ""}", error)
private fun Exception.isOpenJdkKnownIssue() = message?.startsWith("Unknown named curve:") == true
private val handleRegistrationError = { error: Exception ->
when (error) {
is NodeRegistrationException -> error.logAsExpected("Issue with Node registration: ${error.message}")
else -> error.logAsUnexpected("Exception during node registration")
}
}
private val handleStartError = { error: Exception ->
when {
error.isExpectedWhenStartingNode() -> error.logAsExpected()
error is CouldNotCreateDataSourceException -> error.logAsUnexpected()
error is Errors.NativeIoException && error.message?.contains("Address already in use") == true -> error.logAsExpected("One of the ports required by the Corda node is already in use.")
error.isOpenJdkKnownIssue() -> error.logAsExpected("Exception during node startup - ${error.message}. This is a known OpenJDK issue on some Linux distributions, please use OpenJDK from zulu.org or Oracle JDK.")
else -> error.logAsUnexpected("Exception during node startup")
}
}
private fun handleConfigurationLoadingError(configFile: Path) = { error: Exception ->
when (error) {
is UnknownConfigurationKeysException -> error.logAsExpected()
is ConfigException.IO -> error.logAsExpected(configFileNotFoundMessage(configFile), ::println)
else -> error.logAsUnexpected("Unexpected error whilst reading node configuration")
}
}
private fun configFileNotFoundMessage(configFile: Path): String {
return """
Unable to load the node config file from '$configFile'.
Try setting the --base-directory flag to change which directory the node
is looking in, or use the --config-file flag to specify it explicitly.
""".trimIndent()
}
private fun loadConfiguration(): NodeConfiguration {
val (rawConfig, configurationResult) = loadConfigFile()
if (cmdLineOptions.devMode == true) {
println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}")
}
return configurationResult.getOrThrow()
}
private fun checkRegistrationMode(): Boolean {
// If the node was started with `--initial-registration`, create marker file.
// We do this here to ensure the marker is created even if parsing the args with NodeArgsParser fails.
val marker = cmdLineOptions.baseDirectory / INITIAL_REGISTRATION_MARKER
if (!cmdLineOptions.isRegistration && !marker.exists()) {
return false
}
try {
marker.createFile()
} catch (e: Exception) {
logger.warn("Could not create marker file for `--initial-registration`.", e)
}
return true
}
private fun deleteNodeRegistrationMarker(baseDir: Path) {
try {
val marker = File((baseDir / INITIAL_REGISTRATION_MARKER).toUri())
if (marker.exists()) {
marker.delete()
}
} catch (e: Exception) {
e.logAsUnexpected("Could not delete the marker file that was created for `--initial-registration`.", print = logger::warn)
}
}
protected open fun preNetworkRegistration(conf: NodeConfiguration) = Unit
protected open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo)
open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo)
protected open fun startNode(conf: NodeConfiguration, versionInfo: VersionInfo, startTime: Long) {
cmdLineOptions.baseDirectory.createDirectories()
val node = createNode(conf, versionInfo)
if (cmdLineOptions.clearNetworkMapCache) {
node.clearNetworkMapCache()
return
}
if (cmdLineOptions.justGenerateNodeInfo) {
// Perform the minimum required start-up logic to be able to write a nodeInfo to disk
node.generateAndSaveNodeInfo()
return
}
if (cmdLineOptions.justGenerateRpcSslCerts) {
generateRpcSslCertificates(conf)
return
}
if (conf.devMode) {
fun startNode(node: Node, startTime: Long) {
if (node.configuration.devMode) {
Emoji.renderIfSupported {
Node.printWarning("This node is running in developer mode! ${Emoji.developer} This is not safe for production deployment.")
Node.printWarning("This node is running in development mode! ${Emoji.developer} This is not safe for production deployment.")
}
} else {
logger.info("The Corda node is running in production mode. If this is a developer environment you can set 'devMode=true' in the node.conf file.")
@ -256,7 +191,7 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
Node.printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec")
// Don't start the shell if there's no console attached.
if (conf.shouldStartLocalShell()) {
if (node.configuration.shouldStartLocalShell()) {
node.startupComplete.then {
try {
InteractiveShell.runLocalShell(node::stop)
@ -265,8 +200,8 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
}
}
}
if (conf.shouldStartSSHDaemon()) {
Node.printBasicNodeInfo("SSH server listening on port", conf.sshd!!.port.toString())
if (node.configuration.shouldStartSSHDaemon()) {
Node.printBasicNodeInfo("SSH server listening on port", node.configuration.sshd!!.port.toString())
}
},
{ th ->
@ -275,82 +210,6 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
node.run()
}
private fun generateRpcSslCertificates(conf: NodeConfiguration) {
val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(conf.myLegalName.x500Principal)
val keyStorePath = conf.baseDirectory / "certificates" / "rpcsslkeystore.jks"
val trustStorePath = conf.baseDirectory / "certificates" / "export" / "rpcssltruststore.jks"
if (keyStorePath.exists() || trustStorePath.exists()) {
println("Found existing RPC SSL keystores. Command was already run. Exiting..")
exitProcess(0)
}
val console: Console? = System.console()
when (console) {
// In this case, the JVM is not connected to the console so we need to exit.
null -> {
println("Not connected to console. Exiting")
exitProcess(1)
}
// Otherwise we can proceed normally.
else -> {
while (true) {
val keystorePassword1 = console.readPassword("Enter the RPC keystore password => ")
// TODO: consider adding a password strength policy.
if (keystorePassword1.isEmpty()) {
println("The RPC keystore password cannot be an empty String.")
continue
}
val keystorePassword2 = console.readPassword("Re-enter the RPC keystore password => ")
if (!keystorePassword1.contentEquals(keystorePassword2)) {
println("The RPC keystore passwords don't match.")
continue
}
saveToKeyStore(keyStorePath, keyPair, cert, String(keystorePassword1), "rpcssl")
println("The RPC keystore was saved to: $keyStorePath .")
break
}
while (true) {
val trustStorePassword1 = console.readPassword("Enter the RPC truststore password => ")
// TODO: consider adding a password strength policy.
if (trustStorePassword1.isEmpty()) {
println("The RPC truststore password cannot be an empty String.")
continue
}
val trustStorePassword2 = console.readPassword("Re-enter the RPC truststore password => ")
if (!trustStorePassword1.contentEquals(trustStorePassword2)) {
println("The RPC truststore passwords don't match.")
continue
}
saveToTrustStore(trustStorePath, cert, String(trustStorePassword1), "rpcssl")
println("The RPC truststore was saved to: $trustStorePath .")
println("You need to distribute this file along with the password in a secure way to all RPC clients.")
break
}
val dollar = '$'
println("""
|
|The SSL certificates for RPC were generated successfully.
|
|Add this snippet to the "rpcSettings" section of your node.conf:
| useSsl=true
| ssl {
| keyStorePath=$dollar{baseDirectory}/certificates/rpcsslkeystore.jks
| keyStorePassword=the_above_password
| }
|""".trimMargin())
}
}
}
protected open fun logStartupInfo(versionInfo: VersionInfo, conf: NodeConfiguration) {
logger.info("Vendor: ${versionInfo.vendor}")
logger.info("Release: ${versionInfo.releaseVersion}")
@ -376,40 +235,12 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
logger.info(nodeStartedMessage)
}
protected open fun registerWithNetwork(
conf: NodeConfiguration,
versionInfo: VersionInfo,
nodeRegistrationConfig: NodeRegistrationOption
) {
println("\n" +
"******************************************************************\n" +
"* *\n" +
"* Registering as a new participant with a Corda network *\n" +
"* *\n" +
"******************************************************************\n")
NodeRegistrationHelper(conf,
HTTPNetworkRegistrationService(
requireNotNull(conf.networkServices),
versionInfo),
nodeRegistrationConfig).buildKeystore()
// Minimal changes to make registration tool create node identity.
// TODO: Move node identity generation logic from node to registration helper.
createNode(conf, getVersionInfo()).generateAndSaveNodeInfo()
println("Successfully registered Corda node with compatibility zone, node identity keys and certificates are stored in '${conf.certificatesDirectory}', it is advised to backup the private keys and certificates.")
println("Corda node will now terminate.")
}
protected open fun loadConfigFile(): Pair<Config, Try<NodeConfiguration>> = cmdLineOptions.loadConfig()
protected open fun banJavaSerialisation(conf: NodeConfiguration) {
// Note that in dev mode this filter can be overridden by a notary service implementation.
SerialFilter.install(::defaultSerialFilter)
}
protected open fun getVersionInfo(): VersionInfo {
open fun getVersionInfo(): VersionInfo {
return VersionInfo(
PLATFORM_VERSION,
CordaVersionProvider.releaseVersion,
@ -462,18 +293,6 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
}
}
override fun initLogging() {
val loggingLevel = loggingLevel.name.toLowerCase(Locale.ENGLISH)
System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file.
if (verbose) {
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
@ -577,3 +396,66 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
}
}
/** Provide some common logging methods for node startup commands. */
interface NodeStartupLogging {
companion object {
val logger by lazy { contextLogger() }
val startupErrors = setOf(MultipleCordappsForFlowException::class, CheckpointIncompatibleException::class, AddressBindingException::class, NetworkParametersReader::class, DatabaseIncompatibleException::class)
}
fun <RESULT> attempt(action: () -> RESULT): Try<RESULT> = Try.on(action)
fun Exception.logAsExpected(message: String? = this.message, print: (String?) -> Unit = logger::error) = print(message)
fun Exception.logAsUnexpected(message: String? = this.message, error: Exception = this, print: (String?, Throwable) -> Unit = logger::error) = print("$message${this.message?.let { ": $it" } ?: ""}", error)
fun handleRegistrationError(error: Exception) {
when (error) {
is NodeRegistrationException -> error.logAsExpected("Issue with Node registration: ${error.message}")
else -> error.logAsUnexpected("Exception during node registration")
}
}
fun Exception.isOpenJdkKnownIssue() = message?.startsWith("Unknown named curve:") == true
fun Exception.isExpectedWhenStartingNode() = startupErrors.any { error -> error.isInstance(this) }
fun handleStartError(error: Exception) {
when {
error.isExpectedWhenStartingNode() -> error.logAsExpected()
error is CouldNotCreateDataSourceException -> error.logAsUnexpected()
error is Errors.NativeIoException && error.message?.contains("Address already in use") == true -> error.logAsExpected("One of the ports required by the Corda node is already in use.")
error.isOpenJdkKnownIssue() -> error.logAsExpected("Exception during node startup - ${error.message}. This is a known OpenJDK issue on some Linux distributions, please use OpenJDK from zulu.org or Oracle JDK.")
else -> error.logAsUnexpected("Exception during node startup")
}
}
fun handleConfigurationLoadingError(configFile: Path) = { error: Exception ->
when (error) {
is UnknownConfigurationKeysException -> error.logAsExpected()
is ConfigException.IO -> error.logAsExpected(configFileNotFoundMessage(configFile), ::println)
else -> error.logAsUnexpected("Unexpected error whilst reading node configuration")
}
}
private fun configFileNotFoundMessage(configFile: Path): String {
return """
Unable to load the node config file from '$configFile'.
Try setting the --base-directory flag to change which directory the node
is looking in, or use the --config-file flag to specify it explicitly.
""".trimIndent()
}
}
fun CliWrapperBase.initLogging(baseDirectory: Path) {
val loggingLevel = loggingLevel.name.toLowerCase(Locale.ENGLISH)
System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file.
if (verbose) {
System.setProperty("consoleLogLevel", loggingLevel)
Node.renderBasicInfoToConsole = false
}
System.setProperty("log-path", (baseDirectory / NodeCliCommand.LOGS_DIRECTORY_NAME).toString())
SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler.
SLF4JBridgeHandler.install()
}

View File

@ -0,0 +1,14 @@
package net.corda.node.internal.subcommands
import net.corda.node.internal.Node
import net.corda.node.internal.NodeCliCommand
import net.corda.node.internal.NodeStartup
import net.corda.node.internal.RunAfterNodeInitialisation
class ClearNetworkCacheCli(startup: NodeStartup): NodeCliCommand("clear-network-cache", "Clears local copy of network map, on node startup it will be restored from server or file system.", startup) {
override fun runProgram(): Int {
return startup.initialiseAndRun(cmdLineOptions, object: RunAfterNodeInitialisation {
override fun run(node: Node) = node.clearNetworkMapCache()
})
}
}

View File

@ -0,0 +1,16 @@
package net.corda.node.internal.subcommands
import net.corda.node.internal.Node
import net.corda.node.internal.NodeCliCommand
import net.corda.node.internal.NodeStartup
import net.corda.node.internal.RunAfterNodeInitialisation
class GenerateNodeInfoCli(startup: NodeStartup): NodeCliCommand("generate-node-info", "Performs the node start-up tasks necessary to generate the nodeInfo file, saves it to disk, then exits.", startup) {
override fun runProgram(): Int {
return startup.initialiseAndRun(cmdLineOptions, object : RunAfterNodeInitialisation {
override fun run(node: Node) {
node.generateAndSaveNodeInfo()
}
})
}
}

View File

@ -0,0 +1,103 @@
package net.corda.node.internal.subcommands
import net.corda.core.internal.div
import net.corda.core.internal.exists
import net.corda.node.internal.Node
import net.corda.node.internal.NodeCliCommand
import net.corda.node.internal.NodeStartup
import net.corda.node.internal.RunAfterNodeInitialisation
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate
import net.corda.node.utilities.saveToKeyStore
import net.corda.node.utilities.saveToTrustStore
import java.io.Console
import kotlin.system.exitProcess
class GenerateRpcSslCertsCli(startup: NodeStartup): NodeCliCommand("generate-rpc-ssl-settings", "Generates the SSL key and trust stores for a secure RPC connection.", startup) {
override fun runProgram(): Int {
return startup.initialiseAndRun(cmdLineOptions, GenerateRpcSslCerts())
}
}
class GenerateRpcSslCerts: RunAfterNodeInitialisation {
override fun run(node: Node) {
generateRpcSslCertificates(node.configuration)
}
private fun generateRpcSslCertificates(conf: NodeConfiguration) {
val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(conf.myLegalName.x500Principal)
val keyStorePath = conf.baseDirectory / "certificates" / "rpcsslkeystore.jks"
val trustStorePath = conf.baseDirectory / "certificates" / "export" / "rpcssltruststore.jks"
if (keyStorePath.exists() || trustStorePath.exists()) {
println("Found existing RPC SSL keystores. Command was already run. Exiting.")
exitProcess(0)
}
val console: Console? = System.console()
when (console) {
// In this case, the JVM is not connected to the console so we need to exit.
null -> {
println("Not connected to console. Exiting.")
exitProcess(1)
}
// Otherwise we can proceed normally.
else -> {
while (true) {
val keystorePassword1 = console.readPassword("Enter the RPC keystore password:")
// TODO: consider adding a password strength policy.
if (keystorePassword1.isEmpty()) {
println("The RPC keystore password cannot be an empty String.")
continue
}
val keystorePassword2 = console.readPassword("Re-enter the RPC keystore password:")
if (!keystorePassword1.contentEquals(keystorePassword2)) {
println("The RPC keystore passwords don't match.")
continue
}
saveToKeyStore(keyStorePath, keyPair, cert, String(keystorePassword1), "rpcssl")
println("The RPC keystore was saved to: $keyStorePath .")
break
}
while (true) {
val trustStorePassword1 = console.readPassword("Enter the RPC truststore password:")
// TODO: consider adding a password strength policy.
if (trustStorePassword1.isEmpty()) {
println("The RPC truststore password cannot be an empty string.")
continue
}
val trustStorePassword2 = console.readPassword("Re-enter the RPC truststore password:")
if (!trustStorePassword1.contentEquals(trustStorePassword2)) {
println("The RPC truststore passwords don't match.")
continue
}
saveToTrustStore(trustStorePath, cert, String(trustStorePassword1), "rpcssl")
println("The RPC truststore was saved to: $trustStorePath.")
println("You need to distribute this file along with the password in a secure way to all RPC clients.")
break
}
val dollar = '$'
println("""
|
|The SSL certificates for RPC were generated successfully.
|
|Add this snippet to the "rpcSettings" section of your node.conf:
| useSsl=true
| ssl {
| keyStorePath=$dollar{baseDirectory}/certificates/rpcsslkeystore.jks
| keyStorePassword=the_above_password
| }
|""".trimMargin())
}
}
}
}

View File

@ -0,0 +1,110 @@
package net.corda.node.internal.subcommands
import net.corda.cliutils.CliWrapperBase
import net.corda.core.internal.createFile
import net.corda.core.internal.div
import net.corda.core.internal.exists
import net.corda.core.utilities.Try
import net.corda.node.InitialRegistrationCmdLineOptions
import net.corda.node.NodeRegistrationOption
import net.corda.node.internal.*
import net.corda.node.internal.NodeStartupLogging.Companion.logger
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NodeRegistrationHelper
import picocli.CommandLine.Mixin
import picocli.CommandLine.Option
import java.io.File
import java.nio.file.Path
class InitialRegistrationCli(val startup: NodeStartup): CliWrapperBase("initial-registration", "Starts initial node registration with Corda network to obtain certificate from the permissioning server.") {
@Option(names = ["-t", "--network-root-truststore"], description = ["Network root trust store obtained from network operator."])
var networkRootTrustStorePathParameter: Path? = null
@Option(names = ["-p", "--network-root-truststore-password"], description = ["Network root trust store password obtained from network operator."], required = true)
var networkRootTrustStorePassword: String = ""
override fun runProgram() : Int {
val networkRootTrustStorePath: Path = networkRootTrustStorePathParameter ?: cmdLineOptions.baseDirectory / "certificates" / "network-root-truststore.jks"
return startup.initialiseAndRun(cmdLineOptions, InitialRegistration(cmdLineOptions.baseDirectory, networkRootTrustStorePath, networkRootTrustStorePassword, startup))
}
override fun initLogging() = this.initLogging(cmdLineOptions.baseDirectory)
@Mixin
val cmdLineOptions = InitialRegistrationCmdLineOptions()
}
class InitialRegistration(val baseDirectory: Path, private val networkRootTrustStorePath: Path, networkRootTrustStorePassword: String, private val startup: NodeStartup) : RunAfterNodeInitialisation, NodeStartupLogging {
companion object {
private const val INITIAL_REGISTRATION_MARKER = ".initialregistration"
fun checkRegistrationMode(baseDirectory: Path): Boolean {
// If the node was started with `--initial-registration`, create marker file.
// We do this here to ensure the marker is created even if parsing the args with NodeArgsParser fails.
val marker = baseDirectory / INITIAL_REGISTRATION_MARKER
if (!marker.exists()) {
return false
}
try {
marker.createFile()
} catch (e: Exception) {
logger.warn("Could not create marker file for `initial-registration`.", e)
}
return true
}
}
private val nodeRegistration = NodeRegistrationOption(networkRootTrustStorePath, networkRootTrustStorePassword)
private fun registerWithNetwork(conf: NodeConfiguration) {
val versionInfo = startup.getVersionInfo()
println("\n" +
"******************************************************************\n" +
"* *\n" +
"* Registering as a new participant with a Corda network *\n" +
"* *\n" +
"******************************************************************\n")
NodeRegistrationHelper(conf,
HTTPNetworkRegistrationService(
requireNotNull(conf.networkServices),
versionInfo),
nodeRegistration).buildKeystore()
// Minimal changes to make registration tool create node identity.
// TODO: Move node identity generation logic from node to registration helper.
startup.createNode(conf, versionInfo).generateAndSaveNodeInfo()
println("Successfully registered Corda node with compatibility zone, node identity keys and certificates are stored in '${conf.certificatesDirectory}', it is advised to backup the private keys and certificates.")
println("Corda node will now terminate.")
}
private fun initialRegistration(config: NodeConfiguration) {
// Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig]
attempt { registerWithNetwork(config) }.doOnException(this::handleRegistrationError) as? Try.Success
// At this point the node registration was successful. We can delete the marker file.
deleteNodeRegistrationMarker(baseDirectory)
}
private fun deleteNodeRegistrationMarker(baseDir: Path) {
try {
val marker = File((baseDir / INITIAL_REGISTRATION_MARKER).toUri())
if (marker.exists()) {
marker.delete()
}
} catch (e: Exception) {
e.logAsUnexpected( "Could not delete the marker file that was created for `initial-registration`.", print = logger::warn)
}
}
override fun run(node: Node) {
require(networkRootTrustStorePath.exists()) { "Network root trust store path: '$networkRootTrustStorePath' doesn't exist" }
if (checkRegistrationMode(baseDirectory)) {
println("Node was started before with `--initial-registration`, but the registration was not completed.\nResuming registration.")
}
initialRegistration(node.configuration)
}
}

View File

@ -26,11 +26,6 @@
required: false
multiParam: false
acceptableValues: []
- parameterName: "--install-shell-extensions"
parameterType: "boolean"
required: false
multiParam: false
acceptableValues: []
- parameterName: "--just-generate-node-info"
parameterType: "boolean"
required: false
@ -99,11 +94,6 @@
required: false
multiParam: true
acceptableValues: []
- parameterName: "-c"
parameterType: "boolean"
required: false
multiParam: false
acceptableValues: []
- parameterName: "-d"
parameterType: "java.lang.Boolean"
required: false

View File

@ -2,4 +2,4 @@ package net.corda.node.internal
import net.corda.testing.CliBackwardsCompatibleTest
class NodeStartupCompatibilityTest : CliBackwardsCompatibleTest(NodeStartup::class.java)
class NodeStartupCompatibilityTest : CliBackwardsCompatibleTest(NodeStartupCli::class.java)

View File

@ -1,6 +1,8 @@
package net.corda.node.internal
import net.corda.core.internal.div
import net.corda.node.InitialRegistrationCmdLineOptions
import net.corda.node.internal.subcommands.InitialRegistrationCli
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
import org.assertj.core.api.Assertions.assertThat
import org.junit.BeforeClass
@ -11,7 +13,7 @@ import java.nio.file.Path
import java.nio.file.Paths
class NodeStartupTest {
private val startup = NodeStartup()
private val startup = NodeStartupCli()
companion object {
private lateinit var workingDirectory: Path
@ -30,7 +32,6 @@ class NodeStartupTest {
assertThat(startup.cmdLineOptions.configFile).isEqualTo(workingDirectory / "node.conf")
assertThat(startup.verbose).isEqualTo(false)
assertThat(startup.loggingLevel).isEqualTo(Level.INFO)
assertThat(startup.cmdLineOptions.nodeRegistrationOption).isEqualTo(null)
assertThat(startup.cmdLineOptions.noLocalShell).isEqualTo(false)
assertThat(startup.cmdLineOptions.sshdServer).isEqualTo(false)
assertThat(startup.cmdLineOptions.justGenerateNodeInfo).isEqualTo(false)
@ -38,7 +39,7 @@ class NodeStartupTest {
assertThat(startup.cmdLineOptions.unknownConfigKeysPolicy).isEqualTo(UnknownConfigKeysPolicy.FAIL)
assertThat(startup.cmdLineOptions.devMode).isEqualTo(null)
assertThat(startup.cmdLineOptions.clearNetworkMapCache).isEqualTo(false)
assertThat(startup.cmdLineOptions.networkRootTrustStorePath).isEqualTo(workingDirectory / "certificates" / "network-root-truststore.jks")
assertThat(startup.cmdLineOptions.networkRootTrustStorePathParameter).isEqualTo(null)
}
@Test
@ -46,6 +47,6 @@ class NodeStartupTest {
CommandLine.populateCommand(startup, "--base-directory", (workingDirectory / "another-base-dir").toString())
assertThat(startup.cmdLineOptions.baseDirectory).isEqualTo(workingDirectory / "another-base-dir")
assertThat(startup.cmdLineOptions.configFile).isEqualTo(workingDirectory / "another-base-dir" / "node.conf")
assertThat(startup.cmdLineOptions.networkRootTrustStorePath).isEqualTo(workingDirectory / "another-base-dir" / "certificates" / "network-root-truststore.jks")
assertThat(startup.cmdLineOptions.networkRootTrustStorePathParameter).isEqualTo(null)
}
}

View File

@ -322,7 +322,7 @@ class DriverDSLImpl(
} else {
startOutOfProcessMiniNode(
config,
"--initial-registration",
"initial-registration",
"--network-root-truststore=${rootTruststorePath.toAbsolutePath()}",
"--network-root-truststore-password=$rootTruststorePassword"
).map { config }
@ -457,7 +457,7 @@ class DriverDSLImpl(
} else {
// TODO The config we use here is uses a hardocded p2p port which changes when the node is run proper
// This causes two node info files to be generated.
startOutOfProcessMiniNode(config, "--just-generate-node-info").map {
startOutOfProcessMiniNode(config, "generate-node-info").map {
// Once done we have to read the signed node info file that's been generated
val nodeInfoFile = config.corda.baseDirectory.list { paths ->
paths.filter { it.fileName.toString().startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get()

View File

@ -32,6 +32,7 @@ import rx.internal.schedulers.CachedThreadScheduler
import java.nio.file.Path
import java.util.concurrent.Executors
import kotlin.concurrent.thread
import kotlin.test.assertFalse
// TODO Some of the logic here duplicates what's in the driver - the reason why it's not straightforward to replace it by
// using DriverDSLImpl in `init()` and `stopAllNodes()` is because of the platform version passed to nodes (driver doesn't
@ -150,9 +151,8 @@ abstract class NodeBasedTest(private val cordappPackages: List<String> = emptyLi
}
class InProcessNode(configuration: NodeConfiguration, versionInfo: VersionInfo, flowManager: FlowManager = NodeFlowManager(configuration.flowOverrides)) : Node(configuration, versionInfo, false, flowManager = flowManager) {
override fun start() : NodeInfo {
check(isValidJavaVersion()) { "You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8." }
assertFalse(isInvalidJavaVersion(), "You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8." )
return super.start()
}

View File

@ -2,7 +2,7 @@ apply plugin: 'java'
apply plugin: 'kotlin'
dependencies {
compile group: 'info.picocli', name: 'picocli', version: '3.0.1'
compile "info.picocli:picocli:$picocli_version"
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
compile group: "com.fasterxml.jackson.dataformat", name: "jackson-dataformat-yaml", version: "2.9.0"
compile group: "com.fasterxml.jackson.core", name: "jackson-databind", version: "2.9.0"

View File

@ -14,7 +14,6 @@ dependencies {
exclude module: 'node-api'
exclude module: 'finance'
}
testCompile project(':test-utils')
}
jar {

View File

@ -47,7 +47,7 @@ class BlobInspector : CordaCliWrapper("blob-inspector", "Convert AMQP serialised
description = ["Display the owningKey and certPath properties of Party and PartyAndReference objects respectively"])
private var fullParties: Boolean = false
@Option(names = ["--schema"], description = ["Print the blob's schema first"])
@Option(names = ["--schema"], description = ["Prints the blob's schema first"])
private var schema: Boolean = false
override fun runProgram() = run(System.out)

View File

@ -0,0 +1,58 @@
- commandName: "<main class>"
positionalParams:
- parameterName: "0"
parameterType: "java.net.URL"
required: true
multiParam: false
acceptableValues: []
params:
- parameterName: "--format"
parameterType: "net.corda.blobinspector.OutputFormatType"
required: false
multiParam: false
acceptableValues:
- "YAML"
- "JSON"
- parameterName: "--full-parties"
parameterType: "boolean"
required: false
multiParam: false
acceptableValues: []
- parameterName: "--input-format"
parameterType: "net.corda.blobinspector.InputFormatType"
required: false
multiParam: false
acceptableValues:
- "BINARY"
- "HEX"
- "BASE64"
- parameterName: "--log-to-console"
parameterType: "boolean"
required: false
multiParam: false
acceptableValues: []
- parameterName: "--logging-level"
parameterType: "org.slf4j.event.Level"
required: false
multiParam: false
acceptableValues:
- "ERROR"
- "WARN"
- "INFO"
- "DEBUG"
- "TRACE"
- parameterName: "--schema"
parameterType: "boolean"
required: false
multiParam: false
acceptableValues: []
- parameterName: "--verbose"
parameterType: "boolean"
required: false
multiParam: false
acceptableValues: []
- parameterName: "-v"
parameterType: "boolean"
required: false
multiParam: false
acceptableValues: []

View File

@ -25,7 +25,7 @@ class NetworkBootstrapperRunner : CordaCliWrapper("bootstrapper", "Bootstrap a l
@Option(names = ["--no-copy"], description = ["""Don't copy the CorDapp JARs into the nodes' "cordapps" directories."""])
private var noCopy: Boolean = false
@Option(names = ["--minimum-platform-version"], description = ["The minimumPlatformVersion to use in the network-parameters"])
@Option(names = ["--minimum-platform-version"], description = ["The minimumPlatformVersion to use in the network-parameters."])
private var minimumPlatformVersion = PLATFORM_VERSION
override fun runProgram(): Int {

View File

@ -6,11 +6,6 @@
required: false
multiParam: true
acceptableValues: []
- parameterName: "--install-shell-extensions"
parameterType: "boolean"
required: false
multiParam: false
acceptableValues: []
- parameterName: "--log-to-console"
parameterType: "boolean"
required: false

View File

@ -69,57 +69,43 @@ fun CordaCliWrapper.start(args: Array<String>) {
cmd.registerConverter(Path::class.java) { Paths.get(it).toAbsolutePath().normalize() }
cmd.commandSpec.name(alias)
cmd.commandSpec.usageMessage().description(description)
cmd.commandSpec.parser().collectErrors(true)
this.subCommands().forEach {
val subCommand = CommandLine(it)
it.args = args
subCommand.commandSpec.usageMessage().description(it.description)
cmd.commandSpec.addSubcommand(it.alias, subCommand)
}
try {
val defaultAnsiMode = if (CordaSystemUtils.isOsWindows()) {
Help.Ansi.ON
} else {
Help.Ansi.AUTO
}
val results = cmd.parse(*args)
val app = cmd.getCommand<CordaCliWrapper>()
if (cmd.isUsageHelpRequested) {
cmd.usage(System.out, defaultAnsiMode)
exitProcess(ExitCodes.SUCCESS)
}
if (cmd.isVersionHelpRequested) {
cmd.printVersionHelp(System.out, defaultAnsiMode)
exitProcess(ExitCodes.SUCCESS)
}
if (app.installShellExtensionsParser.installShellExtensions) {
System.out.println("Install shell extensions: ${app.installShellExtensionsParser.installShellExtensions}")
// ignore any parsing errors and run the program
exitProcess(app.call())
}
val allErrors = results.flatMap { it.parseResult?.errors() ?: emptyList() }
if (allErrors.any()) {
val parameterExceptions = allErrors.asSequence().filter { it is ParameterException }
if (parameterExceptions.any()) {
System.err.println("${ShellConstants.RED}${parameterExceptions.map{ it.message }.joinToString()}${ShellConstants.RESET}")
parameterExceptions.filter { it is UnmatchedArgumentException}.forEach { (it as UnmatchedArgumentException).printSuggestions(System.out) }
usage(cmd, System.out, defaultAnsiMode)
val results = cmd.parseWithHandlers(RunLast().useOut(System.out).useAnsi(defaultAnsiMode),
DefaultExceptionHandler<List<Any>>().useErr(System.err).useAnsi(defaultAnsiMode),
*args)
// If an error code has been returned, use this and exit
results?.firstOrNull()?.let {
if (it is Int) {
exitProcess(it)
} else {
exitProcess(ExitCodes.FAILURE)
}
throw allErrors.first()
}
exitProcess(app.call())
} catch (e: Exception) {
// If no results returned, picocli ran something without invoking the main program, e.g. --help or --version, so exit successfully
exitProcess(ExitCodes.SUCCESS)
} catch (e: ExecutionException) {
val throwable = e.cause ?: e
if (this.verbose) {
throwable.printStackTrace()
} else {
System.err.println("${ShellConstants.RED}${throwable.rootMessage ?: "Use --verbose for more details"}${ShellConstants.RESET}")
System.err.println("*ERROR*: ${throwable.rootMessage ?: "Use --verbose for more details"}")
}
exitProcess(ExitCodes.FAILURE)
}
}
/**
* Simple base class for handling help, version, verbose and logging-level commands.
* As versionProvider information from the MANIFEST file is used. It can be overwritten by custom version providers (see: Node)
* Picocli will prioritise versionProvider from the `@Command` annotation on the subclass, see: https://picocli.info/#_reuse_combinations
*/
@Command(mixinStandardHelpOptions = true,
versionProvider = CordaVersionProvider::class,
sortOptions = false,
@ -129,9 +115,9 @@ fun CordaCliWrapper.start(args: Array<String>) {
parameterListHeading = "%n@|bold,underline Parameters|@:%n%n",
optionListHeading = "%n@|bold,underline Options|@:%n%n",
commandListHeading = "%n@|bold,underline Commands|@:%n%n")
abstract class CordaCliWrapper(val alias: String, val description: String) : Callable<Int> {
abstract class CliWrapperBase(val alias: String, val description: String) : Callable<Int> {
companion object {
private val logger by lazy { loggerFor<CordaCliWrapper>() }
private val logger by lazy { contextLogger() }
}
// Raw args are provided for use in logging - this is a lateinit var rather than a constructor parameter as the class
@ -148,9 +134,6 @@ abstract class CordaCliWrapper(val alias: String, val description: String) : Cal
)
var loggingLevel: Level = Level.INFO
@Mixin
lateinit var installShellExtensionsParser: InstallShellExtensionsParser
// This needs to be called before loggers (See: NodeStartup.kt:51 logger called by lazy, initLogging happens before).
// Node's logging is more rich. In corda configurations two properties, defaultLoggingLevel and consoleLogLevel, are usually used.
open fun initLogging() {
@ -169,7 +152,32 @@ abstract class CordaCliWrapper(val alias: String, val description: String) : Cal
override fun call(): Int {
initLogging()
logger.info("Application Args: ${args.joinToString(" ")}")
installShellExtensionsParser.installOrUpdateShellExtensions(alias, this.javaClass.name)
return runProgram()
}
}
/**
* Simple base class for handling help, version, verbose and logging-level commands.
* As versionProvider information from the MANIFEST file is used. It can be overwritten by custom version providers (see: Node)
* Picocli will prioritise versionProvider from the `@Command` annotation on the subclass, see: https://picocli.info/#_reuse_combinations
*/
abstract class CordaCliWrapper(alias: String, description: String) : CliWrapperBase(alias, description) {
companion object {
private val logger by lazy { contextLogger() }
}
private val installShellExtensionsParser = InstallShellExtensionsParser(this)
protected open fun additionalSubCommands(): Set<CliWrapperBase> = emptySet()
fun subCommands(): Set<CliWrapperBase> {
return additionalSubCommands() + installShellExtensionsParser
}
override fun call(): Int {
initLogging()
logger.info("Application Args: ${args.joinToString(" ")}")
installShellExtensionsParser.updateShellExtensions()
return runProgram()
}
}

View File

@ -2,13 +2,13 @@ package net.corda.cliutils
import net.corda.core.internal.*
import picocli.CommandLine
import picocli.CommandLine.Command
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import java.util.*
import kotlin.system.exitProcess
private class ShellExtensionsGenerator(val alias: String, val className: String) {
private class ShellExtensionsGenerator(val parent: CordaCliWrapper) {
private class SettingsFile(val filePath: Path) {
private val lines: MutableList<String> by lazy { getFileLines() }
var fileModified: Boolean = false
@ -68,25 +68,27 @@ private class ShellExtensionsGenerator(val alias: String, val className: String)
private fun jarVersion(alias: String) = "# $alias - Version: ${CordaVersionProvider.releaseVersion}, Revision: ${CordaVersionProvider.revision}"
private fun getAutoCompleteFileLocation(alias: String) = userHome / ".completion" / alias
private fun generateAutoCompleteFile(alias: String, className: String) {
private fun generateAutoCompleteFile(alias: String) {
println("Generating $alias auto completion file")
val autoCompleteFile = getAutoCompleteFileLocation(alias)
autoCompleteFile.parent.createDirectories()
picocli.AutoComplete.main("-f", "-n", alias, className, "-o", autoCompleteFile.toStringWithDeWindowsfication())
val hierarchy = CommandLine(parent)
parent.subCommands().forEach { hierarchy.addSubcommand(it.alias, it)}
// Append hash of file to autocomplete file
autoCompleteFile.toFile().appendText(jarVersion(alias))
val builder = StringBuilder(picocli.AutoComplete.bash(alias, hierarchy))
builder.append(jarVersion(alias))
autoCompleteFile.writeText(builder.toString())
}
fun installShellExtensions() {
// Get jar location and generate alias command
val command = "alias $alias='java -jar \"${jarLocation.toStringWithDeWindowsfication()}\"'"
generateAutoCompleteFile(alias, className)
val command = "alias ${parent.alias}='java -jar \"${jarLocation.toStringWithDeWindowsfication()}\"'"
generateAutoCompleteFile(parent.alias)
// Get bash settings file
val bashSettingsFile = SettingsFile(userHome / ".bashrc")
// Replace any existing alias. There can be only one.
bashSettingsFile.addOrReplaceIfStartsWith("alias $alias", command)
bashSettingsFile.addOrReplaceIfStartsWith("alias ${parent.alias}", command)
val completionFileCommand = "for bcfile in ~/.completion/* ; do . \$bcfile; done"
bashSettingsFile.addIfNotExists(completionFileCommand)
bashSettingsFile.updateAndBackupIfNecessary()
@ -95,17 +97,17 @@ private class ShellExtensionsGenerator(val alias: String, val className: String)
val zshSettingsFile = SettingsFile(userHome / ".zshrc")
zshSettingsFile.addIfNotExists("autoload -U +X compinit && compinit")
zshSettingsFile.addIfNotExists("autoload -U +X bashcompinit && bashcompinit")
zshSettingsFile.addOrReplaceIfStartsWith("alias $alias", command)
zshSettingsFile.addOrReplaceIfStartsWith("alias ${parent.alias}", command)
zshSettingsFile.addIfNotExists(completionFileCommand)
zshSettingsFile.updateAndBackupIfNecessary()
println("Installation complete, $alias is available in bash with autocompletion. ")
println("Type `$alias <options>` from the commandline.")
println("Installation complete, ${parent.alias} is available in bash with autocompletion. ")
println("Type `${parent.alias} <options>` from the commandline.")
println("Restart bash for this to take effect, or run `. ~/.bashrc` in bash or `. ~/.zshrc` in zsh to re-initialise your shell now")
}
fun checkForAutoCompleteUpdate() {
val autoCompleteFile = getAutoCompleteFileLocation(alias)
val autoCompleteFile = getAutoCompleteFileLocation(parent.alias)
// If no autocomplete file, it hasn't been installed, so don't do anything
if (!autoCompleteFile.exists()) return
@ -113,25 +115,21 @@ private class ShellExtensionsGenerator(val alias: String, val className: String)
var lastLine = ""
autoCompleteFile.toFile().forEachLine { lastLine = it }
if (lastLine != jarVersion(alias)) {
if (lastLine != jarVersion(parent.alias)) {
println("Old auto completion file detected... regenerating")
generateAutoCompleteFile(alias, className)
generateAutoCompleteFile(parent.alias)
println("Restart bash for this to take effect, or run `. ~/.bashrc` to re-initialise bash now")
}
}
}
class InstallShellExtensionsParser {
@CommandLine.Option(names = ["--install-shell-extensions"], description = ["Install alias and autocompletion for bash and zsh"])
var installShellExtensions: Boolean = false
fun installOrUpdateShellExtensions(alias: String, className: String) {
val generator = ShellExtensionsGenerator(alias, className)
if (installShellExtensions) {
generator.installShellExtensions()
exitProcess(0)
} else {
generator.checkForAutoCompleteUpdate()
}
@Command(helpCommand = true)
class InstallShellExtensionsParser(private val cliWrapper: CordaCliWrapper) : CliWrapperBase("install-shell-extensions", "Install alias and autocompletion for bash and zsh") {
private val generator = ShellExtensionsGenerator(cliWrapper)
override fun runProgram(): Int {
generator.installShellExtensions()
return ExitCodes.SUCCESS
}
fun updateShellExtensions() = generator.checkForAutoCompleteUpdate()
}

View File

@ -25,7 +25,7 @@ class NotaryCopier(val cacheDir: File) : NodeCopier(cacheDir) {
fun generateNodeInfo(dirToGenerateFrom: File): File {
val nodeInfoGeneratorProcess = ProcessBuilder()
.command(listOf("java", "-jar", "corda.jar", "--just-generate-node-info"))
.command(listOf("java", "-jar", "corda.jar", "generate-node-info"))
.directory(dirToGenerateFrom)
.inheritIO()
.start()

View File

@ -21,11 +21,6 @@
required: false
multiParam: false
acceptableValues: []
- parameterName: "--install-shell-extensions"
parameterType: "boolean"
required: false
multiParam: false
acceptableValues: []
- parameterName: "--log-to-console"
parameterType: "boolean"
required: false