/* * Copyright 2008 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.gwt.dev.shell; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.UnableToCompleteException; import com.google.gwt.dev.util.Name; import com.google.gwt.dev.util.Name.BinaryName; import com.google.gwt.dev.util.log.speedtracer.DevModeEventType; import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger; import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.ListIterator; /** * The interface to the low-level browser, this class serves as a 'domain' for a * module, loading all of its classes in a separate, isolated class loader. This * allows us to run multiple modules, both in succession and simultaneously. */ public abstract class ModuleSpace implements ShellJavaScriptHost { private static ThreadLocal<Throwable> sCaughtJavaExceptionObject = new ThreadLocal<Throwable>(); private static ThreadLocal<Throwable> sThrownJavaExceptionObject = new ThreadLocal<Throwable>(); /** * Logger is thread local. */ private static ThreadLocal<TreeLogger> threadLocalLogger = new ThreadLocal<TreeLogger>(); public static void setThrownJavaException(Throwable t) { sThrownJavaExceptionObject.set(t); } /** * Equivalent to {@link #createJavaScriptException(ClassLoader,Object,String) * createJavaScriptException(cl, exception, "")}. */ protected static RuntimeException createJavaScriptException(ClassLoader cl, Object exception) { return createJavaScriptException(cl, exception, ""); } /** * Create a JavaScriptException object. This must be done reflectively, since * this class will have been loaded from a ClassLoader other than the * session's thread. */ protected static RuntimeException createJavaScriptException(ClassLoader cl, Object exception, String message) { Exception caught; try { Class<?> javaScriptExceptionClass = Class.forName( "com.google.gwt.core.client.JavaScriptException", true, cl); Constructor<?> ctor = javaScriptExceptionClass.getDeclaredConstructor( Object.class, String.class); return (RuntimeException) ctor.newInstance(new Object[] {exception, message}); } catch (InstantiationException e) { caught = e; } catch (IllegalAccessException e) { caught = e; } catch (SecurityException e) { caught = e; } catch (ClassNotFoundException e) { caught = e; } catch (NoSuchMethodException e) { caught = e; } catch (IllegalArgumentException e) { caught = e; } catch (InvocationTargetException e) { caught = e; } throw new RuntimeException("Error creating JavaScriptException", caught); } protected static TreeLogger getLogger() { return threadLocalLogger.get(); } /** * Get the JavaScriptObject wrapped by a JavaScriptException. We have to do * this reflectively, since the JavaScriptException object is from an * arbitrary classloader. If the object is not a JavaScriptException, or is * not from the given ClassLoader, we'll return null. */ static Object getJavaScriptExceptionException(ClassLoader cl, Object javaScriptException) { if (javaScriptException.getClass().getClassLoader() != cl) { return null; } Exception caught; try { Class<?> javaScriptExceptionClass = Class.forName( "com.google.gwt.core.client.JavaScriptException", true, cl); if (!javaScriptExceptionClass.isInstance(javaScriptException)) { // Not a JavaScriptException return null; } Method getException = javaScriptExceptionClass.getMethod("getException"); return getException.invoke(javaScriptException); } catch (NoSuchMethodException e) { caught = e; } catch (ClassNotFoundException e) { caught = e; } catch (IllegalArgumentException e) { caught = e; } catch (IllegalAccessException e) { caught = e; } catch (InvocationTargetException e) { caught = e; } throw new RuntimeException("Error getting exception value", caught); } protected final ModuleSpaceHost host; private final TreeLogger logger; private final String moduleName; protected ModuleSpace(TreeLogger logger, ModuleSpaceHost host, String moduleName) { this.host = host; this.moduleName = moduleName; this.logger = logger; threadLocalLogger.set(host.getLogger()); } public void dispose() { // Clear our class loader. getIsolatedClassLoader().clear(); } public void exceptionCaught(Object exception) { Throwable caught; Throwable thrown = sThrownJavaExceptionObject.get(); if (thrown != null && isExceptionSame(thrown, exception)) { // The caught exception was thrown by us. caught = thrown; sThrownJavaExceptionObject.set(null); } else if (exception instanceof Throwable) { caught = (Throwable) exception; } else { caught = createJavaScriptException(getIsolatedClassLoader(), exception); // Remove excess stack frames from the new exception. caught.fillInStackTrace(); StackTraceElement[] trace = caught.getStackTrace(); assert trace.length > 1; assert trace[1].getClassName().equals(JavaScriptHost.class.getName()); assert trace[1].getMethodName().equals("exceptionCaught"); StackTraceElement[] newTrace = new StackTraceElement[trace.length - 1]; System.arraycopy(trace, 1, newTrace, 0, newTrace.length); caught.setStackTrace(newTrace); } sCaughtJavaExceptionObject.set(caught); } /** * Get the module name. * * @return the module name */ public String getModuleName() { return moduleName; } public boolean invokeNativeBoolean(String name, Object jthis, Class<?>[] types, Object[] args) throws Throwable { JsValue result = invokeNative(name, jthis, types, args); String msgPrefix = composeResultErrorMsgPrefix(name, "a boolean"); Boolean value = JsValueGlue.get(result, getIsolatedClassLoader(), boolean.class, msgPrefix); if (value == null) { throw new HostedModeException(msgPrefix + ": return value null received, expected a boolean"); } return value.booleanValue(); } public byte invokeNativeByte(String name, Object jthis, Class<?>[] types, Object[] args) throws Throwable { JsValue result = invokeNative(name, jthis, types, args); String msgPrefix = composeResultErrorMsgPrefix(name, "a byte"); Byte value = JsValueGlue.get(result, null, Byte.TYPE, msgPrefix); if (value == null) { throw new HostedModeException(msgPrefix + ": return value null received, expected a byte"); } return value.byteValue(); } public char invokeNativeChar(String name, Object jthis, Class<?>[] types, Object[] args) throws Throwable { JsValue result = invokeNative(name, jthis, types, args); String msgPrefix = composeResultErrorMsgPrefix(name, "a char"); Character value = JsValueGlue.get(result, null, Character.TYPE, msgPrefix); if (value == null) { throw new HostedModeException(msgPrefix + ": return value null received, expected a char"); } return value.charValue(); } public double invokeNativeDouble(String name, Object jthis, Class<?>[] types, Object[] args) throws Throwable { JsValue result = invokeNative(name, jthis, types, args); String msgPrefix = composeResultErrorMsgPrefix(name, "a double"); Double value = JsValueGlue.get(result, null, Double.TYPE, msgPrefix); if (value == null) { throw new HostedModeException(msgPrefix + ": return value null received, expected a double"); } return value.doubleValue(); } public float invokeNativeFloat(String name, Object jthis, Class<?>[] types, Object[] args) throws Throwable { JsValue result = invokeNative(name, jthis, types, args); String msgPrefix = composeResultErrorMsgPrefix(name, "a float"); Float value = JsValueGlue.get(result, null, Float.TYPE, msgPrefix); if (value == null) { throw new HostedModeException(msgPrefix + ": return value null received, expected a float"); } return value.floatValue(); } public int invokeNativeInt(String name, Object jthis, Class<?>[] types, Object[] args) throws Throwable { JsValue result = invokeNative(name, jthis, types, args); String msgPrefix = composeResultErrorMsgPrefix(name, "an int"); Integer value = JsValueGlue.get(result, null, Integer.TYPE, msgPrefix); if (value == null) { throw new HostedModeException(msgPrefix + ": return value null received, expected an int"); } return value.intValue(); } public long invokeNativeLong(String name, Object jthis, Class<?>[] types, Object[] args) throws Throwable { JsValue result = invokeNative(name, jthis, types, args); String msgPrefix = composeResultErrorMsgPrefix(name, "a long"); Long value = JsValueGlue.get(result, null, Long.TYPE, msgPrefix); if (value == null) { throw new HostedModeException(msgPrefix + ": return value null received, expected a long"); } return value.longValue(); } public Object invokeNativeObject(String name, Object jthis, Class<?>[] types, Object[] args) throws Throwable { JsValue result = invokeNative(name, jthis, types, args); String msgPrefix = composeResultErrorMsgPrefix(name, "a Java object"); return JsValueGlue.get(result, getIsolatedClassLoader(), Object.class, msgPrefix); } public short invokeNativeShort(String name, Object jthis, Class<?>[] types, Object[] args) throws Throwable { JsValue result = invokeNative(name, jthis, types, args); String msgPrefix = composeResultErrorMsgPrefix(name, "a short"); Short value = JsValueGlue.get(result, null, Short.TYPE, msgPrefix); if (value == null) { throw new HostedModeException(msgPrefix + ": return value null received, expected a short"); } return value.shortValue(); } public void invokeNativeVoid(String name, Object jthis, Class<?>[] types, Object[] args) throws Throwable { JsValue result = invokeNative(name, jthis, types, args); if (!result.isUndefined()) { logger.log( TreeLogger.WARN, "JSNI method '" + name + "' returned a value of type " + result.getTypeString() + " but was declared void; it should not have returned a value at all", null); } } /** * Allows client-side code to log to the tree logger. */ public void log(String message, Throwable e) { TreeLogger.Type type = TreeLogger.INFO; if (e != null) { type = TreeLogger.ERROR; } // Log at the top level for visibility. TreeLogger t = getLogger(); if (t != null) { getLogger().log(type, message, e); } } /** * Runs the module's user startup code. */ public final void onLoad(TreeLogger logger) throws UnableToCompleteException { Event moduleSpaceLoadEvent = SpeedTracerLogger.start(DevModeEventType.MODULE_SPACE_LOAD); // Tell the host we're ready for business. // host.onModuleReady(this); // Make sure we can resolve JSNI references to static Java names. // try { createStaticDispatcher(logger); Object staticDispatch = getStaticDispatcher(); invokeNativeVoid("__defineStatic", null, new Class[] {Object.class}, new Object[] {staticDispatch}); } catch (Throwable e) { logger.log(TreeLogger.ERROR, "Unable to initialize static dispatcher", e); throw new UnableToCompleteException(); } // Actually run user code. // String entryPointTypeName = null; try { // Set up GWT-entry code Class<?> implClass = loadClassFromSourceName("com.google.gwt.core.client.impl.Impl"); Method registerEntry = implClass.getDeclaredMethod("registerEntry"); registerEntry.setAccessible(true); registerEntry.invoke(null); Method enter = implClass.getDeclaredMethod("enter"); enter.setAccessible(true); enter.invoke(null); String[] entryPoints = host.getEntryPointTypeNames(); if (entryPoints.length > 0) { try { for (int i = 0; i < entryPoints.length; i++) { entryPointTypeName = entryPoints[i]; Method onModuleLoad = null; Object module; // Try to initialize EntryPoint, else throw up glass panel try { Class<?> clazz = loadClassFromSourceName(entryPointTypeName); try { onModuleLoad = clazz.getMethod("onModuleLoad"); if (!Modifier.isStatic(onModuleLoad.getModifiers())) { // it's non-static, so we need to rebind the class onModuleLoad = null; } } catch (NoSuchMethodException e) { // okay, try rebinding it; maybe the rebind result will have one } module = null; if (onModuleLoad == null) { module = rebindAndCreate(entryPointTypeName); onModuleLoad = module.getClass().getMethod("onModuleLoad"); // Record the rebound name of the class for stats (below). entryPointTypeName = module.getClass().getName().replace( '$', '.'); } } catch (Throwable e) { displayErrorGlassPanel( "EntryPoint initialization exception", entryPointTypeName, e); throw e; } // Try to invoke onModuleLoad, else throw up glass panel try { onModuleLoad.setAccessible(true); invokeNativeVoid("fireOnModuleLoadStart", null, new Class[]{String.class}, new Object[]{entryPointTypeName}); Event onModuleLoadEvent = SpeedTracerLogger.start( DevModeEventType.ON_MODULE_LOAD); try { onModuleLoad.invoke(module); } finally { onModuleLoadEvent.end(); } } catch (Throwable e) { displayErrorGlassPanel( "onModuleLoad() threw an exception", entryPointTypeName, e); throw e; } } } finally { Method exit = implClass.getDeclaredMethod("exit", boolean.class); exit.setAccessible(true); exit.invoke(null, true); } } else { logger.log( TreeLogger.WARN, "The module has no entry points defined, so onModuleLoad() will never be called", null); } } catch (Throwable e) { Throwable caught = e; if (e instanceof InvocationTargetException) { caught = ((InvocationTargetException) e).getTargetException(); } if (caught instanceof ExceptionInInitializerError) { caught = ((ExceptionInInitializerError) caught).getException(); } String unableToLoadMessage = "Unable to load module entry point class " + entryPointTypeName; if (caught != null) { unableToLoadMessage += " (see associated exception for details)"; } logger.log(TreeLogger.ERROR, unableToLoadMessage, caught); throw new UnableToCompleteException(); } finally { moduleSpaceLoadEvent.end(); } } @SuppressWarnings("unchecked") public <T> T rebindAndCreate(String requestedClassName) throws UnableToCompleteException { assert Name.isBinaryName(requestedClassName); Throwable caught = null; String msg = null; String resultName = null; Class<?> resolvedClass = null; Event moduleSpaceRebindAndCreate = SpeedTracerLogger.start(DevModeEventType.MODULE_SPACE_REBIND_AND_CREATE); try { // Rebind operates on source-level names. // String sourceName = BinaryName.toSourceName(requestedClassName); resultName = rebind(sourceName); moduleSpaceRebindAndCreate.addData( "Requested Class", requestedClassName, "Result Name", resultName); resolvedClass = loadClassFromSourceName(resultName); if (Modifier.isAbstract(resolvedClass.getModifiers())) { msg = "Deferred binding result type '" + resultName + "' should not be abstract"; } else { Constructor<?> ctor = resolvedClass.getDeclaredConstructor(); ctor.setAccessible(true); return (T) ctor.newInstance(); } } catch (ClassNotFoundException e) { msg = "Could not load deferred binding result type '" + resultName + "'"; caught = e; } catch (InstantiationException e) { caught = e; } catch (IllegalAccessException e) { caught = e; } catch (ExceptionInInitializerError e) { caught = e.getException(); } catch (NoSuchMethodException e) { // If it is a nested class and not declared as static, // then it's not accessible from outside. // if (resolvedClass.getEnclosingClass() != null && !Modifier.isStatic(resolvedClass.getModifiers())) { msg = "Rebind result '" + resultName + " is a non-static inner class"; } else { msg = "Rebind result '" + resultName + "' has no default (zero argument) constructors."; } caught = e; } catch (InvocationTargetException e) { caught = e.getTargetException(); } finally { moduleSpaceRebindAndCreate.end(); } // Always log here because sometimes this method gets called from static // initializers and other unusual places, which can obscure the problem. // if (msg == null) { msg = "Failed to create an instance of '" + requestedClassName + "' via deferred binding "; } host.getLogger().log(TreeLogger.ERROR, msg, caught); throw new UnableToCompleteException(); } protected String createNativeMethodInjector(String jsniSignature, String[] paramNames, String js) { String newScript = "window[\"" + jsniSignature + "\"] = function("; for (int i = 0; i < paramNames.length; ++i) { if (i > 0) { newScript += ", "; } newScript += paramNames[i]; } newScript += ") { " + js + " };\n"; return newScript; } /** * Create the __defineStatic method. * * @param logger */ protected abstract void createStaticDispatcher(TreeLogger logger); /** * Invokes a native JavaScript function. * * @param name the name of the function to invoke * @param jthis the function's 'this' context * @param types the type of each argument * @param args the arguments to be passed * @return the return value as a Variant. */ protected abstract JsValue doInvoke(String name, Object jthis, Class<?>[] types, Object[] args) throws Throwable; protected CompilingClassLoader getIsolatedClassLoader() { return host.getClassLoader(); } /** * Injects the magic needed to resolve JSNI references from module-space. */ protected abstract Object getStaticDispatcher(); /** * Invokes a native JavaScript function. * * @param name the name of the function to invoke * @param jthis the function's 'this' context * @param types the type of each argument * @param args the arguments to be passed * @return the return value as a Variant. */ protected final JsValue invokeNative(String name, Object jthis, Class<?>[] types, Object[] args) throws Throwable { JsValue result = doInvoke(name, jthis, types, args); // Is an exception active? Throwable thrown = sCaughtJavaExceptionObject.get(); if (thrown == null) { return result; } sCaughtJavaExceptionObject.set(null); scrubStackTrace(thrown); throw thrown; } /** * @param original the thrown exception * @param exception the caught exception */ protected boolean isExceptionSame(Throwable original, Object exception) { // For most platforms, the null exception means we threw it. // IE overrides this. return exception == null; } protected String rebind(String sourceName) throws UnableToCompleteException { try { String result = host.rebind(logger, sourceName); if (result != null) { return result; } else { return sourceName; } } catch (UnableToCompleteException e) { String msg = "Deferred binding failed for '" + sourceName + "'; expect subsequent failures"; host.getLogger().log(TreeLogger.ERROR, msg); throw new UnableToCompleteException(); } } private String composeResultErrorMsgPrefix(String name, String typePhrase) { return "Something other than " + typePhrase + " was returned from JSNI method '" + name + "'"; } private void displayErrorGlassPanel( String summary, String entryPointTypeName, Throwable e) throws Throwable { StringWriter writer = new StringWriter(); e.printStackTrace(new PrintWriter(writer)); String stackTrace = writer.toString().replaceFirst( // (?ms) for regex pattern modifiers MULTILINE and DOTALL "(?ms)(Caused by:.+)", "<b>$1</b>"); String details = "<p>Exception while loading module <b>" + entryPointTypeName + "</b>. See Development Mode for details.</p>" + "<div style='overflow:visisble;white-space:pre;'>" + stackTrace + "</div>"; invokeNativeVoid("__gwt_displayGlassMessage", null, new Class[]{String.class, String.class}, new Object[]{summary, details}); } private boolean isUserFrame(StackTraceElement element) { try { CompilingClassLoader cl = getIsolatedClassLoader(); String className = element.getClassName(); Class<?> clazz = Class.forName(className, false, cl); if (clazz.getClassLoader() == cl) { // Lives in user classLoader. return true; } // At this point, it must be a JRE class to qualify. if (clazz.getClassLoader() != null || !className.startsWith("java.")) { return false; } if (className.startsWith("java.lang.reflect.")) { return false; } return true; } catch (ClassNotFoundException e) { return false; } } /** * Handles loading a class that might be nested given a source type name. */ private Class<?> loadClassFromSourceName(String sourceName) throws ClassNotFoundException { Event moduleSpaceClassLoad = SpeedTracerLogger.start( DevModeEventType.MODULE_SPACE_CLASS_LOAD, "Source Name", sourceName); try { String toTry = sourceName; while (true) { try { return Class.forName(toTry, true, getIsolatedClassLoader()); } catch (ClassNotFoundException e) { // Assume that the last '.' should be '$' and try again. // int i = toTry.lastIndexOf('.'); if (i == -1) { throw e; } toTry = toTry.substring(0, i) + "$" + toTry.substring(i + 1); } } } finally { moduleSpaceClassLoad.end(); } } /** * Clean up the stack trace by removing our hosting frames. But don't do this * if our own frames are at the top of the stack, because we may be the real * cause of the exception. */ private void scrubStackTrace(Throwable thrown) { List<StackTraceElement> trace = new ArrayList<StackTraceElement>( Arrays.asList(thrown.getStackTrace())); boolean seenUserFrame = false; for (ListIterator<StackTraceElement> it = trace.listIterator(); it.hasNext();) { StackTraceElement element = it.next(); if (!isUserFrame(element)) { if (seenUserFrame) { it.remove(); } continue; } seenUserFrame = true; // Remove a JavaScriptHost.invokeNative*() frame. if (element.getClassName().equals(JavaScriptHost.class.getName())) { if (element.getMethodName().equals("exceptionCaught")) { it.remove(); } else if (element.getMethodName().startsWith("invokeNative")) { it.remove(); // Also try to convert the next frame to a true native. if (it.hasNext()) { StackTraceElement next = it.next(); if (next.getLineNumber() == -1) { next = new StackTraceElement(next.getClassName(), next.getMethodName(), next.getFileName(), -2); it.set(next); } } } } } thrown.setStackTrace(trace.toArray(new StackTraceElement[trace.size()])); } }