Merge pull request #458 from corda/stories/ENT-1152/bootstrapper

Standalone packaging using two-stage loader (experimental) [ENT-1152]
This commit is contained in:
Stefano Franz 2018-05-18 14:37:35 +01:00 committed by GitHub
commit 79f18abe60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 448 additions and 3 deletions

8
.idea/compiler.xml generated
View File

@ -52,6 +52,12 @@
<module name="core_test" target="1.8" />
<module name="demobench_main" target="1.8" />
<module name="demobench_test" target="1.8" />
<module name="dist_binFiles" target="1.8" />
<module name="dist_licenseFiles" target="1.8" />
<module name="dist_main" target="1.8" />
<module name="dist_readmeFiles" target="1.8" />
<module name="dist_startupScripts" target="1.8" />
<module name="dist_test" target="1.8" />
<module name="docs_main" target="1.8" />
<module name="docs_source_example-code_integrationTest" target="1.8" />
<module name="docs_source_example-code_main" target="1.8" />
@ -98,6 +104,8 @@
<module name="jfx_test" target="1.8" />
<module name="kryo-hook_main" target="1.8" />
<module name="kryo-hook_test" target="1.8" />
<module name="launcher_main" target="1.8" />
<module name="launcher_test" target="1.8" />
<module name="loadtest_main" target="1.8" />
<module name="loadtest_test" target="1.8" />
<module name="mock_main" target="1.8" />

19
launcher/build.gradle Normal file
View File

@ -0,0 +1,19 @@
group 'com.r3.corda'
version 'R3.CORDA-3.0-SNAPSHOT'
apply plugin: 'java'
apply plugin: 'kotlin'
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
}
ext {
loaderClassName = "net.corda.launcher.Loader"
launcherClassName = "net.corda.launcher.Launcher"
}
jar {
baseName 'corda-launcher'
}

View File

@ -0,0 +1,81 @@
@file:JvmName("Launcher")
package net.corda.launcher
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.system.exitProcess
fun main(args: Array<String>) {
if(args.isEmpty()) {
println("Usage: launcher <main-class-name> [args]")
exitProcess(0)
}
// TODO: --base-directory is specific of the Node app, it should be controllable by a config property
val nodeBaseDir = Paths.get(Settings.WORKING_DIR)
.resolve(getBaseDirectory(args) ?: ".")
.toAbsolutePath()
val appClassLoader = setupClassLoader(nodeBaseDir)
val appMain = try {
appClassLoader
.loadClass(args[0])
.getMethod("main", Array<String>::class.java)
} catch (e: Exception) {
System.err.println("Error looking for method 'main' in class ${args[0]}:")
e.printStackTrace()
exitProcess(1)
}
// Propagate current working directory via system property, to patch it after javapackager
System.setProperty("corda-launcher.cwd", Settings.WORKING_DIR)
System.setProperty("user.dir", Settings.WORKING_DIR)
try {
appMain.invoke(null, args.sliceArray(1..args.lastIndex))
} catch (e: Exception) {
e.printStackTrace()
exitProcess(1)
}
}
private fun setupClassLoader(nodeBaseDir: Path): ClassLoader {
val sysClassLoader = ClassLoader.getSystemClassLoader()
val appClassLoader = (sysClassLoader as? Loader) ?: {
println("WARNING: failed to override system classloader")
Loader(sysClassLoader)
} ()
// Lookup plugins and extend classpath
val pluginURLs = Settings.PLUGINS.flatMap {
val entry = nodeBaseDir.resolve(it)
if (Files.isDirectory(entry)) {
entry.jarFiles()
} else {
setOf(entry)
}
}.map { it.toUri().toURL() }
appClassLoader.augmentClasspath(pluginURLs)
// For logging
System.setProperty("corda-launcher.appclassloader.urls", appClassLoader.urLs.joinToString(":"))
return appClassLoader
}
private fun getBaseDirectory(args: Array<String>): String? {
val idx = args.indexOf("--base-directory")
return if (idx != -1 && idx < args.lastIndex) {
args[idx + 1]
} else null
}
private fun Path.jarFiles(): List<Path> {
return Files.newDirectoryStream(this).filter { it.toString().endsWith(".jar") }
}

View File

@ -0,0 +1,11 @@
package net.corda.launcher
import java.net.URL
import java.net.URLClassLoader
class Loader(parent: ClassLoader?): URLClassLoader(Settings.CLASSPATH.toTypedArray(), parent) {
fun augmentClasspath(urls: List<URL>) {
urls.forEach { addURL(it) }
}
}

View File

@ -0,0 +1,59 @@
package net.corda.launcher
import java.io.FileInputStream
import java.net.URL
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
import kotlin.collections.HashSet
// Expose Corda bootstrapping settings from property file
object Settings {
// JavaPackager reset cwd to the "/apps" subfolder, so its location is in the parent directory
private val LAUNCHER_PATH = Paths.get("..")
// Launcher property file
private val CORDA_RUNTIME_SETTINGS = LAUNCHER_PATH.resolve("runtime.properties")
// The application working directory
val WORKING_DIR: String = System.getenv("CORDA_LAUNCHER_CWD") ?: ".."
// Application classpath
val CLASSPATH: List<URL>
// Plugin directories (all contained jar files are added to classpath)
val PLUGINS: List<Path>
// Path of the "lib" subdirectory in bundle
private val LIBPATH: Path
init {
val settings = Properties().apply {
load(FileInputStream(CORDA_RUNTIME_SETTINGS.toFile()))
}
LIBPATH = Paths.get(settings.getProperty("libpath") ?: ".")
CLASSPATH = parseClasspath(settings)
PLUGINS = parsePlugins(settings)
}
private fun parseClasspath(config: Properties): List<URL> {
val libDir = LAUNCHER_PATH.resolve(LIBPATH).toAbsolutePath()
val cp = config.getProperty("classpath") ?:
throw Error("Missing 'classpath' property from config")
return cp.split(':').map {
libDir.resolve(it).toUri().toURL()
}
}
private fun parsePlugins(config: Properties): List<Path> {
val ext = config.getProperty("plugins")
return ext?.let {
it.split(':').map { Paths.get(it) }
} ?: emptyList()
}
}

View File

@ -71,6 +71,8 @@ dependencies {
compile project(":confidential-identities")
compile project(':client:rpc')
compile project(':tools:shell')
runtime project(':launcher')
compile "net.corda.plugins:cordform-common:$gradle_plugins_version"
// Log4J: logging framework (with SLF4J bindings)

View File

@ -28,6 +28,11 @@ dependencies {
capsuleRuntime "com.typesafe:config:$typesafe_config_version"
}
ext {
quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**)"
applicationClass = 'net.corda.node.Corda'
}
// Force the Caplet to target Java 6. This ensures that running 'java -jar corda.jar' on any Java 6 VM upwards
// will get as far as the Capsule version checks, meaning that if your JVM is too old, you will at least get
// a sensible error message telling you what to do rather than a bytecode version exception that doesn't.
@ -37,8 +42,8 @@ sourceCompatibility = 1.6
targetCompatibility = 1.6
task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) {
applicationClass 'net.corda.node.Corda'
archiveName "corda-r3-${corda_release_version}.jar"
applicationClass 'net.corda.node.Corda'
applicationSource = files(
project(':node').configurations.runtime,
project(':node').jar,
@ -54,7 +59,6 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) {
applicationVersion = corda_release_version
// See experimental/quasar-hook/README.md for how to generate.
def quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**)"
javaAgents = ["quasar-core-${quasar_version}-jdk8.jar=${quasarExcludeExpression}"]
systemProperties['visualvm.display.name'] = 'CordaEnterprise'
minJavaVersion = '1.8.0'

5
node/dist/README.md vendored Normal file
View File

@ -0,0 +1,5 @@
This project adds `buildCordaTarball` task to Gradle. It prepares distributable tarball with JRE built-in, using ``javapackager``
For now, it packs the whatever JRE is available in the system, but this will get standarised over time.
It requires ``javapackager`` to be available in the path.

159
node/dist/build.gradle vendored Normal file
View File

@ -0,0 +1,159 @@
description 'Package Node as stand-alone application'
evaluationDependsOn(":node")
evaluationDependsOn(":docs")
evaluationDependsOn(":launcher")
ext {
outputDir = "$buildDir/corda"
}
def tmpDir = "${buildDir}/tmp/"
configurations {
launcherClasspath
}
// Define location of predefined startup scripts, license and README files
sourceSets {
binFiles {
resources {
srcDir file('src/main/resources/bin')
}
}
readmeFiles {
resources {
srcDir file('src/main/resources/readme')
}
}
}
// Runtime dependencies of launcher
dependencies {
compile project(':node')
launcherClasspath project(':launcher')
launcherClasspath "org.slf4j:jul-to-slf4j:$slf4j_version"
launcherClasspath "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}"
launcherClasspath "org.apache.logging.log4j:log4j-web:${log4j_version}"
// Required by JVM agents:
launcherClasspath "com.google.guava:guava:$guava_version"
launcherClasspath "de.javakaffee:kryo-serializers:0.41"
}
task copyLauncherLibs(type: Copy, dependsOn: [project(':launcher').jar]) {
from configurations.launcherClasspath
into "$buildDir/tmp/launcher-lib"
}
// The launcher building is done as it depends on application-specific settings which
// cannot be overridden later
task buildLauncher(type: Exec, dependsOn: [copyLauncherLibs]) {
description 'Build Launcher executable'
def isLinux = System.properties['os.name'].toLowerCase().contains('linux')
def isMac = System.properties['os.name'].toLowerCase().contains('mac')
if (!isLinux && !isMac)
throw new GradleException("Preparing distribution package is currently only supported on Linux/Mac")
def relativeDir
if (isLinux)
relativeDir = "launcher"
else
relativeDir = "launcher.app/Contents"
def extraArgs = [
"-BjvmOptions=-javaagent:../../lib/quasar-core-${quasar_version}-jdk8.jar=${project(':node:capsule').quasarExcludeExpression}",
'-BuserJvmOptions=-Xmx=4g',
'-BuserJvmOptions=-XX\\:=+UseG1GC',
"-BjvmProperties=java.system.class.loader=${project(':launcher').loaderClassName}"
]
ext {
launcherBinDir = "${tmpDir}/bundles/$relativeDir"
}
workingDir project.projectDir
doFirst {
def launcherLib = copyLauncherLibs.destinationDir
def srcfiles = []
def launcherClasspath = []
fileTree(launcherLib).forEach({ file ->
srcfiles.add("-srcfiles")
srcfiles.add(file.name)
launcherClasspath.add(file.name)
})
commandLine = [
'javapackager',
'-deploy',
'-nosign',
'-native', 'image',
'-outdir', "$tmpDir",
'-outfile', 'launcher',
'-name', 'launcher',
"-BmainJar=${project(':launcher').jar.archiveName}",
"-Bclasspath=${launcherClasspath.join(":")}",
'-appclass', "${project(':launcher').launcherClassName}",
'-srcdir', "$launcherLib"
] + srcfiles + extraArgs
}
// Add configuration for running Node application
doLast {
def nodeClasspath = []
def libRelPath = "../lib/"
project(':node').configurations.runtime.forEach({ file ->
nodeClasspath.add(file.getName())
})
nodeClasspath.add(project(':node').jar.archivePath.getName())
new File("${launcherBinDir}/runtime.properties").text = [
"libpath=${libRelPath}",
"classpath=${nodeClasspath.join(':')}",
"plugins=./drivers:./cordapps"].join("\n")
}
}
task buildNode(type: Copy, dependsOn: [buildLauncher, project(':docs').tasks['makeDocs'], project(':node').tasks['jar']]) {
description 'Build stand-alone Corda Node distribution'
into(outputDir)
from(buildLauncher.launcherBinDir) {
into("launcher")
}
from(project(':node').configurations.runtime) {
into("lib")
}
from(project(':node').jar.archivePath) {
into("lib")
}
from(sourceSets.binFiles.resources) {
into("bin")
}
from(sourceSets.readmeFiles.resources) {
into(".")
}
from(project(':docs').buildDir) {
into("docs")
}
doLast {
new File("${outputDir}/cordapps").mkdirs()
new File("${outputDir}/drivers").mkdirs()
println ("Stand-alone Corda Node application available at:")
println ("${outputDir}")
}
}

20
node/dist/src/main/resources/bin/corda vendored Executable file
View File

@ -0,0 +1,20 @@
#!/bin/sh
# ------------------------
# Corda startup script
# -------------------------
MAINCLASSNAME="net.corda.node.Corda"
READLINK=`which readlink`
# Locate this script and relative launcher executable
SCRIPT_LOCATION=$0
if [ -x "$READLINK" ]; then
while [ -L "$SCRIPT_LOCATION" ]; do
SCRIPT_LOCATION=`"$READLINK" -e "$SCRIPT_LOCATION"`
done
fi
SCRIPT_DIR=`dirname "$SCRIPT_LOCATION"`
LAUNCHER_LOCATION="$SCRIPT_DIR/../launcher/launcher"
# Run Corda
CORDA_LAUNCHER_CWD="`pwd`" ${LAUNCHER_LOCATION} ${MAINCLASSNAME} "$@"

View File

@ -0,0 +1,9 @@
Welcome to Corda Enterprise!
This is a distributon package containing the Java Runtime Environment for convenience.
To start a node, please edit supplied node.conf file so it contains appropriate data for your organization. More - https://docs.corda.net/corda-configuration-file.html
Your CordApps should be placed in cordapps directory, from which they will be loaded automatically.
Linux/Mac: main executable file is ./bin/corda

View File

@ -0,0 +1,61 @@
p2pAddress="localhost:10005"
myLegalName="O=Bank A,L=London,C=GB"
emailAddress = "admin@company.com"
keyStorePassword = "cordacadevpass"
trustStorePassword = "trustpass"
dataSourceProperties = {
dataSourceClassName = org.h2.jdbcx.JdbcDataSource
dataSource.url = "jdbc:h2:file:"${baseDirectory}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port}
dataSource.user = sa
dataSource.password = ""
}
database = {
transactionIsolationLevel = "REPEATABLE_READ"
exportHibernateJMXStatistics = "false"
}
devMode = true
h2port = 0
useTestClock = false
verifierType = InMemory
rpcSettings = {
useSsl = false
standAloneBroker = false
address="localhost:10007"
adminAddress="localhost:10008"
}
security {
authService {
dataSource {
type=INMEMORY
users=[
{
password=default
permissions=[
ALL
]
username=default
}
]
}
}
}
enterpriseConfiguration = {
mutualExclusionConfiguration = {
on = false
machineName = ""
updateInterval = 20000
waitInterval = 40000
}
tuning = {
flowThreadPoolSize = 1
rpcThreadPoolSize = 4
maximumMessagingBatchSize = 256
p2pConfirmationWindowSize = 1048576
brokerConnectionTtlCheckIntervalMs = 20
stateMachine = {
eventQueueSize = 16
sessionDeliverPersistenceStrategy = "OnNextCommit"
}
}
useMultiThreadedSMM = true
}

View File

@ -68,7 +68,12 @@ class NodeArgsParser : AbstractArgsParser<CmdLineOptions>() {
require(!optionSet.has(baseDirectoryArg) || !optionSet.has(configFileArg)) {
"${baseDirectoryArg.options()[0]} and ${configFileArg.options()[0]} cannot be specified together"
}
val baseDirectory = optionSet.valueOf(baseDirectoryArg).normalize().toAbsolutePath()
// Workaround for javapackager polluting cwd: restore it from system property set by launcher.
val baseDirectory = System.getProperty("corda.launcher.cwd")?.let { Paths.get(it) }
?: optionSet.valueOf(baseDirectoryArg)
.normalize()
.toAbsolutePath()
val configFile = baseDirectory / optionSet.valueOf(configFileArg)
val loggingLevel = optionSet.valueOf(loggerLevel)
val logToConsole = optionSet.has(logToConsoleArg)

View File

@ -83,3 +83,5 @@ project(':hsm-tool').with {
name = 'sgx-hsm-tool'
projectDir = file("$settingsDir/sgx-jvm/hsm-tool")
}
include 'launcher'
include 'node:dist'