ENT-1906: Trivial tweaks to DJVM code. (#3890)

* Trivial tweaks to DJVM code.
  - Use ASM Type.getInternalName()
  - Use @JvmDefault annotation
  - Declare test base class as abstract
  - Ensure test Log4J configuration has precedence
  - Replace assert() with require()
  - Replace simple lambdas with function references
* Publish corda-djvm artifact.
* Migrate Utilities class into the CLI tool.
* Configure unit tests for console logging.
This commit is contained in:
Chris Rankin 2018-09-05 10:12:48 +01:00 committed by GitHub
parent a7f9320985
commit 5b255c81a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 158 additions and 185 deletions

View File

@ -331,6 +331,7 @@ bintrayConfig {
'corda-rpc', 'corda-rpc',
'corda-core', 'corda-core',
'corda-core-deterministic', 'corda-core-deterministic',
'corda-djvm',
'corda', 'corda',
'corda-finance', 'corda-finance',
'corda-node', 'corda-node',

View File

@ -1,6 +1,10 @@
plugins { plugins {
id 'com.github.johnrengelman.shadow' version '2.0.4' id 'com.github.johnrengelman.shadow' version '2.0.4'
} }
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
description 'Corda deterministic JVM sandbox'
ext { ext {
// Shaded version of ASM to avoid conflict with root project. // Shaded version of ASM to avoid conflict with root project.
@ -30,8 +34,8 @@ dependencies {
jar.enabled = false jar.enabled = false
shadowJar { shadowJar {
baseName = "djvm" baseName 'corda-djvm'
classifier = "" classifier ''
dependencies { dependencies {
exclude(dependency('com.jcabi:.*:.*')) exclude(dependency('com.jcabi:.*:.*'))
exclude(dependency('org.apache.*:.*:.*')) exclude(dependency('org.apache.*:.*:.*'))
@ -40,10 +44,14 @@ shadowJar {
exclude(dependency('io.github.lukehutch:.*:.*')) exclude(dependency('io.github.lukehutch:.*:.*'))
} }
relocate 'org.objectweb.asm', 'djvm.org.objectweb.asm' relocate 'org.objectweb.asm', 'djvm.org.objectweb.asm'
artifacts {
shadow(tasks.shadowJar.archivePath) {
builtBy shadowJar
}
}
} }
assemble.dependsOn shadowJar assemble.dependsOn shadowJar
artifacts {
publish shadowJar
}
publish {
disableDefaultJar true
name shadowJar.baseName
}

View File

@ -1,8 +1,5 @@
package net.corda.djvm.tools.cli package net.corda.djvm.tools.cli
import net.corda.djvm.tools.Utilities.createCodePath
import net.corda.djvm.tools.Utilities.getFileNames
import net.corda.djvm.tools.Utilities.jarPath
import picocli.CommandLine.Command import picocli.CommandLine.Command
import picocli.CommandLine.Parameters import picocli.CommandLine.Parameters
import java.nio.file.Path import java.nio.file.Path

View File

@ -7,9 +7,6 @@ import net.corda.djvm.execution.*
import net.corda.djvm.references.ClassModule import net.corda.djvm.references.ClassModule
import net.corda.djvm.source.ClassSource import net.corda.djvm.source.ClassSource
import net.corda.djvm.source.SourceClassLoader import net.corda.djvm.source.SourceClassLoader
import net.corda.djvm.tools.Utilities.find
import net.corda.djvm.tools.Utilities.onEmpty
import net.corda.djvm.tools.Utilities.userClassPath
import net.corda.djvm.utilities.Discovery import net.corda.djvm.utilities.Discovery
import djvm.org.objectweb.asm.ClassReader import djvm.org.objectweb.asm.ClassReader
import picocli.CommandLine.Option import picocli.CommandLine.Option

View File

@ -1,7 +1,6 @@
package net.corda.djvm.tools.cli package net.corda.djvm.tools.cli
import net.corda.djvm.source.ClassSource import net.corda.djvm.source.ClassSource
import net.corda.djvm.tools.Utilities.createCodePath
import picocli.CommandLine.Command import picocli.CommandLine.Command
import picocli.CommandLine.Parameters import picocli.CommandLine.Parameters
import java.nio.file.Files import java.nio.file.Files

View File

@ -1,9 +1,5 @@
package net.corda.djvm.tools.cli package net.corda.djvm.tools.cli
import net.corda.djvm.tools.Utilities.baseName
import net.corda.djvm.tools.Utilities.createCodePath
import net.corda.djvm.tools.Utilities.getFiles
import net.corda.djvm.tools.Utilities.openOptions
import picocli.CommandLine.* import picocli.CommandLine.*
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path

View File

@ -1,6 +1,5 @@
package net.corda.djvm.tools.cli package net.corda.djvm.tools.cli
import net.corda.djvm.tools.Utilities.workingDirectory
import picocli.CommandLine.Command import picocli.CommandLine.Command
import java.nio.file.Files import java.nio.file.Files

View File

@ -0,0 +1,104 @@
@file:JvmName("Utilities")
package net.corda.djvm.tools.cli
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
import java.lang.reflect.Modifier
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardOpenOption
/**
* Get the expanded file name of each path in the provided array.
*/
fun Array<Path>?.getFiles(map: (Path) -> Path = { it }) = (this ?: emptyArray()).map {
val pathString = it.toString()
val path = map(it)
when {
'/' in pathString || '\\' in pathString ->
throw Exception("Please provide a pathless file name")
pathString.endsWith(".java", true) -> path
else -> Paths.get("$path.java")
}
}
/**
* Get the string representation of each expanded file name in the provided array.
*/
fun Array<Path>?.getFileNames(map: (Path) -> Path = { it }) = this.getFiles(map).map {
it.toString()
}.toTypedArray()
/**
* Execute inlined action if the collection is empty.
*/
inline fun <T> List<T>.onEmpty(action: () -> Unit): List<T> {
if (!this.any()) {
action()
}
return this
}
/**
* Execute inlined action if the array is empty.
*/
inline fun <reified T> Array<T>?.onEmpty(action: () -> Unit): Array<T> {
return (this ?: emptyArray()).toList().onEmpty(action).toTypedArray()
}
/**
* Derive the set of [StandardOpenOption]'s to use for a file operation.
*/
fun openOptions(force: Boolean) = if (force) {
arrayOf(StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)
} else {
arrayOf(StandardOpenOption.CREATE_NEW)
}
/**
* Get the path of where any generated code will be placed. Create the directory if it does not exist.
*/
fun createCodePath(): Path {
return Paths.get("tmp", "net", "corda", "djvm").let {
Files.createDirectories(it)
}
}
/**
* Return the base name of a file (i.e., its name without extension)
*/
val Path.baseName: String
get() = this.fileName.toString()
.replaceAfterLast('.', "")
.removeSuffix(".")
/**
* The path of the executing JAR.
*/
val jarPath: String = object {}.javaClass.protectionDomain.codeSource.location.toURI().path
/**
* The path of the current working directory.
*/
val workingDirectory: Path = Paths.get(System.getProperty("user.dir"))
/**
* The class path for the current execution context.
*/
val userClassPath: String = System.getProperty("java.class.path")
/**
* Get a reference of each concrete class that implements interface or class [T].
*/
inline fun <reified T> find(scanSpec: String = "net/corda/djvm"): List<Class<*>> {
val references = mutableListOf<Class<*>>()
FastClasspathScanner(scanSpec)
.matchClassesImplementing(T::class.java) { clazz ->
if (!Modifier.isAbstract(clazz.modifiers) && !Modifier.isStatic(clazz.modifiers)) {
references.add(clazz)
}
}
.scan()
return references
}

View File

@ -20,6 +20,7 @@ interface Emitter {
/** /**
* Indication of whether or not the emitter performs instrumentation for tracing inside the sandbox. * Indication of whether or not the emitter performs instrumentation for tracing inside the sandbox.
*/ */
@JvmDefault
val isTracer: Boolean val isTracer: Boolean
get() = false get() = false

View File

@ -12,5 +12,6 @@ interface MemberInformation {
val className: String val className: String
val memberName: String val memberName: String
val signature: String val signature: String
@JvmDefault
val reference: String get() = "$className.$memberName:$signature" val reference: String get() = "$className.$memberName:$signature"
} }

View File

@ -1,5 +1,7 @@
package net.corda.djvm.rewiring package net.corda.djvm.rewiring
import org.objectweb.asm.Type
/** /**
* A class or interface running in a Java application, together with its raw byte code representation and all references * A class or interface running in a Java application, together with its raw byte code representation and all references
* made from within the type. * made from within the type.
@ -16,7 +18,7 @@ class LoadedClass(
* The name of the loaded type. * The name of the loaded type.
*/ */
val name: String val name: String
get() = type.name.replace('.', '/') get() = Type.getInternalName(type)
override fun toString(): String { override fun toString(): String {
return "Class(type=$name, size=${byteCode.bytes.size}, isModified=${byteCode.isModified})" return "Class(type=$name, size=${byteCode.bytes.size}, isModified=${byteCode.isModified})"

View File

@ -4,6 +4,7 @@ import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter import org.objectweb.asm.ClassWriter
import org.objectweb.asm.ClassWriter.COMPUTE_FRAMES import org.objectweb.asm.ClassWriter.COMPUTE_FRAMES
import org.objectweb.asm.ClassWriter.COMPUTE_MAXS import org.objectweb.asm.ClassWriter.COMPUTE_MAXS
import org.objectweb.asm.Type
/** /**
* Class writer for sandbox execution, with configurable a [classLoader] to ensure correct deduction of the used class * Class writer for sandbox execution, with configurable a [classLoader] to ensure correct deduction of the used class
@ -52,7 +53,7 @@ open class SandboxClassWriter(
do { do {
clazz = clazz.superclass clazz = clazz.superclass
} while (!clazz.isAssignableFrom(class2)) } while (!clazz.isAssignableFrom(class2))
clazz.name.replace('.', '/') Type.getInternalName(clazz)
} }
} }
} }

View File

@ -80,7 +80,7 @@ open class SourceClassLoader(
when { when {
!Files.exists(it) -> throw FileNotFoundException("File not found; $it") !Files.exists(it) -> throw FileNotFoundException("File not found; $it")
Files.isDirectory(it) -> { Files.isDirectory(it) -> {
listOf(it.toURL()) + Files.list(it).filter { isJarFile(it) }.map { it.toURL() }.toList() listOf(it.toURL()) + Files.list(it).filter(::isJarFile).map { it.toURL() }.toList()
} }
Files.isReadable(it) && isJarFile(it) -> listOf(it.toURL()) Files.isReadable(it) && isJarFile(it) -> listOf(it.toURL())
else -> throw IllegalArgumentException("Expected JAR or class file, but found $it") else -> throw IllegalArgumentException("Expected JAR or class file, but found $it")

View File

@ -1,114 +0,0 @@
package net.corda.djvm.tools
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
import java.lang.reflect.Modifier
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardOpenOption
/**
* Various utility functions.
*/
@Suppress("unused")
object Utilities {
/**
* Get the expanded file name of each path in the provided array.
*/
fun Array<Path>?.getFiles(map: (Path) -> Path = { it }) = (this ?: emptyArray()).map {
val pathString = it.toString()
val path = map(it)
when {
'/' in pathString || '\\' in pathString ->
throw Exception("Please provide a pathless file name")
pathString.endsWith(".java", true) -> path
else -> Paths.get("$path.java")
}
}
/**
* Get the string representation of each expanded file name in the provided array.
*/
fun Array<Path>?.getFileNames(map: (Path) -> Path = { it }) = this.getFiles(map).map {
it.toString()
}.toTypedArray()
/**
* Execute inlined action if the collection is empty.
*/
inline fun <T> List<T>.onEmpty(action: () -> Unit): List<T> {
if (!this.any()) {
action()
}
return this
}
/**
* Execute inlined action if the array is empty.
*/
inline fun <reified T> Array<T>?.onEmpty(action: () -> Unit): Array<T> {
return (this ?: emptyArray()).toList().onEmpty(action).toTypedArray()
}
/**
* Derive the set of [StandardOpenOption]'s to use for a file operation.
*/
fun openOptions(force: Boolean) = if (force) {
arrayOf(StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)
} else {
arrayOf(StandardOpenOption.CREATE_NEW)
}
/**
* Get the path of where any generated code will be placed. Create the directory if it does not exist.
*/
fun createCodePath(): Path {
val root = Paths.get("tmp")
.resolve("net")
.resolve("corda")
.resolve("djvm")
Files.createDirectories(root)
return root
}
/**
* Return the base name of a file (i.e., its name without extension)
*/
val Path.baseName: String
get() = this.fileName.toString()
.replaceAfterLast('.', "")
.removeSuffix(".")
/**
* The path of the executing JAR.
*/
val jarPath: String = Utilities::class.java.protectionDomain.codeSource.location.toURI().path
/**
* The path of the current working directory.
*/
val workingDirectory: Path = Paths.get(System.getProperty("user.dir"))
/**
* The class path for the current execution context.
*/
val userClassPath: String = System.getProperty("java.class.path")
/**
* Get a reference of each concrete class that implements interface or class [T].
*/
inline fun <reified T> find(scanSpec: String = "net/corda/djvm"): List<Class<*>> {
val references = mutableListOf<Class<*>>()
FastClasspathScanner(scanSpec)
.matchClassesImplementing(T::class.java) { clazz ->
if (!Modifier.isAbstract(clazz.modifiers) && !Modifier.isStatic(clazz.modifiers)) {
references.add(clazz)
}
}
.scan()
return references
}
}

View File

@ -7,6 +7,7 @@ import java.lang.reflect.Modifier
* Find and instantiate types that implement a certain interface. * Find and instantiate types that implement a certain interface.
*/ */
object Discovery { object Discovery {
const val FORBIDDEN_CLASS_MASK = (Modifier.STATIC or Modifier.ABSTRACT)
/** /**
* Get an instance of each concrete class that implements interface or class [T]. * Get an instance of each concrete class that implements interface or class [T].
@ -15,7 +16,7 @@ object Discovery {
val instances = mutableListOf<T>() val instances = mutableListOf<T>()
FastClasspathScanner("net/corda/djvm") FastClasspathScanner("net/corda/djvm")
.matchClassesImplementing(T::class.java) { clazz -> .matchClassesImplementing(T::class.java) { clazz ->
if (!Modifier.isAbstract(clazz.modifiers) && !Modifier.isStatic(clazz.modifiers)) { if (clazz.modifiers and FORBIDDEN_CLASS_MASK == 0) {
try { try {
instances.add(clazz.newInstance()) instances.add(clazz.newInstance())
} catch (exception: Throwable) { } catch (exception: Throwable) {

View File

@ -4,7 +4,7 @@ class StrictFloat : Callable {
override fun call() { override fun call() {
val d = java.lang.Double.MIN_VALUE val d = java.lang.Double.MIN_VALUE
val x = d / 2 * 2 val x = d / 2 * 2
assert(x.toString() == "0.0") require(x.toString() == "0.0")
} }
} }

View File

@ -18,9 +18,10 @@ import net.corda.djvm.utilities.Discovery
import net.corda.djvm.validation.RuleValidator import net.corda.djvm.validation.RuleValidator
import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.Type
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
open class TestBase { abstract class TestBase {
companion object { companion object {
@ -38,8 +39,7 @@ open class TestBase {
/** /**
* Get the full name of type [T]. * Get the full name of type [T].
*/ */
inline fun <reified T> nameOf(prefix: String = "") = inline fun <reified T> nameOf(prefix: String = "") = "$prefix${Type.getInternalName(T::class.java)}"
"$prefix${T::class.java.name.replace('.', '/')}"
} }
@ -115,7 +115,7 @@ open class TestBase {
var thrownException: Throwable? = null var thrownException: Throwable? = null
Thread { Thread {
try { try {
val pinnedTestClasses = pinnedClasses.map { it.name.replace('.', '/') }.toSet() val pinnedTestClasses = pinnedClasses.map(Type::getInternalName).toSet()
val analysisConfiguration = AnalysisConfiguration( val analysisConfiguration = AnalysisConfiguration(
whitelist = whitelist, whitelist = whitelist,
additionalPinnedClasses = pinnedTestClasses, additionalPinnedClasses = pinnedTestClasses,

View File

@ -63,7 +63,7 @@ class SandboxExecutorTest : TestBase() {
val obj = Object() val obj = Object()
val hash1 = obj.hashCode() val hash1 = obj.hashCode()
val hash2 = obj.hashCode() val hash2 = obj.hashCode()
assert(hash1 == hash2) require(hash1 == hash2)
return Object().hashCode() return Object().hashCode()
} }
} }

View File

@ -4,6 +4,7 @@ import net.corda.djvm.annotations.NonDeterministic
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.jetbrains.annotations.NotNull import org.jetbrains.annotations.NotNull
import org.junit.Test import org.junit.Test
import org.objectweb.asm.Type
class MemberModuleTest { class MemberModuleTest {
@ -132,7 +133,7 @@ class MemberModuleTest {
} }
private val java.lang.Class<*>.descriptor: String private val java.lang.Class<*>.descriptor: String
get() = "L${name.replace('.', '/')};" get() = Type.getDescriptor(this)
private fun member(member: String) = private fun member(member: String) =
MemberReference("", member, "") MemberReference("", member, "")

View File

@ -4,6 +4,7 @@ import foo.bar.sandbox.Callable
import net.corda.djvm.TestBase import net.corda.djvm.TestBase
import net.corda.djvm.assertions.AssertionExtensions.assertThat import net.corda.djvm.assertions.AssertionExtensions.assertThat
import org.junit.Test import org.junit.Test
import org.objectweb.asm.Type
import java.util.* import java.util.*
class ReferenceExtractorTest : TestBase() { class ReferenceExtractorTest : TestBase() {
@ -32,7 +33,7 @@ class ReferenceExtractorTest : TestBase() {
@Test @Test
fun `can find field references`() = validate<B> { context -> fun `can find field references`() = validate<B> { context ->
assertThat(context.references) assertThat(context.references)
.hasMember(B::class.java.name.replace('.', '/'), "foo", "Ljava/lang/String;") .hasMember(Type.getInternalName(B::class.java), "foo", "Ljava/lang/String;")
} }
class B { class B {
@ -47,7 +48,7 @@ class ReferenceExtractorTest : TestBase() {
@Test @Test
fun `can find class references`() = validate<C> { context -> fun `can find class references`() = validate<C> { context ->
assertThat(context.references) assertThat(context.references)
.hasClass(A::class.java.name.replace('.', '/')) .hasClass(Type.getInternalName(A::class.java))
} }
class C { class C {

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="info">
<ThresholdFilter level="info"/>
<Appenders>
<Console name="Console-Appender" target="SYSTEM_OUT">
<PatternLayout pattern="%date %highlight{%level %c{1}.%method - %msg%n}{INFO=white,WARN=red,FATAL=bright red}"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console-Appender"/>
</Root>
</Loggers>
</Configuration>

View File

@ -1,39 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="info">
<ThresholdFilter level="info"/>
<Appenders>
<!-- Will generate up to 10 log files for a given day. During every rollover it will delete
those that are older than 60 days, but keep the most recent 10 GB -->
<RollingFile name="RollingFile-Appender"
fileName="djvm.log"
filePattern="djvm.%date{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="%date{ISO8601}{UTC}Z [%-5level] %c - %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<DefaultRolloverStrategy min="1" max="10">
<Delete basePath="" maxDepth="1">
<IfFileName glob="djvm*.log.gz"/>
<IfLastModified age="60d">
<IfAny>
<IfAccumulatedFileSize exceeds="10 GB"/>
</IfAny>
</IfLastModified>
</Delete>
</DefaultRolloverStrategy>
</RollingFile>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="RollingFile-Appender"/>
</Root>
</Loggers>
</Configuration>