aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEvgeny Mandrikov <138671+Godin@users.noreply.github.com>2019-01-21 18:39:49 +0100
committerMarc R. Hoffmann <hoffmann@mountainminds.com>2019-01-21 18:39:49 +0100
commitd0a0577f70b5bc4f42ce6af603b783835a014b82 (patch)
tree005420eb5e3ed6b5e84e01e2f71589a3e5fa9bf2
parent519226e7259c0dd36a59c53de9989d99521cc7e9 (diff)
downloadplatform_external_jacoco-d0a0577f70b5bc4f42ce6af603b783835a014b82.tar.gz
platform_external_jacoco-d0a0577f70b5bc4f42ce6af603b783835a014b82.tar.bz2
platform_external_jacoco-d0a0577f70b5bc4f42ce6af603b783835a014b82.zip
Agent can inject class into Java 9+ bootstrap class loader (#829)
-rw-r--r--jacoco/pom.xml2
-rw-r--r--org.jacoco.agent.rt/src/org/jacoco/agent/rt/internal/PreMain.java55
-rw-r--r--org.jacoco.core.test/src/org/jacoco/core/runtime/InjectedClassRuntimeTest.java62
-rw-r--r--org.jacoco.core/src/org/jacoco/core/runtime/InjectedClassRuntime.java141
-rw-r--r--org.jacoco.doc/docroot/doc/changes.html2
5 files changed, 261 insertions, 1 deletions
diff --git a/jacoco/pom.xml b/jacoco/pom.xml
index 1128f3f2..581875b5 100644
--- a/jacoco/pom.xml
+++ b/jacoco/pom.xml
@@ -110,7 +110,7 @@
<configuration>
<rules>
<requireFilesSize>
- <maxsize>4300000</maxsize>
+ <maxsize>4400000</maxsize>
<minsize>3400000</minsize>
<files>
<file>${project.build.directory}/jacoco-${qualified.bundle.version}.zip</file>
diff --git a/org.jacoco.agent.rt/src/org/jacoco/agent/rt/internal/PreMain.java b/org.jacoco.agent.rt/src/org/jacoco/agent/rt/internal/PreMain.java
index 118fa7e6..205fa5b6 100644
--- a/org.jacoco.agent.rt/src/org/jacoco/agent/rt/internal/PreMain.java
+++ b/org.jacoco.agent.rt/src/org/jacoco/agent/rt/internal/PreMain.java
@@ -12,9 +12,13 @@
package org.jacoco.agent.rt.internal;
import java.lang.instrument.Instrumentation;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
import org.jacoco.core.runtime.AgentOptions;
import org.jacoco.core.runtime.IRuntime;
+import org.jacoco.core.runtime.InjectedClassRuntime;
import org.jacoco.core.runtime.ModifiedSystemClassRuntime;
/**
@@ -52,7 +56,58 @@ public final class PreMain {
private static IRuntime createRuntime(final Instrumentation inst)
throws Exception {
+
+ if (redefineJavaBaseModule(inst)) {
+ return new InjectedClassRuntime(Object.class, "$JaCoCo");
+ }
+
return ModifiedSystemClassRuntime.createFor(inst, "java/lang/UnknownError");
}
+ /**
+ * Opens {@code java.base} module for {@link InjectedClassRuntime} when
+ * executed on Java 9 JREs or higher.
+ *
+ * @return <code>true</code> when running on Java 9 or higher,
+ * <code>false</code> otherwise
+ * @throws Exception
+ * if unable to open
+ */
+ private static boolean redefineJavaBaseModule(
+ final Instrumentation instrumentation) throws Exception {
+ try {
+ Class.forName("java.lang.Module");
+ } catch (final ClassNotFoundException e) {
+ return false;
+ }
+
+ Instrumentation.class.getMethod("redefineModule", //
+ Class.forName("java.lang.Module"), //
+ Set.class, //
+ Map.class, //
+ Map.class, //
+ Set.class, //
+ Map.class //
+ ).invoke(instrumentation, // instance
+ getModule(Object.class), // module
+ Collections.emptySet(), // extraReads
+ Collections.emptyMap(), // extraExports
+ Collections.singletonMap("java.lang",
+ Collections.singleton(
+ getModule(InjectedClassRuntime.class))), // extraOpens
+ Collections.emptySet(), // extraUses
+ Collections.emptyMap() // extraProvides
+ );
+ return true;
+ }
+
+ /**
+ * @return {@code cls.getModule()}
+ */
+ private static Object getModule(final Class<?> cls) throws Exception {
+ return Class.class //
+ .getMethod("getModule") //
+ .invoke(cls);
+ }
+
}
diff --git a/org.jacoco.core.test/src/org/jacoco/core/runtime/InjectedClassRuntimeTest.java b/org.jacoco.core.test/src/org/jacoco/core/runtime/InjectedClassRuntimeTest.java
new file mode 100644
index 00000000..c3fc8008
--- /dev/null
+++ b/org.jacoco.core.test/src/org/jacoco/core/runtime/InjectedClassRuntimeTest.java
@@ -0,0 +1,62 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2019 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Evgeny Mandrikov - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.core.runtime;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.lang.reflect.InvocationTargetException;
+
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.internal.AssumptionViolatedException;
+import org.junit.rules.TestName;
+
+/**
+ * Unit test for {@link InjectedClassRuntime}.
+ */
+public class InjectedClassRuntimeTest extends RuntimeTestBase {
+
+ @Rule
+ public TestName testName = new TestName();
+
+ @BeforeClass
+ public static void requires_at_least_Java_9() {
+ try {
+ Class.forName("java.lang.Module");
+ } catch (final ClassNotFoundException e) {
+ throw new AssumptionViolatedException(
+ "this test requires at least Java 9");
+ }
+ }
+
+ @Override
+ public IRuntime createRuntime() {
+ return new InjectedClassRuntime(InjectedClassRuntimeTest.class,
+ testName.getMethodName());
+ }
+
+ @Test
+ public void startup_should_not_create_duplicate_class_definition()
+ throws Exception {
+ try {
+ createRuntime().startup(null);
+ fail("exception expected");
+ } catch (final InvocationTargetException e) {
+ assertTrue(e.getCause() instanceof LinkageError);
+ assertTrue(e.getCause().getMessage()
+ .contains("duplicate class definition"));
+ }
+ }
+
+}
diff --git a/org.jacoco.core/src/org/jacoco/core/runtime/InjectedClassRuntime.java b/org.jacoco.core/src/org/jacoco/core/runtime/InjectedClassRuntime.java
new file mode 100644
index 00000000..ee7aa1ac
--- /dev/null
+++ b/org.jacoco.core/src/org/jacoco/core/runtime/InjectedClassRuntime.java
@@ -0,0 +1,141 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2019 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Evgeny Mandrikov - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.core.runtime;
+
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * {@link IRuntime} which defines a new class using
+ * {@code java.lang.invoke.MethodHandles.Lookup.defineClass} introduced in Java
+ * 9. Module where class will be defined must be opened to at least module of
+ * this class.
+ */
+public class InjectedClassRuntime extends AbstractRuntime {
+
+ private static final String FIELD_NAME = "data";
+
+ private static final String FIELD_TYPE = "Ljava/lang/Object;";
+
+ private final Class<?> locator;
+
+ private final String injectedClassName;
+
+ /**
+ * Creates a new runtime which will define a class to the same class loader
+ * and in the same package and protection domain as given class.
+ *
+ * @param locator
+ * class to identify the target class loader and package
+ * @param simpleClassName
+ * simple name of the class to be defined
+ */
+ public InjectedClassRuntime(final Class<?> locator,
+ final String simpleClassName) {
+ this.locator = locator;
+ this.injectedClassName = locator.getPackage().getName().replace('.',
+ '/') + '/' + simpleClassName;
+ }
+
+ @Override
+ public void startup(final RuntimeData data) throws Exception {
+ super.startup(data);
+ Lookup //
+ .privateLookupIn(locator, Lookup.lookup()) //
+ .defineClass(createClass(injectedClassName)) //
+ .getField(FIELD_NAME) //
+ .set(null, data);
+ }
+
+ public void shutdown() {
+ // nothing to do
+ }
+
+ public int generateDataAccessor(final long classid, final String classname,
+ final int probecount, final MethodVisitor mv) {
+ mv.visitFieldInsn(Opcodes.GETSTATIC, injectedClassName, FIELD_NAME,
+ FIELD_TYPE);
+
+ RuntimeData.generateAccessCall(classid, classname, probecount, mv);
+
+ return 6;
+ }
+
+ private static byte[] createClass(final String name) {
+ final ClassWriter cw = new ClassWriter(0);
+ cw.visit(Opcodes.V9, Opcodes.ACC_SYNTHETIC | Opcodes.ACC_PUBLIC,
+ name.replace('.', '/'), null, "java/lang/Object", null);
+ cw.visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, FIELD_NAME,
+ FIELD_TYPE, null, null);
+ cw.visitEnd();
+ return cw.toByteArray();
+ }
+
+ /**
+ * Provides access to classes {@code java.lang.invoke.MethodHandles} and
+ * {@code java.lang.invoke.MethodHandles.Lookup} introduced in Java 8.
+ */
+ private static class Lookup {
+
+ private final Object instance;
+
+ private Lookup(final Object instance) {
+ this.instance = instance;
+ }
+
+ /**
+ * @return a lookup object for the caller of this method
+ */
+ static Lookup lookup() throws Exception {
+ return new Lookup(Class //
+ .forName("java.lang.invoke.MethodHandles") //
+ .getMethod("lookup") //
+ .invoke(null));
+ }
+
+ /**
+ * See corresponding method introduced in Java 9.
+ *
+ * @param targetClass
+ * the target class
+ * @param lookup
+ * the caller lookup object
+ * @return a lookup object for the target class, with private access
+ */
+ static Lookup privateLookupIn(final Class<?> targetClass,
+ final Lookup lookup) throws Exception {
+ return new Lookup(Class //
+ .forName("java.lang.invoke.MethodHandles") //
+ .getMethod("privateLookupIn", Class.class,
+ Class.forName(
+ "java.lang.invoke.MethodHandles$Lookup")) //
+ .invoke(null, targetClass, lookup.instance));
+ }
+
+ /**
+ * See corresponding method introduced in Java 9.
+ *
+ * @param bytes
+ * the class bytes
+ * @return class
+ */
+ Class<?> defineClass(final byte[] bytes) throws Exception {
+ return (Class<?>) Class //
+ .forName("java.lang.invoke.MethodHandles$Lookup")
+ .getMethod("defineClass", byte[].class)
+ .invoke(this.instance, new Object[] { bytes });
+ }
+
+ }
+
+}
diff --git a/org.jacoco.doc/docroot/doc/changes.html b/org.jacoco.doc/docroot/doc/changes.html
index e15e653f..c8334399 100644
--- a/org.jacoco.doc/docroot/doc/changes.html
+++ b/org.jacoco.doc/docroot/doc/changes.html
@@ -53,6 +53,8 @@
(GitHub <a href="https://github.com/jacoco/jacoco/issues/819">#819</a>).</li>
<li>Empty class and sourcefile nodes are preserved and available in XML report
(GitHub <a href="https://github.com/jacoco/jacoco/issues/817">#817</a>).</li>
+ <li>Agent avoids conflicts with other agents when running on Java 9+
+ (GitHub <a href="https://github.com/jacoco/jacoco/issues/829">#829</a>).</li>
</ul>
<h3>Fixed Bugs</h3>