Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite injection of CTCGraphicsEnvironment #16

Merged
merged 19 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,12 @@ target/

# OS X
.DS_Store

# jenv


# jenv
.java-version

# log files
*.log
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ This is because Java only allows to set the toolkit once, and it cannot be unloa

The `add-exports` and `add-opens` jvm args are required with Java 17, since these are internal packages that aren't exported, these can't be added to a `module-info.java` file.

With Java 18+, you may also want to add a argument to suppress warnings about an agent (ByteBuddyAgent) being loaded: `-XX:+EnableDynamicAgentLoading` .

You can change the resolution of the virtual screen by setting the `cacio.managed.screensize` system property.

For example:
Expand Down
4 changes: 2 additions & 2 deletions cacio-shared/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<source>${cacio.java.version}</source>
<target>${cacio.java.version}</target>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to define this once, I'm a bit of a Maven newbie, doesn't this need to be added to a properties section somewhere?

<compilerArgs>
<arg>-XDignore.symbol.file=true</arg>
<arg>--add-exports=java.desktop/java.awt.peer=ALL-UNNAMED</arg>
Expand Down
15 changes: 13 additions & 2 deletions cacio-tta/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@
<artifactId>jide-oss</artifactId>
<version>3.6.18</version>
</dependency>
<!-- https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy -->
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.14.11</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.14.11</version>
</dependency>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if these should have a provided scope or perhaps shadow the jar. In my case I'll want to use whichever version the Spring Boot bom defines. I'll leave this for now but might consider that change later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am less familiar with Spring Boot and boms. I generally go for the simplest thing that works and makes sense to me.

</dependencies>

<build>
Expand All @@ -54,8 +65,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<source>${cacio.java.version}</source>
<target>${cacio.java.version}</target>
<compilerArgs>
<arg>-XDignore.symbol.file=true</arg>
<arg>--add-exports=java.desktop/java.awt.peer=ALL-UNNAMED</arg>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,14 @@

public class CTCGraphicsEnvironment extends SunGraphicsEnvironment {

private static final CTCGraphicsEnvironment INSTANCE = new CTCGraphicsEnvironment();
public CTCGraphicsEnvironment() {
SurfaceManagerFactory.setInstance(new CTCSurfaceManagerFactory());
}

public static CTCGraphicsEnvironment getInstance() {
return INSTANCE;
}
@Override
protected int getNumScreens() {
return 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import sun.awt.image.VolatileSurfaceManager;
import sun.java2d.SurfaceManagerFactory;

class CTCSurfaceManagerFactory extends SurfaceManagerFactory {
public class CTCSurfaceManagerFactory extends SurfaceManagerFactory {

@Override
public VolatileSurfaceManager createVolatileManager(SunVolatileImage image,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import sun.awt.image.VolatileSurfaceManager;
import sun.java2d.SurfaceData;

class CTCVolatileSurfaceManager extends VolatileSurfaceManager {
public class CTCVolatileSurfaceManager extends VolatileSurfaceManager {

protected CTCVolatileSurfaceManager(SunVolatileImage vImg, Object context) {
super(vImg, context);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.github.caciocavallosilano.cacio.ctc.junit;

import com.github.caciocavallosilano.cacio.ctc.CTCGraphicsEnvironment;
import net.bytebuddy.implementation.bind.annotation.*;

import java.awt.*;


public class CTCInterceptor {
@RuntimeType
public static GraphicsEnvironment intercept() {
return CTCGraphicsEnvironment.getInstance();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,46 @@
*/
package com.github.caciocavallosilano.cacio.ctc.junit;

import com.github.caciocavallosilano.cacio.ctc.CTCGraphicsEnvironment;
import com.github.caciocavallosilano.cacio.ctc.CTCToolkit;
import com.github.caciocavallosilano.cacio.ctc.*;
import com.github.caciocavallosilano.cacio.peer.PlatformWindowFactory;
import com.github.caciocavallosilano.cacio.peer.managed.FullScreenWindowFactory;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.ClassFileLocator;
import net.bytebuddy.dynamic.loading.ClassInjector;
import net.bytebuddy.dynamic.loading.ClassReloadingStrategy;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.*;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.pool.TypePool;
import net.bytebuddy.agent.ByteBuddyAgent;
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.platform.commons.util.AnnotationUtils;

import javax.swing.plaf.metal.MetalLookAndFeel;
import java.awt.*;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.Method;
import java.util.Map;


import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;


public class CacioExtension implements ExecutionCondition {
// https://stackoverflow.com/a/56043252/1050369
private static final VarHandle MODIFIERS;

static {
try {
ByteBuddyAgent.install();

var lookup = MethodHandles.privateLookupIn(Field.class, MethodHandles.lookup());
MODIFIERS = lookup.findVarHandle(Field.class, "modifiers", int.class);
} catch (IllegalAccessException | NoSuchFieldException ex) {
Expand All @@ -54,42 +73,87 @@ public class CacioExtension implements ExecutionCondition {

static {
try {
injectCTCGraphicsEnvironment();

Field toolkit = Toolkit.class.getDeclaredField("toolkit");
toolkit.setAccessible(true);
toolkit.set(null, new CTCToolkit());

Field defaultHeadlessField = java.awt.GraphicsEnvironment.class.getDeclaredField("defaultHeadless");
defaultHeadlessField.setAccessible(true);
defaultHeadlessField.set(null, Boolean.TRUE);
defaultHeadlessField.set(null, Boolean.FALSE);
Field headlessField = java.awt.GraphicsEnvironment.class.getDeclaredField("headless");
headlessField.setAccessible(true);
headlessField.set(null, Boolean.TRUE);

Class<?> geCls = Class.forName("java.awt.GraphicsEnvironment$LocalGE");
Field ge = geCls.getDeclaredField("INSTANCE");
ge.setAccessible(true);
defaultHeadlessField.set(null, Boolean.FALSE);
headlessField.set(null, Boolean.FALSE);

makeNonFinal(ge);

Class<?> smfCls = Class.forName("sun.java2d.SurfaceManagerFactory");
Field smf = smfCls.getDeclaredField("instance");
smf.setAccessible(true);
smf.set(null, null);

ge.set(null, new CTCGraphicsEnvironment());
} catch (Exception e) {
e.printStackTrace();
}

System.setProperty("swing.defaultlaf", MetalLookAndFeel.class.getName());
}

public static void makeNonFinal(Field field) {
int mods = field.getModifiers();
if (Modifier.isFinal(mods)) {
MODIFIERS.set(field, mods & ~Modifier.FINAL);
public static void injectCTCGraphicsEnvironment() throws ClassNotFoundException, IOException {
/*
* ByteBuddy is used to intercept the methods that return the graphics environment in use
* (java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment() and
* sun.awt.PlatformGraphicsInfo.createGE())
*
* Since java.awt.GraphicsEnvironment is loaded by the bootstrap class loader,
* all classes used by CTCGraphicsEnvironment also need to be available to the bootstrap class loader,
* as that class loader also loads the CTCInterceptor class, which will instantiate CTCGraphicsEnvironment.
*/
injectClassIntoBootstrapClassLoader(
CTCInterceptor.class,
CTCGraphicsEnvironment.class,
CTCSurfaceManagerFactory.class,
CTCGraphicsConfiguration.class,
PlatformWindowFactory.class,
FullScreenWindowFactory.class,
CTCGraphicsDevice.class,
CTCVolatileSurfaceManager.class);

ByteBuddy byteBuddy = new ByteBuddy();

byteBuddy
.redefine(
TypePool.Default.ofSystemLoader().describe("java.awt.GraphicsEnvironment").resolve(),
ClassFileLocator.ForClassLoader.ofSystemLoader())
.method(ElementMatchers.named("getLocalGraphicsEnvironment"))
.intercept(
MethodDelegation.to(CTCInterceptor.class))
.make()
.load(
Object.class.getClassLoader(),
ClassReloadingStrategy.fromInstalledAgent());

TypeDescription platformGraphicInfosType;
platformGraphicInfosType = TypePool.Default.ofSystemLoader().describe("sun.awt.PlatformGraphicsInfo").resolve();
ClassFileLocator locator = ClassFileLocator.ForClassLoader.ofSystemLoader();

byteBuddy
.redefine(
platformGraphicInfosType,
locator)
.method(
nameStartsWith("createGE"))
.intercept(
MethodDelegation.to(GraphicsEnvironmentInterceptor.class))
.make()
.load(
Thread.currentThread().getContextClassLoader(),
ClassReloadingStrategy.fromInstalledAgent());

}

public static class GraphicsEnvironmentInterceptor {
@RuntimeType
public static Object intercept(@Origin Method method, @AllArguments final Object[] args) throws Exception {
return CTCGraphicsEnvironment.getInstance();
}
}

Expand All @@ -100,4 +164,12 @@ public final ConditionEvaluationResult evaluateExecutionCondition(ExtensionConte
.map(annotation -> ConditionEvaluationResult.enabled("@GUITest is present"))
.orElse(ConditionEvaluationResult.enabled("@GUITest is not present"));
}

private static void injectClassIntoBootstrapClassLoader(Class... classes) throws IOException {
for (Class<?> clazz: classes) {
final byte[] buffer = clazz.getClassLoader().getResourceAsStream(clazz.getName().replace('.', '/').concat(".class")).readAllBytes();
ClassInjector.UsingUnsafe injector = new ClassInjector.UsingUnsafe(null);
injector.injectRaw(Map.of(clazz.getName(), buffer));
}
}
}
9 changes: 7 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
<module>cacio-tta</module>
</modules>

<properties>
<cacio.java.version>17</cacio.java.version>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

disregard my other comment, I see it now

</properties>

<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
Expand Down Expand Up @@ -112,8 +116,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<source>${cacio.java.version}</source>
<target>${cacio.java.version}</target>
<compilerArgs>
<arg>-XDignore.symbol.file=true</arg>
</compilerArgs>
Expand All @@ -130,6 +134,7 @@
<java.awt.headless>false</java.awt.headless>
</systemPropertyVariables>
<argLine>
-XX:+EnableDynamicAgentLoading
--add-exports=java.desktop/java.awt=ALL-UNNAMED
--add-exports=java.desktop/java.awt.peer=ALL-UNNAMED
--add-exports=java.desktop/sun.awt.image=ALL-UNNAMED
Expand Down