tags:

views:

478

answers:

2

I have code which detects if the Java AWT event queue is frozen (busy processing some event or waiting for a lock) for an excessive amount of time, due to buggy foreign code which fails to use SwingWorker or similar, and I want to offer to recover. Just killing the event dispatch thread with Thread.stop works, but it could be hazardous (and EQ might be blocked simply because the computer is temporarily overloaded), so I would prefer to prompt the user for confirmation. However displaying a dialog requires waiting for EQ to be unblocked, which is exactly what is not possible.

Is there any way, from a reasonably portable Java program, to display a dialog (or really any kind of UI element which can respond to input events) without involving the normal event queue?

I already tried running SystemTray.add(TrayIcon), which does manage to display a tray item when called from another thread... but the icon is not painted, and ActionEvents are not delivered, so this is not helpful.

It also seems possible to display a new JFrame and even paint a label on it using paintImmediately, but there is still no apparent way to receive mouse events.

Following Tom Hawtin's hint, I tried to create a new AppContext. On JDK 6u18, a dialog shown in the new context seems to paint correctly but does not receive mouse events until the main event queue is unblocked, defeating the purpose:

import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.WindowConstants;
import sun.awt.AppContext;
import sun.awt.SunToolkit;
public class Main {
    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            public @Override void run() {
                new MainWindow().setVisible(true);
                System.err.println("main context: " + AppContext.getAppContext());
            }
        });
        new TrackEQ(1000*3);
    }
    private static class MainWindow extends JFrame {
        MainWindow() {
            setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
            JButton pause = new JButton("Pause");
            pause.addActionListener(new ActionListener() {
                public @Override void actionPerformed(ActionEvent e) {
                    try {
                        Thread.sleep(15000);
                    } catch (InterruptedException x) {
                        x.printStackTrace();
                    }
                }
            });
            getContentPane().add(pause);
            pack();
            setLocation(100, 100);
        }
    }
    private static class TrackEQ implements Runnable {
        private final ScheduledExecutorService svc;
        private final int timeout;
        private boolean stuck = false;
        private boolean wait = false;
        private Thread eq;
        TrackEQ(int timeout) {
            this.timeout = timeout;
            svc = Executors.newSingleThreadScheduledExecutor();
            svc.schedule(this, 0, TimeUnit.MILLISECONDS);
        }
        public @Override synchronized void run() {
            if (EventQueue.isDispatchThread()) {
                stuck = false;
                eq = Thread.currentThread();
            } else {
                if (stuck && !wait) {
                    System.err.println("UI is stuck!");
                    wait = true;
                    Map<Thread,StackTraceElement[]> stackTraces = Thread.getAllStackTraces();
                    StackTraceElement[] stack = stackTraces.get(eq);
                    if (stack != null) {
                        for (StackTraceElement el : stack) {
                            System.err.println("stuck at " + el);
                        }
                        ThreadGroup grp = new ThreadGroup("showing dialog");
                        grp.setDaemon(true);
                        new Thread(grp, new Runnable() {
                            public @Override void run() {
                                System.err.println("created new app context in " + Thread.currentThread().getThreadGroup());
                                SunToolkit.createNewAppContext();
                                EventQueue.invokeLater(new Runnable() {
                                    public @Override void run() {
                                        System.err.println("main EQ=" + eq + " whereas my EQ=" + Thread.currentThread());
                                        System.err.println("will show dialog in " + AppContext.getAppContext());
                                        final JDialog dlg = new JDialog();
                                        dlg.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
                                        JButton fix = new JButton("Fix!");
                                        fix.addActionListener(new ActionListener() {
                                            @SuppressWarnings("deprecation")
                                            public @Override void actionPerformed(ActionEvent e) {
                                                System.err.println("agreed to fix");
                                                eq.stop();
                                                wait = false;
                                                dlg.setVisible(false);
                                            }
                                        });
                                        dlg.getContentPane().add(fix);
                                        dlg.pack();
                                        dlg.setLocation(200, 100);
                                        dlg.setVisible(true);
                                        System.err.println("showed dialog");
                                    }
                                });
                            }
                        }, "showing dialog").start();
                    } else {
                        System.err.println("no stack trace for " + eq + "; listed threads: " + stackTraces.keySet());
                    }
                } else {
                    stuck = true;
                }
                EventQueue.invokeLater(this);
                svc.schedule(this, timeout, TimeUnit.MILLISECONDS);
            }
        }
    }
    private Main() {}
}
+1  A: 

The SwingWorker class is available in some versions of Java.

It allows you to run your trime-consuming tasks on a separate thread.

Depending what JRE version you are using you may be able to plug the class in to run your existing tasks on separate threads.

These examples are from Wikipedia:

Pre-Java 6:

SwingWorker worker = new SwingWorker() {
    public Object construct() {
        ... //add the code for the background thread
    }
    public void finished() {
    ... //code that you add here will run in the UI thread
    }

};

Or for Java 6:

SwingWorker worker = new SwingWorker<Document, Void>() {
  public Document doInBackground() {
      Document intDoc = loadXML();
      return intDoc;
  }
};
martinr
The SwingWorker backport is compatible with the current version: https://swingworker.dev.java.net/
trashgod
I know about SwingWorker but this was not the question.
Jesse Glick
+1  A: 

The AWT APIs are written as if there was only event queue. So we are talking mutable statics and hence bad design and evil hacks.

The Sun PlugIn and WebStart use an undocumented API to hack in a context. Check out the AppContext class. Context is looked up first by ThreadGroup and, if that is inconclusive, then by examining ClassLoaders on the stack.

Obvious comments: You could of course run a separate process. Most application just make sure they don't block critical resources including the EDT.

Tom Hawtin - tackline
This looks like the right direction; AppContext is what I was searching for, and code for AppletClassLoader and SunToolkit looks like the right model. (Clearly this code is going to be specific to the Sun JRE so a portable app would need to include some kind of fallback - killing EQ without a prompt.)Unfortunately it does not fully work: a dialog shown in the alternate app context paints fine but does not receive any input events until the main event queue resumes.
Jesse Glick