CORDA-1755: Modify the node to run using picocli (#3872)

* Modify Corda Node to use picocli

* Make --sshd parameter actually work

* * Some refactoring
* Fixing the issue with the --confg-file parameter
* Updating the tests

* Restore original devMode behaviour

* Update documentation

* Add return code to network bootstrapper

* Use the root jar for the shell alias for jars packaged with capsule

* Update Corda jar description

* Fix issue with logging not initialising early enough in node
Make initLogging overridable
Combine --verbose and --log-to-console options

* Tidy up

* Make sure all command line options are documented properly

* Fix compilation error

* Remove code that's no longer needed (single slash options no longer supported unless explicitly specified)

* Remove comment

* Remove pointless comment

* Log commandline arguments

* Address review comments

* Address more review comments

* Remove ConfigFilePathArgsParser

* Remove some unused importss

* Only display config when in dev mode

* Force Ansi ON if on Windows else set to AUTO.

* Make ExitCodes class open
This commit is contained in:
Anthony Keenan 2018-09-06 09:37:04 +01:00 committed by GitHub
parent 304dba704e
commit 3284a61afd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 382 additions and 495 deletions

View File

@ -47,22 +47,75 @@ Command-line options
~~~~~~~~~~~~~~~~~~~~
The node can optionally be started with the following command-line options:
* ``--base-directory``: The node working directory where all the files are kept (default: ``.``)
* ``--base-directory``, ``-b``: The node working directory where all the files are kept (default: ``.``).
* ``--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
* ``--config-file``: The path to the config file (default: ``node.conf``)
* ``--help``
addresses), acting as a seed for other nodes to join the cluster.
* ``--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.
* ``--help``, ``-h``: Displays the help message and exits.
* ``--initial-registration``: Start initial node registration with Corda network to obtain certificate from the permissioning
server
server.
* ``--install-shell-extensions``: Installs an alias and auto-completion for users of ``bash`` or ``zsh``. See below for more information.
* ``--just-generate-node-info``: Perform the node start-up task necessary to generate its nodeInfo, save it to disk, then
quit
* ``--log-to-console``: If set, prints logging to the console as well as to a file
* ``--logging-level <[ERROR,WARN,INFO, DEBUG,TRACE]>``: Enable logging at this level and higher (default: INFO)
* ``--network-root-truststore``: Network root trust store obtained from network operator
* ``--network-root-truststore-password``: Network root trust store password obtained from network operator
* ``--no-local-shell``: Do not start the embedded shell locally
* ``--sshd``: Enables SSHD server for node administration
* ``--version``: Print the version and exit
quit.
* ``--just-generate-rpc-ssl-settings``: Generate the ssl keystore and truststore for a secure RPC connection.
* ``--log-to-console``, ``--verbose``, ``-v``: If set, prints logging to the console as well as to a file.
* ``--logging-level <[ERROR,WARN,INFO,DEBUG,TRACE]>``: Enable logging at this level and higher. Defaults to INFO.
* ``--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.
* ``--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.
* ``--version``, ``-V``: Prints the version and exits.
.. _installing-shell-extensions:
Installing shell extensions
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Users of ``bash`` or ``zsh`` can install an alias and command line completion for Corda. Run:
.. code-block:: shell
java -jar corda.jar --install-shell-extensions
Then, either restart your shell, or for ``bash`` users run:
.. code-block:: shell
. ~/.bashrc
Or, for ``zsh`` run:
.. code-block:: shell
. ~/.zshrc
You will now be able to run a Corda node from anywhere by running the following:
.. code-block:: shell
corda --<option>
Upgrading shell extensions
~~~~~~~~~~~~~~~~~~~~~~~~~~
Once the shell extensions have been installed, you can upgrade them in one of two ways.
1) Overwrite the existing ``corda.jar`` with the newer version. The next time you run Corda, it will automatically update
the completion file. Either restart the shell or see :ref:`above<installing-shell-extensions>` for instructions
on making the changes take effect immediately.
2) If you wish to use a new ``corda.jar`` from a different directory, navigate to that directory and run:
.. code-block:: shell
java -jar corda.jar
Which will update the ``corda`` alias to point to the new location, and update command line completion functionality. Either
restart the shell or see :ref:`above<installing-shell-extensions>` for instructions on making the changes take effect immediately.
.. _enabling-remote-debugging:

View File

@ -71,6 +71,7 @@ dependencies {
compile project(":confidential-identities")
compile project(':client:rpc')
compile project(':tools:shell')
compile project(':tools:cliutils')
// Log4J: logging framework (with SLF4J bindings)
compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}"
@ -104,9 +105,6 @@ dependencies {
exclude group: 'org.apache.qpid', module: 'proton-j'
}
// JAnsi: for drawing things to the terminal in nicely coloured ways.
compile "org.fusesource.jansi:jansi:$jansi_version"
// Manifests: for reading stuff from the manifest file
compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version"

View File

@ -21,16 +21,8 @@ public class CordaCaplet extends Capsule {
private Config parseConfigFile(List<String> args) {
String baseDirOption = getOption(args, "--base-directory");
// Ensure consistent behaviour with NodeArgsParser.kt, see CORDA-1598.
if (null == baseDirOption || baseDirOption.isEmpty()) {
baseDirOption = getOption(args, "-base-directory");
}
this.baseDir = Paths.get((baseDirOption == null) ? "." : baseDirOption).toAbsolutePath().normalize().toString();
String config = getOption(args, "--config-file");
// Same as for baseDirOption.
if (null == config || config.isEmpty()) {
config = getOption(args, "-config-file");
}
File configFile = (config == null) ? new File(baseDir, "node.conf") : new File(config);
try {
ConfigParseOptions parseOptions = ConfigParseOptions.defaults().setAllowMissing(false);

View File

@ -3,12 +3,12 @@
package net.corda.node
import net.corda.cliutils.start
import net.corda.node.internal.NodeStartup
import kotlin.system.exitProcess
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.
exitProcess(if (NodeStartup(args).run()) 0 else 1)
NodeStartup().start(args)
}

View File

@ -1,142 +0,0 @@
package net.corda.node
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import joptsimple.OptionSet
import joptsimple.util.EnumConverter
import joptsimple.util.PathConverter
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.parseAsNodeConfiguration
import net.corda.node.utilities.AbstractArgsParser
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
import org.slf4j.event.Level
import java.nio.file.Path
import java.nio.file.Paths
// NOTE: Do not use any logger in this class as args parsing is done before the logger is setup.
class NodeArgsParser : AbstractArgsParser<CmdLineOptions>() {
// The intent of allowing a command line configurable directory and config path is to allow deployment flexibility.
// Other general configuration should live inside the config file unless we regularly need temporary overrides on the command line
private val baseDirectoryArg = optionParser
.accepts("base-directory", "The node working directory where all the files are kept")
.withRequiredArg()
.withValuesConvertedBy(PathConverter())
.defaultsTo(Paths.get("."))
private val configFileArg = optionParser
.accepts("config-file", "The path to the config file")
.withRequiredArg()
.defaultsTo("node.conf")
private val loggerLevel = optionParser
.accepts("logging-level", "Enable logging at this level and higher")
.withRequiredArg()
.withValuesConvertedBy(object : EnumConverter<Level>(Level::class.java) {})
.defaultsTo(Level.INFO)
private val logToConsoleArg = optionParser.accepts("log-to-console", "If set, prints logging to the console as well as to a file.")
private val sshdServerArg = optionParser.accepts("sshd", "Enables SSHD server for node administration.")
private val noLocalShellArg = optionParser.accepts("no-local-shell", "Do not start the embedded shell locally.")
private val isRegistrationArg = optionParser.accepts("initial-registration", "Start initial node registration with Corda network to obtain certificate from the permissioning server.")
private val networkRootTrustStorePathArg = optionParser.accepts("network-root-truststore", "Network root trust store obtained from network operator.")
.withRequiredArg()
.withValuesConvertedBy(PathConverter())
.defaultsTo((Paths.get("certificates") / "network-root-truststore.jks"))
private val networkRootTrustStorePasswordArg = optionParser.accepts("network-root-truststore-password", "Network root trust store password obtained from network operator.")
.withRequiredArg()
private val unknownConfigKeysPolicy = optionParser.accepts("on-unknown-config-keys", "How to behave on unknown node configuration.")
.withRequiredArg()
.withValuesConvertedBy(object : EnumConverter<UnknownConfigKeysPolicy>(UnknownConfigKeysPolicy::class.java) {})
.defaultsTo(UnknownConfigKeysPolicy.FAIL)
private val devModeArg = optionParser.accepts("dev-mode", "Run the node in developer mode. Unsafe for production.")
private val isVersionArg = optionParser.accepts("version", "Print the version and exit")
private val justGenerateNodeInfoArg = optionParser.accepts("just-generate-node-info",
"Perform the node start-up task necessary to generate its nodeInfo, save it to disk, then quit")
private val justGenerateRpcSslCertsArg = optionParser.accepts("just-generate-rpc-ssl-settings",
"Generate the ssl keystore and truststore for a secure RPC connection.")
private val bootstrapRaftClusterArg = optionParser.accepts("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.")
private val clearNetworkMapCache = optionParser.accepts("clear-network-map-cache", "Clears local copy of network map, on node startup it will be restored from server or file system.")
override fun doParse(optionSet: OptionSet): CmdLineOptions {
require(optionSet.nonOptionArguments().isEmpty()) { "Unrecognized argument(s): ${optionSet.nonOptionArguments().joinToString(separator = ", ")}"}
val baseDirectory = optionSet.valueOf(baseDirectoryArg).normalize().toAbsolutePath()
val configFilePath = Paths.get(optionSet.valueOf(configFileArg))
val configFile = if (configFilePath.isAbsolute) configFilePath else baseDirectory / configFilePath.toString()
val loggingLevel = optionSet.valueOf(loggerLevel)
val logToConsole = optionSet.has(logToConsoleArg)
val isRegistration = optionSet.has(isRegistrationArg)
val isVersion = optionSet.has(isVersionArg)
val noLocalShell = optionSet.has(noLocalShellArg)
val sshdServer = optionSet.has(sshdServerArg)
val justGenerateNodeInfo = optionSet.has(justGenerateNodeInfoArg)
val justGenerateRpcSslCerts = optionSet.has(justGenerateRpcSslCertsArg)
val bootstrapRaftCluster = optionSet.has(bootstrapRaftClusterArg)
val networkRootTrustStorePath = optionSet.valueOf(networkRootTrustStorePathArg)
val networkRootTrustStorePassword = optionSet.valueOf(networkRootTrustStorePasswordArg)
val unknownConfigKeysPolicy = optionSet.valueOf(unknownConfigKeysPolicy)
val devMode = optionSet.has(devModeArg)
val clearNetworkMapCache = optionSet.has(clearNetworkMapCache)
val registrationConfig = 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
}
return CmdLineOptions(baseDirectory,
configFile,
loggingLevel,
logToConsole,
registrationConfig,
isVersion,
noLocalShell,
sshdServer,
justGenerateNodeInfo,
justGenerateRpcSslCerts,
bootstrapRaftCluster,
unknownConfigKeysPolicy,
devMode,
clearNetworkMapCache)
}
}
data class NodeRegistrationOption(val networkRootTrustStorePath: Path, val networkRootTrustStorePassword: String)
data class CmdLineOptions(val baseDirectory: Path,
val configFile: Path,
val loggingLevel: Level,
val logToConsole: Boolean,
val nodeRegistrationOption: NodeRegistrationOption?,
val isVersion: Boolean,
val noLocalShell: Boolean,
val sshdServer: Boolean,
val justGenerateNodeInfo: Boolean,
val justGenerateRpcSslCerts: Boolean,
val bootstrapRaftCluster: Boolean,
val unknownConfigKeysPolicy: UnknownConfigKeysPolicy,
val devMode: Boolean,
val clearNetworkMapCache: Boolean) {
fun loadConfig(): Pair<Config, Try<NodeConfiguration>> {
val rawConfig = ConfigHelper.loadConfig(
baseDirectory,
configFile,
configOverrides = ConfigFactory.parseMap(mapOf("noLocalShell" to this.noLocalShell) +
if (devMode) mapOf("devMode" to this.devMode) else emptyMap<String, Any>())
)
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."
}
}
}
}
}
}

View File

@ -0,0 +1,135 @@
package net.corda.node
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
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.parseAsNodeConfiguration
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
import picocli.CommandLine.Option
import java.nio.file.Path
import java.nio.file.Paths
class NodeCmdLineOptions {
@Option(
names = ["-b", "--base-directory"],
description = ["The node working directory where all the files are kept."]
)
var baseDirectory: Path = Paths.get(".")
@Option(
names = ["-f", "--config-file"],
description = ["The path to the config file. By default this is node.conf in the base directory."]
)
var configFileArgument: Path? = null
val configFile : Path
get() = configFileArgument ?: (baseDirectory / "node.conf")
@Option(
names = ["--sshd"],
description = ["If set, enables SSH server for node administration."]
)
var sshdServer: Boolean = false
@Option(
names = ["--sshd-port"],
description = ["The port to start the SSH server on, if enabled."]
)
var sshdServerPort: Int = 2222
@Option(
names = ["-n", "--no-local-shell"],
description = ["Do not start the embedded shell locally."]
)
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."]
)
var networkRootTrustStorePath = Paths.get("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"]
)
var justGenerateNodeInfo: Boolean = false
@Option(
names = ["--just-generate-rpc-ssl-settings"],
description = ["Generate the SSL key and trust stores for a secure RPC connection."]
)
var justGenerateRpcSslCerts: Boolean = false
@Option(
names = ["--bootstrap-raft-cluster"],
description = ["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."]
)
var bootstrapRaftCluster: 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."]
)
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
}
}
fun loadConfig(): Pair<Config, Try<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())
)
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."
}
}
}
}
}
}
data class NodeRegistrationOption(val networkRootTrustStorePath: Path, val networkRootTrustStorePassword: String)

View File

@ -5,8 +5,9 @@ import com.typesafe.config.Config
import com.typesafe.config.ConfigException
import com.typesafe.config.ConfigRenderOptions
import io.netty.channel.unix.Errors
import joptsimple.OptionParser
import joptsimple.util.PathConverter
import net.corda.cliutils.CordaCliWrapper
import net.corda.cliutils.CordaVersionProvider
import net.corda.cliutils.ExitCodes
import net.corda.core.crypto.Crypto
import net.corda.core.internal.*
import net.corda.core.internal.concurrent.thenMatch
@ -22,8 +23,8 @@ import net.corda.node.services.config.shouldStartSSHDaemon
import net.corda.node.services.transactions.bftSMaRtSerialFilter
import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NodeRegistrationHelper
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
@ -32,8 +33,8 @@ import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException
import net.corda.tools.shell.InteractiveShell
import org.fusesource.jansi.Ansi
import org.fusesource.jansi.AnsiConsole
import org.slf4j.bridge.SLF4JBridgeHandler
import picocli.CommandLine.Mixin
import sun.misc.VMSupport
import java.io.Console
import java.io.File
@ -49,7 +50,7 @@ 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>) {
open class NodeStartup: CordaCliWrapper("corda", "Runs a Corda Node") {
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"
@ -57,31 +58,33 @@ open class NodeStartup(val args: Array<String>) {
private const val INITIAL_REGISTRATION_MARKER = ".initialregistration"
}
@Mixin
var cmdLineOptions = NodeCmdLineOptions()
/**
* @return true if the node startup was successful. This value is intended to be the exit code of the process.
* @return exit code based on the success of the node startup. This value is intended to be the exit code of the process.
*/
open fun run(): Boolean {
override fun runProgram(): Int {
val startTime = System.currentTimeMillis()
if (!canNormalizeEmptyPath()) {
println("You are using a version of Java that is not supported (${System.getProperty("java.version")}). Please upgrade to the latest version.")
println("Corda will now exit...")
return false
return ExitCodes.FAILURE
}
val registrationMode = checkRegistrationMode()
val cmdlineOptions: CmdLineOptions = if (registrationMode && !args.contains("--initial-registration")) {
if (registrationMode && !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.
NodeArgsParser().parseOrExit(*args.plus("--initial-registration"))
} else {
NodeArgsParser().parseOrExit(*args)
cmdLineOptions.isRegistration = true
}
// 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)
enforceSingleNodeIsRunning(cmdLineOptions.baseDirectory)
initLogging(cmdlineOptions)
initLogging()
// 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).
@ -89,40 +92,34 @@ open class NodeStartup(val args: Array<String>) {
val versionInfo = getVersionInfo()
if (cmdlineOptions.isVersion) {
println("${versionInfo.vendor} ${versionInfo.releaseVersion}")
println("Revision ${versionInfo.revision}")
println("Platform Version ${versionInfo.platformVersion}")
return true
}
drawBanner(versionInfo)
Node.printBasicNodeInfo(LOGS_CAN_BE_FOUND_IN_STRING, System.getProperty("log-path"))
val configuration = (attempt { loadConfiguration(cmdlineOptions) }.doOnException(handleConfigurationLoadingError(cmdlineOptions.configFile)) as? Try.Success)?.let(Try.Success<NodeConfiguration>::value) ?: return false
val configuration = (attempt { loadConfiguration() }.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 false
return ExitCodes.FAILURE
}
attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success ?: return false
attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success ?: return ExitCodes.FAILURE
attempt { preNetworkRegistration(configuration) }.doOnException(handleRegistrationError) as? Try.Success ?: return false
attempt { preNetworkRegistration(configuration) }.doOnException(handleRegistrationError) as? Try.Success ?: return ExitCodes.FAILURE
cmdlineOptions.nodeRegistrationOption?.let {
cmdLineOptions.nodeRegistrationOption?.let {
// Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig]
attempt { registerWithNetwork(configuration, versionInfo, cmdlineOptions.nodeRegistrationOption) }.doOnException(handleRegistrationError) as? Try.Success ?: return false
attempt { registerWithNetwork(configuration, versionInfo, it) }.doOnException(handleRegistrationError) as? Try.Success ?: return ExitCodes.FAILURE
// At this point the node registration was successful. We can delete the marker file.
deleteNodeRegistrationMarker(cmdlineOptions.baseDirectory)
return true
deleteNodeRegistrationMarker(cmdLineOptions.baseDirectory)
return ExitCodes.SUCCESS
}
logStartupInfo(versionInfo, cmdlineOptions, configuration)
logStartupInfo(versionInfo, configuration)
return attempt { startNode(configuration, versionInfo, startTime, cmdlineOptions) }.doOnSuccess { logger.info("Node exiting successfully") }.doOnException(handleStartError).isSuccess
attempt { startNode(configuration, versionInfo, startTime) }.doOnSuccess { logger.info("Node exiting successfully") }.doOnException(handleStartError) as? Try.Success ?: return ExitCodes.FAILURE
return ExitCodes.SUCCESS
}
private fun <RESULT> attempt(action: () -> RESULT): Try<RESULT> = Try.on(action)
@ -138,13 +135,11 @@ open class NodeStartup(val args: Array<String>) {
private fun Exception.isOpenJdkKnownIssue() = message?.startsWith("Unknown named curve:") == true
private fun Exception.errorCode(): String {
val hash = staticLocationBasedHash()
return Integer.toOctalString(hash)
}
private fun Throwable.staticLocationBasedHash(visited: Set<Throwable> = setOf(this)): Int {
val cause = this.cause
return when {
cause != null && !visited.contains(cause) -> Objects.hash(this::class.java.name, stackTrace.customHashCode(), cause.staticLocationBasedHash(visited + cause))
@ -199,14 +194,13 @@ open class NodeStartup(val args: Array<String>) {
""".trimIndent()
}
private fun loadConfiguration(cmdlineOptions: CmdLineOptions): NodeConfiguration {
val (rawConfig, configurationResult) = loadConfigFile(cmdlineOptions)
if (cmdlineOptions.devMode) {
private fun loadConfiguration(): NodeConfiguration {
val (rawConfig, configurationResult) = loadConfigFile()
if (cmdLineOptions.devMode == true) {
println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}")
}
val configuration = configurationResult.getOrThrow()
return if (cmdlineOptions.bootstrapRaftCluster) {
return if (cmdLineOptions.bootstrapRaftCluster) {
println("Bootstrapping raft cluster (starting up as seed node).")
// Ignore the configured clusterAddresses to make the node bootstrap a cluster instead of joining.
(configuration as NodeConfigurationImpl).copy(notary = configuration.notary?.copy(raft = configuration.notary?.raft?.copy(clusterAddresses = emptyList())))
@ -216,27 +210,11 @@ open class NodeStartup(val args: Array<String>) {
}
private fun checkRegistrationMode(): Boolean {
// Parse the command line args just to get the base directory. The base directory is needed to determine
// if the node registration marker file exists, _before_ we call NodeArgsParser.parse().
// If it does exist, we call NodeArgsParser with `--initial-registration` added to the argument list. This way
// we make sure that the initial registration is completed, even if the node was restarted before the first
// attempt to register succeeded and the node administrator forgets to specify `--initial-registration` upon
// restart.
val optionParser = OptionParser()
optionParser.allowsUnrecognizedOptions()
val baseDirectoryArg = optionParser
.accepts("base-directory", "The node working directory where all the files are kept")
.withRequiredArg()
.withValuesConvertedBy(PathConverter())
.defaultsTo(Paths.get("."))
val isRegistrationArg =
optionParser.accepts("initial-registration", "Start initial node registration with Corda network to obtain certificate from the permissioning server.")
val optionSet = optionParser.parse(*args)
val baseDirectory = optionSet.valueOf(baseDirectoryArg).normalize().toAbsolutePath()
val baseDirectory = cmdLineOptions.baseDirectory.normalize().toAbsolutePath()
// 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 = File((baseDirectory / INITIAL_REGISTRATION_MARKER).toUri())
if (!optionSet.has(isRegistrationArg) && !marker.exists()) {
if (!cmdLineOptions.isRegistration && !marker.exists()) {
return false
}
try {
@ -262,20 +240,19 @@ open class NodeStartup(val args: Array<String>) {
protected open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo)
protected open fun startNode(conf: NodeConfiguration, versionInfo: VersionInfo, startTime: Long, cmdlineOptions: CmdLineOptions) {
cmdlineOptions.baseDirectory.createDirectories()
protected open fun startNode(conf: NodeConfiguration, versionInfo: VersionInfo, startTime: Long) {
cmdLineOptions.baseDirectory.createDirectories()
val node = createNode(conf, versionInfo)
if (cmdlineOptions.clearNetworkMapCache) {
if (cmdLineOptions.clearNetworkMapCache) {
node.clearNetworkMapCache()
return
}
if (cmdlineOptions.justGenerateNodeInfo) {
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) {
if (cmdLineOptions.justGenerateRpcSslCerts) {
val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(conf.myLegalName.x500Principal)
val keyStorePath = conf.baseDirectory / "certificates" / "rpcsslkeystore.jks"
@ -374,7 +351,7 @@ open class NodeStartup(val args: Array<String>) {
node.run()
}
protected open fun logStartupInfo(versionInfo: VersionInfo, cmdlineOptions: CmdLineOptions, conf: NodeConfiguration) {
protected open fun logStartupInfo(versionInfo: VersionInfo, conf: NodeConfiguration) {
logger.info("Vendor: ${versionInfo.vendor}")
logger.info("Release: ${versionInfo.releaseVersion}")
logger.info("Platform Version: ${versionInfo.platformVersion}")
@ -383,12 +360,11 @@ open class NodeStartup(val args: Array<String>) {
logger.info("PID: ${info.name.split("@").firstOrNull()}") // TODO Java 9 has better support for this
logger.info("Main class: ${NodeConfiguration::class.java.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}")
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")}")
@ -416,21 +392,18 @@ open class NodeStartup(val args: Array<String>) {
println("Corda node will now terminate.")
}
protected open fun loadConfigFile(cmdlineOptions: CmdLineOptions): Pair<Config, Try<NodeConfiguration>> = cmdlineOptions.loadConfig()
protected open fun loadConfigFile(): Pair<Config, Try<NodeConfiguration>> = cmdLineOptions.loadConfig()
protected open fun banJavaSerialisation(conf: NodeConfiguration) {
SerialFilter.install(if (conf.notary?.bftSMaRt != null) ::bftSMaRtSerialFilter else ::defaultSerialFilter)
}
protected open 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
return VersionInfo(
manifestValue("Corda-Platform-Version")?.toInt() ?: 1,
manifestValue("Corda-Release-Version") ?: "Unknown",
manifestValue("Corda-Revision") ?: "Unknown",
manifestValue("Corda-Vendor") ?: "Unknown"
CordaVersionProvider.platformVersion,
CordaVersionProvider.releaseVersion,
CordaVersionProvider.revision,
CordaVersionProvider.vendor
)
}
@ -467,14 +440,14 @@ open class NodeStartup(val args: Array<String>) {
}
}
protected open fun initLogging(cmdlineOptions: CmdLineOptions) {
val loggingLevel = cmdlineOptions.loggingLevel.name.toLowerCase(Locale.ENGLISH)
override fun initLogging() {
val loggingLevel = loggingLevel.name().toLowerCase(Locale.ENGLISH)
System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file.
if (cmdlineOptions.logToConsole) {
if (verbose) {
System.setProperty("consoleLogLevel", loggingLevel)
Node.renderBasicInfoToConsole = false
}
System.setProperty("log-path", (cmdlineOptions.baseDirectory / LOGS_DIRECTORY_NAME).toString())
System.setProperty("log-path", (cmdLineOptions.baseDirectory / LOGS_DIRECTORY_NAME).toString())
SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler.
SLF4JBridgeHandler.install()
}
@ -515,9 +488,6 @@ open class NodeStartup(val args: Array<String>) {
}
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.",

View File

@ -3,6 +3,7 @@ package net.corda.node.services.config
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigParseOptions
import net.corda.cliutils.CordaSystemUtils
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
@ -105,15 +106,3 @@ fun MutualSslConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500N
}
}
}
/** This is generally covered by commons-lang. */
object CordaSystemUtils {
const val OS_NAME = "os.name"
const val MAC_PREFIX = "Mac"
const val WIN_PREFIX = "Windows"
fun isOsMac() = getOsName().startsWith(MAC_PREFIX)
fun isOsWindows() = getOsName().startsWith(WIN_PREFIX)
fun getOsName() = System.getProperty(OS_NAME)
}

View File

@ -1,189 +0,0 @@
package net.corda.node
import joptsimple.OptionException
import net.corda.core.internal.delete
import net.corda.core.internal.div
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
import net.corda.nodeapi.internal.crypto.X509KeyStore
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.BeforeClass
import org.junit.Test
import org.slf4j.event.Level
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
class NodeArgsParserTest {
private val parser = NodeArgsParser()
companion object {
private lateinit var workingDirectory: Path
private lateinit var buildDirectory: Path
@BeforeClass
@JvmStatic
fun initDirectories() {
workingDirectory = Paths.get(".").normalize().toAbsolutePath()
buildDirectory = workingDirectory.resolve("build")
}
}
@Test
fun `no command line arguments`() {
assertThat(parser.parse()).isEqualTo(CmdLineOptions(
baseDirectory = workingDirectory,
configFile = workingDirectory / "node.conf",
logToConsole = false,
loggingLevel = Level.INFO,
nodeRegistrationOption = null,
isVersion = false,
noLocalShell = false,
sshdServer = false,
justGenerateNodeInfo = false,
justGenerateRpcSslCerts = false,
bootstrapRaftCluster = false,
unknownConfigKeysPolicy = UnknownConfigKeysPolicy.FAIL,
devMode = false,
clearNetworkMapCache = false))
}
@Test
fun `base-directory with relative path`() {
val expectedBaseDir = Paths.get("tmp").normalize().toAbsolutePath()
val cmdLineOptions = parser.parse("--base-directory", "tmp")
assertThat(cmdLineOptions.baseDirectory).isEqualTo(expectedBaseDir)
assertThat(cmdLineOptions.configFile).isEqualTo(expectedBaseDir / "node.conf")
}
@Test
fun `base-directory with absolute path`() {
val baseDirectory = Paths.get("tmp").normalize().toAbsolutePath()
val cmdLineOptions = parser.parse("--base-directory", baseDirectory.toString())
assertThat(cmdLineOptions.baseDirectory).isEqualTo(baseDirectory)
assertThat(cmdLineOptions.configFile).isEqualTo(baseDirectory / "node.conf")
}
@Test
fun `config-file with relative path`() {
val cmdLineOptions = parser.parse("--config-file", "different.conf")
assertThat(cmdLineOptions.baseDirectory).isEqualTo(workingDirectory)
assertThat(cmdLineOptions.configFile).isEqualTo(workingDirectory / "different.conf")
}
@Test
fun `config-file with absolute path`() {
val configFile = Paths.get("tmp", "a.conf").normalize().toAbsolutePath()
val cmdLineOptions = parser.parse("--config-file", configFile.toString())
assertThat(cmdLineOptions.baseDirectory).isEqualTo(workingDirectory)
assertThat(cmdLineOptions.configFile).isEqualTo(configFile)
}
@Test
fun `base-directory without argument`() {
assertThatExceptionOfType(OptionException::class.java).isThrownBy {
parser.parse("--base-directory")
}.withMessageContaining("base-directory")
}
@Test
fun `config-file without argument`() {
assertThatExceptionOfType(OptionException::class.java).isThrownBy {
parser.parse("--config-file")
}.withMessageContaining("config-file")
}
@Test
fun `log-to-console`() {
val cmdLineOptions = parser.parse("--log-to-console")
assertThat(cmdLineOptions.logToConsole).isTrue()
}
@Test
fun `logging-level`() {
for (level in Level.values()) {
val cmdLineOptions = parser.parse("--logging-level", level.name)
assertThat(cmdLineOptions.loggingLevel).isEqualTo(level)
}
}
@Test
fun `logging-level without argument`() {
assertThatExceptionOfType(OptionException::class.java).isThrownBy {
parser.parse("--logging-level")
}.withMessageContaining("logging-level")
}
@Test
fun `logging-level with invalid argument`() {
assertThatExceptionOfType(OptionException::class.java).isThrownBy {
parser.parse("--logging-level", "not-a-level")
}.withMessageContaining("logging-level")
}
@Test
fun `initial-registration`() {
// Create this temporary file in the "build" directory so that "clean" can delete it.
val truststorePath = buildDirectory / "truststore" / "file.jks"
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
parser.parse("--initial-registration", "--network-root-truststore", "$truststorePath", "--network-root-truststore-password", "password-test")
}.withMessageContaining("Network root trust store path").withMessageContaining("doesn't exist")
X509KeyStore.fromFile(truststorePath, "dummy_password", createNew = true)
try {
val cmdLineOptions = parser.parse("--initial-registration", "--network-root-truststore", "$truststorePath", "--network-root-truststore-password", "password-test")
assertNotNull(cmdLineOptions.nodeRegistrationOption)
assertEquals(truststorePath.toAbsolutePath(), cmdLineOptions.nodeRegistrationOption?.networkRootTrustStorePath)
assertEquals("password-test", cmdLineOptions.nodeRegistrationOption?.networkRootTrustStorePassword)
} finally {
truststorePath.delete()
}
}
@Test
fun version() {
val cmdLineOptions = parser.parse("--version")
assertThat(cmdLineOptions.isVersion).isTrue()
}
@Test
fun `generate node infos`() {
val cmdLineOptions = parser.parse("--just-generate-node-info")
assertThat(cmdLineOptions.justGenerateNodeInfo).isTrue()
}
@Test
fun `clear network map cache`() {
val cmdLineOptions = parser.parse("--clear-network-map-cache")
assertThat(cmdLineOptions.clearNetworkMapCache).isTrue()
}
@Test
fun `bootstrap raft cluster`() {
val cmdLineOptions = parser.parse("--bootstrap-raft-cluster")
assertThat(cmdLineOptions.bootstrapRaftCluster).isTrue()
}
@Test
fun `on-unknown-config-keys options`() {
UnknownConfigKeysPolicy.values().forEach { onUnknownConfigKeyPolicy ->
val cmdLineOptions = parser.parse("--on-unknown-config-keys", onUnknownConfigKeyPolicy.name)
assertThat(cmdLineOptions.unknownConfigKeysPolicy).isEqualTo(onUnknownConfigKeyPolicy)
}
}
@Test
fun `invalid argument`() {
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
parser.parse("foo")
}.withMessageContaining("Unrecognized argument(s): foo")
}
@Test
fun `invalid arguments`() {
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
parser.parse("foo", "bar")
}.withMessageContaining("Unrecognized argument(s): foo, bar")
}
}

View File

@ -0,0 +1,44 @@
package net.corda.node
import net.corda.core.internal.div
import net.corda.node.internal.NodeStartup
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
import org.apache.logging.log4j.Level
import org.assertj.core.api.Assertions.assertThat
import org.junit.BeforeClass
import org.junit.Test
import java.nio.file.Path
import java.nio.file.Paths
class NodeCmdLineOptionsTest {
private val parser = NodeStartup()
companion object {
private lateinit var workingDirectory: Path
private lateinit var buildDirectory: Path
@BeforeClass
@JvmStatic
fun initDirectories() {
workingDirectory = Paths.get(".").normalize().toAbsolutePath()
buildDirectory = workingDirectory.resolve("build")
}
}
@Test
fun `no command line arguments`() {
assertThat(parser.cmdLineOptions.baseDirectory.normalize().toAbsolutePath()).isEqualTo(workingDirectory)
assertThat(parser.cmdLineOptions.configFile.normalize().toAbsolutePath()).isEqualTo(workingDirectory / "node.conf")
assertThat(parser.verbose).isEqualTo(false)
assertThat(parser.loggingLevel).isEqualTo(Level.INFO)
assertThat(parser.cmdLineOptions.nodeRegistrationOption).isEqualTo(null)
assertThat(parser.cmdLineOptions.noLocalShell).isEqualTo(false)
assertThat(parser.cmdLineOptions.sshdServer).isEqualTo(false)
assertThat(parser.cmdLineOptions.justGenerateNodeInfo).isEqualTo(false)
assertThat(parser.cmdLineOptions.justGenerateRpcSslCerts).isEqualTo(false)
assertThat(parser.cmdLineOptions.bootstrapRaftCluster).isEqualTo(false)
assertThat(parser.cmdLineOptions.unknownConfigKeysPolicy).isEqualTo(UnknownConfigKeysPolicy.FAIL)
assertThat(parser.cmdLineOptions.devMode).isEqualTo(null)
assertThat(parser.cmdLineOptions.clearNetworkMapCache).isEqualTo(false)
}
}

View File

@ -8,7 +8,7 @@ import java.nio.file.Path
import java.nio.file.Paths
fun main(args: Array<String>) {
NetworkBootstrapperRunner().start(*args)
NetworkBootstrapperRunner().start(args)
}
class NetworkBootstrapperRunner : CordaCliWrapper("bootstrapper", "Bootstrap a local test Corda network using a set of node configuration files and CorDapp JARs") {
@ -24,7 +24,8 @@ 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
override fun runProgram() {
override fun runProgram(): Int {
NetworkBootstrapper().bootstrap(dir.toAbsolutePath().normalize(), copyCordapps = !noCopy)
return 0 //exit code
}
}

View File

@ -10,5 +10,8 @@ dependencies {
compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version"
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
// JAnsi: for drawing things to the terminal in nicely coloured ways.
compile "org.fusesource.jansi:jansi:$jansi_version"
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
}

View File

@ -1,25 +0,0 @@
package net.corda.cliutils
import net.corda.core.internal.exists
import net.corda.core.internal.isReadable
import picocli.CommandLine
import java.nio.file.Path
/**
* When a config file is required as part of setup, use this class to check that it exists and is formatted correctly. Add it as
* `@CommandLine.Mixin
* lateinit var configParser: ConfigFilePathArgsParser`
* in your command class and then call `validate()`
*/
@CommandLine.Command(description = ["Parse configuration file. Checks if given configuration file exists"])
class ConfigFilePathArgsParser : Validated {
@CommandLine.Option(names = ["--config-file", "-f"], required = true, paramLabel = "FILE", description = ["The path to the config file"])
lateinit var configFile: Path
override fun validator(): List<String> {
val res = mutableListOf<String>()
if (!configFile.exists()) res += "Config file ${configFile.toAbsolutePath().normalize()} does not exist!"
if (!configFile.isReadable) res += "Config file ${configFile.toAbsolutePath().normalize()} is not readable"
return res
}
}

View File

@ -2,12 +2,15 @@ package net.corda.cliutils
import net.corda.core.internal.rootMessage
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.loggerFor
import org.apache.logging.log4j.Level
import org.fusesource.jansi.AnsiConsole
import picocli.CommandLine
import picocli.CommandLine.*
import kotlin.system.exitProcess
import java.util.*
import java.util.concurrent.Callable
/**
* When we have errors in command line flags that are not handled by picocli (e.g. non existing files), an error is thrown
@ -34,19 +37,46 @@ interface Validated {
logger.error(RED + "Exceptions when parsing command line arguments:")
logger.error(errors.joinToString("\n") + RESET)
CommandLine(this).usage(System.err)
exitProcess(1)
exitProcess(ExitCodes.FAILURE)
}
}
}
fun CordaCliWrapper.start(vararg args: String) {
/** This is generally covered by commons-lang. */
object CordaSystemUtils {
const val OS_NAME = "os.name"
const val MAC_PREFIX = "Mac"
const val WIN_PREFIX = "Windows"
fun isOsMac() = getOsName().startsWith(MAC_PREFIX)
fun isOsWindows() = getOsName().startsWith(WIN_PREFIX)
fun getOsName() = System.getProperty(OS_NAME)
}
fun CordaCliWrapper.start(args: Array<String>) {
// This line makes sure ANSI escapes work on Windows, where they aren't supported out of the box.
AnsiConsole.systemInstall()
val cmd = CommandLine(this)
this.args = args
cmd.commandSpec.name(alias)
cmd.commandSpec.usageMessage().description(description)
try {
cmd.parseWithHandlers(RunLast().useOut(System.out).useAnsi(Help.Ansi.AUTO),
DefaultExceptionHandler<List<Any>>().useErr(System.err).useAnsi(Help.Ansi.AUTO),
val defaultAnsiMode = if (CordaSystemUtils.isOsWindows()) { Help.Ansi.ON } else { Help.Ansi.AUTO }
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)
}
}
// 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) {
@ -54,7 +84,7 @@ fun CordaCliWrapper.start(vararg args: String) {
} else {
System.err.println("*ERROR*: ${throwable.rootMessage ?: "Use --verbose for more details"}")
}
exitProcess(1)
exitProcess(ExitCodes.FAILURE)
}
}
@ -72,8 +102,16 @@ fun CordaCliWrapper.start(vararg args: 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) : Runnable {
@Option(names = ["-v", "--verbose"], description = ["If set, prints logging to the console as well as to a file."])
abstract class CordaCliWrapper(val alias: String, val description: String) : Callable<Int> {
companion object {
private val logger by lazy { loggerFor<CordaCliWrapper>() }
}
// Raw args are provided for use in logging - this is a lateinit var rather than a constructor parameter as the class
// needs to be parameterless for autocomplete to work.
lateinit var args: Array<String>
@Option(names = ["-v", "--verbose", "--log-to-console"], description = ["If set, prints logging to the console as well as to a file."])
var verbose: Boolean = false
@Option(names = ["--logging-level"],
@ -88,7 +126,7 @@ abstract class CordaCliWrapper(val alias: String, val description: String) : Run
// 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.
private fun initLogging() {
open fun initLogging() {
val loggingLevel = loggingLevel.name().toLowerCase(Locale.ENGLISH)
System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file.
if (verbose) {
@ -96,13 +134,15 @@ abstract class CordaCliWrapper(val alias: String, val description: String) : Run
}
}
// Override this function with the actual method to be run once all the arguments have been parsed
abstract fun runProgram()
// Override this function with the actual method to be run once all the arguments have been parsed. The return number
// is the exit code to be returned
abstract fun runProgram(): Int
final override fun run() {
installShellExtensionsParser.installOrUpdateShellExtensions(alias, this.javaClass.name)
override fun call(): Int {
initLogging()
runProgram()
logger.info("Application Args: ${args.joinToString(" ")}")
installShellExtensionsParser.installOrUpdateShellExtensions(alias, this.javaClass.name)
return runProgram()
}
}

View File

@ -9,13 +9,17 @@ import picocli.CommandLine
*/
class CordaVersionProvider : CommandLine.IVersionProvider {
companion object {
val releaseVersion: String by lazy { Manifests.read("Corda-Release-Version") }
val revision: String by lazy { Manifests.read("Corda-Revision") }
private fun manifestValue(name: String): String? = if (Manifests.exists(name)) Manifests.read(name) else null
val releaseVersion: String by lazy { manifestValue("Corda-Release-Version") ?: "Unknown" }
val revision: String by lazy { manifestValue("Corda-Revision") ?: "Unknown" }
val vendor: String by lazy { manifestValue("Corda-Vendor") ?: "Unknown" }
val platformVersion: Int by lazy { manifestValue("Corda-Platform-Version")?.toInt() ?: 1 }
}
override fun getVersion(): Array<String> {
return if (Manifests.exists("Corda-Release-Version") && Manifests.exists("Corda-Revision")) {
arrayOf("Version: $releaseVersion", "Revision: $revision")
arrayOf("Version: $releaseVersion", "Revision: $revision", "Platform Version: $platformVersion", "Vendor: $vendor")
} else {
arrayOf("No version data is available in the MANIFEST file.")
}

View File

@ -0,0 +1,8 @@
package net.corda.cliutils
open class ExitCodes {
companion object {
const val SUCCESS: Int = 0
const val FAILURE: Int = 1
}
}

View File

@ -53,7 +53,14 @@ private class ShellExtensionsGenerator(val alias: String, val className: String)
}
private val userHome: Path by lazy { Paths.get(System.getProperty("user.home")) }
private val jarLocation: Path by lazy { this.javaClass.location.toPath() }
private val jarLocation: Path by lazy {
val capsuleJarProperty = System.getProperty("capsule.jar")
if (capsuleJarProperty != null) {
Paths.get(capsuleJarProperty)
} else {
this.javaClass.location.toPath()
}
}
// If on Windows, Path.toString() returns a path with \ instead of /, but for bash Windows users we want to convert those back to /'s
private fun Path.toStringWithDeWindowsfication(): String = this.toAbsolutePath().toString().replace("\\", "/")
@ -114,7 +121,6 @@ private class ShellExtensionsGenerator(val alias: String, val className: String)
}
}
@CommandLine.Command(description = [""])
class InstallShellExtensionsParser {
@CommandLine.Option(names = ["--install-shell-extensions"], description = ["Install alias and autocompletion for bash and zsh"])
var installShellExtensions: Boolean = false