diff --git a/jacoco/pom.xml b/jacoco/pom.xml index 1128f3f2a..581875b56 100644 --- a/jacoco/pom.xml +++ b/jacoco/pom.xml @@ -110,7 +110,7 @@ - 4300000 + 4400000 3400000 ${project.build.directory}/jacoco-${qualified.bundle.version}.zip 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 118fa7e65..205fa5b61 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 static void premain(final String options, final Instrumentation inst) 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 true when running on Java 9 or higher, + * false 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 000000000..c3fc8008b --- /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 000000000..ee7aa1acc --- /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 e15e653f6..c83343993 100644 --- a/org.jacoco.doc/docroot/doc/changes.html +++ b/org.jacoco.doc/docroot/doc/changes.html @@ -53,6 +53,8 @@

New Features

(GitHub #819).
  • Empty class and sourcefile nodes are preserved and available in XML report (GitHub #817).
  • +
  • Agent avoids conflicts with other agents when running on Java 9+ + (GitHub #829).
  • Fixed Bugs