Created
October 18, 2012 16:15
-
-
Save retronym/3912889 to your computer and use it in GitHub Desktop.
REPL classloader
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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:
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.
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);
/**
*/
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;
}
/**
*
*/
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();
} 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.
}
}
/**
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);
} 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);
} 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() {
});
runnerThread.start();
return runnerThread;
}
public static void main(String[] args) {
JarSignersHardLinker17.execute();
}
}