From 3e19d456735ff185c0dd95bb09ec9e858c22d46f Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 5 Dec 2019 12:01:22 -0500 Subject: [PATCH] clientloader: don't lazy load client classes Currently if the patched jar is modified on disk after the client starts, the client can classload in from the modified jar instead, which is problematic when running multiple different versions at the same time. --- .../net/runelite/client/rs/ClientLoader.java | 73 ++++++++++++++++--- 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/rs/ClientLoader.java b/runelite-client/src/main/java/net/runelite/client/rs/ClientLoader.java index fb04be2ec2..10c9f76bc8 100644 --- a/runelite-client/src/main/java/net/runelite/client/rs/ClientLoader.java +++ b/runelite-client/src/main/java/net/runelite/client/rs/ClientLoader.java @@ -30,6 +30,7 @@ import com.google.archivepatcher.applier.FileByFileV1DeltaApplier; import com.google.common.base.Strings; import com.google.common.hash.Hashing; import com.google.common.hash.HashingOutputStream; +import com.google.common.io.ByteStreams; import com.google.common.io.Files; import java.applet.Applet; import java.io.ByteArrayOutputStream; @@ -39,8 +40,6 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.URL; -import java.net.URLClassLoader; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; @@ -51,8 +50,10 @@ import java.security.cert.CertificateFactory; import java.util.Arrays; import java.util.Collection; import java.util.Map; +import java.util.Enumeration; import java.util.function.Supplier; import java.util.jar.JarEntry; +import java.util.jar.JarFile; import java.util.jar.JarInputStream; import javax.swing.SwingUtilities; import lombok.extern.slf4j.Slf4j; @@ -120,6 +121,7 @@ public class ClientLoader implements Supplier SplashScreen.stage(.05, null, "Waiting for other clients to start"); LOCK_FILE.getParentFile().mkdirs(); + ClassLoader classLoader; try (FileChannel lockfile = FileChannel.open(LOCK_FILE.toPath(), StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE); FileLock flock = lockfile.lock()) @@ -132,14 +134,17 @@ public class ClientLoader implements Supplier SplashScreen.stage(.35, null, "Patching"); applyPatch(); } - } - File jarFile = updateCheckMode == AUTO ? PATCHED_CACHE : VANILLA_CACHE; - URL jar = jarFile.toURI().toURL(); + SplashScreen.stage(.40, null, "Loading client"); + File jarFile = updateCheckMode == AUTO ? PATCHED_CACHE : VANILLA_CACHE; + // create the classloader for the jar while we hold the lock, and eagerly load and link all classes + // in the jar. Otherwise the jar can change on disk and can break future classloads. + classLoader = createJarClassLoader(jarFile); + } SplashScreen.stage(.465, "Starting", "Starting Old School RuneScape"); - Applet rs = loadClient(jar); + Applet rs = loadClient(classLoader); SplashScreen.stage(.5, null, "Starting core classes"); @@ -424,12 +429,62 @@ public class ClientLoader implements Supplier } } - private Applet loadClient(URL url) throws ClassNotFoundException, IllegalAccessException, InstantiationException + private ClassLoader createJarClassLoader(File jar) throws IOException, ClassNotFoundException { - URLClassLoader rsClassLoader = new URLClassLoader(new URL[]{url}, ClientLoader.class.getClassLoader()); + try (JarFile jarFile = new JarFile(jar)) + { + ClassLoader classLoader = new ClassLoader(ClientLoader.class.getClassLoader()) + { + @Override + protected Class findClass(String name) throws ClassNotFoundException + { + String entryName = name.replace('.', '/').concat(".class"); + JarEntry jarEntry = jarFile.getJarEntry(entryName); + if (jarEntry == null) + { + throw new ClassNotFoundException(name); + } + try + { + InputStream inputStream = jarFile.getInputStream(jarEntry); + if (inputStream == null) + { + throw new ClassNotFoundException(name); + } + + byte[] bytes = ByteStreams.toByteArray(inputStream); + return defineClass(name, bytes, 0, bytes.length); + } + catch (IOException e) + { + throw new ClassNotFoundException(null, e); + } + } + }; + + // load all of the classes in this jar; after the jar is closed the classloader + // will no longer be able to look up classes + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) + { + JarEntry jarEntry = entries.nextElement(); + String name = jarEntry.getName(); + if (name.endsWith(".class")) + { + name = name.substring(0, name.length() - 6); + classLoader.loadClass(name); + } + } + + return classLoader; + } + } + + private Applet loadClient(ClassLoader classLoader) throws ClassNotFoundException, IllegalAccessException, InstantiationException + { String initialClass = config.getInitialClass(); - Class clientClass = rsClassLoader.loadClass(initialClass); + Class clientClass = classLoader.loadClass(initialClass); Applet rs = (Applet) clientClass.newInstance(); rs.setStub(new RSAppletStub(config));