mirror of
https://github.com/corda/corda.git
synced 2024-12-24 07:06:44 +00:00
CORDA-1833: Create a picocli base class (#3826)
* Add shell extensions to CLI utils class and move into its own module * Fix issue with completion script generation and slight refactor * Fix autocompletion for logging level * Delete uneeded comment * More tidying up * Make run function final * Fixed an issue with the program being run twice. * Address review comments
This commit is contained in:
parent
98eef2f960
commit
bcfadfeebf
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@ -26,6 +26,8 @@
|
|||||||
<module name="canonicalizer_test" target="1.8" />
|
<module name="canonicalizer_test" target="1.8" />
|
||||||
<module name="client_main" target="1.8" />
|
<module name="client_main" target="1.8" />
|
||||||
<module name="client_test" target="1.8" />
|
<module name="client_test" target="1.8" />
|
||||||
|
<module name="cliutils_main" target="1.8" />
|
||||||
|
<module name="cliutils_test" target="1.8" />
|
||||||
<module name="common_main" target="1.8" />
|
<module name="common_main" target="1.8" />
|
||||||
<module name="common_test" target="1.8" />
|
<module name="common_test" target="1.8" />
|
||||||
<module name="confidential-identities_main" target="1.8" />
|
<module name="confidential-identities_main" target="1.8" />
|
||||||
|
@ -70,7 +70,7 @@ buildscript {
|
|||||||
ext.snappy_version = '0.4'
|
ext.snappy_version = '0.4'
|
||||||
ext.fast_classpath_scanner_version = '2.12.3'
|
ext.fast_classpath_scanner_version = '2.12.3'
|
||||||
ext.jcabi_manifests_version = '1.1'
|
ext.jcabi_manifests_version = '1.1'
|
||||||
ext.picocli_version = '3.3.0'
|
ext.picocli_version = '3.5.2'
|
||||||
|
|
||||||
// Name of the IntelliJ SDK created for the deterministic Java rt.jar.
|
// Name of the IntelliJ SDK created for the deterministic Java rt.jar.
|
||||||
// ext.deterministic_idea_sdk = '1.8 (Deterministic)'
|
// ext.deterministic_idea_sdk = '1.8 (Deterministic)'
|
||||||
|
@ -43,6 +43,7 @@ include 'tools:blobinspector'
|
|||||||
include 'tools:shell'
|
include 'tools:shell'
|
||||||
include 'tools:shell-cli'
|
include 'tools:shell-cli'
|
||||||
include 'tools:network-bootstrapper'
|
include 'tools:network-bootstrapper'
|
||||||
|
include 'tools:cliutils'
|
||||||
include 'example-code'
|
include 'example-code'
|
||||||
project(':example-code').projectDir = file("$settingsDir/docs/source/example-code")
|
project(':example-code').projectDir = file("$settingsDir/docs/source/example-code")
|
||||||
include 'samples:attachment-demo'
|
include 'samples:attachment-demo'
|
||||||
|
@ -7,6 +7,7 @@ description 'Network bootstrapper'
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(':node-api')
|
compile project(':node-api')
|
||||||
|
compile project(':tools:cliutils')
|
||||||
compile "info.picocli:picocli:$picocli_version"
|
compile "info.picocli:picocli:$picocli_version"
|
||||||
compile "org.slf4j:jul-to-slf4j:$slf4j_version"
|
compile "org.slf4j:jul-to-slf4j:$slf4j_version"
|
||||||
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
||||||
|
@ -1,38 +1,17 @@
|
|||||||
package net.corda.bootstrapper
|
package net.corda.bootstrapper
|
||||||
|
|
||||||
import com.jcabi.manifests.Manifests
|
import net.corda.cliutils.CordaCliWrapper
|
||||||
import net.corda.core.internal.*
|
import net.corda.cliutils.start
|
||||||
import net.corda.nodeapi.internal.network.NetworkBootstrapper
|
import net.corda.nodeapi.internal.network.NetworkBootstrapper
|
||||||
import picocli.CommandLine
|
import picocli.CommandLine.Option
|
||||||
import picocli.CommandLine.*
|
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
val main = Main()
|
NetworkBootstrapperRunner().start(*args)
|
||||||
try {
|
|
||||||
CommandLine.run(main, *args)
|
|
||||||
} catch (e: ExecutionException) {
|
|
||||||
val throwable = e.cause ?: e
|
|
||||||
if (main.verbose) {
|
|
||||||
throwable.printStackTrace()
|
|
||||||
} else {
|
|
||||||
System.err.println("*ERROR*: ${throwable.rootMessage ?: "Use --verbose for more details"}")
|
|
||||||
}
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Command(
|
class NetworkBootstrapperRunner : CordaCliWrapper("bootstrapper", "Bootstrap a local test Corda network using a set of node configuration files and CorDapp JARs") {
|
||||||
name = "Network Bootstrapper",
|
|
||||||
versionProvider = CordaVersionProvider::class,
|
|
||||||
mixinStandardHelpOptions = true,
|
|
||||||
showDefaultValues = true,
|
|
||||||
description = ["Bootstrap a local test Corda network using a set of node configuration files and CorDapp JARs"]
|
|
||||||
)
|
|
||||||
class Main : Runnable {
|
|
||||||
@Option(
|
@Option(
|
||||||
names = ["--dir"],
|
names = ["--dir"],
|
||||||
description = [
|
description = [
|
||||||
@ -45,143 +24,7 @@ class Main : Runnable {
|
|||||||
@Option(names = ["--no-copy"], description = ["""Don't copy the CorDapp JARs into the nodes' "cordapps" directories."""])
|
@Option(names = ["--no-copy"], description = ["""Don't copy the CorDapp JARs into the nodes' "cordapps" directories."""])
|
||||||
private var noCopy: Boolean = false
|
private var noCopy: Boolean = false
|
||||||
|
|
||||||
@Option(names = ["--verbose"], description = ["Enable verbose output."])
|
override fun runProgram() {
|
||||||
var verbose: Boolean = false
|
|
||||||
|
|
||||||
@Option(names = ["--install-shell-extensions"], description = ["Install bootstrapper alias and autocompletion for bash and zsh"])
|
|
||||||
var installShellExtensions: Boolean = false
|
|
||||||
|
|
||||||
private class SettingsFile(val filePath: Path) {
|
|
||||||
private val lines: MutableList<String> by lazy { getFileLines() }
|
|
||||||
var fileModified: Boolean = false
|
|
||||||
|
|
||||||
// Return the lines in the file if it exists, else return an empty mutable list
|
|
||||||
private fun getFileLines(): MutableList<String> {
|
|
||||||
return if (filePath.exists()) {
|
|
||||||
filePath.toFile().readLines().toMutableList()
|
|
||||||
} else {
|
|
||||||
emptyList<String>().toMutableList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addOrReplaceIfStartsWith(startsWith: String, replaceWith: String) {
|
|
||||||
val index = lines.indexOfFirst { it.startsWith(startsWith) }
|
|
||||||
if (index >= 0) {
|
|
||||||
if (lines[index] != replaceWith) {
|
|
||||||
lines[index] = replaceWith
|
|
||||||
fileModified = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
lines.add(replaceWith)
|
|
||||||
fileModified = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addIfNotExists(line: String) {
|
|
||||||
if (!lines.contains(line)) {
|
|
||||||
lines.add(line)
|
|
||||||
fileModified = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateAndBackupIfNecessary() {
|
|
||||||
if (fileModified) {
|
|
||||||
val backupFilePath = filePath.parent / "${filePath.fileName}.backup"
|
|
||||||
println("Updating settings in ${filePath.fileName} - existing settings file has been backed up to $backupFilePath")
|
|
||||||
if (filePath.exists()) filePath.copyTo(backupFilePath, REPLACE_EXISTING)
|
|
||||||
filePath.writeLines(lines)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val userHome: Path by lazy { Paths.get(System.getProperty("user.home")) }
|
|
||||||
private val jarLocation: Path by lazy { 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("\\", "/")
|
|
||||||
private fun jarVersion(alias: String) = "# $alias - Version: ${CordaVersionProvider.releaseVersion}, Revision: ${CordaVersionProvider.revision}"
|
|
||||||
private fun getAutoCompleteFileLocation(alias: String) = userHome / ".completion" / alias
|
|
||||||
|
|
||||||
private fun generateAutoCompleteFile(alias: String) {
|
|
||||||
println("Generating $alias auto completion file")
|
|
||||||
val autoCompleteFile = getAutoCompleteFileLocation(alias)
|
|
||||||
autoCompleteFile.parent.createDirectories()
|
|
||||||
picocli.AutoComplete.main("-f", "-n", alias, this.javaClass.name, "-o", autoCompleteFile.toStringWithDeWindowsfication())
|
|
||||||
|
|
||||||
// Append hash of file to autocomplete file
|
|
||||||
autoCompleteFile.toFile().appendText(jarVersion(alias))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun installShellExtensions(alias: String) {
|
|
||||||
// Get jar location and generate alias command
|
|
||||||
val command = "alias $alias='java -jar \"${jarLocation.toStringWithDeWindowsfication()}\"'"
|
|
||||||
generateAutoCompleteFile(alias)
|
|
||||||
|
|
||||||
// Get bash settings file
|
|
||||||
val bashSettingsFile = SettingsFile(userHome / ".bashrc")
|
|
||||||
// Replace any existing bootstrapper alias. There can be only one.
|
|
||||||
bashSettingsFile.addOrReplaceIfStartsWith("alias $alias", command)
|
|
||||||
val completionFileCommand = "for bcfile in ~/.completion/* ; do . \$bcfile; done"
|
|
||||||
bashSettingsFile.addIfNotExists(completionFileCommand)
|
|
||||||
bashSettingsFile.updateAndBackupIfNecessary()
|
|
||||||
|
|
||||||
// Get zsh settings file
|
|
||||||
val zshSettingsFile = SettingsFile(userHome / ".zshrc")
|
|
||||||
zshSettingsFile.addIfNotExists("autoload -U +X compinit && compinit")
|
|
||||||
zshSettingsFile.addIfNotExists("autoload -U +X bashcompinit && bashcompinit")
|
|
||||||
zshSettingsFile.addOrReplaceIfStartsWith("alias $alias", command)
|
|
||||||
zshSettingsFile.addIfNotExists(completionFileCommand)
|
|
||||||
zshSettingsFile.updateAndBackupIfNecessary()
|
|
||||||
|
|
||||||
println("Installation complete, $alias is available in bash with autocompletion. ")
|
|
||||||
println("Type `$alias <options>` from the commandline.")
|
|
||||||
println("Restart bash for this to take effect, or run `. ~/.bashrc` in bash or `. ~/.zshrc` in zsh to re-initialise your shell now")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkForAutoCompleteUpdate(alias: String) {
|
|
||||||
val autoCompleteFile = getAutoCompleteFileLocation(alias)
|
|
||||||
|
|
||||||
// If no autocomplete file, it hasn't been installed, so don't do anything
|
|
||||||
if (!autoCompleteFile.exists()) return
|
|
||||||
|
|
||||||
var lastLine = ""
|
|
||||||
autoCompleteFile.toFile().forEachLine { lastLine = it }
|
|
||||||
|
|
||||||
if (lastLine != jarVersion(alias)) {
|
|
||||||
println("Old auto completion file detected... regenerating")
|
|
||||||
generateAutoCompleteFile(alias)
|
|
||||||
println("Restart bash for this to take effect, or run `. ~/.bashrc` to re-initialise bash now")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun installOrUpdateShellExtensions(alias: String) {
|
|
||||||
if (installShellExtensions) {
|
|
||||||
installShellExtensions(alias)
|
|
||||||
exitProcess(0)
|
|
||||||
} else {
|
|
||||||
checkForAutoCompleteUpdate(alias)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
installOrUpdateShellExtensions("bootstrapper")
|
|
||||||
if (verbose) {
|
|
||||||
System.setProperty("logLevel", "trace")
|
|
||||||
}
|
|
||||||
NetworkBootstrapper().bootstrap(dir.toAbsolutePath().normalize(), copyCordapps = !noCopy)
|
NetworkBootstrapper().bootstrap(dir.toAbsolutePath().normalize(), copyCordapps = !noCopy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CordaVersionProvider : IVersionProvider {
|
|
||||||
companion object {
|
|
||||||
val releaseVersion: String by lazy { Manifests.read("Corda-Release-Version") }
|
|
||||||
val revision: String by lazy { Manifests.read("Corda-Revision") }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getVersion(): Array<String> {
|
|
||||||
return arrayOf(
|
|
||||||
"Version: $releaseVersion",
|
|
||||||
"Revision: $revision"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
14
tools/cliutils/build.gradle
Normal file
14
tools/cliutils/build.gradle
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
apply plugin: 'java'
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
|
||||||
|
description 'CLI Utilities'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile project(":core")
|
||||||
|
|
||||||
|
compile "info.picocli:picocli:$picocli_version"
|
||||||
|
compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version"
|
||||||
|
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
||||||
|
|
||||||
|
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -1,54 +1,17 @@
|
|||||||
package net.corda.node.utilities
|
package net.corda.cliutils
|
||||||
|
|
||||||
import com.jcabi.manifests.Manifests
|
import net.corda.core.internal.rootMessage
|
||||||
import net.corda.core.internal.exists
|
|
||||||
import net.corda.core.internal.isReadable
|
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
|
|
||||||
import org.apache.logging.log4j.Level
|
import org.apache.logging.log4j.Level
|
||||||
import picocli.CommandLine
|
import picocli.CommandLine
|
||||||
import picocli.CommandLine.*
|
import picocli.CommandLine.*
|
||||||
import java.nio.file.Path
|
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Something heavily used in network services, I am not sure it's of much use in corda, but who knows. Definitely it was the key to making DevOps happy.
|
* When we have errors in command line flags that are not handled by picocli (e.g. non existing files), an error is thrown
|
||||||
* Add it as
|
* without any command line help afterwards. This can be called from run() method.
|
||||||
* `@CommandLine.Mixin
|
|
||||||
* lateinit var configParser: ConfigFilePathArgsParser`
|
|
||||||
*
|
|
||||||
* in your command class and then validate()
|
|
||||||
*/
|
|
||||||
@Command(description = ["Parse configuration file. Checks if given configuration file exists"])
|
|
||||||
class ConfigFilePathArgsParser : Validated {
|
|
||||||
@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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple version printing when command is called with --version or -V flag. Assuming that we reuse Corda-Release-Version and Corda-Revision
|
|
||||||
* in the manifest file.
|
|
||||||
*/
|
|
||||||
class CordaVersionProvider : IVersionProvider {
|
|
||||||
override fun getVersion(): Array<String> {
|
|
||||||
return if (Manifests.exists("Corda-Release-Version") && Manifests.exists("Corda-Revision")) {
|
|
||||||
arrayOf("Version: ${Manifests.read("Corda-Release-Version")}", "Revision: ${Manifests.read("Corda-Revision")}")
|
|
||||||
} else {
|
|
||||||
arrayOf("No version data is available in the MANIFEST file.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Usually when we have errors in some command line flags that are not handled by picocli (e.g. non existing file). Error is thrown
|
|
||||||
* and no CommandLine help afterwards. This can be called from run() method.
|
|
||||||
*/
|
*/
|
||||||
interface Validated {
|
interface Validated {
|
||||||
companion object {
|
companion object {
|
||||||
@ -56,6 +19,7 @@ interface Validated {
|
|||||||
const val RED = "\u001B[31m"
|
const val RED = "\u001B[31m"
|
||||||
const val RESET = "\u001B[0m"
|
const val RESET = "\u001B[0m"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check that provided command line parameters are valid, e.g. check file existence. Return list of error strings.
|
* Check that provided command line parameters are valid, e.g. check file existence. Return list of error strings.
|
||||||
*/
|
*/
|
||||||
@ -75,6 +39,25 @@ interface Validated {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun CordaCliWrapper.start(vararg args: String) {
|
||||||
|
val cmd = CommandLine(this)
|
||||||
|
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),
|
||||||
|
*args)
|
||||||
|
} catch (e: ExecutionException) {
|
||||||
|
val throwable = e.cause ?: e
|
||||||
|
if (this.verbose) {
|
||||||
|
throwable.printStackTrace()
|
||||||
|
} else {
|
||||||
|
System.err.println("*ERROR*: ${throwable.rootMessage ?: "Use --verbose for more details"}")
|
||||||
|
}
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple base class for handling help, version, verbose and logging-level commands.
|
* Simple base class for handling help, version, verbose and logging-level commands.
|
||||||
* As versionProvider information from the MANIFEST file is used. It can be overwritten by custom version providers (see: Node)
|
* As versionProvider information from the MANIFEST file is used. It can be overwritten by custom version providers (see: Node)
|
||||||
@ -83,30 +66,44 @@ interface Validated {
|
|||||||
@Command(mixinStandardHelpOptions = true,
|
@Command(mixinStandardHelpOptions = true,
|
||||||
versionProvider = CordaVersionProvider::class,
|
versionProvider = CordaVersionProvider::class,
|
||||||
sortOptions = false,
|
sortOptions = false,
|
||||||
|
showDefaultValues = true,
|
||||||
synopsisHeading = "%n@|bold,underline Usage|@:%n%n",
|
synopsisHeading = "%n@|bold,underline Usage|@:%n%n",
|
||||||
descriptionHeading = "%n@|bold,underline Description|@:%n%n",
|
descriptionHeading = "%n@|bold,underline Description|@:%n%n",
|
||||||
parameterListHeading = "%n@|bold,underline Parameters|@:%n%n",
|
parameterListHeading = "%n@|bold,underline Parameters|@:%n%n",
|
||||||
optionListHeading = "%n@|bold,underline Options|@:%n%n",
|
optionListHeading = "%n@|bold,underline Options|@:%n%n",
|
||||||
commandListHeading = "%n@|bold,underline Commands|@:%n%n")
|
commandListHeading = "%n@|bold,underline Commands|@:%n%n")
|
||||||
abstract class ArgsParser {
|
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."])
|
@Option(names = ["-v", "--verbose"], description = ["If set, prints logging to the console as well as to a file."])
|
||||||
var verbose: Boolean = false
|
var verbose: Boolean = false
|
||||||
|
|
||||||
@Option(names = ["--logging-level"],
|
@Option(names = ["--logging-level"],
|
||||||
// TODO For some reason I couldn't make picocli COMPLETION-CANDIDATES work
|
completionCandidates = LoggingLevelConverter.LoggingLevels::class,
|
||||||
description = ["Enable logging at this level and higher. Defaults to INFO. Possible values: OFF, INFO, WARN, TRACE, DEBUG, ERROR, FATAL, ALL"],
|
description = ["Enable logging at this level and higher. Possible values: \${COMPLETION-CANDIDATES}"],
|
||||||
converter = [LoggingLevelConverter::class])
|
converter = [LoggingLevelConverter::class]
|
||||||
|
)
|
||||||
var loggingLevel: Level = Level.INFO
|
var loggingLevel: Level = Level.INFO
|
||||||
|
|
||||||
|
@Mixin
|
||||||
|
private lateinit var installShellExtensionsParser: InstallShellExtensionsParser
|
||||||
|
|
||||||
// This needs to be called before loggers (See: NodeStartup.kt:51 logger called by lazy, initLogging happens before).
|
// 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.
|
// Node's logging is more rich. In corda configurations two properties, defaultLoggingLevel and consoleLogLevel, are usually used.
|
||||||
protected open fun initLogging() {
|
private fun initLogging() {
|
||||||
val loggingLevel = loggingLevel.name().toLowerCase(Locale.ENGLISH)
|
val loggingLevel = loggingLevel.name().toLowerCase(Locale.ENGLISH)
|
||||||
System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file.
|
System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file.
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
System.setProperty("consoleLogLevel", loggingLevel)
|
System.setProperty("consoleLogLevel", loggingLevel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override this function with the actual method to be run once all the arguments have been parsed
|
||||||
|
abstract fun runProgram()
|
||||||
|
|
||||||
|
final override fun run() {
|
||||||
|
installShellExtensionsParser.installOrUpdateShellExtensions(alias, this.javaClass.name)
|
||||||
|
initLogging()
|
||||||
|
runProgram()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -114,6 +111,9 @@ abstract class ArgsParser {
|
|||||||
*/
|
*/
|
||||||
class LoggingLevelConverter : ITypeConverter<Level> {
|
class LoggingLevelConverter : ITypeConverter<Level> {
|
||||||
override fun convert(value: String?): Level {
|
override fun convert(value: String?): Level {
|
||||||
return value?.let { Level.getLevel(it) } ?: throw TypeConversionException("Unknown option for --logging-level: $value")
|
return value?.let { Level.getLevel(it) }
|
||||||
|
?: throw TypeConversionException("Unknown option for --logging-level: $value")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LoggingLevels : ArrayList<String>(Level.values().map { it.toString() })
|
||||||
}
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package net.corda.cliutils
|
||||||
|
|
||||||
|
import com.jcabi.manifests.Manifests
|
||||||
|
import picocli.CommandLine
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple version printing when command is called with --version or -V flag. Assuming that we reuse Corda-Release-Version and Corda-Revision
|
||||||
|
* in the manifest file.
|
||||||
|
*/
|
||||||
|
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") }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getVersion(): Array<String> {
|
||||||
|
return if (Manifests.exists("Corda-Release-Version") && Manifests.exists("Corda-Revision")) {
|
||||||
|
arrayOf("Version: $releaseVersion", "Revision: $revision")
|
||||||
|
} else {
|
||||||
|
arrayOf("No version data is available in the MANIFEST file.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,131 @@
|
|||||||
|
package net.corda.cliutils
|
||||||
|
|
||||||
|
import net.corda.core.internal.*
|
||||||
|
import picocli.CommandLine
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
private class ShellExtensionsGenerator(val alias: String, val className: String) {
|
||||||
|
private class SettingsFile(val filePath: Path) {
|
||||||
|
private val lines: MutableList<String> by lazy { getFileLines() }
|
||||||
|
var fileModified: Boolean = false
|
||||||
|
|
||||||
|
// Return the lines in the file if it exists, else return an empty mutable list
|
||||||
|
private fun getFileLines(): MutableList<String> {
|
||||||
|
return if (filePath.exists()) {
|
||||||
|
filePath.toFile().readLines().toMutableList()
|
||||||
|
} else {
|
||||||
|
Collections.emptyList<String>().toMutableList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addOrReplaceIfStartsWith(startsWith: String, replaceWith: String) {
|
||||||
|
val index = lines.indexOfFirst { it.startsWith(startsWith) }
|
||||||
|
if (index >= 0) {
|
||||||
|
if (lines[index] != replaceWith) {
|
||||||
|
lines[index] = replaceWith
|
||||||
|
fileModified = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lines.add(replaceWith)
|
||||||
|
fileModified = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addIfNotExists(line: String) {
|
||||||
|
if (!lines.contains(line)) {
|
||||||
|
lines.add(line)
|
||||||
|
fileModified = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateAndBackupIfNecessary() {
|
||||||
|
if (fileModified) {
|
||||||
|
val backupFilePath = filePath.parent / "${filePath.fileName}.backup"
|
||||||
|
println("Updating settings in ${filePath.fileName} - existing settings file has been backed up to $backupFilePath")
|
||||||
|
if (filePath.exists()) filePath.copyTo(backupFilePath, StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
filePath.writeLines(lines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val userHome: Path by lazy { Paths.get(System.getProperty("user.home")) }
|
||||||
|
private val jarLocation: Path by lazy { 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("\\", "/")
|
||||||
|
|
||||||
|
private fun jarVersion(alias: String) = "# $alias - Version: ${CordaVersionProvider.releaseVersion}, Revision: ${CordaVersionProvider.revision}"
|
||||||
|
private fun getAutoCompleteFileLocation(alias: String) = userHome / ".completion" / alias
|
||||||
|
|
||||||
|
private fun generateAutoCompleteFile(alias: String, className: String) {
|
||||||
|
println("Generating $alias auto completion file")
|
||||||
|
val autoCompleteFile = getAutoCompleteFileLocation(alias)
|
||||||
|
autoCompleteFile.parent.createDirectories()
|
||||||
|
picocli.AutoComplete.main("-f", "-n", alias, className, "-o", autoCompleteFile.toStringWithDeWindowsfication())
|
||||||
|
|
||||||
|
// Append hash of file to autocomplete file
|
||||||
|
autoCompleteFile.toFile().appendText(jarVersion(alias))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun installShellExtensions() {
|
||||||
|
// Get jar location and generate alias command
|
||||||
|
val command = "alias $alias='java -jar \"${jarLocation.toStringWithDeWindowsfication()}\"'"
|
||||||
|
generateAutoCompleteFile(alias, className)
|
||||||
|
|
||||||
|
// Get bash settings file
|
||||||
|
val bashSettingsFile = SettingsFile(userHome / ".bashrc")
|
||||||
|
// Replace any existing alias. There can be only one.
|
||||||
|
bashSettingsFile.addOrReplaceIfStartsWith("alias $alias", command)
|
||||||
|
val completionFileCommand = "for bcfile in ~/.completion/* ; do . \$bcfile; done"
|
||||||
|
bashSettingsFile.addIfNotExists(completionFileCommand)
|
||||||
|
bashSettingsFile.updateAndBackupIfNecessary()
|
||||||
|
|
||||||
|
// Get zsh settings file
|
||||||
|
val zshSettingsFile = SettingsFile(userHome / ".zshrc")
|
||||||
|
zshSettingsFile.addIfNotExists("autoload -U +X compinit && compinit")
|
||||||
|
zshSettingsFile.addIfNotExists("autoload -U +X bashcompinit && bashcompinit")
|
||||||
|
zshSettingsFile.addOrReplaceIfStartsWith("alias $alias", command)
|
||||||
|
zshSettingsFile.addIfNotExists(completionFileCommand)
|
||||||
|
zshSettingsFile.updateAndBackupIfNecessary()
|
||||||
|
|
||||||
|
println("Installation complete, $alias is available in bash with autocompletion. ")
|
||||||
|
println("Type `$alias <options>` from the commandline.")
|
||||||
|
println("Restart bash for this to take effect, or run `. ~/.bashrc` in bash or `. ~/.zshrc` in zsh to re-initialise your shell now")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkForAutoCompleteUpdate() {
|
||||||
|
val autoCompleteFile = getAutoCompleteFileLocation(alias)
|
||||||
|
|
||||||
|
// If no autocomplete file, it hasn't been installed, so don't do anything
|
||||||
|
if (!autoCompleteFile.exists()) return
|
||||||
|
|
||||||
|
var lastLine = ""
|
||||||
|
autoCompleteFile.toFile().forEachLine { lastLine = it }
|
||||||
|
|
||||||
|
if (lastLine != jarVersion(alias)) {
|
||||||
|
println("Old auto completion file detected... regenerating")
|
||||||
|
generateAutoCompleteFile(alias, className)
|
||||||
|
println("Restart bash for this to take effect, or run `. ~/.bashrc` to re-initialise bash now")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CommandLine.Command(description = [""])
|
||||||
|
class InstallShellExtensionsParser {
|
||||||
|
@CommandLine.Option(names = ["--install-shell-extensions"], description = ["Install alias and autocompletion for bash and zsh"])
|
||||||
|
var installShellExtensions: Boolean = false
|
||||||
|
|
||||||
|
fun installOrUpdateShellExtensions(alias: String, className: String) {
|
||||||
|
val generator = ShellExtensionsGenerator(alias, className)
|
||||||
|
if (installShellExtensions) {
|
||||||
|
generator.installShellExtensions()
|
||||||
|
exitProcess(0)
|
||||||
|
} else {
|
||||||
|
generator.checkForAutoCompleteUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user