From 657aad80f19cdb7f0754f6b14c3e4a49da2e0e09 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 09:52:14 +0000 Subject: [PATCH 1/5] Run MCPC+ / Cauldron 1.6.4 servers on Java 17 - Spoof java.version on any non-Java 7 runtime to pass the MCPC+ check. - Patch launchwrapper's Launch constructor: the app class loader is no longer a URLClassLoader, so build LaunchClassLoader from java.class.path and delegate platform-loader packages (javax.sql, javax.script...). - Open every JDK package to unnamed modules and remove the Java 12+ reflection filters (Field.modifiers), as on Java 8. - Redirect sun.reflect.ReflectionFactory / ConstructorAccessor / FieldAccessor to shims so Forge's EnumHelper can still add enum values. - Rename the agent to mcpc-java17-agent and require Java 17 to build. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DszbR9jSTnJF2dtbGNiQRv --- README.md | 57 +++-- build.gradle | 17 +- settings.gradle | 2 +- .../gg/nationsglory/mcpcj17/ClassPatcher.java | 190 +++++++++++++++++ .../gg/nationsglory/mcpcj17/LaunchHooks.java | 63 ++++++ .../nationsglory/mcpcj17/McpcJava17Agent.java | 37 ++++ .../gg/nationsglory/mcpcj17/ModuleAccess.java | 71 +++++++ .../mcpcj17/shim/ConstructorAccessor.java | 12 ++ .../mcpcj17/shim/FieldAccessor.java | 43 ++++ .../shim/MethodHandleConstructorAccessor.java | 35 ++++ .../mcpcj17/shim/ReflectionFactory.java | 29 +++ .../mcpcj17/shim/UnsafeFieldAccessor.java | 194 ++++++++++++++++++ .../mcpcj8/Java8ClassPatcher.java | 77 ------- .../nationsglory/mcpcj8/McpcJava8Agent.java | 48 ----- .../{mcpcj8 => mcpcj17}/ClassReader.bin | Bin .../gg/nationsglory/mcpcj17/Launch.bin | Bin 0 -> 5487 bytes .../{mcpcj8 => mcpcj17}/MethodInsnNode.bin | Bin .../{mcpcj8 => mcpcj17}/Remapper.bin | Bin .../gg/nationsglory/mcpcj8/Launch.bin | Bin 5551 -> 0 bytes 19 files changed, 720 insertions(+), 155 deletions(-) create mode 100644 src/main/java/gg/nationsglory/mcpcj17/ClassPatcher.java create mode 100644 src/main/java/gg/nationsglory/mcpcj17/LaunchHooks.java create mode 100644 src/main/java/gg/nationsglory/mcpcj17/McpcJava17Agent.java create mode 100644 src/main/java/gg/nationsglory/mcpcj17/ModuleAccess.java create mode 100644 src/main/java/gg/nationsglory/mcpcj17/shim/ConstructorAccessor.java create mode 100644 src/main/java/gg/nationsglory/mcpcj17/shim/FieldAccessor.java create mode 100644 src/main/java/gg/nationsglory/mcpcj17/shim/MethodHandleConstructorAccessor.java create mode 100644 src/main/java/gg/nationsglory/mcpcj17/shim/ReflectionFactory.java create mode 100644 src/main/java/gg/nationsglory/mcpcj17/shim/UnsafeFieldAccessor.java delete mode 100644 src/main/java/gg/nationsglory/mcpcj8/Java8ClassPatcher.java delete mode 100644 src/main/java/gg/nationsglory/mcpcj8/McpcJava8Agent.java rename src/main/resources/gg/nationsglory/{mcpcj8 => mcpcj17}/ClassReader.bin (100%) create mode 100644 src/main/resources/gg/nationsglory/mcpcj17/Launch.bin rename src/main/resources/gg/nationsglory/{mcpcj8 => mcpcj17}/MethodInsnNode.bin (100%) rename src/main/resources/gg/nationsglory/{mcpcj8 => mcpcj17}/Remapper.bin (100%) delete mode 100644 src/main/resources/gg/nationsglory/mcpcj8/Launch.bin diff --git a/README.md b/README.md index d0caacc..ee6414f 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,78 @@ -# MCPC+ Java 8 Agent +# MCPC+ Java 17 Agent -Lance un serveur **MCPC+ / Cauldron 1.6.4** sous **Java 8**, sans modifier le jar du serveur ni les mods. +Lance un serveur **MCPC+ / Cauldron 1.6.4** sous **Java 17**, sans modifier le jar du serveur ni les mods. -Ces serveurs refusent de démarrer sous Java 8, ou plantent pendant le chargement de Forge. Cet agent lève les trois blocages au démarrage. +Ces serveurs refusent de démarrer sur autre chose que Java 7, puis plantent pendant le chargement de launchwrapper et de Forge, car ils s'appuient sur des comportements de Java 7/8 qui n'existent plus en Java 17. Cet agent lève ces blocages au démarrage. ## Installation -1. Récupérez `mcpc-java8-agent.jar` depuis les [releases](../../releases), ou compilez-le (voir plus bas). +1. Récupérez `mcpc-java17-agent.jar` depuis les [releases](../../releases), ou compilez-le (voir plus bas). 2. Placez-le où vous voulez, par exemple à côté du jar du serveur. 3. Ajoutez `-javaagent:` à votre ligne de lancement, **avant** `-jar` : ```sh -java -javaagent:mcpc-java8-agent.jar -Xmx4G -jar mcpc.jar nogui +java -javaagent:mcpc-java17-agent.jar -Xmx4G -jar mcpc.jar nogui ``` -C'est tout. Aucun fichier de configuration, aucune modification du serveur ou des mods. +C'est tout. Aucun `--add-opens`, aucun fichier de configuration, aucune modification du serveur ou des mods. ### Exemple de start.sh ```sh #!/bin/sh -java -javaagent:mcpc-java8-agent.jar \ +java -javaagent:mcpc-java17-agent.jar \ -Xms2G -Xmx4G \ -jar mcpc.jar nogui ``` +### Options JVM à retirer + +Certaines options courantes des anciens scripts de lancement n'existent plus en Java 17 : + +- `-XX:+UseConcMarkSweepGC` et les options `CMS*` : supprimées, **la JVM refuse de démarrer**. Retirez-les (G1, le GC par défaut, convient très bien). +- `-XX:PermSize` / `-XX:MaxPermSize` : ignorées depuis Java 8, vous pouvez les retirer. + ### Vérifier que ça marche Au démarrage, la console affiche : ``` -[mcpc-j8] Java 8 detected (1.8.0_412), reporting 1.7.0_80 to bypass the MCPC+ version check. -[mcpc-j8] 4 class patch(es) loaded. -[mcpc-j8] Patched: net/minecraft/launchwrapper/Launch +[mcpc-j17] Java 17.0.x detected, reporting 1.7.0_80 to bypass the MCPC+ version check. +[mcpc-j17] Opened 741 JDK packages to the server. +[mcpc-j17] Reflection filters removed. +[mcpc-j17] 4 class patch(es) loaded. +[mcpc-j17] Patched: net/minecraft/launchwrapper/Launch +[mcpc-j17] LaunchClassLoader delegates 241 platform packages to the JDK. +[mcpc-j17] Redirected sun.reflect in: net/minecraftforge/common/EnumHelper ``` -Si vous ne voyez aucune ligne `[mcpc-j8]`, l'agent n'est pas chargé : vérifiez que `-javaagent:` est bien **avant** `-jar` et que le chemin du jar est correct. +Si vous ne voyez aucune ligne `[mcpc-j17]`, l'agent n'est pas chargé : vérifiez que `-javaagent:` est bien **avant** `-jar` et que le chemin du jar est correct. ## Ce que l'agent corrige -**1. Le refus explicite de Java 8.** `FMLLaunchHandler` teste `java.version` et arrête le serveur avec le message `Java 8 is not supported on MCPC+8.11! Please run MCPC+ using Java 7.` +**1. Le refus de toute version autre que Java 7.** `FMLLaunchHandler` teste `java.version` et arrête le serveur (`... is not supported on MCPC+8.11! Please run MCPC+ using Java 7.`). + +L'agent présente une version `1.7.x` à cette vérification. Passer `-Djava.version=1.7.0_80` sur la ligne de commande **ne fonctionne pas** : la JVM réécrit cette propriété pendant son initialisation. Il faut le faire depuis un agent, qui s'exécute après l'initialisation de la JVM mais avant le `main` du serveur. Un coremod ne peut pas corriger ce point : `FMLLaunchHandler` est exclu du `LaunchClassLoader`. + +**2. Le démarrage de launchwrapper.** `Launch` construit son class loader à partir de `(URLClassLoader) getClass().getClassLoader()`. Depuis Java 9, le class loader de l'application n'est plus un `URLClassLoader` : `ClassCastException` immédiate. La version corrigée de `Launch` lit le class path depuis `java.class.path`, comme les versions récentes de launchwrapper. -L'agent présente une version `1.7.x` à cette vérification. Passer `-Djava.version=1.7.0_80` sur la ligne de commande **ne fonctionne pas** : la JVM réécrit cette propriété pendant son initialisation. Il faut le faire depuis un agent, qui s'exécute après l'initialisation de la JVM mais avant le `main` du serveur. +La version corrigée garde aussi le correctif de la `ConcurrentModificationException` dans `Launch.launch` (liste des tweakers modifiée pendant son parcours). -Un coremod ne peut pas corriger ce point : `FMLLaunchHandler` est explicitement exclu du `LaunchClassLoader`, donc hors de portée de tout `IClassTransformer`. +**3. Les classes du JDK hors de `java.base`.** Le `LaunchClassLoader` a pour parent le class loader de bootstrap. En Java 8, celui-ci voyait tout le JDK ; depuis Java 9, des modules comme `java.sql` ou `java.scripting` sont chargés par le *platform class loader*, et des classes comme `javax.sql.DataSource` (Ebean, plugins Bukkit) deviennent introuvables. L'agent fait déléguer ces packages au JDK. -**2. Un plantage dans launchwrapper.** `Launch.launch` parcourt la liste des tweakers tout en la modifiant, ce qui lève une `ConcurrentModificationException` sous Java 8. +**4. L'encapsulation des modules du JDK.** Forge, Bukkit et les mods appellent `setAccessible` sur des classes internes du JDK, ce que Java 17 refuse par défaut (`InaccessibleObjectException`). L'agent ouvre tous les packages du JDK au serveur, l'équivalent d'une longue liste de `--add-opens ...=ALL-UNNAMED`. Il retire aussi les filtres de réflexion ajoutés en Java 12 (qui cachent notamment `Field.modifiers`). -Si votre serveur utilise déjà launchwrapper 1.11 ou supérieur, ce point est déjà réglé de votre côté ; l'agent reste inoffensif. +**5. `sun.reflect.ReflectionFactory`, supprimé en Java 9.** L'`EnumHelper` de Forge s'en sert pour ajouter des valeurs aux enums (biomes, types de créatures, matériaux Bukkit de MCPC+...). Les références à `sun.reflect.ReflectionFactory`, `ConstructorAccessor` et `FieldAccessor` sont redirigées, dans toutes les classes chargées, vers des équivalents fournis par l'agent qui fonctionnent sous Java 17. -**3. Des plantages du déobfuscateur de Forge.** La version d'ASM embarquée dans ces serveurs échoue sur du bytecode produit par Java 8 (`Remapper`, `ClassReader`, `MethodInsnNode`). +**6. Le déobfuscateur de Forge.** La version d'ASM embarquée dans ces serveurs échoue sur du bytecode récent (`Remapper`, `ClassReader`, `MethodInsnNode`). Les versions corrigées acceptent les classes jusqu'à Java 17. ## Portée et limites -L'agent débloque le **lancement**. Il ne rend pas compatibles des mods qui dépendraient d'API supprimées en Java 8 — un cas rare sur cette génération de mods. +L'agent débloque le **lancement**. Il ne rend pas compatibles des mods ou plugins qui dépendraient d'API supprimées du JDK (moteur JavaScript Nashorn, `javax.xml.bind`, `Thread.stop`...) ; ces cas restent à traiter au cas par cas. Il ne touche à rien d'autre : pas de modification du monde, des mods, ni du comportement du serveur. Si un correctif ne peut pas s'appliquer, la classe d'origine est laissée intacte et le démarrage continue. -Testé sur MCPC+ 8.11 / Cauldron 1.6.4 avec OpenJDK 8 et Zulu 8, en production. +L'agent requiert Java 17 ou plus récent ; il ne se charge pas sous Java 8. ## Compiler @@ -65,10 +80,10 @@ Testé sur MCPC+ 8.11 / Cauldron 1.6.4 avec OpenJDK 8 et Zulu 8, en production. ./gradlew jar ``` -Le jar est produit dans `build/libs/mcpc-java8-agent.jar`. Aucune dépendance externe. +Nécessite un JDK 17 ou plus récent. Le jar est produit dans `build/libs/mcpc-java17-agent.jar`. Aucune dépendance externe. ## Contenu -Les fichiers `.bin` sont les classes d'origine de **launchwrapper** et **ASM**, recompilées avec la correction minimale nécessaire. Elles restent sous la licence de leurs projets respectifs : MIT pour launchwrapper, BSD 3-clauses pour ASM. +Les fichiers `.bin` sont les classes d'origine de **launchwrapper** et **ASM**, recompilées avec la correction minimale nécessaire (pour `Launch`, le constructeur appelle `gg.nationsglory.mcpcj17.LaunchHooks`). Elles restent sous la licence de leurs projets respectifs : MIT pour launchwrapper, BSD 3-clauses pour ASM. Le code de l'agent lui-même est publié sous licence MIT. diff --git a/build.gradle b/build.gradle index aa95ebc..05b6f84 100644 --- a/build.gradle +++ b/build.gradle @@ -3,23 +3,24 @@ plugins { } group = 'gg.nationsglory' -version = '1.0.0' +version = '2.0.0' -sourceCompatibility = '1.7' -targetCompatibility = '1.7' - -compileJava { options.encoding = 'UTF-8' } +compileJava { + options.encoding = 'UTF-8' + options.release = 17 +} repositories { mavenCentral() } jar { - archiveBaseName = 'mcpc-java8-agent' + archiveBaseName = 'mcpc-java17-agent' archiveVersion = '' manifest { attributes( - 'Premain-Class': 'gg.nationsglory.mcpcj8.McpcJava8Agent', + 'Premain-Class': 'gg.nationsglory.mcpcj17.McpcJava17Agent', + 'Can-Redefine-Classes': 'false', 'Can-Retransform-Classes': 'false', - 'Implementation-Title': 'MCPC+ Java 8 Agent', + 'Implementation-Title': 'MCPC+ Java 17 Agent', 'Implementation-Version': project.version ) } diff --git a/settings.gradle b/settings.gradle index b826c81..2d022a4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'mcpc-java8-agent' +rootProject.name = 'mcpc-java17-agent' diff --git a/src/main/java/gg/nationsglory/mcpcj17/ClassPatcher.java b/src/main/java/gg/nationsglory/mcpcj17/ClassPatcher.java new file mode 100644 index 0000000..820d52c --- /dev/null +++ b/src/main/java/gg/nationsglory/mcpcj17/ClassPatcher.java @@ -0,0 +1,190 @@ +package gg.nationsglory.mcpcj17; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.lang.instrument.ClassFileTransformer; +import java.nio.charset.StandardCharsets; +import java.security.ProtectionDomain; +import java.util.HashMap; +import java.util.Map; + +public final class ClassPatcher implements ClassFileTransformer { + + private static final Map TARGETS = new HashMap(); + static { + TARGETS.put("net/minecraft/launchwrapper/Launch", "Launch.bin"); + TARGETS.put("org/objectweb/asm/commons/Remapper", "Remapper.bin"); + TARGETS.put("org/objectweb/asm/ClassReader", "ClassReader.bin"); + TARGETS.put("org/objectweb/asm/tree/MethodInsnNode", "MethodInsnNode.bin"); + } + + /** JDK 8 internals removed in Java 9, and their replacements in this agent. */ + private static final String[][] REDIRECTS = { + {"sun/reflect/ReflectionFactory", "gg/nationsglory/mcpcj17/shim/ReflectionFactory"}, + {"sun/reflect/ConstructorAccessor", "gg/nationsglory/mcpcj17/shim/ConstructorAccessor"}, + {"sun/reflect/FieldAccessor", "gg/nationsglory/mcpcj17/shim/FieldAccessor"}, + }; + private static final byte[] SUN_REFLECT_SLASH = "sun/reflect/".getBytes(StandardCharsets.US_ASCII); + private static final byte[] SUN_REFLECT_DOT = "sun.reflect.".getBytes(StandardCharsets.US_ASCII); + + private final Map patches = new HashMap(); + + public ClassPatcher() { + for (Map.Entry entry : TARGETS.entrySet()) { + byte[] bytes = readResource(entry.getValue()); + if (bytes != null) { + patches.put(entry.getKey(), bytes); + } else { + System.out.println("[mcpc-j17] Missing patch resource: " + entry.getValue()); + } + } + System.out.println("[mcpc-j17] " + patches.size() + " class patch(es) loaded."); + } + + @Override + public byte[] transform(ClassLoader loader, String className, Class redefined, + ProtectionDomain domain, byte[] original) { + try { + if (className != null) { + byte[] patch = patches.get(className); + if (patch != null) { + System.out.println("[mcpc-j17] Patched: " + className); + return patch; + } + if (loader != null && !className.startsWith("gg/nationsglory/mcpcj17/") + && (indexOf(original, SUN_REFLECT_SLASH) >= 0 || indexOf(original, SUN_REFLECT_DOT) >= 0)) { + byte[] redirected = redirectSunReflect(original); + if (redirected != null) { + System.out.println("[mcpc-j17] Redirected sun.reflect in: " + className); + return redirected; + } + } + } + } catch (Throwable t) { + System.out.println("[mcpc-j17] Could not patch " + className + ", leaving it untouched: " + t); + } + return null; + } + + /** + * Rewrites the UTF-8 constants of the class that name one of the {@link #REDIRECTS}, in internal + * form (type references, descriptors) or dotted form (Class.forName strings). Returns null when + * nothing changed. + */ + static byte[] redirectSunReflect(byte[] b) { + int count = u2(b, 8); + ByteArrayOutputStream out = new ByteArrayOutputStream(b.length + 256); + out.write(b, 0, 10); + boolean changed = false; + int pos = 10; + for (int i = 1; i < count; i++) { + int tag = b[pos] & 0xFF; + int size; + switch (tag) { + case 1: + int length = u2(b, pos + 1); + String value = new String(b, pos + 3, length, StandardCharsets.ISO_8859_1); + String replaced = redirect(value); + if (replaced != value) { + byte[] bytes = replaced.getBytes(StandardCharsets.ISO_8859_1); + out.write(1); + out.write(bytes.length >>> 8); + out.write(bytes.length); + out.write(bytes, 0, bytes.length); + changed = true; + pos += 3 + length; + continue; + } + size = 3 + length; + break; + case 3: case 4: case 9: case 10: case 11: case 12: case 17: case 18: + size = 5; + break; + case 5: case 6: + size = 9; + i++; + break; + case 7: case 8: case 16: case 19: case 20: + size = 3; + break; + case 15: + size = 4; + break; + default: + throw new IllegalStateException("Unknown constant pool tag " + tag); + } + out.write(b, pos, size); + pos += size; + } + if (!changed) { + return null; + } + out.write(b, pos, b.length - pos); + return out.toByteArray(); + } + + /** Returns {@code value} itself when it holds no redirected name. */ + private static String redirect(String value) { + if (!value.contains("sun/reflect/") && !value.contains("sun.reflect.")) { + return value; + } + String result = value; + for (String[] redirect : REDIRECTS) { + result = replaceName(result, redirect[0], redirect[1]); + result = replaceName(result, redirect[0].replace('/', '.'), redirect[1].replace('/', '.')); + } + return result.equals(value) ? value : result; + } + + /** Replaces whole class names only: sun/reflect/FieldAccessor, not sun/reflect/FieldAccessorImpl. */ + private static String replaceName(String value, String from, String to) { + StringBuilder sb = null; + int copied = 0; + int search = 0; + int index; + while ((index = value.indexOf(from, search)) >= 0) { + int end = index + from.length(); + search = end; + if (end < value.length() && Character.isJavaIdentifierPart(value.charAt(end))) { + continue; + } + if (sb == null) { + sb = new StringBuilder(value.length() + 32); + } + sb.append(value, copied, index).append(to); + copied = end; + } + if (sb == null) { + return value; + } + return sb.append(value, copied, value.length()).toString(); + } + + private static int u2(byte[] b, int pos) { + return ((b[pos] & 0xFF) << 8) | (b[pos + 1] & 0xFF); + } + + private static int indexOf(byte[] data, byte[] pattern) { + outer: + for (int i = 0; i <= data.length - pattern.length; i++) { + for (int j = 0; j < pattern.length; j++) { + if (data[i + j] != pattern[j]) { + continue outer; + } + } + return i; + } + return -1; + } + + private static byte[] readResource(String name) { + try (InputStream in = ClassPatcher.class.getResourceAsStream(name)) { + if (in == null) { + return null; + } + return in.readAllBytes(); + } catch (Throwable t) { + return null; + } + } +} diff --git a/src/main/java/gg/nationsglory/mcpcj17/LaunchHooks.java b/src/main/java/gg/nationsglory/mcpcj17/LaunchHooks.java new file mode 100644 index 0000000..af1fe63 --- /dev/null +++ b/src/main/java/gg/nationsglory/mcpcj17/LaunchHooks.java @@ -0,0 +1,63 @@ +package gg.nationsglory.mcpcj17; + +import java.io.File; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +/** + * Called by the patched {@code net.minecraft.launchwrapper.Launch} constructor. + */ +public final class LaunchHooks { + + /** + * launchwrapper builds its class loader from {@code ((URLClassLoader) getClassLoader()).getURLs()}; + * the application class loader is no longer a URLClassLoader since Java 9. + */ + public static URL[] classPath() { + List urls = new ArrayList(); + String classPath = System.getProperty("java.class.path", ""); + for (String entry : classPath.split(File.pathSeparator)) { + if (entry.isEmpty()) { + continue; + } + try { + urls.add(new File(entry).getAbsoluteFile().toURI().toURL()); + } catch (Exception e) { + System.out.println("[mcpc-j17] Ignoring class path entry " + entry + ": " + e); + } + } + return urls.toArray(new URL[0]); + } + + /** + * LaunchClassLoader has the bootstrap loader as parent. On Java 8 that covered the whole JDK; + * since Java 9, modules such as java.sql or java.scripting live in the platform loader, so their + * packages (javax.sql, javax.script...) must be delegated explicitly. + */ + public static void configure(ClassLoader launchClassLoader) { + try { + Method exclude = launchClassLoader.getClass().getMethod("addClassLoaderExclusion", String.class); + exclude.invoke(launchClassLoader, "gg.nationsglory.mcpcj17."); + int count = 0; + for (Module module : ModuleLayer.boot().modules()) { + if (module.getClassLoader() == null) { + continue; + } + for (String pkg : module.getPackages()) { + if (!pkg.startsWith("java.")) { + exclude.invoke(launchClassLoader, pkg + "."); + count++; + } + } + } + System.out.println("[mcpc-j17] LaunchClassLoader delegates " + count + " platform packages to the JDK."); + } catch (Throwable t) { + System.out.println("[mcpc-j17] Could not configure LaunchClassLoader, continuing: " + t); + } + } + + private LaunchHooks() { + } +} diff --git a/src/main/java/gg/nationsglory/mcpcj17/McpcJava17Agent.java b/src/main/java/gg/nationsglory/mcpcj17/McpcJava17Agent.java new file mode 100644 index 0000000..eaba231 --- /dev/null +++ b/src/main/java/gg/nationsglory/mcpcj17/McpcJava17Agent.java @@ -0,0 +1,37 @@ +package gg.nationsglory.mcpcj17; + +import java.lang.instrument.Instrumentation; + +public final class McpcJava17Agent { + + private static final String SPOOFED_JAVA_VERSION = "1.7.0_80"; + + public static void premain(String agentArgs, Instrumentation inst) { + spoofJavaVersion(); + ModuleAccess.openJdkModules(inst); + ModuleAccess.removeReflectionFilters(); + installClassPatches(inst); + } + + private static void spoofJavaVersion() { + String actual = System.getProperty("java.version"); + if (actual != null && !actual.startsWith("1.7")) { + System.setProperty("java.version", SPOOFED_JAVA_VERSION); + System.out.println("[mcpc-j17] Java " + actual + " detected, reporting " + + SPOOFED_JAVA_VERSION + " to bypass the MCPC+ version check."); + } else { + System.out.println("[mcpc-j17] java.version = " + actual + ", nothing to bypass."); + } + } + + private static void installClassPatches(Instrumentation inst) { + try { + inst.addTransformer(new ClassPatcher()); + } catch (Throwable t) { + System.out.println("[mcpc-j17] Could not install class patches, continuing: " + t); + } + } + + private McpcJava17Agent() { + } +} diff --git a/src/main/java/gg/nationsglory/mcpcj17/ModuleAccess.java b/src/main/java/gg/nationsglory/mcpcj17/ModuleAccess.java new file mode 100644 index 0000000..a2f0063 --- /dev/null +++ b/src/main/java/gg/nationsglory/mcpcj17/ModuleAccess.java @@ -0,0 +1,71 @@ +package gg.nationsglory.mcpcj17; + +import java.lang.instrument.Instrumentation; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.lang.ref.SoftReference; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Gives the server the reflective access it had on Java 8. + */ +final class ModuleAccess { + + /** + * Equivalent of {@code --add-opens =ALL-UNNAMED}: Forge, Bukkit and mods + * call setAccessible on JDK internals, which Java 17 refuses by default. + */ + static void openJdkModules(Instrumentation inst) { + try { + Module self = ModuleAccess.class.getModule(); + inst.redefineModule(Object.class.getModule(), Set.of(), Map.of(), + Map.of("java.lang", Set.of(self)), Set.of(), Map.of()); + + Method openToAllUnnamed = Module.class.getDeclaredMethod("implAddOpensToAllUnnamed", String.class); + openToAllUnnamed.setAccessible(true); + int count = 0; + for (Module module : ModuleLayer.boot().modules()) { + for (String pkg : module.getPackages()) { + openToAllUnnamed.invoke(module, pkg); + count++; + } + } + System.out.println("[mcpc-j17] Opened " + count + " JDK packages to the server."); + } catch (Throwable t) { + System.out.println("[mcpc-j17] Could not open JDK modules, continuing: " + t); + } + } + + /** + * Java 12+ hides some fields from reflection, notably {@code Field.modifiers}, which Forge's + * EnumHelper uses to write static final fields. Java 8 hid nothing of the kind. + */ + static void removeReflectionFilters() { + try { + Class reflection = Class.forName("jdk.internal.reflect.Reflection"); + VarHandle filterMap = MethodHandles.privateLookupIn(reflection, MethodHandles.lookup()) + .findStaticVarHandle(reflection, "fieldFilterMap", Map.class); + @SuppressWarnings("unchecked") + Map, ?> filtered = (Map, ?>) filterMap.getVolatile(); + filterMap.setVolatile(new HashMap, Set>()); + + // Drop the reflection caches that may already hold the filtered view. + if (filtered != null) { + VarHandle reflectionData = MethodHandles.privateLookupIn(Class.class, MethodHandles.lookup()) + .findVarHandle(Class.class, "reflectionData", SoftReference.class); + for (Class type : filtered.keySet()) { + reflectionData.setVolatile(type, (SoftReference) null); + } + } + System.out.println("[mcpc-j17] Reflection filters removed."); + } catch (Throwable t) { + System.out.println("[mcpc-j17] Could not remove reflection filters, continuing: " + t); + } + } + + private ModuleAccess() { + } +} diff --git a/src/main/java/gg/nationsglory/mcpcj17/shim/ConstructorAccessor.java b/src/main/java/gg/nationsglory/mcpcj17/shim/ConstructorAccessor.java new file mode 100644 index 0000000..d491345 --- /dev/null +++ b/src/main/java/gg/nationsglory/mcpcj17/shim/ConstructorAccessor.java @@ -0,0 +1,12 @@ +package gg.nationsglory.mcpcj17.shim; + +import java.lang.reflect.InvocationTargetException; + +/** + * Stands in for {@code sun.reflect.ConstructorAccessor}. + */ +public interface ConstructorAccessor { + + Object newInstance(Object[] args) + throws InstantiationException, IllegalArgumentException, InvocationTargetException; +} diff --git a/src/main/java/gg/nationsglory/mcpcj17/shim/FieldAccessor.java b/src/main/java/gg/nationsglory/mcpcj17/shim/FieldAccessor.java new file mode 100644 index 0000000..bb565f0 --- /dev/null +++ b/src/main/java/gg/nationsglory/mcpcj17/shim/FieldAccessor.java @@ -0,0 +1,43 @@ +package gg.nationsglory.mcpcj17.shim; + +/** + * Stands in for {@code sun.reflect.FieldAccessor}. + */ +public interface FieldAccessor { + + Object get(Object target) throws IllegalArgumentException; + + boolean getBoolean(Object target) throws IllegalArgumentException; + + byte getByte(Object target) throws IllegalArgumentException; + + char getChar(Object target) throws IllegalArgumentException; + + short getShort(Object target) throws IllegalArgumentException; + + int getInt(Object target) throws IllegalArgumentException; + + long getLong(Object target) throws IllegalArgumentException; + + float getFloat(Object target) throws IllegalArgumentException; + + double getDouble(Object target) throws IllegalArgumentException; + + void set(Object target, Object value) throws IllegalArgumentException, IllegalAccessException; + + void setBoolean(Object target, boolean value) throws IllegalArgumentException, IllegalAccessException; + + void setByte(Object target, byte value) throws IllegalArgumentException, IllegalAccessException; + + void setChar(Object target, char value) throws IllegalArgumentException, IllegalAccessException; + + void setShort(Object target, short value) throws IllegalArgumentException, IllegalAccessException; + + void setInt(Object target, int value) throws IllegalArgumentException, IllegalAccessException; + + void setLong(Object target, long value) throws IllegalArgumentException, IllegalAccessException; + + void setFloat(Object target, float value) throws IllegalArgumentException, IllegalAccessException; + + void setDouble(Object target, double value) throws IllegalArgumentException, IllegalAccessException; +} diff --git a/src/main/java/gg/nationsglory/mcpcj17/shim/MethodHandleConstructorAccessor.java b/src/main/java/gg/nationsglory/mcpcj17/shim/MethodHandleConstructorAccessor.java new file mode 100644 index 0000000..2698d42 --- /dev/null +++ b/src/main/java/gg/nationsglory/mcpcj17/shim/MethodHandleConstructorAccessor.java @@ -0,0 +1,35 @@ +package gg.nationsglory.mcpcj17.shim; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +/** + * Calls a constructor through a method handle, which, unlike {@code Constructor.newInstance}, + * also works for enum constructors. + */ +final class MethodHandleConstructorAccessor implements ConstructorAccessor { + + private final MethodHandle handle; + + MethodHandleConstructorAccessor(Constructor constructor) { + try { + handle = MethodHandles.privateLookupIn(constructor.getDeclaringClass(), MethodHandles.lookup()) + .unreflectConstructor(constructor); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Cannot access " + constructor, e); + } + } + + @Override + public Object newInstance(Object[] args) throws InvocationTargetException { + try { + return handle.invokeWithArguments(args == null ? new Object[0] : args); + } catch (ClassCastException | IllegalArgumentException | NullPointerException e) { + throw new IllegalArgumentException(e); + } catch (Throwable t) { + throw new InvocationTargetException(t); + } + } +} diff --git a/src/main/java/gg/nationsglory/mcpcj17/shim/ReflectionFactory.java b/src/main/java/gg/nationsglory/mcpcj17/shim/ReflectionFactory.java new file mode 100644 index 0000000..eceb85f --- /dev/null +++ b/src/main/java/gg/nationsglory/mcpcj17/shim/ReflectionFactory.java @@ -0,0 +1,29 @@ +package gg.nationsglory.mcpcj17.shim; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; + +/** + * Stands in for {@code sun.reflect.ReflectionFactory}, removed in Java 9. Forge's EnumHelper (and + * similar code in MCPC+ and mods) uses it to create enum constants and write static final fields; + * references to it are redirected here by {@link gg.nationsglory.mcpcj17.ClassPatcher}. + */ +public final class ReflectionFactory { + + private static final ReflectionFactory INSTANCE = new ReflectionFactory(); + + public static ReflectionFactory getReflectionFactory() { + return INSTANCE; + } + + public ConstructorAccessor newConstructorAccessor(Constructor constructor) { + return new MethodHandleConstructorAccessor(constructor); + } + + public FieldAccessor newFieldAccessor(Field field, boolean override) { + return new UnsafeFieldAccessor(field); + } + + private ReflectionFactory() { + } +} diff --git a/src/main/java/gg/nationsglory/mcpcj17/shim/UnsafeFieldAccessor.java b/src/main/java/gg/nationsglory/mcpcj17/shim/UnsafeFieldAccessor.java new file mode 100644 index 0000000..ba9e887 --- /dev/null +++ b/src/main/java/gg/nationsglory/mcpcj17/shim/UnsafeFieldAccessor.java @@ -0,0 +1,194 @@ +package gg.nationsglory.mcpcj17.shim; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import sun.misc.Unsafe; + +/** + * Reads and writes a field through {@link Unsafe}, including static final fields, as + * {@code sun.reflect.FieldAccessor} allowed on Java 8. + */ +@SuppressWarnings({"deprecation", "removal"}) +final class UnsafeFieldAccessor implements FieldAccessor { + + private static final Unsafe UNSAFE; + static { + try { + Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); + theUnsafe.setAccessible(true); + UNSAFE = (Unsafe) theUnsafe.get(null); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + + private final Field field; + private final boolean isStatic; + private final Object staticBase; + private final long offset; + + UnsafeFieldAccessor(Field field) { + this.field = field; + this.isStatic = Modifier.isStatic(field.getModifiers()); + if (isStatic) { + this.staticBase = UNSAFE.staticFieldBase(field); + this.offset = UNSAFE.staticFieldOffset(field); + } else { + this.staticBase = null; + this.offset = UNSAFE.objectFieldOffset(field); + } + } + + private Object base(Object target) { + if (isStatic) { + return staticBase; + } + if (!field.getDeclaringClass().isInstance(target)) { + throw new IllegalArgumentException("Cannot access field " + field + " on " + target); + } + return target; + } + + private Class type() { + return field.getType(); + } + + @Override + public Object get(Object target) { + Object base = base(target); + Class type = type(); + if (!type.isPrimitive()) { + return UNSAFE.getObjectVolatile(base, offset); + } else if (type == boolean.class) { + return UNSAFE.getBooleanVolatile(base, offset); + } else if (type == byte.class) { + return UNSAFE.getByteVolatile(base, offset); + } else if (type == char.class) { + return UNSAFE.getCharVolatile(base, offset); + } else if (type == short.class) { + return UNSAFE.getShortVolatile(base, offset); + } else if (type == int.class) { + return UNSAFE.getIntVolatile(base, offset); + } else if (type == long.class) { + return UNSAFE.getLongVolatile(base, offset); + } else if (type == float.class) { + return UNSAFE.getFloatVolatile(base, offset); + } else { + return UNSAFE.getDoubleVolatile(base, offset); + } + } + + @Override + public void set(Object target, Object value) { + Object base = base(target); + Class type = type(); + if (!type.isPrimitive()) { + if (value != null && !type.isInstance(value)) { + throw new IllegalArgumentException("Cannot set " + field + " to " + value.getClass().getName()); + } + UNSAFE.putObjectVolatile(base, offset, value); + } else if (value == null) { + throw new IllegalArgumentException("Cannot set primitive field " + field + " to null"); + } else if (type == boolean.class) { + UNSAFE.putBooleanVolatile(base, offset, (Boolean) value); + } else if (type == byte.class) { + UNSAFE.putByteVolatile(base, offset, (Byte) value); + } else if (type == char.class) { + UNSAFE.putCharVolatile(base, offset, (Character) value); + } else if (type == short.class) { + UNSAFE.putShortVolatile(base, offset, ((Number) value).shortValue()); + } else if (type == int.class) { + UNSAFE.putIntVolatile(base, offset, value instanceof Character ? (Character) value : ((Number) value).intValue()); + } else if (type == long.class) { + UNSAFE.putLongVolatile(base, offset, value instanceof Character ? (Character) value : ((Number) value).longValue()); + } else if (type == float.class) { + UNSAFE.putFloatVolatile(base, offset, value instanceof Character ? (Character) value : ((Number) value).floatValue()); + } else { + UNSAFE.putDoubleVolatile(base, offset, value instanceof Character ? (Character) value : ((Number) value).doubleValue()); + } + } + + @Override + public boolean getBoolean(Object target) { + return (Boolean) get(target); + } + + @Override + public byte getByte(Object target) { + return ((Number) get(target)).byteValue(); + } + + @Override + public char getChar(Object target) { + return (Character) get(target); + } + + @Override + public short getShort(Object target) { + return ((Number) get(target)).shortValue(); + } + + @Override + public int getInt(Object target) { + Object value = get(target); + return value instanceof Character ? (Character) value : ((Number) value).intValue(); + } + + @Override + public long getLong(Object target) { + Object value = get(target); + return value instanceof Character ? (Character) value : ((Number) value).longValue(); + } + + @Override + public float getFloat(Object target) { + Object value = get(target); + return value instanceof Character ? (Character) value : ((Number) value).floatValue(); + } + + @Override + public double getDouble(Object target) { + Object value = get(target); + return value instanceof Character ? (Character) value : ((Number) value).doubleValue(); + } + + @Override + public void setBoolean(Object target, boolean value) { + set(target, value); + } + + @Override + public void setByte(Object target, byte value) { + set(target, value); + } + + @Override + public void setChar(Object target, char value) { + set(target, value); + } + + @Override + public void setShort(Object target, short value) { + set(target, value); + } + + @Override + public void setInt(Object target, int value) { + set(target, value); + } + + @Override + public void setLong(Object target, long value) { + set(target, value); + } + + @Override + public void setFloat(Object target, float value) { + set(target, value); + } + + @Override + public void setDouble(Object target, double value) { + set(target, value); + } +} diff --git a/src/main/java/gg/nationsglory/mcpcj8/Java8ClassPatcher.java b/src/main/java/gg/nationsglory/mcpcj8/Java8ClassPatcher.java deleted file mode 100644 index d18c46c..0000000 --- a/src/main/java/gg/nationsglory/mcpcj8/Java8ClassPatcher.java +++ /dev/null @@ -1,77 +0,0 @@ -package gg.nationsglory.mcpcj8; - -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.lang.instrument.ClassFileTransformer; -import java.security.ProtectionDomain; -import java.util.HashMap; -import java.util.Map; - -public final class Java8ClassPatcher implements ClassFileTransformer { - - private static final Map TARGETS = new HashMap(); - static { - TARGETS.put("net/minecraft/launchwrapper/Launch", "Launch.bin"); - TARGETS.put("org/objectweb/asm/commons/Remapper", "Remapper.bin"); - TARGETS.put("org/objectweb/asm/ClassReader", "ClassReader.bin"); - TARGETS.put("org/objectweb/asm/tree/MethodInsnNode", "MethodInsnNode.bin"); - } - - private final Map patches = new HashMap(); - - public Java8ClassPatcher() { - for (Map.Entry entry : TARGETS.entrySet()) { - byte[] bytes = readResource(entry.getValue()); - if (bytes != null) { - patches.put(entry.getKey(), bytes); - } else { - System.out.println("[mcpc-j8] Missing patch resource: " + entry.getValue()); - } - } - System.out.println("[mcpc-j8] " + patches.size() + " class patch(es) loaded."); - } - - public boolean hasPatches() { - return !patches.isEmpty(); - } - - @Override - public byte[] transform(ClassLoader loader, String className, Class redefined, - ProtectionDomain domain, byte[] original) { - try { - if (className != null) { - byte[] patch = patches.get(className); - if (patch != null) { - System.out.println("[mcpc-j8] Patched: " + className); - return patch; - } - } - } catch (Throwable t) { - return null; - } - return null; - } - - private static byte[] readResource(String name) { - InputStream in = Java8ClassPatcher.class.getResourceAsStream(name); - if (in == null) { - return null; - } - try { - ByteArrayOutputStream out = new ByteArrayOutputStream(8192); - byte[] buffer = new byte[8192]; - int read; - while ((read = in.read(buffer)) > 0) { - out.write(buffer, 0, read); - } - return out.toByteArray(); - } catch (Throwable t) { - return null; - } finally { - try { - in.close(); - } catch (Throwable ignored) { - } - } - } -} diff --git a/src/main/java/gg/nationsglory/mcpcj8/McpcJava8Agent.java b/src/main/java/gg/nationsglory/mcpcj8/McpcJava8Agent.java deleted file mode 100644 index 55f7c5c..0000000 --- a/src/main/java/gg/nationsglory/mcpcj8/McpcJava8Agent.java +++ /dev/null @@ -1,48 +0,0 @@ -package gg.nationsglory.mcpcj8; - -import java.lang.instrument.Instrumentation; - -public final class McpcJava8Agent { - - private static final String SPOOFED_JAVA_VERSION = "1.7.0_80"; - - public static void premain(String agentArgs, Instrumentation inst) { - spoofJavaVersion(); - installClassPatches(inst); - } - - public static void premain(String agentArgs) { - spoofJavaVersion(); - System.out.println("[mcpc-j8] No Instrumentation available: only the version check is bypassed."); - } - - private static void spoofJavaVersion() { - String actual = System.getProperty("java.version"); - if (actual != null && actual.startsWith("1.8")) { - System.setProperty("java.version", SPOOFED_JAVA_VERSION); - System.out.println("[mcpc-j8] Java 8 detected (" + actual + "), reporting " - + SPOOFED_JAVA_VERSION + " to bypass the MCPC+ version check."); - } else { - System.out.println("[mcpc-j8] java.version = " + actual + ", nothing to bypass."); - } - } - - private static void installClassPatches(Instrumentation inst) { - if (inst == null) { - return; - } - try { - Java8ClassPatcher patcher = new Java8ClassPatcher(); - if (patcher.hasPatches()) { - inst.addTransformer(patcher); - } else { - System.out.println("[mcpc-j8] No class patches bundled: only the version check is bypassed."); - } - } catch (Throwable t) { - System.out.println("[mcpc-j8] Could not install class patches, continuing: " + t); - } - } - - private McpcJava8Agent() { - } -} diff --git a/src/main/resources/gg/nationsglory/mcpcj8/ClassReader.bin b/src/main/resources/gg/nationsglory/mcpcj17/ClassReader.bin similarity index 100% rename from src/main/resources/gg/nationsglory/mcpcj8/ClassReader.bin rename to src/main/resources/gg/nationsglory/mcpcj17/ClassReader.bin diff --git a/src/main/resources/gg/nationsglory/mcpcj17/Launch.bin b/src/main/resources/gg/nationsglory/mcpcj17/Launch.bin new file mode 100644 index 0000000000000000000000000000000000000000..c1bd37bbf5a296be54b97e4d712141ce384ace28 GIT binary patch literal 5487 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 NWeU=bWTFo@{TD$p6afGL From 2302ed022b2550f429bbfe1c4a609c884b88d427 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 10:19:22 +0000 Subject: [PATCH 2/5] Keep the server's own Launch and ASM when they are already Java 9+ ready The NGServer build of MCPC+ 1.6.4 ships a Launch that no longer casts the app class loader, and is compiled against ASM 9: replacing those classes with the stock launchwrapper 1.8 / ASM 4 ones broke it. - Replace Launch only if it still references URLClassLoader. - Replace the ASM classes only if the loader's ClassReader predates ASM 5. - Configure LaunchClassLoader (platform packages, agent shims) from the transformer when it defines its first class, so it no longer depends on the patched Launch. Tested: NGServer MCPC+ 1.6.4 boots to Done and stops cleanly on JDK 17. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DszbR9jSTnJF2dtbGNiQRv --- README.md | 15 ++++- .../gg/nationsglory/mcpcj17/ClassPatcher.java | 53 +++++++++++++++++- .../gg/nationsglory/mcpcj17/Launch.bin | Bin 5487 -> 5432 bytes 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ee6414f..62512af 100644 --- a/README.md +++ b/README.md @@ -46,25 +46,36 @@ Au démarrage, la console affiche : [mcpc-j17] Redirected sun.reflect in: net/minecraftforge/common/EnumHelper ``` +Sur un serveur dont `Launch` ou ASM sont déjà modernisés (voir plus bas), la ligne `Patched: .../Launch` est absente et `Recent ASM detected, keeping the server's ASM classes.` apparaît : c'est normal. + Si vous ne voyez aucune ligne `[mcpc-j17]`, l'agent n'est pas chargé : vérifiez que `-javaagent:` est bien **avant** `-jar` et que le chemin du jar est correct. ## Ce que l'agent corrige **1. Le refus de toute version autre que Java 7.** `FMLLaunchHandler` teste `java.version` et arrête le serveur (`... is not supported on MCPC+8.11! Please run MCPC+ using Java 7.`). -L'agent présente une version `1.7.x` à cette vérification. Passer `-Djava.version=1.7.0_80` sur la ligne de commande **ne fonctionne pas** : la JVM réécrit cette propriété pendant son initialisation. Il faut le faire depuis un agent, qui s'exécute après l'initialisation de la JVM mais avant le `main` du serveur. Un coremod ne peut pas corriger ce point : `FMLLaunchHandler` est exclu du `LaunchClassLoader`. +L'agent présente une version `1.7.x` à cette vérification (inoffensif si le serveur ne fait plus ce test). Passer `-Djava.version=1.7.0_80` sur la ligne de commande **ne fonctionne pas** : la JVM réécrit cette propriété pendant son initialisation. Il faut le faire depuis un agent, qui s'exécute après l'initialisation de la JVM mais avant le `main` du serveur. Un coremod ne peut pas corriger ce point : `FMLLaunchHandler` est exclu du `LaunchClassLoader`. **2. Le démarrage de launchwrapper.** `Launch` construit son class loader à partir de `(URLClassLoader) getClass().getClassLoader()`. Depuis Java 9, le class loader de l'application n'est plus un `URLClassLoader` : `ClassCastException` immédiate. La version corrigée de `Launch` lit le class path depuis `java.class.path`, comme les versions récentes de launchwrapper. La version corrigée garde aussi le correctif de la `ConcurrentModificationException` dans `Launch.launch` (liste des tweakers modifiée pendant son parcours). +`Launch` n'est remplacé que s'il fait encore ce cast : un `Launch` déjà adapté par le serveur est conservé. + **3. Les classes du JDK hors de `java.base`.** Le `LaunchClassLoader` a pour parent le class loader de bootstrap. En Java 8, celui-ci voyait tout le JDK ; depuis Java 9, des modules comme `java.sql` ou `java.scripting` sont chargés par le *platform class loader*, et des classes comme `javax.sql.DataSource` (Ebean, plugins Bukkit) deviennent introuvables. L'agent fait déléguer ces packages au JDK. **4. L'encapsulation des modules du JDK.** Forge, Bukkit et les mods appellent `setAccessible` sur des classes internes du JDK, ce que Java 17 refuse par défaut (`InaccessibleObjectException`). L'agent ouvre tous les packages du JDK au serveur, l'équivalent d'une longue liste de `--add-opens ...=ALL-UNNAMED`. Il retire aussi les filtres de réflexion ajoutés en Java 12 (qui cachent notamment `Field.modifiers`). **5. `sun.reflect.ReflectionFactory`, supprimé en Java 9.** L'`EnumHelper` de Forge s'en sert pour ajouter des valeurs aux enums (biomes, types de créatures, matériaux Bukkit de MCPC+...). Les références à `sun.reflect.ReflectionFactory`, `ConstructorAccessor` et `FieldAccessor` sont redirigées, dans toutes les classes chargées, vers des équivalents fournis par l'agent qui fonctionnent sous Java 17. -**6. Le déobfuscateur de Forge.** La version d'ASM embarquée dans ces serveurs échoue sur du bytecode récent (`Remapper`, `ClassReader`, `MethodInsnNode`). Les versions corrigées acceptent les classes jusqu'à Java 17. +**6. Le déobfuscateur de Forge.** ASM 4, fourni avec ces serveurs, échoue sur du bytecode récent (`Remapper`, `ClassReader`, `MethodInsnNode`). Les versions corrigées acceptent les classes jusqu'à Java 17. + +Ces classes ne sont remplacées que si le serveur utilise bien ASM 4. Un serveur recompilé contre un ASM récent (5 à 9) garde son propre ASM. + +## Testé sur + +- Le build NGServer de MCPC+ 1.6.4 (Forge 9.11.1.965), compilé contre ASM 9, sous OpenJDK 17.0.20 : le serveur démarre (`Done`), génère le monde et s'arrête proprement. Sans l'agent, il plante au chargement du monde (`EnumHelper` : `sun.reflect.ReflectionFactory` absent, `java.lang` fermé). +- Un banc reproduisant launchwrapper 1.8 d'origine et l'`EnumHelper` de Forge 1.6.4, sous OpenJDK 17 et 21. ## Portée et limites diff --git a/src/main/java/gg/nationsglory/mcpcj17/ClassPatcher.java b/src/main/java/gg/nationsglory/mcpcj17/ClassPatcher.java index 820d52c..0eb5bd6 100644 --- a/src/main/java/gg/nationsglory/mcpcj17/ClassPatcher.java +++ b/src/main/java/gg/nationsglory/mcpcj17/ClassPatcher.java @@ -5,8 +5,10 @@ import java.lang.instrument.ClassFileTransformer; import java.nio.charset.StandardCharsets; import java.security.ProtectionDomain; +import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.WeakHashMap; public final class ClassPatcher implements ClassFileTransformer { @@ -27,7 +29,15 @@ public final class ClassPatcher implements ClassFileTransformer { private static final byte[] SUN_REFLECT_SLASH = "sun/reflect/".getBytes(StandardCharsets.US_ASCII); private static final byte[] SUN_REFLECT_DOT = "sun.reflect.".getBytes(StandardCharsets.US_ASCII); + private static final String LAUNCH = "net/minecraft/launchwrapper/Launch"; + private static final String ASM_PREFIX = "org/objectweb/asm/"; + private static final String LAUNCH_CLASS_LOADER = "net.minecraft.launchwrapper.LaunchClassLoader"; + private final Map patches = new HashMap(); + private final Map legacyAsm = + Collections.synchronizedMap(new WeakHashMap()); + private final Map configuredLoaders = + Collections.synchronizedMap(new WeakHashMap()); public ClassPatcher() { for (Map.Entry entry : TARGETS.entrySet()) { @@ -45,9 +55,13 @@ public ClassPatcher() { public byte[] transform(ClassLoader loader, String className, Class redefined, ProtectionDomain domain, byte[] original) { try { + if (loader != null && LAUNCH_CLASS_LOADER.equals(loader.getClass().getName()) + && configuredLoaders.put(loader, Boolean.TRUE) == null) { + LaunchHooks.configure(loader); + } if (className != null) { byte[] patch = patches.get(className); - if (patch != null) { + if (patch != null && needsPatch(loader, className, original)) { System.out.println("[mcpc-j17] Patched: " + className); return patch; } @@ -66,6 +80,43 @@ public byte[] transform(ClassLoader loader, String className, Class redefined return null; } + /** + * The bundled classes replace the stock launchwrapper 1.8 and ASM 4 ones. Servers that already + * ship a Java 9+ ready Launch, or a recent ASM, keep their own classes. + */ + private boolean needsPatch(ClassLoader loader, String className, byte[] original) { + if (className.equals(LAUNCH)) { + // The stock constructor casts the application class loader to URLClassLoader. + return indexOf(original, "java/net/URLClassLoader".getBytes(StandardCharsets.US_ASCII)) >= 0; + } + if (className.startsWith(ASM_PREFIX)) { + return isLegacyAsm(loader); + } + return true; + } + + /** ASM 5 added type annotations; anything older is the ASM 4 these patches were made from. */ + private boolean isLegacyAsm(ClassLoader loader) { + ClassLoader key = loader != null ? loader : ClassLoader.getPlatformClassLoader(); + Boolean legacy = legacyAsm.get(key); + if (legacy == null) { + byte[] classReader = null; + try (InputStream in = key.getResourceAsStream("org/objectweb/asm/ClassReader.class")) { + if (in != null) { + classReader = in.readAllBytes(); + } + } catch (Throwable ignored) { + } + legacy = classReader != null + && indexOf(classReader, "readTypeAnnotations".getBytes(StandardCharsets.US_ASCII)) < 0; + if (!legacy) { + System.out.println("[mcpc-j17] Recent ASM detected, keeping the server's ASM classes."); + } + legacyAsm.put(key, legacy); + } + return legacy; + } + /** * Rewrites the UTF-8 constants of the class that name one of the {@link #REDIRECTS}, in internal * form (type references, descriptors) or dotted form (Class.forName strings). Returns null when diff --git a/src/main/resources/gg/nationsglory/mcpcj17/Launch.bin b/src/main/resources/gg/nationsglory/mcpcj17/Launch.bin index c1bd37bbf5a296be54b97e4d712141ce384ace28..27ed3268b46d8291eeafb7b909a6ce0a088def7c 100644 GIT binary patch delta 2299 zcma)8d3YRU8UKBI&Ft*#HQO|qCZXGwgiV?fYfD>(RtY67D`}!6rS4evLb8th*_&pyd}zrXkQ ze(yf}QvF1I@QHH|KLTI{t7BNqo7mzbjaRwWMQ=8Dn2p4exfh2Mk;x{A(Gi_#N&lw}jX66Yi$6E77Fj^|*@i`pp6B>Qr9F)qJUEVQd3~ z>U2(J8#koVmV|5g4W6a!4g5Awvb6=96kLl|87f1hsZg+rg?L{oZ}k?l`Q0+!#Gn*+ zTIMb5(`KO{qn)9=L+J5dVMthox_B`gPiM_O8Q1f;w~Tf1RbEr>!Ddl-3qR;xCEtLz z2-I)o&wAI$CT%J**{ zurkG#RG}ziFT(|^Y`2OM-lJfjsP8GH_E-$=L|szF`|y6z_5);2BUD%0<|Lc)jnS*gbJ5r&KD5mg0}PKK42!1*S**7ztsCTiWypv_H? zELwS!wj$vGZF!qnXtj0~9XOj++=F`q7{({~gK}Ixh(kj86h9%?1U`e$3jCj=#qnkS za(Xc%e4#%fAI9hXxE~MjHvc;IAm8t=@O+V>mhe=UW&6AGW-ccH9ia~CY(GO2-y5#E zOSx$67{07v6knl1^VUFihoxd1WuoB(#hQJ6DjqKNoFa0PtsB}@94$3_gl|^j@o9zt zJ)UAR(`W5!+gf6fDE&GfQ_ziX@L}Z=`Eh(xDBt48m6g>`;M)qS@g0Wn9GlLfC47%B z2(;9EA3sp=Bz{PxGuF;zrcg99y@c$?I3(;R1f5g-_CW2GKPB=~h30`=akq-6aj)?F zoWa|hw#>YWXT<^L@C$~}9QERPNcdF%Cvi7_IZ(-d!-MJ)`FHre0uTPc*O2l&UJ#vL zOQ``!!kpF`-3QprJKNPI3 z!3eqps%B-}7v8wMoK+}nKGSJ+?aiId*EOrG(%B5FqU*mtW8RduoO5=% z%IqS3Yj}%1#^R#tB7QP#u_b(CWMSpS^d;y*7=A<$CD~1qh8P&D;d>+1td<{)ln>UB zCqX_&|Euc9Q8|U`o(U`&N9`CAQ>gD5$I>w@J3@X@ucA}|@Z=!Tb;I@86qZxaYd1C6 zxU4~2tzB+oosIRa4O+9du_S9r8*PTCL3^Wi-K#C$!3M2E>zp@*aik7Jh+7{xMekFHGO zU2b#<<=uLqq)cI7&)_)TYpCaggH9F2#%-P($8m=d67e6>L#23TgmvQN6_C-5U1Kbgjmq3I!Av+-2dY)L)*ID8{B8By5}xWBnYn*tW}o2gnFHsJ>B0ZyZ}H%N2^=Cw z0iEFdn_&XyZ04fer}gk8^AbLy++8Ap!lE(eBT!HFxV1amZKfDnjH$CIBMv?m(KR+} z3FrY@sCZdy9z`$c8bLX2mR0Ma7#mzjZ_Xn&M86a02#&iRa6N=ZxY#B1-0y)pgXa;E z;F55sgyj;hebwhK30oy}OQ7Ep^x1b_7y{0~Ct>&u){*}VIwcI9!h*AiOE^jPQ`meO it}_TpYcYHpi5YZ~{C_tAlh7EW1J2GnB_*7*^u7Y57*9$7 delta 2399 zcma)7dt6&*8GgQ8I4373ftP_QW#8faRIZMW`p)v=CF90gq%looZTa7x2M5|cnL zUYI(y-lp3(CtY=$b;>SUh0X&E+o7}5P2HyT+Ig9mI;V3NH+8M7_0kbmU(S1f zpZEFR?_{#>`8waD=N~u$U>#e{P{kWr)mZ)Ijs{OpGO;_>H<-2<%EK-F=8#z*HxqsJ zTjFLW(~>lMt@Nha4i&YiQ?LxHc+yc?yIR2-l&bK-?|}nr8G?(MP5XMRR5q4ONO%K3 z>Zow7SFnLU<_MKsj1m&DkwF@=(isYoVQ{B&IlGisJEQU&@h0KFj9*Wt%lQ#!W62e0 zQm_SEDW%UGu&#)uWxScc;#?Md3xk?#8cWu1jm0eqExgXPina0%S98N#@iql*xLStF zP}G>oSlLXdH_5_WQ@+mzpq zcZiDbELNZxHwwe27z%Q2so_n0Oe%A6h7ccihnx~V!(WhU z*=KpVTw@%Nxmq5@-2&r1{BgO0-N&Dm!}3>g)Pt|!YrMjJU zO~tqJJ-^LYDxuJK7`${#vdxKJYhT;$JWoX2!}zX(ZhVjTDJ$hi@qMBEfZwXDuY3%T zE2zX1426qpmfJuHKju#>E!984PZgZP&!}|5+S{DSWX(hmA^QdH682vTLLcPYy)~Ep zis*}Fng&wY{VINgJB8=B4DOz|Wu{g9PTaLDe$U`vBtO4YfAr!E4)YVz#&Kk!eX{EMGa>mz4zPALCgxG{^H zo00G;acX8nH-_r)PCh0FdDN%Lb9C!)o-sb)tEpyA<|2s9O>Fd8*%os!(X*%BOp~lK z3C~e^Ci6*OyX<8uL1I2$?5}2p-0-iL15BgCV?jRTzt~&CN=2b13}yU9Uzt;4IzQ#V zvSBH!P*^#uBs9^cj;5=dRJJU4fGk9?uSuA@;#Me|48?P;Wmd(nD%>egu-A*y)%<9o zwP_{2G9B>Hhb}G29&u%JPkQ%(v2e~`NcJKGNOqE>AxNHD-da>STt_yADgYIfS4mPa zN`#jz5sGH?4Ao6w=`s@ISrt8DBsWL?%>HWD+~b_^*}+zU!tmsjWHZo@N;Y!~?oZ0wuByA4I?6L^o| z9mjBk_C8&i!VOMzj-x^QuO>mA2rm4U|+6^V&jvpYbWq&!!N>b*8TZ#W)$k= z`?#=UI@!ajU%iFD643VjYn+!(8iDSfQzh?__>YKGZ-;6-LGplp6r~js24tr zg3-BzDEI6CDW_{j;qd5ORQKC>>NXt64d@pP1%&AjdSD8FVxXGZ{7CIW%8fK)1Vza| z&zB6^c-F=X(fMhD#I{6ik0UjXwCd^U< zy6?XUthzwp2qE$cII|3s%we+v$~~g)#cP~iQ3F>XhBvq zkFqF-j}_}0o40s}buXQ$SS?mY;jil)#S%JNwp{lI*$Ub@ml%oFjL}7S#BrbFDAvKj zR?)g2hB}95P$a=2;T8!SB(%Ned#i+g3EdLtPm;a|7laXD76lS+pT%bKZ=Xengpso- se-$AKXUK6DJI=u|3qQ%{5S~Ma5dYsMpf}UHAgkjWi_c1t+_iN72MnlWkpKVy From a6140f3662d6a5aab37511b440d7ec14dff42da5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 11:00:44 +0000 Subject: [PATCH 3/5] Bundle ASM 9.7 and substitute it for MCPC+'s ASM 4 ASM 4.1 cannot read Java 8+ bytecode nor run coremods written against a recent ASM API, so mods built for Java 8 could not load. The agent now ships ASM 9.7 and serves its classes in place of ASM 4 ones when the server uses ASM 4 (servers already on ASM 5+ keep theirs). The ASM 4 class patches are superseded and removed. The bundled ASM is adapted at build time so Forge 1.6.4 and ASM 4 era code keep working: - Remapping*Adapter classes, removed after ASM 7.0, come from ASM 7.0. - Type.getType(String) accepts internal names, as ASM 4 did (Forge's EventTransformer). - Visitors declared with the ASM 4 API no longer throw 'requires ASMx' on newer bytecode (FML's mod parser), and the interface flag of method calls survives them (LegacyAsmApi). LaunchClassLoader delegates org.objectweb.asm to the application class loader so a single ASM is used. Tested on JDK 17 with a Java 8 mod + ASM9 coremod: stock MCPC+ 1.6.4 and the NGServer build (with ASM 4 or ASM 9 in libraries) boot to Done with the mod active and no FML errors. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DszbR9jSTnJF2dtbGNiQRv --- README.md | 26 ++- build.gradle | 169 +++++++++++++++++- .../gg/nationsglory/mcpcj17/ClassPatcher.java | 59 ++++-- .../gg/nationsglory/mcpcj17/LaunchHooks.java | 9 +- .../java/org/objectweb/asm/LegacyAsmApi.java | 36 ++++ .../gg/nationsglory/mcpcj17/ClassReader.bin | Bin 18727 -> 0 bytes .../nationsglory/mcpcj17/MethodInsnNode.bin | Bin 1076 -> 0 bytes .../gg/nationsglory/mcpcj17/Remapper.bin | Bin 4352 -> 0 bytes 8 files changed, 280 insertions(+), 19 deletions(-) create mode 100644 src/main/java/org/objectweb/asm/LegacyAsmApi.java delete mode 100644 src/main/resources/gg/nationsglory/mcpcj17/ClassReader.bin delete mode 100644 src/main/resources/gg/nationsglory/mcpcj17/MethodInsnNode.bin delete mode 100644 src/main/resources/gg/nationsglory/mcpcj17/Remapper.bin diff --git a/README.md b/README.md index 62512af..701f6dc 100644 --- a/README.md +++ b/README.md @@ -40,13 +40,14 @@ Au démarrage, la console affiche : [mcpc-j17] Java 17.0.x detected, reporting 1.7.0_80 to bypass the MCPC+ version check. [mcpc-j17] Opened 741 JDK packages to the server. [mcpc-j17] Reflection filters removed. -[mcpc-j17] 4 class patch(es) loaded. +[mcpc-j17] 1 class patch(es) loaded. [mcpc-j17] Patched: net/minecraft/launchwrapper/Launch +[mcpc-j17] ASM 4 detected, upgrading it to the bundled ASM. [mcpc-j17] LaunchClassLoader delegates 241 platform packages to the JDK. [mcpc-j17] Redirected sun.reflect in: net/minecraftforge/common/EnumHelper ``` -Sur un serveur dont `Launch` ou ASM sont déjà modernisés (voir plus bas), la ligne `Patched: .../Launch` est absente et `Recent ASM detected, keeping the server's ASM classes.` apparaît : c'est normal. +Sur un serveur dont `Launch` ou ASM sont déjà modernisés (voir plus bas), la ligne `Patched: .../Launch` est absente et `Recent ASM detected, keeping the server's ASM classes.` remplace la ligne sur ASM : c'est normal. Si vous ne voyez aucune ligne `[mcpc-j17]`, l'agent n'est pas chargé : vérifiez que `-javaagent:` est bien **avant** `-jar` et que le chemin du jar est correct. @@ -68,13 +69,22 @@ La version corrigée garde aussi le correctif de la `ConcurrentModificationExcep **5. `sun.reflect.ReflectionFactory`, supprimé en Java 9.** L'`EnumHelper` de Forge s'en sert pour ajouter des valeurs aux enums (biomes, types de créatures, matériaux Bukkit de MCPC+...). Les références à `sun.reflect.ReflectionFactory`, `ConstructorAccessor` et `FieldAccessor` sont redirigées, dans toutes les classes chargées, vers des équivalents fournis par l'agent qui fonctionnent sous Java 17. -**6. Le déobfuscateur de Forge.** ASM 4, fourni avec ces serveurs, échoue sur du bytecode récent (`Remapper`, `ClassReader`, `MethodInsnNode`). Les versions corrigées acceptent les classes jusqu'à Java 17. +**6. ASM 4.** MCPC+ est fourni avec ASM 4.1 (`libraries/org/ow2/asm/asm-all/4.1`), qui ne sait pas lire le bytecode Java 8+ (lambdas, méthodes static/default d'interface...) et ne connaît pas les API récentes (`Opcodes.ASM5` à `ASM9`) qu'utilisent les coremods modernes. -Ces classes ne sont remplacées que si le serveur utilise bien ASM 4. Un serveur recompilé contre un ASM récent (5 à 9) garde son propre ASM. +L'agent embarque **ASM 9.7** et le substitue à ASM 4 au chargement. Rien à changer dans `libraries/`. Pour que Forge 1.6.4, MCPC+ et les mods écrits pour ASM 4 continuent de fonctionner, cet ASM 9.7 est adapté au build : + +- les classes `Remapping*Adapter`, supprimées après ASM 7.0 et utilisées par Forge, sont reprises d'ASM 7.0 ; +- `Type.getType(String)` accepte un nom interne comme le faisait ASM 4 (Forge s'en sert dans `EventTransformer`) ; +- les visiteurs déclarés avec l'API ASM 4 (le parseur de mods de FML, par exemple) ne lèvent plus `This feature requires ASM5` sur du bytecode récent : l'information est transmise au visiteur suivant, y compris le caractère « interface » des appels de méthodes, indispensable aux méthodes static d'interface. + +Un serveur qui fournit déjà un ASM 5 ou plus récent garde le sien. ## Testé sur -- Le build NGServer de MCPC+ 1.6.4 (Forge 9.11.1.965), compilé contre ASM 9, sous OpenJDK 17.0.20 : le serveur démarre (`Done`), génère le monde et s'arrête proprement. Sans l'agent, il plante au chargement du monde (`EnumHelper` : `sun.reflect.ReflectionFactory` absent, `java.lang` fermé). +Sous OpenJDK 17.0.20, avec un mod de test compilé en Java 8 (lambdas, streams, méthodes static/default d'interface) et contenant un coremod qui utilise l'API `Opcodes.ASM9` : + +- MCPC+ 1.6.4 d'origine (Forge 9.11.1.965) avec son ASM 4.1 : le serveur démarre (`Done`), le mod est chargé et actif, sans aucune erreur dans les logs FML. Sans l'agent, le serveur plante dès le lancement (`ClassCastException` dans `Launch`). +- Le build NGServer de MCPC+ 1.6.4, compilé contre ASM 9 : même résultat, qu'il y ait ASM 9 ou l'ASM 4.1 d'origine dans `libraries/`. Sans l'agent, il plante au chargement du monde (`EnumHelper` : `sun.reflect.ReflectionFactory` absent, `java.lang` fermé). - Un banc reproduisant launchwrapper 1.8 d'origine et l'`EnumHelper` de Forge 1.6.4, sous OpenJDK 17 et 21. ## Portée et limites @@ -91,10 +101,12 @@ L'agent requiert Java 17 ou plus récent ; il ne se charge pas sous Java 8. ./gradlew jar ``` -Nécessite un JDK 17 ou plus récent. Le jar est produit dans `build/libs/mcpc-java17-agent.jar`. Aucune dépendance externe. +Nécessite un JDK 17 ou plus récent. Le jar est produit dans `build/libs/mcpc-java17-agent.jar` et contient ASM (téléchargé depuis Maven Central et adapté au build, voir `build.gradle`) ; il n'a besoin de rien d'autre à l'exécution. ## Contenu -Les fichiers `.bin` sont les classes d'origine de **launchwrapper** et **ASM**, recompilées avec la correction minimale nécessaire (pour `Launch`, le constructeur appelle `gg.nationsglory.mcpcj17.LaunchHooks`). Elles restent sous la licence de leurs projets respectifs : MIT pour launchwrapper, BSD 3-clauses pour ASM. +`Launch.bin` est la classe d'origine de **launchwrapper** (MIT), recompilée avec la correction minimale nécessaire : son constructeur appelle `gg.nationsglory.mcpcj17.LaunchHooks`. + +Le jar embarque **ASM** (BSD 3-clauses) : ASM 9.7, les `Remapping*Adapter` d'ASM 7.0, et `org.objectweb.asm.LegacyAsmApi`, ajouté par l'agent. Le code de l'agent lui-même est publié sous licence MIT. diff --git a/build.gradle b/build.gradle index 05b6f84..662f352 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,13 @@ +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.* + +buildscript { + repositories { mavenCentral() } + dependencies { classpath 'org.ow2.asm:asm-tree:9.7' } +} + plugins { id 'java' } @@ -12,16 +22,173 @@ compileJava { repositories { mavenCentral() } +def asmVersion = '9.7' + +configurations { + // ASM shipped inside the agent, replacing the ASM 4 of MCPC+ at runtime. + bundledAsm { transitive = false } + // The Remapping*Adapter classes Forge 1.6.4 still uses were removed after ASM 7.0. + legacyAsmCommons { transitive = false } +} + +dependencies { + bundledAsm "org.ow2.asm:asm:${asmVersion}" + bundledAsm "org.ow2.asm:asm-tree:${asmVersion}" + bundledAsm "org.ow2.asm:asm-commons:${asmVersion}" + bundledAsm "org.ow2.asm:asm-analysis:${asmVersion}" + bundledAsm "org.ow2.asm:asm-util:${asmVersion}" + legacyAsmCommons 'org.ow2.asm:asm-commons:7.0' +} + +/* + * The bundled ASM is patched to behave like the ASM 4 that Forge 1.6.4, MCPC+ and their mods were + * written for: + * - Type.getType(String) accepts an internal name ("net/minecraftforge/event/Event"), as ASM 4 + * did; Forge's EventTransformer relies on it. It returns the object type of that name. + * - Visitors declared with the ASM 4 API no longer throw "This feature requires ASM5" when they + * read Java 8+ bytecode (lambdas, interface static methods, type annotations...): the call is + * forwarded to the next visitor, as for any method the visitor does not override. + * - The interface flag of method calls survives ASM 4 visitors (see LegacyAsmApi). + */ +def patchedAsmDir = layout.buildDirectory.dir('patched-asm') +def legacyApi = 'org/objectweb/asm/LegacyAsmApi' +def asmPatches = [ + 'org/objectweb/asm/Type': { ClassNode node -> + def method = node.methods.find { it.name == 'getTypeInternal' } + def create = method.instructions.find { + it.opcode == Opcodes.NEW && it.desc == 'java/lang/IllegalArgumentException' + } + if (create == null) { + throw new GradleException('ASM Type.getTypeInternal: invalid descriptor branch not found') + } + def lenient = new InsnList() + lenient.add(new TypeInsnNode(Opcodes.NEW, 'org/objectweb/asm/Type')) + lenient.add(new InsnNode(Opcodes.DUP)) + lenient.add(new IntInsnNode(Opcodes.BIPUSH, 12)) // Type.INTERNAL + lenient.add(new VarInsnNode(Opcodes.ALOAD, 0)) + lenient.add(new VarInsnNode(Opcodes.ILOAD, 1)) + lenient.add(new VarInsnNode(Opcodes.ILOAD, 2)) + lenient.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, 'org/objectweb/asm/Type', '', + '(ILjava/lang/String;II)V', false)) + lenient.add(new InsnNode(Opcodes.ARETURN)) + replaceThrow(method, create, lenient) + }, + 'org/objectweb/asm/ClassVisitor': { ClassNode node -> removeApiChecks(node) }, + 'org/objectweb/asm/FieldVisitor': { ClassNode node -> removeApiChecks(node) }, + 'org/objectweb/asm/commons/InstructionAdapter': { ClassNode node -> removeApiChecks(node) }, + 'org/objectweb/asm/MethodVisitor': { ClassNode node -> + def fiveArgs = node.methods.find { + it.name == 'visitMethodInsn' && it.desc == '(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V' + } + def fourArgs = node.methods.find { + it.name == 'visitMethodInsn' && it.desc == '(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V' + } + // 5 args, ASM 4 visitor: record the interface flag instead of throwing, call the 4 args + // method, then drop the flag if nothing consumed it. + def interfaceCheck = fiveArgs.instructions.find { + it.opcode == Opcodes.LDC && it.cst instanceof String && it.cst.startsWith('INVOKESPECIAL/STATIC') + } + if (interfaceCheck == null) { + throw new GradleException('ASM MethodVisitor.visitMethodInsn: interface check not found') + } + def pend = new InsnList() + pend.add(new VarInsnNode(Opcodes.ILOAD, 5)) + pend.add(new MethodInsnNode(Opcodes.INVOKESTATIC, legacyApi, 'pendInterface', '(Z)V', false)) + replaceThrow(fiveArgs, previousNew(interfaceCheck), pend) + def legacyCall = fiveArgs.instructions.find { + it instanceof MethodInsnNode && it.name == 'visitMethodInsn' && it.desc == fourArgs.desc + } + fiveArgs.instructions.insert(legacyCall, + new MethodInsnNode(Opcodes.INVOKESTATIC, legacyApi, 'clearInterface', '()V', false)) + // 4 args: use the recorded flag rather than guessing it from the opcode. + def forward = fourArgs.instructions.find { + it instanceof MethodInsnNode && it.name == 'visitMethodInsn' && it.desc == fiveArgs.desc + } + fourArgs.instructions.insertBefore(forward, + new MethodInsnNode(Opcodes.INVOKESTATIC, legacyApi, 'interfaceFlag', '(Z)Z', false)) + removeApiChecks(node) + }, +] + +/** The NEW of the exception thrown right after {@code insn}. */ +def previousNew(AbstractInsnNode insn) { + while (insn.opcode != Opcodes.NEW) { + insn = insn.previous + } + return insn +} + +/** Replaces the instructions from {@code create} to the next ATHROW. */ +def replaceThrow(MethodNode method, AbstractInsnNode create, InsnList replacement) { + def insn = create + while (insn.opcode != Opcodes.ATHROW) { + def next = insn.next + method.instructions.remove(insn) + insn = next + } + method.instructions.insertBefore(insn, replacement) + method.instructions.remove(insn) +} + +/** Removes every {@code throw new UnsupportedOperationException(" requires ASMx")}. */ +def removeApiChecks(ClassNode node) { + int removed = 0 + node.methods.each { method -> + def checks = method.instructions.findAll { + it.opcode == Opcodes.LDC && it.cst instanceof String && it.cst ==~ /.* requires ASM\d+/ + } + checks.each { check -> + replaceThrow(method, previousNew(check), new InsnList()) + removed++ + } + } + if (removed == 0) { + throw new GradleException("ASM ${node.name}: no API check found") + } +} + +tasks.register('patchAsm') { + inputs.files configurations.bundledAsm + inputs.property 'patches', asmPatches.keySet().join(',') + outputs.dir patchedAsmDir + doLast { + asmPatches.each { name, patch -> + def bytes = configurations.bundledAsm.collect { jar -> + zipTree(jar).matching { include "${name}.class" }.files + }.flatten().first().bytes + def node = new ClassNode() + new ClassReader(bytes).accept(node, 0) + patch(node) + def writer = new ClassWriter(ClassWriter.COMPUTE_MAXS) + node.accept(writer) + def out = patchedAsmDir.get().file("${name}.class").asFile + out.parentFile.mkdirs() + out.bytes = writer.toByteArray() + } + } +} + jar { + dependsOn 'patchAsm' + from(patchedAsmDir) archiveBaseName = 'mcpc-java17-agent' archiveVersion = '' + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from({ configurations.bundledAsm.collect { zipTree(it) } }) { + include 'org/objectweb/asm/**' + exclude asmPatches.keySet().collect { "${it}.class" } + } + from({ configurations.legacyAsmCommons.collect { zipTree(it) } }) { + include 'org/objectweb/asm/commons/Remapping*Adapter*.class' + } manifest { attributes( 'Premain-Class': 'gg.nationsglory.mcpcj17.McpcJava17Agent', 'Can-Redefine-Classes': 'false', 'Can-Retransform-Classes': 'false', 'Implementation-Title': 'MCPC+ Java 17 Agent', - 'Implementation-Version': project.version + 'Implementation-Version': project.version, + 'Bundled-ASM-Version': asmVersion ) } } diff --git a/src/main/java/gg/nationsglory/mcpcj17/ClassPatcher.java b/src/main/java/gg/nationsglory/mcpcj17/ClassPatcher.java index 0eb5bd6..9eaf0d8 100644 --- a/src/main/java/gg/nationsglory/mcpcj17/ClassPatcher.java +++ b/src/main/java/gg/nationsglory/mcpcj17/ClassPatcher.java @@ -1,23 +1,24 @@ package gg.nationsglory.mcpcj17; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.InputStream; import java.lang.instrument.ClassFileTransformer; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.security.ProtectionDomain; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.WeakHashMap; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; public final class ClassPatcher implements ClassFileTransformer { private static final Map TARGETS = new HashMap(); static { TARGETS.put("net/minecraft/launchwrapper/Launch", "Launch.bin"); - TARGETS.put("org/objectweb/asm/commons/Remapper", "Remapper.bin"); - TARGETS.put("org/objectweb/asm/ClassReader", "ClassReader.bin"); - TARGETS.put("org/objectweb/asm/tree/MethodInsnNode", "MethodInsnNode.bin"); } /** JDK 8 internals removed in Java 9, and their replacements in this agent. */ @@ -34,6 +35,8 @@ public final class ClassPatcher implements ClassFileTransformer { private static final String LAUNCH_CLASS_LOADER = "net.minecraft.launchwrapper.LaunchClassLoader"; private final Map patches = new HashMap(); + /** This agent's jar, which also carries a recent ASM. */ + private final JarFile agentJar; private final Map legacyAsm = Collections.synchronizedMap(new WeakHashMap()); private final Map configuredLoaders = @@ -49,6 +52,20 @@ public ClassPatcher() { } } System.out.println("[mcpc-j17] " + patches.size() + " class patch(es) loaded."); + + JarFile jar = null; + try { + URL url = ClassPatcher.class.getProtectionDomain().getCodeSource().getLocation(); + jar = new JarFile(new File(url.toURI())); + if (jar.getEntry("org/objectweb/asm/ClassReader.class") == null) { + System.out.println("[mcpc-j17] No ASM bundled in the agent jar."); + jar.close(); + jar = null; + } + } catch (Throwable t) { + System.out.println("[mcpc-j17] Could not open the agent jar, ASM will not be upgraded: " + t); + } + this.agentJar = jar; } @Override @@ -57,9 +74,14 @@ public byte[] transform(ClassLoader loader, String className, Class redefined try { if (loader != null && LAUNCH_CLASS_LOADER.equals(loader.getClass().getName()) && configuredLoaders.put(loader, Boolean.TRUE) == null) { - LaunchHooks.configure(loader); + LaunchHooks.configure(loader, agentJar != null && isLegacyAsm(loader)); } - if (className != null) { + if (className != null && className.startsWith(ASM_PREFIX)) { + byte[] upgraded = agentJar != null && isLegacyAsm(loader) ? bundledClass(className) : null; + if (upgraded != null) { + return upgraded; + } + } else if (className != null) { byte[] patch = patches.get(className); if (patch != null && needsPatch(loader, className, original)) { System.out.println("[mcpc-j17] Patched: " + className); @@ -89,13 +111,28 @@ private boolean needsPatch(ClassLoader loader, String className, byte[] original // The stock constructor casts the application class loader to URLClassLoader. return indexOf(original, "java/net/URLClassLoader".getBytes(StandardCharsets.US_ASCII)) >= 0; } - if (className.startsWith(ASM_PREFIX)) { - return isLegacyAsm(loader); - } return true; } - /** ASM 5 added type annotations; anything older is the ASM 4 these patches were made from. */ + /** The bundled ASM version of a class, or null for an ASM 4 class that no longer exists. */ + private byte[] bundledClass(String className) { + JarEntry entry = agentJar.getJarEntry(className + ".class"); + if (entry == null) { + return null; + } + try (InputStream in = agentJar.getInputStream(entry)) { + return in.readAllBytes(); + } catch (Throwable t) { + System.out.println("[mcpc-j17] Could not read bundled " + className + ": " + t); + return null; + } + } + + /** + * MCPC+ ships ASM 4, which cannot read Java 8+ bytecode nor run coremods written for a recent + * ASM: it is then replaced by the ASM bundled in this agent. ASM 5 added type annotations; + * servers already on ASM 5 or later keep their own. + */ private boolean isLegacyAsm(ClassLoader loader) { ClassLoader key = loader != null ? loader : ClassLoader.getPlatformClassLoader(); Boolean legacy = legacyAsm.get(key); @@ -109,7 +146,9 @@ private boolean isLegacyAsm(ClassLoader loader) { } legacy = classReader != null && indexOf(classReader, "readTypeAnnotations".getBytes(StandardCharsets.US_ASCII)) < 0; - if (!legacy) { + if (legacy && agentJar != null) { + System.out.println("[mcpc-j17] ASM 4 detected, upgrading it to the bundled ASM."); + } else if (!legacy) { System.out.println("[mcpc-j17] Recent ASM detected, keeping the server's ASM classes."); } legacyAsm.put(key, legacy); diff --git a/src/main/java/gg/nationsglory/mcpcj17/LaunchHooks.java b/src/main/java/gg/nationsglory/mcpcj17/LaunchHooks.java index af1fe63..7512f62 100644 --- a/src/main/java/gg/nationsglory/mcpcj17/LaunchHooks.java +++ b/src/main/java/gg/nationsglory/mcpcj17/LaunchHooks.java @@ -35,11 +35,18 @@ public static URL[] classPath() { * LaunchClassLoader has the bootstrap loader as parent. On Java 8 that covered the whole JDK; * since Java 9, modules such as java.sql or java.scripting live in the platform loader, so their * packages (javax.sql, javax.script...) must be delegated explicitly. + * + * @param shareAsm when ASM 4 is replaced by the ASM bundled in the agent: the loader then + * delegates ASM to the application class loader, which also finds the classes + * that did not exist in ASM 4 */ - public static void configure(ClassLoader launchClassLoader) { + public static void configure(ClassLoader launchClassLoader, boolean shareAsm) { try { Method exclude = launchClassLoader.getClass().getMethod("addClassLoaderExclusion", String.class); exclude.invoke(launchClassLoader, "gg.nationsglory.mcpcj17."); + if (shareAsm) { + exclude.invoke(launchClassLoader, "org.objectweb.asm."); + } int count = 0; for (Module module : ModuleLayer.boot().modules()) { if (module.getClassLoader() == null) { diff --git a/src/main/java/org/objectweb/asm/LegacyAsmApi.java b/src/main/java/org/objectweb/asm/LegacyAsmApi.java new file mode 100644 index 0000000..8ee0aa9 --- /dev/null +++ b/src/main/java/org/objectweb/asm/LegacyAsmApi.java @@ -0,0 +1,36 @@ +package org.objectweb.asm; + +/** + * Support for visitors written against the ASM 4 API, patched into the bundled ASM at build time. + * + *

An ASM 4 visitor only has {@code visitMethodInsn(opcode, owner, name, descriptor)}: ASM 9 + * cannot pass it whether the owner is an interface, and refuses the call when that flag cannot be + * guessed from the opcode (invokestatic of a Java 8 static interface method). The patched + * {@link MethodVisitor} records the flag here before calling the 4 argument method, and restores it + * when that method forwards the instruction to the next visitor. + */ +public final class LegacyAsmApi { + + private static final ThreadLocal PENDING_INTERFACE = new ThreadLocal(); + + public static void pendInterface(boolean isInterface) { + PENDING_INTERFACE.set(isInterface); + } + + public static void clearInterface() { + PENDING_INTERFACE.remove(); + } + + /** The recorded flag if any, else the one guessed from the opcode. */ + public static boolean interfaceFlag(boolean guessed) { + Boolean pending = PENDING_INTERFACE.get(); + if (pending == null) { + return guessed; + } + PENDING_INTERFACE.remove(); + return pending; + } + + private LegacyAsmApi() { + } +} diff --git a/src/main/resources/gg/nationsglory/mcpcj17/ClassReader.bin b/src/main/resources/gg/nationsglory/mcpcj17/ClassReader.bin deleted file mode 100644 index d536feaf4e07c271c44726033b486ec225943cba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18727 zcmbt+3w%`7o&WFLx%13TGRfp5xg;YXKEh-OHtJ{xM0rXyBuID&6irA52!;vfp-RXn*ZG}uF8P-?zIcj=nv8xy(f)z$^ZU2%Omz?4n%X+QYjD^6rF~t4gKJV< zJ*fdd2|?kVUAJ`2@9Ro$pWm(q`pF|G*l}&k>Km7~FIz51w0M9ILqn=@+43dpuI3g) zP>E_;v1akALR6s8I$+&D?9dSZT>XHg|ZxPQJyRpjH+ zJ1mmAbTOFSo*G)*-JKd7R8xYGEVL+nwRP=E394$CTTEbTl4_WsnueCmON)%M=$({g zE+JsX+A+L4HK4JUqS}%kN)2r50&PLEVQx{qoZHYkjWul;=mq!!R!{ZRAuU6xU4B}u z6`=ICG(<~inV^7@>Dm>STeL!RceS8oL(9}jzMU4ffiSf&?rUHtj2qLks8y?H756I2 z?Sjhls$bF9-!jahcriYgF4h^pDS8bZ%6&7{S+7PXwb=gKwN<6U+LCxAvv{1aAoU0bG;Str~KfMcvdBqOAnQn+?k} zs_l$whoIVqmg)2yrk%{dO%8KQ_70^i+NEWXo<1UI9r|fE91yxXr--f@)p|sd{(%J+ z4bWhaZYKEE6>nu1n~UILqgVh-RXV$c8NO9eL&MwAvvU40?PXEi25xs+^d9!1CG=j@ zFz?#w=vj0-?dO5-V+U0H-yx`es>$YO>Zd#5FuR9`xM}ggK-XSD=jjksgi0O`68Zpr zFhqCJUx6v8(4JNb7Trzvuq;31yYf}UQ2YbPHjD1%S=~(caokX)kFg0SSXh4pyIIoT zKQuTr(6xJ2YG_A)&tQQ57V$E5^Ke(6^5Ozn>cGBW?k0;4(kDaIM4xhE?fZ*iMN4{Odp z$0n;8>g}K3l7>JL7E@ijRM3bhZ{&{lg2?-MK{JZOY?`}y35((wbM`o|1T|dxdyZWN zvvyV#6_)b;u3MK3Z`;NTU@k;s9NrNwcK0FvA%Zn5EgnF>Kwo6ZorI9PcJEH5u}sB_ zk(eoD3^bd475BNt+B+syl?5+KJ!9ER3;_$t9 z5Dv5^HP}Bq(4AU5sD&wL?(__uC3lPdiT*i6XXsxL4Hb!5>HeWv+xmyoJpuX(3w1O^ zkI~n#Oi!pMuQxya8!QN!LvLRfBAG?sWRv&?eG9>gBk#mQjrH+6^j+@y-lPbqXW*yr zGaB}*?*85Itytj%93w1T58Ngd1b#q24AH;S^RU3~zWzaQ@nb>`@C|x_i7I}6!rH=` zx*JA*-n4;geeYl|hc#doR5vvqEFMB~vUPX}5&Nftmb@jNV!l3sgQ-~b61~bc`!ncs zDm{4RP-+iWrJslBH8#(4-W(%4!!Lsv`YTR;5G!7z-*BF_ltahwxcXn5y(wc^(c6~_ z(0?PUwWQOj0j2sBEb(>N!SXcA5owqXDFON)Xr}`yPuI}!0HFR1Zr2Q_hkAFVSY%NA z;xw$Li&G__cmqV1rFIXby1Rx_JxKdJm?45$uw!p}Xh&+Ow>uy_9x}sdtJ2&oOKlzA zzI@M63Yk@ZIv{+IzeAxVealoDBEX>=S=MVJBq-hR=5m?hiVJwzY$*oR+O;*+w_t9O zpq^J8#4IdmO_X9Cean%^L0=JJ=&p!<+4&u#^NUK@FHaRF+J;CvmFc(BQY2-AnTaYP zLn0!ok*X^aTIzx>Ec7!cP);So7;1&h7-qmc6oz)Vfwme!?|fTWisP*pQO1iTUE|GL z66b(ZVKx=j&4l?m(p+GPSzi&`z+~sT^T~Tx@%c=h;nWt66rxeY8Sy+pR~F+!Ij}qXB6D?O38Idg;(VUs zMFn*o9vYgEWNg(($j;T>|d-FG_wdsWn<1Rk4sC}hMX?g`$Zvey&OLSsILk)W+(QAorU4sqIoElgs zP2DfHA)QlJn^&EqXpVrz3@<-0FTWk4Hw4ai@>T}{Wh(zgBsLX~cE3J$zg>dnOyzwF zh+p)F#BOmZ6Gt>Fx-l*u-zJHHqZ4A=AX6STO|qNR%|jLc`T-T`DZhhY9%g} z<%{=lG}tb%tGcRK;F(OMhz#?}4pjb&QG(|6?acE`9m zgx@Xh;dy_Et-rs!t8abRKrg4wYxyT2KEgc0#xvqx80_Q`dw08|?#If~+M7#HU!@uZT|z+EQ%V%F!Li{r^mr zz9X=4d_^2)!|LLn9{gzM<^)yz9o%`BC9>KQKZ{!5Sto+d`e2EN#pf8}5sXP&;`6%p z7{Xc6Dd^nUvHHi^2A*&iFUO|pi@`Y-1#KM{qpYo*yOQUHjP5 z_zyS+1NMK!7C`v}tWkHI#{L~0XJAgMuF)HsC!y^)7`q~D;nmj$mq3m zu}OE_+6({U0fH{)ly*9iO+Pah!`H>XF+bnHc39zV?eD*7c=u%F)1!W~xB=wf65nRz z-*KgYvqC+i@97ym=XSLAsHvWHxZox)!1V9p2aM^5Zr-R85{>IeIFCfvk6lyJF;n;b zhwj4;wRWm_R(7R(a6l+ta#t|yhWc+xE!&&!+STjuv$jZ6DSpl%uOY50MXf?A)w{SYb*nltbAhMwrt;fgieEACuifxG zDgFuSdwa9Y`_}aHev6>4xcRO49dm>IvUZtOU3&&C@!#4e{s3)E6(_0?&lV>dLEBl=}+R%A@M)rFR->kak=~F1p$Hcwpu0RBH5;j7lm;8PUY=x zoeW_Zn7o1epSRc_7Fd6k;YF5WVB=nv*8v}d`VE`_m<_nK!C{gS3K>D81UYxXp#J{8 zR9D(>lwt$CVsUH7ax4G^+~a_g`xjV7xe*Q;Wkv7LLq@%U&8XJ;^7Qa7%b3Mm)ZNBx z9A-hYtfA?S{E8a|IG;?*T#}`W5 zIr}%>VKfJgi;RnL5Z4XSFUBbc-^4Aoj7tq1(KQ*^TKZes)-Jz#`5Mc3ryiYTi)d|M zyw)-n>Xs`2*4o~7wPjqX8?FMvW$o*hw89*g=%%Gm-{A1pL1p!DRX&!UDq_o6ZmeL! zxNN!v2S|AXa#v*}`Ndq+$q;xOZQz>8FtD@ptZiRy8LgWBD)890uC>)N+I7R~5VEs+ zq(OX)%GYju?XeU`tPld56jvckln`dL1+=Ep+lO{|`4#|Y>u`2lcrJA=JdKUa_NGZr z0~O6E3IL7&lJxN4}!+8(rLcL&(@_baM%pNm~t=G+zMfsk4b&#&&J9 zJ9Lj;vUJZ*-E$K|V_|#Q*hRB(#AkxpAlc+4AFhcnL(L#R>Vbkf_lFAVC8%2k^-`+f zdt1O(A{nkwZ(NtEH!e%@uHdUuywAt!Ax`{3X$fi))N#m2!C0fG@zfa2cpMczbj048fag=7p>chUy9L)(|afC`v()k<5=z^zXXbqq>_GD<3;<4itKb0_J z(tL`VM(H9?!do8>btZgf!Y}>O9HGzh5EOl8XM3acUHmZ9y-U&NNf2#w>g)05=UvXj zWB75~-M8Dm-|1Il0*OGDE(!+|fs7x?@&&#a69ku?iGU19?-7c~K*F0P%g-ZyJ~aDe z0F3hZ@eMKQ9i?UfjnXBqXfCD1g!BxEilv9iTmr0*pu*C-j4pSEL~)ACLn_n|Zzl#e zKk3cU!eh?RE2bIhpE`6AUFl%GMi~pyBBl#vER5BG`W5vMc3b@+YS2`YCd4ki^&?c1 z5ym{}jn&yENOxqsnA^|hX%+|ubCt$xY=GgF!2Yh)2v*@0KeZ_O6fH@bCgX~Zh(B|q z^fo1YD9nU7doE7+PtuZ&(mO`WlL6^hsuV|+B(yGn3ZZcQ19(1;=Sz6LsoPZh2GrdL z842SjmK&D43)$hZ4+en zI7stY&K8X3C`a$kQsMj+Q+D7u?X%5OyAxp5Hf5-Jl-|#8JD?2a4U^HEO=eHR3%VK9 zW@4f+K6QlIu#wz+q^aD42c$DsMjE>UBwbvLhvRkx~iFq*IQYdyd0?H>6 zY@-Mh&}qUaeTV20RJ@(@pp2xy@__Bn(Wehk5XP4!Tbf}wu<3^dv~_kS%#3e5JN{Ma zJxJ!`XxH&4;qxHIm>g{&ap@bOJK*VNPdL<*fTPL)NCEwWIXVKl%Yc$rqC}P)qS-RU z0~o(M9;PGiZzG%iHr1##nwr^E-{LZ9*V!Z!pNT`gKswmS&+tqu3KB3+; zNlq`(%T6?{f$aG6EBKM;DSD+oNB`-H9`}rTkmorFUT(cewUEXTwv(ZZ8StwA4k#{GCatU(s{EtVQ>r==k*w&f{FBqRG(UP_vWzPs_K!r3tT{k) zcv*|fnkHs+WwP9@xBf{99@Mr0p5n&d# zdR6O_5#e`QXM-;l77{g@T?zJ{#Ddh9EX~sW5fPkBsWMCZ5M8u9uF)Wv)RUoPjVDp7 z=9sW$tz83M%(iP}t+d;vjX-6qaM5Y*=}g#}TIl3?D7$7?Q*u)a@%5iEl&cEnz&ka7iCHz;&U<{ zqfvTc{K`yoVqvn>E;rG$4|pX#rt|g@Cqon$EE;Hp4R8Vl%W$Z7V0Pvda+P#$RMtSsc~Lxxsh0 zPE6kdZPy^CU-$O3?Mbk<`7$+G;z?MZM5!lH=1G)$65(i~LL{LxW<4S!heStS|5XvO z6kfr3j0Bm#CN zQd_yQw`(W0g(vK-s))TwTe7`LyOLEWla-V0ErQ_q`2u^x5}6b2$%q|M_GU-EB(6I^ zciWL?#C6Fk4o7Q`i|b=YX%FHjYVSHqX$X?@bVRA+VhcRa%n~wBnWZZrUDbG)noS;b z3JEU4)F)|9Cin*T+$gJ?v^hUOtdJ}PRZ#kol+dVMB`Ze7R=X-HD|$vn3ie-ifM(f| zoVW=~vMjSrXkkpGH*l~FKq}&9X(>au*jlQj#qm*ADG%Xr2oJ#uEtT)XLk8GGWTr}a zNT~uW)qtgM0tgS81XzB6!t8c_g?>y7%dlz*bBo80pvA6aFRElOs;*)WB}_uc7CH`Pu%v6ireD0e^k7iXSy$DN8&OtCf>)j_fNQ5Slq5%EtG+)%_Iu( z9%W|IR1Sx@$`e6o(K)!^2}9Vddjio!Fv=V85NGI5iA)lKGGKPvB{}iI4N(~y5yMd# z>_NtDMlY_Q@qLNXC&XXNQhksh_*8)ZRp7zSk~2SXjee^-m-$((AKHR(L%LtJqmMp^ z=PzhuTz4bm%+h#FTCpf^+U7W%WIP@uEfdXc6U_+3-vDc7r^B2qt)R?dn2yJnrLl|$ z2KYFluC#pndKM{oj;?xoP9SJ=1r9OdKK4oofx_S z&dChfWzJY&JekAk)iu4bZ)!(}+I4?70!uN-Q1#w(I4<5dQvQr+iNz*Sjj zbwqsBN$uXv7?3-OqblG+dCvM<6GfU=bBJ)lQ|lzOHg4#-%Lzx;(;o+>fEFwPUu%N?iX;#dMh`({#ZgklypTf}Mx5JR)bb_Z* z@rg{WsYpMQC1k~JV%xxE{Kyt7WURFMbY6A_?MCLkL|w49~XxlG?zFnii$(i_R3&5(8F?lFj;HYa=fqA`EYHrDpP~C?AYX}&nfX- z7y}2Y`{Nw#jZraTM?9nAGwSz<`aP{;!%`*s&q^u=ml{8BK}^6c^#Eu zS<&h38KGr3){qy61Bdyv0eeu6LMr5+FRNu>1X0k$-o9G9ZUvn@Ocp23D)gvjHz$LO zI0l(I21)2cHYcm?>eF<7Bcve4SdyHxqwTo(#33p}GYZ^y(vF;_JM74DF(TyyWT_JN z%)52r0b0Q;NPxMY*=TShW_p|_c3fbd$HlRncsfh78o3Nn92bzm+5NKe5W#B0vJ64L zgyZ|OiEyS8)|r9pWXEyRmWsrhmv6jI~8ty9+c z_T|JsWl3gge%ct8g^dcP0`M$dH|jR$#J_S}GG0eVcs2~m?Q84Qecr6`!-BvILsfyd zuF;8C+92oT#Md|!8^?gh=>a1g=8R8s6qOv888^q;M6K&Ow&OZ&i5YT+v=N$lZK^}` z^(5+Ly7z%NRQF$Qk#0M<-BEc_>p$aH3)r?%Oy%ZYDhjpjEaC2j0SLByee5Y7%PSzn)3-~{$(T>{Z zfbaU8_?cQgTOE-qA>@SS_cEN9y6FK*-p-4aNg|zy!c>vzu*A^e(_ttle#6H3r@Num zK=Wy`SiJ$5{6wgEozu$SiKLXs5?4uNhsY~9RrKwE9l`QW>Fsy3Es?*i8^7cf+;j*qf&B*&03 z%y^z@2a(Y)%!Ck&f1ogYDbL(_4s#oDSLXwB*J_krpp0TUklsTypRvSab$nl8L`iOWR~#v5UC5&fWfoMV}^Oehc?{xH2@)*yb5(Rbo&w;JkMM9zGMj z3eWj?HsiSnPYTcMxHt50_G|_gIE`yaq7QA znRD*vox1Z@V^Mb7xjx+^z~i)i68OJAeLuqq%u~JZee-eC(zx9==f#wQpo<4{h96qW zdTvAONh7#X9pV%`JOPOJE~dXsW}VJ{bR-q zcSKl?s6T7OePhNPL#s%*1N7X2EqAO?2 zW#i2nAIlpi-@m}{ygfLeH|OPydCDdVoeD;Pf5tleEWxu05AU_&Xsgxj;s^T8W5jkm z59aIKR_vkQ6mM5_9+O_aGvKQrzx1*T!$;3j$=ZzYs0*4OgKyiX<~*JiUjV4d2YU|a zh3ckIMSfRb?uKLn!*h%w;9|HH7=lTh6-*Eho=yDz^^=GPUC@O9^(I3FprNxt?^`zs zG~|NfMg(ylCsPj9nFyF?O}X8Le1Z8baUkgoAWM?SU?w1~vmm#uo&>2)c|1!W0;DgA zb1(<;T)Xrv$TnQKUgrWo&tywova12?PvQh^0vJdBlXSEN7p`k20hhVJusC0##Vsrd zaF6Q%C+hWW-V}jsEnEe4~=vEe?@*%gf7?u5QWf>}iU>W>|H*?0scmKb28d?wI(*Un< zY720_rMoq7T>4OD(3L>aH2o;E(-8@e!|{eM`^NVhB{pM6@+;djj!yoFbq&j|_=WA| zBX(as&d?^U7zy0>l;J=p;m7p}@A$mf@g2pb=~W|CE%BL#?Zx5KXH@e;$W5icg7%Nl zTprPr2q4MB4#`u8lWB9zxOSoK#UT|&gBy&#L5HmXFVTSe!(lr2%>m}|u-6~NB{$xG z8lkxD#Q_-bp(gz}@H=XIaZYfAsUAfY1+e>2mi|it9Hz=i?9%hxJU&8Ov7DDM&=GYI ziTD77Jvi+`;t*9VJt~8lAZ~b^d8mBhB{dIF*TW}M?<}Bxi(=z5dbUB#miOOkIKPhI z#zF@3ghH860ihtWiI>iXd}tELFA=Z|n!^aE9v|G0cTJ;3n*n`ZyC!aw zu~6`Ju9JO9pI$8X!Cm+t+?VF#xg5`8JlEjqz{6hyaW0Z>tbf92gV}B30{g^uTws4# z$ptq5E4jcJ90D9qymJNkKz*he`>>U`!^g}Taak$8#^V&i5wXPGAdj|hkNSu6+lYMyPIHjd@RbGEVz{Y?1>v#>$2Z4R()!(#^vcHgn1PE9X$k}1`#-4X zE{wGkbFakr2m;9YQ=#}NO--9Cgj$L960=SU9jJj~~>i;d?y&UEGBZOx?6F_q#y& z0~rp`RSwW4lYnaTgu>xH`{U4pq6!XBadvkTEZ`4dIN0U_8*c%ktm?Kq7su+<4*$+7 zW5ey2nX|&P_EwwkdlUl@O!*IhIAALQ#NR*aPr?@Qml4r}w%Zx1@I+_(R}bx0)O!Lp!9gN}Edq^6DYJkmF2Y<7&4|DcOA zw|Yly;eynIH&^aR{0#>^Qt%g(Ru8ou~pT#j2S)XbSlV>#*A&pVo_%I zgwgAiCK~!s%2B^k7{0v0_Y3;0hQHm@U(O}5$fD?+$2hGk`7;&gi@0R)q3hP{>ykDOMQH;wrtqiu27g)d9a#ZswQqu|o8-Gc@-XNC}TrN#;Ln}9JSJuM6?MyFd zS`!t1QwBf6IE0>ozl0tmiku{h9ELH;mmKocd7gF=LxOR^DqTQW^)X`B))+I^a(s;V z=g4KyCjxR^W&BxqLQlJ%ObBy-ERvaCX4jKq{!jB=MqTqdzdVu_?O^#OSc zSiGdJks2d4X_dxEPW?u7yGnnBnMlO`Rbd#=-*%#7k}S56Bsflv^800U%&lbe&1#9 z`P<$1ZUSh-dw%T2XZ>ix8EMW+^Eqh@KlWfmMsW?Jev}DS8BA%;Nt2Mh^Fo!B(FGZe z$tWeGaSdrdGMFesyC1cT$07h3Te);qw~4;72pQ zD9y9N@HrVhFU<=weNjd)3Fj*szT`(3QyO0OBPwd{!Ix#_E7E*b))E?C@na{xCVO9( z<{KJb)$p2vZ(lN<%Z#@IBZVF6}AR@ zE*O`L)|8P>wD#w+$#kNlrF2Q5qCaQE$705W#lkWwZ8I8}o*>J%U{5BSXw8gVFypz) z=18kCIo>K;WVgbGLK*cCnn{IVaJW=#i^H0RZ_->U#IrFocRn*JmT34E{b6$)G*VO4 z=pobkEWI8wt+1q^lT&{(kv4KuS)SYG0Lf_wOz0|lNyF<3o8np1$eAZha+)B7_qiPy zH70UqR^f~jJd;a8TkjtjOim_qnXDsdk039Gqt_Ux6~`Q`CWI}nHse@xj9wv_h+WEakDEaabID3$jok+ zpx5znwCngbzN6#2ctgkc@O>RKxT@n5*stRUctc^`^7J@3l}wGASsg#bkED51$B*U6 zPoz05=YA>-kILwpj1JYm-v;!+T}zCr_P;|=utt@^=%RN zj*edo*Wci`3a*wxS^FK2I_H?e`qGV~MtU@5>X;Lo-xakFin7;b^m}Ora8l&`LB}5j zf^aDdhfq9W8ieJ;QlH8unHM@vpkGHG^Ez%|USVgcsCZ_4Jd>VmT`{>t)jx^S#9|06 zd(9ujMAR&Dh+PO2Y8|SLLST61`V}daB*cqu261d zv&M8hGcm0YD%Pb?;Jb^AU0Ujv>(OUkoHA0hw9esF%W1|4g&)W!rj~FfsiU!vh>(X^ zFWs+{?HmB5ph^B0d~?F&{gk$0C?D+WK74jy==c$4_{#CQ!P#>KraqcCU?g~H3h0;9 zk`@wawkKp{zY*Gl#Sj*ZC<1lG6$|8)nNH-+vk)8cxH(aX*Bu}AIuv)=3=~d&#KR8w zJX}-=am%2mi7%|$HTX2@U|=oIV;x>VJw8e5jqu&Z77d+uP^sa7T14|A)NNF8vdCAhOK!D- z(f{Dg!!y%i+WR&K{F~$_#){+yeoygBgh)^@IOg8p$k}ppX`PrGyW*y~|B4XAIXNU2 z$0F~;&y}iKc*52yfvdiO)y1ZUEMzxgGyfxeiSOpS`WXA;H2fsC;S7Qp<2(8?winDS z+k6KVwuMcL@Z3f%C!I8WQIuD5GZxJ3pqb*sn(!@n6*ZtP^QfVA51_7un-pz?V<+R| z2}_0>NKchBQ z=Xn!)*d1}#d2V2hXMf{Waer;3&J*z6LRr8&3va;JZ-vSA#u;c43nogjW1yXW2V>gB zsP-^aD;WW67`P2I?=h+wMknLq04WY}_Xy4I;?>;2*f?q#F%17*Xd1edx`Q2rNFejw zV?=TBQ(U*U>~*+ga*x(J?73x+bpHhw;nj$A7YX@TvcbXMsh(s|s-}*PUpoVDc#cS~ zi!EUNP)%SS^@Us^WPxai0JPaDl*fismihlCc|*G=;JJ>*w@?+W*(h5~q)l_t>Xtk< zhXRElyuW@QrroIExQC$|qxbu)B&lccZXp=E*theQ(aA_U%EalzDb9wF;G<7cW-UCc z?qaouZVktnFb%g6U>wn|C&|&S56|B#&w6uAM ziSRMW^(}ckX7wCz<+m-5pv>7<&!eR{_UjpOV1#QpOT7&y^9bD=r>>@@p9ik$J=7zH z-hwu-Q5QMW8D?q`?a)<0dja90@H`^D3y87~ZEyEjfVH-J8>n$q;`8xYlv{%x0%hBS z_}^+J{TQ)J5y><$$zT&En0Xf&Vp(FCBZikOeO41Mbr)MT^l3P*;VCa&&*xx4xn|BJ z-TVvKITU8;-6dj*OYbwB@ugazZ(`U;6)F#fBT;uAPb?>Y)*>G)kk_-9ICDjo)4j-@ zQw)6DV?rk$b6EwoSp3V}8Cp^N?&WM>vB*DI{HvtnuL!nsp@?`vp;Y|q+!>Y~eCB@| C+ab#U From f83374df23f80999f5b8c0649b68e7e8683d3ff0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 11:09:37 +0000 Subject: [PATCH 4/5] Build the agent on GitHub Actions and publish releases from tags Builds the jar on every push and pull request (downloadable as a workflow artifact), and attaches it to a GitHub release when a v* tag is pushed. Make gradlew executable so the wrapper runs in CI. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DszbR9jSTnJF2dtbGNiQRv --- .github/workflows/build.yml | 38 +++++++++++++++++++++++++++++++++++++ README.md | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..1914186 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,38 @@ +name: Build + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + + - uses: gradle/actions/setup-gradle@v4 + + - name: Build + run: ./gradlew jar + + - uses: actions/upload-artifact@v4 + with: + name: mcpc-java17-agent + path: build/libs/mcpc-java17-agent.jar + + - name: Release + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + files: build/libs/mcpc-java17-agent.jar + generate_release_notes: true diff --git a/README.md b/README.md index 701f6dc..557821d 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ L'agent requiert Java 17 ou plus récent ; il ne se charge pas sous Java 8. ./gradlew jar ``` -Nécessite un JDK 17 ou plus récent. Le jar est produit dans `build/libs/mcpc-java17-agent.jar` et contient ASM (téléchargé depuis Maven Central et adapté au build, voir `build.gradle`) ; il n'a besoin de rien d'autre à l'exécution. +Nécessite un JDK 17 ou plus récent. La CI GitHub Actions construit le jar à chaque push ; pousser un tag `vX.Y.Z` publie une release avec le jar attaché. Le jar est produit dans `build/libs/mcpc-java17-agent.jar` et contient ASM (téléchargé depuis Maven Central et adapté au build, voir `build.gradle`) ; il n'a besoin de rien d'autre à l'exécution. ## Contenu From 3a11d49e4cc51e31044b48b10ceb407aac2458b9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 11:11:42 +0000 Subject: [PATCH 5/5] Make gradlew executable Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DszbR9jSTnJF2dtbGNiQRv --- gradlew | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 gradlew diff --git a/gradlew b/gradlew old mode 100644 new mode 100755