From aa1c04ebee23d0803880d6d68ae73109c1a5c178 Mon Sep 17 00:00:00 2001 From: Sven Gothel <sgothel@jausoft.com> Date: Mon, 12 May 2014 01:07:34 +0200 Subject: JAWTWindow: Non intrusive workaround for Bug 1004 and providing AppContextInfo to mitigate related bugs, e.g. Bug 983 Bug 1004, as well as Bug 983, are caused by issueing certain AWT tasks from a Thread which ThreadGroup is not mapped to a valid sun.awt.AppContext (AppContext). The 'certain AWT tasks' are all quering the current EventQueue instance, which is associated to the AppContext. This operation will fail and cause a NullPointerException. This workaround simply gathers a ThreadGroup which is mapped to the desired AppContext. This AppContext ThreadGroup is being used to launch a new Thread which is then mapped to an AppContext and hence can issue all AWT commands. +++ In the Bug 1004 scenario, JAWTWindow is constructed from within the AWT EDT, which ThreadGroup does belong to the AppContext. Here the issue is that an AWT operation was invoked from the OSX main thread, which itself does not belong to the AppContext. The workaround as described above solves this issue. +++ For Bug 983 the scenario is different, since JAWTWindow is _not_ constructed from a thread which ThreadGroup is mapped to the AppContext. [It is also not constructed on the AWT-EDT]. It is recommended to have Java3D gathering the AppContextInfo itself early and issues the JAWTWindow creation on an eligible thread using AppContextInfo.invokeOnAppContextThread(..) similar to JAWTWindow.attachSurfaceLayer(..). This will allow removing the more intrusive remedy of Java3D commit bdda2ac20bfef85271da764d1989ec3434d5c67a and simply issuing the crucial commands on a proper thread. +++ The more intrusive workaround of above commit does not work in general at least for Bug 1004 (OSX and Applets). While forcing the mapping of the 'alien' thread-group to the AppContext work for the 1st launch w/ the 1st AppContext, a second launch w/ a new AppContext will fail. Here we did update the new AppContext knowledge in AppContextInfo, however a NPE is received in getEventQueue() .. since the AppContext is gathered after patching, but the EventQueue is still null. Further more, using static knowledge of AppContext/ThreadGroup mapping violates at least the Applet lifecycle. Here we can have one ClassLoader with multiple AppContext - i.e. Applets. --- .../jogamp/nativewindow/awt/AppContextInfo.java | 199 +++++++++++++++++++++ .../com/jogamp/nativewindow/awt/JAWTWindow.java | 26 ++- 2 files changed, 221 insertions(+), 4 deletions(-) create mode 100644 src/nativewindow/classes/com/jogamp/nativewindow/awt/AppContextInfo.java (limited to 'src/nativewindow/classes/com/jogamp') diff --git a/src/nativewindow/classes/com/jogamp/nativewindow/awt/AppContextInfo.java b/src/nativewindow/classes/com/jogamp/nativewindow/awt/AppContextInfo.java new file mode 100644 index 000000000..54762bb77 --- /dev/null +++ b/src/nativewindow/classes/com/jogamp/nativewindow/awt/AppContextInfo.java @@ -0,0 +1,199 @@ +package com.jogamp.nativewindow.awt; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Method; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import com.jogamp.common.util.RunnableTask; + +import jogamp.nativewindow.jawt.JAWTUtil; + +/** + * Instance of this class holds information about a {@link ThreadGroup} associated {@link sun.awt.AppContext}. + * <p> + * Non intrusive workaround for Bug 983 and Bug 1004, see {@link #getCachedThreadGroup()}. + * </p> + */ +public class AppContextInfo { + private static final boolean DEBUG; + + private static final Method getAppContextMethod; + private static final Object mainThreadAppContextLock = new Object(); + private volatile WeakReference<Object> mainThreadAppContextWR = null; + private volatile WeakReference<ThreadGroup> mainThreadGroupWR = null; + + static { + DEBUG = JAWTUtil.DEBUG; + final Method[] _getAppContextMethod = { null }; + AccessController.doPrivileged(new PrivilegedAction<Object>() { + @Override + public Object run() { + try { + final Class<?> appContextClass = Class.forName("sun.awt.AppContext"); + _getAppContextMethod[0] = appContextClass.getMethod("getAppContext"); + } catch(Throwable ex) { + System.err.println("Bug 1004: Catched @ static: "+ex.getMessage()); + ex.printStackTrace(); + } + return null; + } } ); + getAppContextMethod = _getAppContextMethod[0]; + } + + public AppContextInfo(final String info) { + update(info); + } + + /** + * Returns <code>true</code> if this instance has valid {@link sun.awt.AppContext} information, + * i.e. {@link #getCachedThreadGroup()} returns not <code>null</code>. + */ + public final boolean isValid() { + return null != getCachedThreadGroup(); + } + + /** + * Returns the {@link ThreadGroup} belonging to the + * last known {@link sun.awt.AppContext} as queried via {@link #update(String)}. + * <p> + * Returns <code>null</code> if no {@link sun.awt.AppContext} has been queried. + * </p> + * <p> + * The returned {@link ThreadGroup} allows users to create a custom thread + * belonging to it and hence mitigating Bug 983 and Bug 1004. + * </p> + * <p> + * {@link #update(String)} should be called from a thread belonging to the + * desired {@link sun.awt.AppContext}, i.e. early from within the special threaded application. + * </p> + * <p> + * E.g. {@link JAWTWindow} issues {@link #update(String)} in it's constructor. + * </p> + */ + public final ThreadGroup getCachedThreadGroup() { + final WeakReference<ThreadGroup> tgRef = mainThreadGroupWR; + return null != tgRef ? tgRef.get() : null; + } + + /** + * Invokes <code>runnable</code> on a {@link Thread} belonging to the {@link sun.awt.AppContext} {@link ThreadGroup}, + * see {@link #getCachedThreadGroup()}. + * <p> + * {@link #update(String)} is issued first, which returns <code>true</code> + * if the current thread belongs to an AppContext {@link ThreadGroup}. + * In this case the <code>runnable</code> is invoked on the current thread, + * otherwise a new {@link Thread} will be started. + * </p> + * <p> + * If a new {@link Thread} is required, the AppContext {@link ThreadGroup} is being used + * if {@link #isValid() available}, otherwise the default system {@link ThreadGroup}. + * </p> + * + * @param waitUntilDone if <code>true</code>, waits until <code>runnable</code> execution is completed, otherwise returns immediately. + * @param runnable the {@link Runnable} to be executed. If <code>waitUntilDone</code> is <code>true</code>, + * the runnable <b>must exist</b>, i.e. not loop forever. + * @param threadBaseName the base name for the new thread if required. + * The resulting thread name will have either '-OnAppContextTG' or '-OnSystemTG' appended + * @return the {@link Thread} used to invoke the <code>runnable</code>, which may be the current {@link Thread} or a newly created one, see above. + */ + public Thread invokeOnAppContextThread(final boolean waitUntilDone, final Runnable runnable, final String threadBaseName) { + final Thread t; + if( update("invoke") ) { + t = Thread.currentThread(); + if( DEBUG ) { + System.err.println("Bug 1004: Invoke.0 on current AppContext thread: "+t+" "+toHexString(t.hashCode())); + } + runnable.run(); + } else { + final ThreadGroup tg = getCachedThreadGroup(); + final String tName = threadBaseName + ( null != tg ? "-OnAppContextTG" : "-OnSystemTG" ); + t = RunnableTask.invokeOnNewThread(tg, waitUntilDone, runnable, tName); + if( DEBUG ) { + final int tgHash = null != tg ? tg.hashCode() : 0; + System.err.println("Bug 1004: Invoke.1 on new AppContext thread: "+t+" "+toHexString(t.hashCode())+", tg "+tg+" "+toHexString(tgHash)); + } + } + return t; + } + + /** + * Update {@link sun.awt.AppContext} information for the current ThreadGroup if uninitialized or {@link sun.awt.AppContext} changed. + * <p> + * See {@link #getCachedThreadGroup()} for usage. + * </p> + * @param info informal string for logging purposes + * @return <code>true</code> if the current ThreadGroup is mapped to an {@link sun.awt.AppContext} and the information is good, otherwise false. + */ + public final boolean update(final String info) { + if ( null != getAppContextMethod ) { + // Test whether the current thread's ThreadGroup is mapped to an AppContext. + final Object thisThreadAppContext = fetchAppContext(); + final boolean tgMapped = null != thisThreadAppContext; + + final Thread thread = Thread.currentThread(); + final ThreadGroup threadGroup = thread.getThreadGroup(); + final Object mainThreadAppContext; + { + final WeakReference<Object> _mainThreadAppContextWR = mainThreadAppContextWR; + mainThreadAppContext = null != _mainThreadAppContextWR ? _mainThreadAppContextWR.get() : null; + } + + if( tgMapped ) { // null != thisThreadAppContext + // Update info is possible + if( null == mainThreadAppContext || + mainThreadAppContext != thisThreadAppContext ) { + // GC'ed or 1st fetch ! + final int mainThreadAppContextHash = null != mainThreadAppContext ? mainThreadAppContext.hashCode() : 0; + final int thisThreadAppContextHash; + synchronized(mainThreadAppContextLock) { + mainThreadGroupWR = new WeakReference<ThreadGroup>(threadGroup); + mainThreadAppContextWR = new WeakReference<Object>(thisThreadAppContext); + thisThreadAppContextHash = thisThreadAppContext.hashCode(); + } + if( DEBUG ) { + System.err.println("Bug 1004[TGMapped "+tgMapped+"]: Init AppContext @ "+info+" on thread "+thread.getName()+" "+toHexString(thread.hashCode())+ + ": tg "+threadGroup.getName()+" "+toHexString(threadGroup.hashCode())+ + " -> appCtx [ main "+mainThreadAppContext+" "+toHexString(mainThreadAppContextHash)+ + " -> this "+thisThreadAppContext+" "+toHexString(thisThreadAppContextHash) + " ] "); + } + } else { + // old info is OK + if( DEBUG ) { + final int mainThreadAppContextHash = null != mainThreadAppContext ? mainThreadAppContext.hashCode() : 0; + final int thisThreadAppContextHash = null != thisThreadAppContext ? thisThreadAppContext.hashCode() : 0; + System.err.println("Bug 1004[TGMapped "+tgMapped+"]: OK AppContext @ "+info+" on thread "+thread.getName()+" "+toHexString(thread.hashCode())+ + ": tg "+threadGroup.getName()+" "+toHexString(threadGroup.hashCode())+ + " : appCtx [ this "+thisThreadAppContext+" "+toHexString(thisThreadAppContextHash)+ + " , main "+mainThreadAppContext+" "+toHexString(mainThreadAppContextHash) + " ] "); + } + } + return true; + } else { + if( DEBUG ) { + final int mainThreadAppContextHash = null != mainThreadAppContext ? mainThreadAppContext.hashCode() : 0; + final int thisThreadAppContextHash = null != thisThreadAppContext ? thisThreadAppContext.hashCode() : 0; + System.err.println("Bug 1004[TGMapped "+tgMapped+"]: No AppContext @ "+info+" on thread "+thread.getName()+" "+toHexString(thread.hashCode())+ + ": tg "+threadGroup.getName()+" "+toHexString(threadGroup.hashCode())+ + " -> appCtx [ this "+thisThreadAppContext+" "+toHexString(thisThreadAppContextHash)+ + " -> main "+mainThreadAppContext+" "+toHexString(mainThreadAppContextHash) + " ] "); + } + } + } + return false; + } + private static Object fetchAppContext() { + try { + return getAppContextMethod.invoke(null); + } catch(Exception ex) { + System.err.println("Bug 1004: Catched: "+ex.getMessage()); + ex.printStackTrace(); + return null; + } + } + + private static String toHexString(final int i) { + return "0x"+Integer.toHexString(i); + } + +} diff --git a/src/nativewindow/classes/com/jogamp/nativewindow/awt/JAWTWindow.java b/src/nativewindow/classes/com/jogamp/nativewindow/awt/JAWTWindow.java index bb7b44795..e2a4ad4bd 100644 --- a/src/nativewindow/classes/com/jogamp/nativewindow/awt/JAWTWindow.java +++ b/src/nativewindow/classes/com/jogamp/nativewindow/awt/JAWTWindow.java @@ -84,6 +84,7 @@ public abstract class JAWTWindow implements NativeWindow, OffscreenLayerSurface, // lifetime: forever protected final Component component; + private final AppContextInfo appContextInfo; private final AWTGraphicsConfiguration config; // control access due to delegation private final SurfaceUpdatedHelper surfaceUpdatedHelper = new SurfaceUpdatedHelper(); private final RecursiveLock surfaceLock = LockFactory.createRecursiveLock(); @@ -114,6 +115,7 @@ public abstract class JAWTWindow implements NativeWindow, OffscreenLayerSurface, if(! ( config instanceof AWTGraphicsConfiguration ) ) { throw new NativeWindowException("Error: AbstractGraphicsConfiguration is not an AWTGraphicsConfiguration: "+config); } + appContextInfo = new AppContextInfo("<init>"); this.component = (Component)comp; this.config = (AWTGraphicsConfiguration) config; this.jawtComponentListener = new JAWTComponentListener(); @@ -121,7 +123,7 @@ public abstract class JAWTWindow implements NativeWindow, OffscreenLayerSurface, this.isApplet = false; this.offscreenSurfaceLayer = 0; } - private static String id(Object obj) { return "0x" + ( null!=obj ? Integer.toHexString(obj.hashCode()) : "nil" ); } + private static String id(Object obj) { return ( null!=obj ? toHexString(obj.hashCode()) : "nil" ); } private String jawtStr() { return "JAWTWindow["+id(JAWTWindow.this)+"]"; } private class JAWTComponentListener implements ComponentListener, HierarchyListener { @@ -332,8 +334,20 @@ public abstract class JAWTWindow implements NativeWindow, OffscreenLayerSurface, } attachSurfaceLayerImpl(layerHandle); offscreenSurfaceLayer = layerHandle; - component.repaint(); + appContextInfo.invokeOnAppContextThread(false /* waitUntilDone */, repaintTask, "Repaint"); } + private final Runnable repaintTask = new Runnable() { + @Override + public void run() { + final Component c = component; + if( DEBUG ) { + System.err.println("Bug 1004: RepaintTask on "+Thread.currentThread()+": Has Comp "+(null != c)); + } + if( null != c ) { + c.repaint(); + } + } }; + protected void attachSurfaceLayerImpl(final long layerHandle) { throw new UnsupportedOperationException("offscreen layer not supported"); } @@ -744,7 +758,7 @@ public abstract class JAWTWindow implements NativeWindow, OffscreenLayerSurface, append(Platform.JAVA_VERSION_NUMBER). append(" update ").append(Platform.JAVA_VERSION_UPDATE).append(")").append(Platform.getNewline()); if(null != jawt) { - sb.append("JAWT version: 0x").append(Integer.toHexString(jawt.getCachedVersion())). + sb.append("JAWT version: ").append(toHexString(jawt.getCachedVersion())). append(", CA_LAYER: ").append(JAWTUtil.isJAWTUsingOffscreenLayer(jawt)). append(", isLayeredSurface ").append(isOffscreenLayerSurfaceEnabled()).append(", bounds ").append(bounds).append(", insets ").append(insets); } else { @@ -775,7 +789,11 @@ public abstract class JAWTWindow implements NativeWindow, OffscreenLayerSurface, return sb.toString(); } - protected final String toHexString(long l) { + protected static final String toHexString(final long l) { return "0x"+Long.toHexString(l); } + protected static final String toHexString(final int i) { + return "0x"+Integer.toHexString(i); + } + } -- cgit v1.2.3