Skip to content

Commit

Permalink
Launch service plugin API. Rough sketch!
Browse files Browse the repository at this point in the history
  • Loading branch information
cpw committed Dec 16, 2017
1 parent 8abeebe commit aa568d7
Show file tree
Hide file tree
Showing 12 changed files with 150 additions and 34 deletions.
28 changes: 16 additions & 12 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ buildscript {
// maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
}
dependencies {
classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.0'
classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.2'
}
}

Expand All @@ -26,7 +26,7 @@ sourceSets {
}

jacoco {
toolVersion = "0.7.6.201602180812"
toolVersion = "0.7.9"
}

group = 'cpw.mods'
Expand All @@ -35,19 +35,23 @@ archivesBaseName = 'modlauncher'
version = '0.1-SNAPSHOT'
//noinspection GroovyUnusedAssignment
sourceCompatibility = 1.8

targetCompatibility = 1.8

dependencies {
compile group: 'com.google.code.findbugs', name: 'jsr305', version: '1.3.9'
compile 'net.sf.jopt-simple:jopt-simple:5.0.3'
apiCompile 'org.ow2.asm:asm-debug-all:5.2'
compile 'org.ow2.asm:asm-debug-all:5.2'
compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.9.0'
compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.9.0'
compile group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2'
compile 'net.sf.jopt-simple:jopt-simple:5.0.4'
apiCompile 'org.ow2.asm:asm:6.0'
compile 'org.ow2.asm:asm:6.0'
apiCompile 'org.ow2.asm:asm-tree:6.0'
compile 'org.ow2.asm:asm-tree:6.0'
apiCompile 'org.ow2.asm:asm-commons:6.0'
compile 'org.ow2.asm:asm-commons:6.0'
compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.10.0'
compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.10.0'
compile sourceSets.api.output
testCompile "org.junit.jupiter:junit-jupiter-api:5.0.0"
testCompile(group: 'org.powermock', name: 'powermock-core', version: '1.6.6')
testRuntime "org.junit.jupiter:junit-jupiter-engine:5.0.0"
testCompile "org.junit.jupiter:junit-jupiter-api:5.0.2"
testCompile(group: 'org.powermock', name: 'powermock-core', version: '1.7.3')
testRuntime "org.junit.jupiter:junit-jupiter-engine:5.0.2"
testRuntime sourceSets.testJars.runtimeClasspath
}

Expand Down
70 changes: 70 additions & 0 deletions plumbing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Modlauncher plumbing

## Major parts

* ```ILaunchPluginService```

ServiceLoader element. Intended for systemwide transformation capabilities: Mixin, Access Transformers.

Loaded from the classpath at system instantiation, as an immutable list.

Plugin architecture so the individual elements can be upgraded without the core system needing a change.

Separated API package for minimal required surface area for consumers of this API.

It is not expected to receive widespread adoption as custom launch profiles will be needed.

**Forge note** It's possible we could implement a ForgePatcher plugin here.

* ```ILaunchHandlerService```

ServiceLoader element. Intended to control the launch target (the main class to be launched after setup is complete).
Has a default to launch vanilla minecraft.

Loaded from the classpath at system instantiation, as an immutable list. Target is selected by name from list of
targets through command line argument ```--launchTarget```.

Plugin architecture so mods can provide alternatives.

It is not expected to receive widespread adoption.

**Note** Metadata about the launch target should be provided (NYI)

**Forge note** Forge will provide a semi-patched and deobfuscated minecraft module here.

* ```ITransformationService```

ServiceLoader element. Intended to allow mod systems to inject class transforming code into the classloader and
additional command line arguments to the system for configuration purposes.

Loaded from classpath at system instantiation, as a semi-immutable list. All transformers will be loaded and given
lifecycle events where they can create ```ITransformer``` instances which ultimately act on classloaded code.

There will be the possibility to add additional services to this list during early setup phases (example: FML discovers
LiteLoader in the FML mods directory).

Plugin architecture because this is what mod systems will be expected to deliver.

It is expected that high level mod systems tweakers, such as FML, LiteLoader and others will implement this.

There may be a vanilla-legacy instance of this as well.

**Note** Metadata for communication between systems is NYI

## How TransformationService should work

* During first init phase, it should identify any resources which it believes should be a loading candidate, and offer
them to the system. This will allow modlauncher to identify additional transformation services.
* Subsequent phases should create and register transformers, prior to launch.

## How LaunchPlugins should work
* They will receive an initialization step, where they can query the launch service that has been targetted. This will
contain the metadata they need to discover structure. Artifacts offered to modlauncher (Jars for loading) will be offered
to launch plugins as well, so they can integrate additional changes.

##How forge might work
* ForgeFML provides a TransformationService.
* ForgePatchedMC is a LaunchHandlerService - providing a pre-patched and deobfuscated Minecraft for launch into.
* ForgePatcherPlugin is a LaunchPluginService providing additional hot-patching if necessary.
* AccessTransformer is a LaunchPluginService providing AT capabilities.
* Mixin is a LaunchPluginService providing enhanced runtime patch capabilities.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package cpw.mods.modlauncher.serviceapi;

import org.objectweb.asm.*;
import org.objectweb.asm.tree.*;

import java.nio.file.*;
Expand Down Expand Up @@ -36,9 +37,10 @@ public interface ILaunchPluginService {
* but ordering between plugins is not known.
*
* @param classNode the classnode to process
* @param classType the name of the class
* @return the processed classnode
*/
ClassNode processClass(ClassNode classNode);
ClassNode processClass(ClassNode classNode, final Type classType);

/**
* Get a plugin specific extension object from the plugin. This can be used to expose proprietary interfaces
Expand All @@ -48,4 +50,11 @@ public interface ILaunchPluginService {
* @return An extension object
*/
<T> T getExtension();

/**
* If this plugin wants to receive the {@link ClassNode} into {@link #processClass}
* @param classType the class to consider
* @return if this plugin wants to receive a call on processClass with the classNode
*/
boolean handlesClass(Type classType);
}
11 changes: 9 additions & 2 deletions src/main/java/cpw/mods/modlauncher/ClassTransformer.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,19 @@
public class ClassTransformer {
private static final byte[] EMPTY = new byte[0];
private final TransformStore transformers;
private final LaunchPluginHandler pluginHandler;

ClassTransformer(TransformStore transformers) {
ClassTransformer(TransformStore transformers, LaunchPluginHandler pluginHandler) {
this.transformers = transformers;
this.pluginHandler = pluginHandler;
}

byte[] transform(byte[] inputClass, String className) {
Type classDesc = Type.getObjectType(className.replaceAll("\\.", "/"));
if (!transformers.needsTransforming(className)) {

List<String> plugins = pluginHandler.getPluginsTransforming(classDesc);

if (!transformers.needsTransforming(className) && plugins.isEmpty()) {
return inputClass;
}

Expand All @@ -40,6 +45,8 @@ byte[] transform(byte[] inputClass, String className) {
digest = getSha256().digest(EMPTY);
empty = true;
}

clazz = pluginHandler.offerClassNodeToPlugins(plugins, clazz, classDesc);
VotingContext context = new VotingContext(className, empty, digest);

List<FieldNode> fieldList = new ArrayList<>(clazz.fields.size());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,21 @@
* Default launch handler service - will launch minecraft
*/
public class DefaultLaunchHandlerService implements ILaunchHandlerService {
public static final String LAUNCH_PROPERTY = "minecraft.client.jar";
public static final String LAUNCH_PATH_STRING = System.getProperty(LAUNCH_PROPERTY);

@Override
public String name() {
return "minecraft";
}

@Override
public Path[] identifyTransformationTargets() {
final URL resource = getClass().getClassLoader().getResource("net/minecraft/client/main/Main.class");
try {
JarURLConnection urlConnection = (JarURLConnection) resource.openConnection();
return new Path[]{FileSystems.getDefault().getPath(urlConnection.getJarFile().getName())};
} catch (IOException | NullPointerException e) {
e.printStackTrace();
return new Path[0];
if (LAUNCH_PATH_STRING == null) {
throw new IllegalStateException("Missing "+ LAUNCH_PROPERTY +" environment property. Update your launcher!");
}

return new Path[] { FileSystems.getDefault().getPath(LAUNCH_PATH_STRING) };
}

@Override
Expand Down
21 changes: 20 additions & 1 deletion src/main/java/cpw/mods/modlauncher/LaunchPluginHandler.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package cpw.mods.modlauncher;

import cpw.mods.modlauncher.serviceapi.*;
import org.objectweb.asm.*;
import org.objectweb.asm.tree.*;

import javax.annotation.*;
import java.util.*;
import java.util.stream.*;

Expand All @@ -11,12 +14,28 @@ public class LaunchPluginHandler {

private final Map<String, ILaunchPluginService> plugins;

LaunchPluginHandler() {
public LaunchPluginHandler() {
ServiceLoader<ILaunchPluginService> services = ServiceLoader.load(ILaunchPluginService.class);
plugins = ServiceLoaderStreamUtils.toMap(services, ILaunchPluginService::name);
launcherLog.info("Found launch plugins: [{}]", ()-> plugins.keySet().stream().collect(Collectors.joining()));
}
public Optional<ILaunchPluginService> get(final String name) {
return Optional.ofNullable(plugins.get(name));
}

public List<String> getPluginsTransforming(final Type className) {
return plugins.entrySet().stream().filter(p -> p.getValue().handlesClass(className)).
peek(e->launcherLog.debug("LaunchPluginService {} wants to handle {}", e.getKey(), className)).
collect(ArrayList<String>::new, (l,e) -> l.add(e.getKey()), ArrayList::addAll);
}

public ClassNode offerClassNodeToPlugins(final List<String> pluginNames, @Nullable final ClassNode node, final Type className) {
ClassNode intermediate = node;
for (String plugin: pluginNames) {
final ILaunchPluginService iLaunchPluginService = plugins.get(plugin);
launcherLog.debug("LauncherPluginService {} transforming {}", plugin, className);
intermediate = iLaunchPluginService.processClass(intermediate, className);
}
return intermediate;
}
}
2 changes: 1 addition & 1 deletion src/main/java/cpw/mods/modlauncher/Launcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ private void run(String... args) {
this.argumentHandler.setArgs(args);
this.transformationServicesHandler.initializeTransformationServices(this.argumentHandler, this.environment);
Path[] specialJars = this.launchService.identifyTransformationTargets(this.argumentHandler);
this.classLoader = this.transformationServicesHandler.buildTransformingClassLoader(specialJars);
this.classLoader = this.transformationServicesHandler.buildTransformingClassLoader(this.launchPlugins, specialJars);
Thread.currentThread().setContextClassLoader(this.classLoader);
this.launchService.launch(this.argumentHandler, this.classLoader);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ void initializeTransformationServices(ArgumentHandler argumentHandler, Environme
initialiseServiceTransformers();
}

TransformingClassLoader buildTransformingClassLoader(Path... specialJars) {
return new TransformingClassLoader(transformStore, specialJars);
TransformingClassLoader buildTransformingClassLoader(final LaunchPluginHandler pluginHandler, Path... specialJars) {
return new TransformingClassLoader(transformStore, pluginHandler, specialJars);
}

private void processArguments(ArgumentHandler argumentHandler, Environment environment) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ public class TransformingClassLoader extends ClassLoader {
private final DelegatedClassLoader delegatedClassLoader;
private final URL[] specialJars;

public TransformingClassLoader(TransformStore transformStore, Path... specialJars) {
public TransformingClassLoader(TransformStore transformStore, LaunchPluginHandler pluginHandler, Path... specialJars) {
super();
this.classTransformer = new ClassTransformer(transformStore);
this.classTransformer = new ClassTransformer(transformStore, pluginHandler);
this.specialJars = Stream.of(specialJars).map(rethrowFunction(f -> f.toUri().toURL())).toArray(URL[]::new);
this.delegatedClassLoader = new DelegatedClassLoader();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ class ClassTransformerTests {
@Test
void testClassTransformer() throws Exception {
final TransformStore transformStore = new TransformStore();
final ClassTransformer classTransformer = Whitebox.invokeConstructor(ClassTransformer.class, transformStore);

final LaunchPluginHandler lph = new LaunchPluginHandler();
final ClassTransformer classTransformer = Whitebox.invokeConstructor(ClassTransformer.class, transformStore, lph);
Whitebox.invokeMethod(transformStore, "addTransformer", new TransformTargetLabel("test.MyClass"), classTransformer());
byte[] result = Whitebox.invokeMethod(classTransformer, "transform", new Class[]{byte[].class, String.class}, new byte[0], "test.MyClass");
assertAll("Class loads and is valid",
() -> assertNotNull(result),
() -> assertNotNull(new TransformingClassLoader(transformStore, FileSystems.getDefault().getPath(".")).getClass("test.MyClass", result)),
() -> assertNotNull(new TransformingClassLoader(transformStore, lph, FileSystems.getDefault().getPath(".")).getClass("test.MyClass", result)),
() ->
{
ClassReader cr = new ClassReader(result);
Expand All @@ -47,7 +47,7 @@ void testClassTransformer() throws Exception {
byte[] result1 = Whitebox.invokeMethod(classTransformer, "transform", new Class[]{byte[].class, String.class}, cw.toByteArray(), "test.DummyClass");
assertAll("Class loads and is valid",
() -> assertNotNull(result1),
() -> assertNotNull(new TransformingClassLoader(transformStore, FileSystems.getDefault().getPath(".")).getClass("test.DummyClass", result1)),
() -> assertNotNull(new TransformingClassLoader(transformStore, lph, FileSystems.getDefault().getPath(".")).getClass("test.DummyClass", result1)),
() ->
{
ClassReader cr = new ClassReader(result1);
Expand Down
8 changes: 7 additions & 1 deletion src/test/java/cpw/mods/modlauncher/test/PluginTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import cpw.mods.modlauncher.serviceapi.*;
import org.junit.jupiter.api.*;
import org.objectweb.asm.*;
import org.objectweb.asm.tree.*;

import java.nio.file.*;
Expand All @@ -23,14 +24,19 @@ public void addResource(final Path resource) {
}

@Override
public ClassNode processClass(final ClassNode classNode) {
public ClassNode processClass(final ClassNode classNode, final Type classType) {
return null;
}

@Override
public String getExtension() {
return "CHEESE";
}

@Override
public boolean handlesClass(final Type classType) {
return true;
}
};

String s = plugin.getExtension();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ public List<ITransformer> transformers() {
};

TransformStore transformStore = new TransformStore();
LaunchPluginHandler lph = new LaunchPluginHandler();
TransformationServiceDecorator sd = Whitebox.invokeConstructor(TransformationServiceDecorator.class, mockTransformerService);
sd.gatherTransformers(transformStore);
TransformingClassLoader tcl = new TransformingClassLoader(transformStore, FileSystems.getDefault().getPath("."));
TransformingClassLoader tcl = new TransformingClassLoader(transformStore, lph, FileSystems.getDefault().getPath("."));
final Class<?> aClass = Class.forName("cheese.Puffs", true, tcl);
assertEquals(Whitebox.getField(aClass, "testfield").getType(), String.class);
assertEquals(Whitebox.getField(aClass, "testfield").get(null), "CHEESE!");
Expand Down

0 comments on commit aa568d7

Please sign in to comment.