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-core',
'corda-core-deterministic',
'corda-djvm',
'corda',
'corda-finance',
'corda-node',

View File

@ -1,6 +1,10 @@
plugins {
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 {
// Shaded version of ASM to avoid conflict with root project.
@ -30,8 +34,8 @@ dependencies {
jar.enabled = false
shadowJar {
baseName = "djvm"
classifier = ""
baseName 'corda-djvm'
classifier ''
dependencies {
exclude(dependency('com.jcabi:.*:.*'))
exclude(dependency('org.apache.*:.*:.*'))
@ -40,10 +44,14 @@ shadowJar {
exclude(dependency('io.github.lukehutch:.*:.*'))
}
relocate 'org.objectweb.asm', 'djvm.org.objectweb.asm'
artifacts {
shadow(tasks.shadowJar.archivePath) {
builtBy 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
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.Parameters
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.source.ClassSource
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 djvm.org.objectweb.asm.ClassReader
import picocli.CommandLine.Option

View File

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

View File

@ -1,9 +1,5 @@
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 java.nio.file.Files
import java.nio.file.Path

View File

@ -1,6 +1,5 @@
package net.corda.djvm.tools.cli
import net.corda.djvm.tools.Utilities.workingDirectory
import picocli.CommandLine.Command
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.
*/
@JvmDefault
val isTracer: Boolean
get() = false

View File

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

View File

@ -1,5 +1,7 @@
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
* made from within the type.
@ -16,7 +18,7 @@ class LoadedClass(
* The name of the loaded type.
*/
val name: String
get() = type.name.replace('.', '/')
get() = Type.getInternalName(type)
override fun toString(): String {
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.COMPUTE_FRAMES
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
@ -52,7 +53,7 @@ open class SandboxClassWriter(
do {
clazz = clazz.superclass
} while (!clazz.isAssignableFrom(class2))
clazz.name.replace('.', '/')
Type.getInternalName(clazz)
}
}
}

View File

@ -80,7 +80,7 @@ open class SourceClassLoader(
when {
!Files.exists(it) -> throw FileNotFoundException("File not found; $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())
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.
*/
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].
@ -15,7 +16,7 @@ object Discovery {
val instances = mutableListOf<T>()
FastClasspathScanner("net/corda/djvm")
.matchClassesImplementing(T::class.java) { clazz ->
if (!Modifier.isAbstract(clazz.modifiers) && !Modifier.isStatic(clazz.modifiers)) {
if (clazz.modifiers and FORBIDDEN_CLASS_MASK == 0) {
try {
instances.add(clazz.newInstance())
} catch (exception: Throwable) {

View File

@ -4,7 +4,7 @@ class StrictFloat : Callable {
override fun call() {
val d = java.lang.Double.MIN_VALUE
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 org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.Type
import java.lang.reflect.InvocationTargetException
open class TestBase {
abstract class TestBase {
companion object {
@ -38,8 +39,7 @@ open class TestBase {
/**
* Get the full name of type [T].
*/
inline fun <reified T> nameOf(prefix: String = "") =
"$prefix${T::class.java.name.replace('.', '/')}"
inline fun <reified T> nameOf(prefix: String = "") = "$prefix${Type.getInternalName(T::class.java)}"
}
@ -115,7 +115,7 @@ open class TestBase {
var thrownException: Throwable? = null
Thread {
try {
val pinnedTestClasses = pinnedClasses.map { it.name.replace('.', '/') }.toSet()
val pinnedTestClasses = pinnedClasses.map(Type::getInternalName).toSet()
val analysisConfiguration = AnalysisConfiguration(
whitelist = whitelist,
additionalPinnedClasses = pinnedTestClasses,

View File

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

View File

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

View File

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