mirror of
https://github.com/corda/corda.git
synced 2025-01-19 19:26:27 +00:00
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:
parent
304dba704e
commit
3284a61afd
@ -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:
|
||||
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
}
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
135
node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt
Normal file
135
node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt
Normal 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)
|
@ -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.",
|
||||
|
@ -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)
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.")
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
package net.corda.cliutils
|
||||
|
||||
open class ExitCodes {
|
||||
companion object {
|
||||
const val SUCCESS: Int = 0
|
||||
const val FAILURE: Int = 1
|
||||
}
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user