Skip to content

Instantly share code, notes, and snippets.

@retronym
Created October 18, 2012 16:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save retronym/3912889 to your computer and use it in GitHub Desktop.
Save retronym/3912889 to your computer and use it in GitHub Desktop.
REPL classloader
package util.jnlp;
import util.ThrowableUtil$;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarFile;
/**
* A utility class for working around the java webstart jar signing/security bug
*
* see http://bugs.sun.com/view_bug.do?bug_id=6967414 and http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6805618
* @author Scott Chann
*/
public class JarSignersHardLinker {
private static final Logger LOG = LoggerFactory.getLogger(JarSignersHardLinker.class);
private static final String JRE_1_6_0 = "1.6.0_";
private static final String JRE_1_7 = "1.7";
/**
* the 1.6.0 update where this problem first occurredd
*/
private static final int PROBLEM_JRE_UPDATE = 19;
public static final List sm_hardRefs = new ArrayList();
protected static void makeHardSignersRef(JarFile jar) throws java.io.IOException {
//System.out.println("Making hard refs for: " + jar);
if (jar != null && jar.getClass().getName().equals("com.sun.deploy.cache.CachedJarFile")) {
//lets attempt to get at the each of the soft links.
//first neet to call the relevant no-arg method to ensure that the soft ref is populated
//then we access the private member, resolve the softlink and throw it in a static list.
callNoArgMethod("getSigners", jar);
makeHardLink("signersRef", jar);
callNoArgMethod("getSignerMap", jar);
makeHardLink("signerMapRef", jar);
// callNoArgMethod("getCodeSources", jar);
// makeHardLink("codeSourcesRef", jar);
callNoArgMethod("getCodeSourceCache", jar);
makeHardLink("codeSourceCacheRef", jar);
}
}
/**
* if the specified field for the given instance is a Soft Reference
* That soft reference is resolved and the returned ref is stored in a static list,
* making it a hard link that should never be garbage collected
* @param fieldName
* @param instance
*/
private static void makeHardLink(String fieldName, Object instance) {
// System.out.println("attempting hard ref to " + instance.getClass().getName() + "." + fieldName);
try {
Field signersRef = instance.getClass().getDeclaredField(fieldName);
signersRef.setAccessible(true);
Object o = signersRef.get(instance);
if (o instanceof SoftReference) {
SoftReference r = (SoftReference) o;
Object o2 = r.get();
sm_hardRefs.add(o2);
} else {
// System.out.println("noooo!");
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
return;
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* Call the given no-arg method on the given instance
* @param methodName
* @param instance
*/
private static void callNoArgMethod(String methodName, Object instance) {
// System.out.println("calling noarg method hard ref to " + instance.getClass().getName() + "." + methodName + "()");
try {
Method m = instance.getClass().getDeclaredMethod(methodName);
m.setAccessible(true);
m.invoke(instance);
} catch (SecurityException e1) {
e1.printStackTrace();
} catch (NoSuchMethodException e1) {
e1.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
/**
* is the preloader enabled. ie: will the preloader run in the current environment
* @return
*/
public static boolean isHardLinkerEnabled() {
return isRunningOnJre1_6_0_19OrHigher() && isRunningOnWebstart();
}
/**
* is the application currently running on webstart
*
* detect the presence of a JNLPclassloader
*
* @return
*/
public static boolean isRunningOnWebstart() {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
while (cl != null) {
if (cl.getClass().getName().equals("com.sun.jnlp.JNLPClassLoader")) {
return true;
}
cl = cl.getParent();
}
return false;
}
/**
* Is the JRE 1.6.0_19 or higher?
* @return
*/
public static boolean isRunningOnJre1_6_0_19OrHigher() {
String javaVersion = System.getProperty("java.version");
if (javaVersion.startsWith(JRE_1_6_0)) {
//then lets figure out what update we are on
String updateStr = javaVersion.substring(JRE_1_6_0.length());
try {
return Integer.parseInt(updateStr) >= PROBLEM_JRE_UPDATE;
} catch (NumberFormatException e) {
//then unable to determine updatedate level
return false;
}
} else if (javaVersion.startsWith(JRE_1_7)) {
return true;
}
//all other cases
return false;
}
/**
* get all the JarFile objects for all of the jars in the classpath
* @return
*/
public static Set<JarFile> getAllJarsFilesInClassPath() {
Set<JarFile> jars = new LinkedHashSet<JarFile>();
for (URL url : getAllJarUrls()) {
try {
jars.add(getJarFile(url));
} catch (IOException e) {
System.out.println("unable to retrieve jar at URL: " + url);
}
}
return jars;
}
/**
* Returns set of URLS for the jars in the classpath.
* URLS will have the protocol of jar eg: jar:http://HOST/PATH/JARNAME.jar!/META-INF/MANIFEST.MF
*/
static Set<URL> getAllJarUrls() {
try {
Set<URL> urls = new LinkedHashSet<URL>();
Enumeration<URL> mfUrls = Thread.currentThread().getContextClassLoader().getResources("META-INF/MANIFEST.MF");
while (mfUrls.hasMoreElements()) {
URL jarUrl = mfUrls.nextElement();
// System.out.println(jarUrl);
if (!jarUrl.getProtocol().equals("jar")) continue;
urls.add(jarUrl);
}
return urls;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* get the jarFile object for the given url
* @param jarUrl
* @return
* @throws IOException
*/
public static JarFile getJarFile(URL jarUrl) throws IOException {
URLConnection urlConnnection = jarUrl.openConnection();
if (urlConnnection instanceof JarURLConnection) {
// Using a JarURLConnection will load the JAR from the cache when using Webstart 1.6
// In Webstart 1.5, the URL will point to the cached JAR on the local filesystem
JarURLConnection jcon = (JarURLConnection) urlConnnection;
return jcon.getJarFile();
} else {
throw new AssertionError("Expected JarURLConnection");
}
}
/**
* Spawn a new thread to run through each jar in the classpath and create a hardlink
* to the jars softly referenced signers information.
*/
public static void go() {
if (!isHardLinkerEnabled()) {
System.out.println("Skipping Resource Preloader Hardlinker");
return;
}
System.out.println("Starting Resource Preloader Hardlinker");
Thread t = new Thread(new Runnable() {
public void run() {
try {
Set<JarFile> jars = getAllJarsFilesInClassPath();
for (JarFile jar : jars) {
makeHardSignersRef(jar);
}
} catch (Exception e) {
System.out.println("Problem preloading resources: " + ThrowableUtil$.MODULE$.fullStackTrace(e));
} catch (Error e) {
System.out.println("Error preloading resources: " + ThrowableUtil$.MODULE$.fullStackTrace(e));
}
}
});
t.start();
}
}
/**
* To run the Scala compiler programatically, we need to provide it with a
* classpath, as we would if we invoked it from the command line. We need
* to introspect our classloader (accounting for differences between execution
* environments like IntelliJ, SBT, or WebStart), and find the paths to JAR
* files on disk.
*/
final class ReplClassloader(parent: ClassLoader) extends ClassLoader(parent) with Logger {
override def getResource(name: String): URL = {
// Rather pass `settings.usejavacp.value = true` (which doesn't work
// under SBT) we do the same as SBT and respond to a resource request
// by the compiler for the magic name "app.classpath", write the JAR files
// from our classloader to a temporary file, and return that as the resource.
if (name == "app.class.path") {
def writeTemp(content: String): File = {
val f = File.createTempFile("classpath", ".txt")
IO.writeFile(f, content)
f
}
logInfo("Attempting to configure Scala classpath based on classloader: " + getClass.getClassLoader)
val superResource = super.getResource(name)
if (superResource != null) superResource // In SBT, let it do it's thing
else getClass.getClassLoader match {
case x if JarSignersHardLinker.isRunningOnWebstart =>
// Okay, we're in JNLP, things are even trickier. Luckily, the
// the hard work was already done in `JarSignersHardLinker`; we
// grab the JAR files from there and construct our classpath.
val allJarFiles = JarSignersHardLinker.getAllJarsFilesInClassPath.asScala
val jarsWithCorrectExtension = for {j <- allJarFiles} yield {
val orig = new File(j.getName)
if (orig.getName endsWith(".jar")) orig
else {
// The webstart cache holds JARS without an extension, for example:
// %APPDATA%\LocalLow\Sun\Java\Deployment\cache\6.0\46\3ddc2bee-62a4d248
// We need to copy these to files with .jar extension for them to be
// considered by the Scala compiler.
val copy = new File(orig.getParentFile, orig.getName + ".jar")
if (!copy.exists()) {
try {
FileUtils.copyFile(orig, copy)
} catch {
case x: IOException =>
FileUtils.deleteQuietly(copy)
throw x
}
}
copy
}
}
val content = jarsWithCorrectExtension.map(_.getAbsolutePath).mkString(File.pathSeparator)
val f = writeTemp(content)
logInfo("Setting classpath for Scala Compiler: " + content)
f.toURI.toURL
case u: URLClassLoader =>
// Rather pass `settings.usejavacp.value = true` (which doesn't work
// under SBT) we do the same as SBT and respond to a resource request
// by the compiler for the magic name "app.classpath"
val files = u.getURLs.map(x => new java.io.File(x.toURI))
val f = writeTemp(files.mkString(File.pathSeparator))
f.toURI.toURL
case _ =>
// We're hosed here.
null
}
} else super.getResource(name)
}
}
object Interpreter {
def evaluate[T](code: String)(binder: IMain => Unit): T = {
val settings = new Settings(sys.error(_))
settings.Yreplsync.value = true
val myLoader = new ReplClassloader(getClass.getClassLoader)
settings.embeddedDefaults(myLoader)
val stringWriter = new StringWriter
val output = new PrintWriter(stringWriter)
val intp = new IMain(settings, output)
if (intp.global == null) sys.error("unable to create a Scala interpreter: " + stringWriter.toString)
// To return a value from the interpreted code, we pass in a mutable cell.
val holder = new Holder(null)
// Compile and execute the code, and extract the evaluated value from the holder.
try {
intp.bind("_holder", holder)
// allow the caller to bind variables in scope.
binder(intp)
intp.interpret("{ _holder.value = {%s\n}}".format(code)) match {
case Results.Success => holder.value.asInstanceOf[T]
case Results.Error | Results.Incomplete =>
sys.error("Unable to interpret code: [%s]".format(stringWriter.toString))
}
} finally {
intp.close()
}
}
}
final case class Holder(var value: Any)
@Nayansonthalia
Copy link

using the JarSignersHardLinker.java code with JAVA 7u40 and windos machine is giving following exception

java.lang.NoSuchMethodException: com.sun.deploy.cache.CachedJarFile.getSigners()
at java.lang.Class.getDeclaredMethod(Unknown Source)
at com.tullib.ui.main.JarSignersHardLinker.callNoArgMethod(JarSignersHardLinker.java:96)
at com.tullib.ui.main.JarSignersHardLinker.makeHardSignersRef(JarSignersHardLinker.java:45)
at com.tullib.ui.main.JarSignersHardLinker$1.run(JarSignersHardLinker.java:262)
at java.lang.Thread.run(Unknown Source)
java.lang.NoSuchFieldException: signersRef
at java.lang.Class.getDeclaredField(Unknown Source)
at com.tullib.ui.main.JarSignersHardLinker.makeHardLink(JarSignersHardLinker.java:69)
at com.tullib.ui.main.JarSignersHardLinker.makeHardSignersRef(JarSignersHardLinker.java:46)
at com.tullib.ui.main.JarSignersHardLinker$1.run(JarSignersHardLinker.java:262)
at java.lang.Thread.run(Unknown Source)

The list of exception go on and on for all the 52 jars that we are trying to hard link. Any help here is really appreciated.

@jcolquist
Copy link

I've been able to modify the original "JarSignersHardLinker" code that worked on Java 1.6 to also work on Java 1.7. The original code failed on Java 1.7 for a few reasons:

  1. The mechanism for getting the list of jar files loaded by the JNLP in the original JarSignersHardLinker used the method:

Thread.currentThread().getContextClassLoader().getResources("META-INF/MANIFEST.MF")

However, in later versions of java 1.7 (I was using 1.7_45), when you access the Enumeration returned by that method, java throws up a security dialog telling you that you are accessing mixed signed/unsigned code. This dialog forces the user to select "Unblock" to allow the application to continue. This is true EVEN if all of your jars are signed! See this link describing that issue: https://community.oracle.com/thread/2593279

To work around this, we need to get the list of jar URLs in a different way. Since we know that we are running from JNLP, we know that the class loader is of type JNLPClassLoader. We can use methods on that class to get the jar list available at:

(JNLPClassLoader) (Thread.currentThread().getContextClassLoader()).getLaunchDesc().getResources().getLocalJarDescs()

The code uses reflection to call that method to get the jar list. This does not trigger the security dialog. Refer to this link (https://code.google.com/p/flyway/issues/detail?id=287) for where I got the idea on how to do this.

  1. The structure of class "com.sun.deploy.cache.CachedJarFile" is changed in java 1.7. Under java 1.6, the original JarSignersHardLinker code would preserve the fields "signersRef", "signerMapRef" and "codeSourceCacheRef" by creating hard links to them. These fields no longer exist in the Java 1.7 version of CachedJarFile. Instead, under java 1.7 all of the signing data is consolidated in field "signingDataRef". So the updated linker code, in "makeHardSignersRef" only make a hard link to "signingDataRef" field.

The code has gone through a full cycle of testing without seeing the garbage collection issue with signed jars under java 1.7_45 and Java Web Start. I'm posting my code in case anyone else is struggling with the same issue. My updated class is attached to this post.

Note that this code ONLY works on 1.7...it is NOT backwards compatible with 1.6. In our system, I've created a simple branch that calls the older version if we are running on java 1.6, and this new updated code if on java 1.7.

Here is the code:

package jarutil;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.lang.ref.SoftReference;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarFile;

/**

  • A utility class for working around the java webstart jar signing/security bug.

  • see http://bugs.sun.com/view_bug.do?bug_id=6967414 and http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6805618 .

  • NOTE: This version only works on Java 1.7 versions.
    *

  • @author Scott Chan (original 1.6 version)

  • @author Jim Colquist (modifications for 1.7)
    */
    public class JarSignersHardLinker17 {
    private static final Logger LOG = LoggerFactory.getLogger(JarSignersHardLinker17.class);

    /**

    • Hard references stored for signed jars.
      */
      public static final List SM_HARD_REFS = new ArrayList();

    /**

    • Is the preloader enable? ie: will the preloader run in the current environment.
      *

    • @return true if enabled.
      */
      public static boolean isHardLinkerEnabled() {
      // You can add additional logic here to enabled/disable jar linker if needed.
      boolean isHardLinkerEnabled = true;

      return isHardLinkerEnabled && isRunningOnJre17() && isRunningOnWebstart();
      }

    /**

    • Is the JRE 1.7 or higher?
      *

    • @return true if running on JRE 1.7 or higher
      */
      public static boolean isRunningOnJre17() {
      String javaVersion = System.getProperty("java.version");

      if (javaVersion.startsWith("1.7.0_")) {
      LOG.info("Jar linker has detected JRE 1.7");
      return true;
      }

      //all other cases
      LOG.info("Jar linker has NOT detected JRE 1.7");
      return false;
      }

    /**

    • Is the application currently running on webstart? Determine this by detecting the presence of a JNLPclassloader
      *

    • @return true if running on webstart.
      */
      protected static boolean isRunningOnWebstart() {
      ClassLoader cl = Thread.currentThread().getContextClassLoader();

      while (cl != null) {
      if (cl.getClass().getName().equals("com.sun.jnlp.JNLPClassLoader")) {
      LOG.info("Jar linker has determined that we are running on webstart");
      return true;
      }
      cl = cl.getParent();
      }

      LOG.info("Jar linker has determined that we are NOT running on webstart");
      return false;
      }

    /**

    • Get the jarFile object for the given url.
      *
    • @param jarUrl url to find jar for
    • @return the jarFile object for the given url
    • @throws java.io.IOException unable to get jar
      */
      protected static JarFile getJarFile(URL jarUrl) throws IOException {
      URLConnection urlConnnection = jarUrl.openConnection();
      if (urlConnnection instanceof JarURLConnection) {
      // Using a JarURLConnection will load the JAR from the cache when using Webstart 1.6
      // In Webstart 1.5, the URL will point to the cached JAR on the local filesystem
      JarURLConnection jcon = (JarURLConnection) urlConnnection;
      return jcon.getJarFile();
      } else {
      throw new AssertionError("Expected JarURLConnection");
      }
      }

    /**

    • Get all the JarFile objects for all of the jars in the classpath .
      *

    • @return jars in the classpath
      */
      public static Set getAllJarsFilesInClassPath() {

      Set jars = new LinkedHashSet();

      for (URL url : getAllJarUrls()) {
      try {
      JarFile jarFile = getJarFile(url);
      LOG.info("For jar URL '{}', adding jar file '{}' to jar list", url.toString(), jarFile.toString());
      jars.add(jarFile);
      } catch (IOException e) {
      LOG.info("unable to retrieve jar at URL: {}", url);
      }
      }

      return jars;
      }

    /**

    • Returns set of URLS for the jars in the classpath. URLS will have the protocol of jar eg:

    • jar:http://HOST/PATH/JARNAME.jar!/META-INF/MANIFEST.MF .
      *

    • @return set of urls for jars in the classpath
      */
      static Set getAllJarUrls() {
      try {
      LOG.info("Getting Jar URLs...");
      Set urls = new LinkedHashSet();

      /*
          With java 1.7, we cannot use the same method for getting the list of jar urls that is used in
          the java 1.6 version of this app.  This is because, if you call
          Thread.currentThread().getContextClassLoader().getResources()
          method, whenever you attempt to access the enumeration returned, even just to ready the URL, you will
          get a security dialog in java 1.7_45 telling you that you are accessing mixed signed/unsigned code.
          This dialog forces the user to select "Unblock" to allow the application to continue.
      
          See this link: https://community.oracle.com/thread/2593279
      
          To work around this, we need to get the list of jar urls in a different way.  Since we know that we
          are running from JNLP, the class loader is of type JNLPClassLoader.  We can use methods on that class
          to get the jar list available at
      
          (JNLPClassLoader) (Thread.currentThread().getContextClassLoader()).getLaunchDesc().getResources().getLocalJarDescs()
      
          Unfortunately, you cannot compile your java code if you include these classes, because they are available
          only in JNLP runtime.  So they only way to call these methods is to use reflection (ugh!) to get down
          to the jar list. Refer to this link (https://code.google.com/p/flyway/issues/detail?id=287)
          for where I got the idea on how to do this.
      
          Ugly reflection code to follow.
       */
      
      ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
      if (classLoader.getClass().getName().equals("com.sun.jnlp.JNLPClassLoader")) {
          Method getLaunchDescMethod = classLoader.getClass().getMethod("getLaunchDesc");
          Object launchDesc = getLaunchDescMethod.invoke(classLoader);
          Method getResourcesMethod = launchDesc.getClass().getMethod("getResources");
          Object resourcesDesc = getResourcesMethod.invoke(launchDesc);
          Method getLocalJarDescs = resourcesDesc.getClass().getMethod("getLocalJarDescs");
          Object[] jarDescs = (Object[]) getLocalJarDescs.invoke(resourcesDesc);
          // Found list of jars, iterate through them.
          for (Object jarDesc : jarDescs) {
              Method getUrlMethod = jarDesc.getClass().getMethod("getLocation");
              URL location = (URL) getUrlMethod.invoke(jarDesc);
              // Convert location url to a jar url and add to list
              URL jarUrl = new URL("jar:" + location.toString() + "!/META-INF/MANIFEST.MF");
              LOG.debug("Adding jar URL '{}' to url list ", jarUrl.toString());
              urls.add(jarUrl);
          }
      } else {
          throw new Exception("Class loader is not JNLPClassLoader");
      }
      
      return urls;
      

      } catch (Exception e) {
      throw new RuntimeException(e);
      }
      }

    /**

    • Make soft reference into hard reference.
      *

    • @param jar jar file reference.

    • @throws java.io.IOException error accessing jar file
      */
      protected static void makeHardSignersRef(JarFile jar) throws IOException {
      if (jar == null) {
      return;
      }

      LOG.info("Making hard refs for jar: {}, jar class is {}", jar.getName(), jar.getClass());

      if (jar.getClass().getName().equals("com.sun.deploy.cache.CachedJarFile")) {
      // Lets attempt to get at the each of the soft links.
      // First need to call the relevant no-arg method to ensure that the soft ref is populated
      // then we access the private member, resolve the softlink and throw it in a static list.

      callNoArgMethod("getSigningData", jar);
      makeHardLink("signingDataRef", jar);
      

      }
      }

    /**

    • Call the given no-arg method on the given instance
      *

    • @param methodName method to call

    • @param instance object to call method on
      */
      protected static void callNoArgMethod(String methodName, Object instance) {
      LOG.debug("Calling noarg method hard ref to {}.{}()", instance.getClass().getName(), methodName);
      try {
      Method m = instance.getClass().getDeclaredMethod(methodName);
      m.setAccessible(true);

      m.invoke(instance);
      

      } catch (Exception e1) {
      LOG.info("Cannot call no arg method {}", methodName, e1);
      }
      }

    /**

    • If the specified field for the given instance is a Soft reference That soft reference is resolved and the

    • returned ref is stored in a static list, making it a hard link that should never be garbage collected
      *

    • @param fieldName field name to link

    • @param instance object to pull link from
      */
      protected static void makeHardLink(String fieldName, Object instance) {

      LOG.debug("Making hard ref to {}.{}", instance.getClass().getName(), fieldName);

      try {
      Field reference = instance.getClass().getDeclaredField(fieldName);

      reference.setAccessible(true);
      
      Object o = reference.get(instance);
      
      if (o != null && o instanceof SoftReference) {
          SoftReference r = (SoftReference) o;
          Object o2 = r.get();
          SM_HARD_REFS.add(o2);
      } else {
          LOG.info("Object is not a soft ref, or null!");
      }
      

      } catch (Exception e) {
      LOG.info("Cannot make hard link for field {}", fieldName, e);
      }
      }

    /**

    • Spawn a new thread to run through each jar in the classpath and create a hardlink to the jars softly referenced

    • signers information.
      */
      public static Thread execute() {
      if (!isHardLinkerEnabled()) {
      return null;
      }

      LOG.info("Starting Resource Preloader Hardlinker");

      Thread runnerThread = new Thread(new Runnable() {

      public void run() {
      
          try {
              Set<JarFile> jars = getAllJarsFilesInClassPath();
      
              for (JarFile jar : jars) {
                  makeHardSignersRef(jar);
              }
      
          } catch (Exception e) {
              LOG.info("Problem preloading resources");
              e.printStackTrace();
          } catch (Error e) {
              LOG.info("Error preloading resources");
              e.printStackTrace();
          }
      }
      

      });

      runnerThread.start();
      return runnerThread;

    }

    public static void main(String[] args) {
    JarSignersHardLinker17.execute();
    }
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment