Code refactor and update the implementation of isWaitingForNextEvent

Change-Id: Ic6f4db7cc91c2e4c18d921704f638cfdc7299e2c
diff --git a/src/main/java/org/jdesktop/swinghelper/debug/DispatchInfo.java b/src/main/java/org/jdesktop/swinghelper/debug/DispatchInfo.java
new file mode 100644
index 0000000..f675efc
--- /dev/null
+++ b/src/main/java/org/jdesktop/swinghelper/debug/DispatchInfo.java
@@ -0,0 +1,146 @@
+package org.jdesktop.swinghelper.debug;
+
+import static java.lang.Math.min;
+
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadInfo;
+import java.lang.management.ThreadMXBean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+final class DispatchInfo {
+
+  // Provides the index to each hang event.
+  private static final AtomicInteger hangCount = new AtomicInteger(0);
+
+  // The last-dumped hung stack trace for this dispatch.
+  private StackTraceElement[] lastReportedStack;
+
+  // If so; what was the identifying hang number?
+  private int hangNumber;
+
+  // The EDT for this dispatch (for the purpose of getting stack traces).
+  final Thread eventDispatchThread = Thread.currentThread();
+
+  // The last time in milliseconds at which we saw a dispatch on the above thread.
+  long lastDispatchTimeMillis = System.currentTimeMillis();
+
+  private final Consumer<String> logConsumer;
+
+  DispatchInfo(Consumer<String> logConsumer) {
+    this.logConsumer = logConsumer;
+  }
+
+  void checkForHang() {
+    if (timeSoFar() > EventDispatchThreadHangMonitor.UNREASONABLE_DISPATCH_DURATION_MS) {
+      examineHang();
+    }
+  }
+
+  private void examineHang() {
+    StackTraceElement[] currentStack = eventDispatchThread.getStackTrace();
+    if (isWaitingForNextEvent(currentStack)) {
+      // Don't be fooled by a modal dialog if it's waiting for its next event.
+      // As long as the modal dialog's event pump doesn't get stuck, it's okay for the outer pump to
+      // be suspended.
+      return;
+    }
+
+    if (stacksEqual(lastReportedStack, currentStack)) {
+      // Skips reporting the same hang every time the timer goes off.
+      return;
+    }
+
+    lastReportedStack = currentStack;
+    hangNumber = hangCount.addAndGet(/* delta= */ 1);
+
+    logConsumer.accept(
+        "(hang #"
+            + hangNumber
+            + ") event dispatch thread stuck processing event for "
+            + timeSoFar()
+            + " ms:"
+            + stackTraceToString(currentStack));
+    checkForDeadlock();
+  }
+
+  private long timeSoFar() {
+    return System.currentTimeMillis() - lastDispatchTimeMillis;
+  }
+
+  public void dispose() {
+    if (lastReportedStack != null) {
+      logConsumer.accept(
+          "(hang #" + hangNumber + ") event dispatch thread unstuck after " + timeSoFar() + " ms.");
+    }
+  }
+
+  // Checks whether the given stack looks like it's waiting for another event.
+  // This relies on JDK implementation details.
+  private static boolean isWaitingForNextEvent(StackTraceElement[] currentStack) {
+    for (int i = 0; i < min(5, currentStack.length); i++) {
+      if (stackTraceElementIs(currentStack[i], "java.awt.EventQueue", "getNextEvent", false)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  // We can't use StackTraceElement.equals because that insists on checking the filename and line
+  // number.
+  // That would be version-specific.
+  private static boolean stackTraceElementIs(
+      StackTraceElement e, String className, String methodName, boolean isNative) {
+    return e.getClassName().equals(className)
+        && e.getMethodName().equals(methodName)
+        && e.isNativeMethod() == isNative;
+  }
+
+  private String stackTraceToString(StackTraceElement[] stackTrace) {
+    StringBuilder result = new StringBuilder();
+    // We used to avoid showing any code above where this class gets
+    // involved in event dispatch, but that hides potentially useful
+    // information when dealing with modal dialogs. Maybe we should
+    // reinstate that, but search from the other end of the stack?
+    for (StackTraceElement stackTraceElement : stackTrace) {
+      String indentation = "    ";
+      result.append("\n").append(indentation).append(stackTraceElement);
+    }
+    return result.toString();
+  }
+
+  private boolean stacksEqual(StackTraceElement[] a, StackTraceElement[] b) {
+    if (a == null) {
+      return false;
+    }
+    if (a.length != b.length) {
+      return false;
+    }
+    for (int i = 0; i < a.length; ++i) {
+      if (!a[i].equals(b[i])) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private void checkForDeadlock() {
+    ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
+    long[] threadIds = threadBean.findMonitorDeadlockedThreads();
+    if (threadIds == null) {
+      return;
+    }
+    logConsumer.accept("Deadlock detected involving the following threads:");
+    for (ThreadInfo info : threadBean.getThreadInfo(threadIds, Integer.MAX_VALUE)) {
+      logConsumer.accept(
+          String.format(
+              "Thread #%d %s (%s) waiting on lock(%s) held by %s stack:%s",
+              info.getThreadId(),
+              info.getThreadName(),
+              info.getThreadState(),
+              info.getLockName(),
+              info.getLockOwnerName(),
+              stackTraceToString(info.getStackTrace())));
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/main/java/org/jdesktop/swinghelper/debug/EventDispatchThreadHangMonitor.java b/src/main/java/org/jdesktop/swinghelper/debug/EventDispatchThreadHangMonitor.java
index 47541af..dbc8295 100644
--- a/src/main/java/org/jdesktop/swinghelper/debug/EventDispatchThreadHangMonitor.java
+++ b/src/main/java/org/jdesktop/swinghelper/debug/EventDispatchThreadHangMonitor.java
@@ -1,463 +1,118 @@
-/*
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
- */
-
 package org.jdesktop.swinghelper.debug;
 
-import java.awt.*;
-import java.awt.event.*;
-import java.lang.management.*;
-import java.util.*;
-import java.util.Timer;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
-import javax.swing.*;
+import java.awt.AWTEvent;
+import java.awt.EventQueue;
+import java.awt.Toolkit;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.Consumer;
 
 /**
- * Monitors the AWT event dispatch thread for events that take longer than
- * a certain time to be dispatched.
+ * Monitors the AWT event dispatch thread for events that take longer than a certain time to be
+ * dispatched.
  * <p/>
- * The principle is to record the time at which we start processing an event,
- * and have another thread check frequently to see if we're still processing.
- * If the other thread notices that we've been processing a single event for
- * too long, it prints a stack trace showing what the event dispatch thread
- * is doing, and continues to time it until it finally finishes.
+ * The principle is to record the time at which we start processing an event, and have another
+ * thread check frequently to see if we're still processing. If the other thread notices that we've
+ * been processing a single event for too long, it prints a stack trace showing what the event
+ * dispatch thread is doing, and continues to time it until it finally finishes.
  * <p/>
- * This is useful in determining what code is causing your Java application's
- * GUI to be unresponsive.
- * 
- * <p>The original blog can be found here<br>  
- * <a href="http://elliotth.blogspot.com/2005/05/automatically-detecting-awt-event.html">
- * Automatically detecting AWT event dispatch thread hangs</a>
- * </p>
+ * This is useful in determining what code is causing your Java application's GUI to be
+ * unresponsive.
  *
- * @author Elliott Hughes <enh@jessies.org>
- * 
- * Advice, bug fixes, and test cases from
- * Alexander Potochkin and Oleg Sukhodolsky.
- * 
- * https://swinghelper.dev.java.net/
+ * <p>The original source code can be found here<br>
+ * <a href="https://github.com/floscher/swinghelper/blob/master/src/java/org/jdesktop/swinghelper/debug/EventDispatchThreadHangMonitor.java">
+ * EventDispatchThreadHangMonitor.java</a>
+ * </p>
  */
-public final class EventDispatchThreadHangMonitor extends EventQueue {
-    private static final EventDispatchThreadHangMonitor INSTANCE = new EventDispatchThreadHangMonitor();
+public final class EventDispatchThreadHangMonitor extends EventQueue implements Runnable {
 
-    // Time to wait between checks that the event dispatch thread isn't hung.
-    private static final long CHECK_INTERVAL_MS = 100;
+  // Time to wait between checks that the event dispatch thread isn't hung.
+  private static final long CHECK_INTERVAL_MS = 100;
 
-    // Maximum time we won't warn about. This used to be 500 ms, but 1.5 on
-    // late-2004 hardware isn't really up to it; there are too many parts of
-    // the JDK that can go away for that long (often code that has to be
-    // called on the event dispatch thread, like font loading).
-    private static final long UNREASONABLE_DISPATCH_DURATION_MS = 1000;
+  // Maximum time we won't warn about. This used to be 500 ms, but 1.5 on
+  // late-2004 hardware isn't really up to it; there are too many parts of
+  // the JDK that can go away for that long (often code that has to be
+  // called on the event dispatch thread, like font loading).
+  static final long UNREASONABLE_DISPATCH_DURATION_MS = 1000;
 
-    // Help distinguish multiple hangs in the log, and match start and end too.
-    // Only access this via getNewHangNumber.
-    private static int hangCount = 0;
+  // The currently outstanding event dispatches. The implementation of
+  // modal dialogs is a common cause for multiple outstanding dispatches.
+  private final Deque<DispatchInfo> dispatches = new ArrayDeque<>();
 
-    // Prevents us complaining about hangs during start-up, which are probably
-    // the JVM vendor's fault.
-    private boolean haveShownSomeComponent = false;
+  // The scheduler to regularly check whether a hang event is detected or not.
+  private final ScheduledExecutorService scheduler =
+      Executors.newSingleThreadScheduledExecutor(
+          runnable -> {
+            Thread t = new Thread(runnable);
+            t.setDaemon(true);
+            return t;
+          });
 
-    // The currently outstanding event dispatches. The implementation of
-    // modal dialogs is a common cause for multiple outstanding dispatches.
-    private final LinkedList<DispatchInfo> dispatches = new LinkedList<DispatchInfo>();
+  private final Consumer<String> logConsumer;
 
-    private static class DispatchInfo {
-        // The last-dumped hung stack trace for this dispatch.
-        private StackTraceElement[] lastReportedStack;
-        // If so; what was the identifying hang number?
-        private int hangNumber;
+  /**
+   * Constructs {@link EventDispatchThreadHangMonitor}.
+   */
+  public EventDispatchThreadHangMonitor() {
+    this(System.out::println);
+  }
 
-        // The EDT for this dispatch (for the purpose of getting stack traces).
-        // I don't know of any API for getting the event dispatch thread,
-        // but we can assume that it's the current thread if we're in the
-        // middle of dispatching an AWT event...
-        // We can't cache this because the EDT can die and be replaced by a
-        // new EDT if there's an uncaught exception.
-        private final Thread eventDispatchThread = Thread.currentThread();
+  /**
+   * Constructs {@link EventDispatchThreadHangMonitor}.
+   *
+   * @param logConsumer the custom log consumer
+   */
+  public EventDispatchThreadHangMonitor(Consumer<String> logConsumer) {
+    this.logConsumer = logConsumer;
+  }
 
-        // The last time in milliseconds at which we saw a dispatch on the above thread.
-        private long lastDispatchTimeMillis = System.currentTimeMillis();
+  /**
+   * Sets up hang detection for the event dispatch thread.
+   */
+  public void initMonitoring() {
+    Toolkit.getDefaultToolkit().getSystemEventQueue().push(this);
+    Future<?> unused = scheduler.scheduleWithFixedDelay(this, 0, CHECK_INTERVAL_MS, MILLISECONDS);
+  }
 
-        public DispatchInfo() {
-            // All initialization is done by the field initializers.
+  @Override
+  protected void dispatchEvent(AWTEvent event) {
+    DispatchInfo currentDispatchInfo = new DispatchInfo(logConsumer);
+    try {
+      synchronized (dispatches) {
+        dispatches.addLast(currentDispatchInfo);
+      }
+      super.dispatchEvent(event);
+    } finally {
+      synchronized (dispatches) {
+        // We've finished the most nested dispatch, and don't need it any longer.
+        dispatches.removeLast();
+        currentDispatchInfo.dispose();
+
+        // The other dispatches, which have been waiting, need to be credited extra time.
+        // We do this rather simplistically by pretending they've just been redispatched.
+        Thread currentEventDispatchThread = Thread.currentThread();
+        for (DispatchInfo dispatchInfo : dispatches) {
+          if (dispatchInfo.eventDispatchThread == currentEventDispatchThread) {
+            dispatchInfo.lastDispatchTimeMillis = System.currentTimeMillis();
+          }
         }
-
-        public void checkForHang() {
-            if (timeSoFar() > UNREASONABLE_DISPATCH_DURATION_MS) {
-                examineHang();
-            }
-        }
-
-        // We can't use StackTraceElement.equals because that insists on checking the filename and line number.
-        // That would be version-specific.
-        private static boolean stackTraceElementIs(StackTraceElement e, String className, String methodName, boolean isNative) {
-            return e.getClassName().equals(className) && e.getMethodName().equals(methodName) && e.isNativeMethod() == isNative;
-        }
-
-        // Checks whether the given stack looks like it's waiting for another event.
-        // This relies on JDK implementation details.
-        private boolean isWaitingForNextEvent(StackTraceElement[] currentStack) {
-            return stackTraceElementIs(currentStack[0], "java.lang.Object", "wait", true) && stackTraceElementIs(currentStack[1], "java.lang.Object", "wait", false) && stackTraceElementIs(currentStack[2], "java.awt.EventQueue", "getNextEvent", false);
-        }
-
-        private void examineHang() {
-            StackTraceElement[] currentStack = eventDispatchThread.getStackTrace();
-
-            if (isWaitingForNextEvent(currentStack)) {
-                // Don't be fooled by a modal dialog if it's waiting for its next event.
-                // As long as the modal dialog's event pump doesn't get stuck, it's okay for the outer pump to be suspended.
-                return;
-            }
-
-            if (stacksEqual(lastReportedStack, currentStack)) {
-                // Don't keep reporting the same hang every time the timer goes off.
-                return;
-            }
-
-            hangNumber = getNewHangNumber();
-            String stackTrace = stackTraceToString(currentStack);
-            lastReportedStack = currentStack;
-            Log.warn("(hang #" + hangNumber + ") event dispatch thread stuck processing event for " + timeSoFar() + " ms:" + stackTrace);
-            checkForDeadlock();
-        }
-
-        private static boolean stacksEqual(StackTraceElement[] a, StackTraceElement[] b) {
-            if (a == null) {
-                return false;
-            }
-            if (a.length != b.length) {
-                return false;
-            }
-            for (int i = 0; i < a.length; ++i) {
-                if (a[i].equals(b[i]) == false) {
-                    return false;
-                }
-            }
-            return true;
-        }
-
-        /**
-         * Returns how long this dispatch has been going on (in milliseconds).
-         */
-        private long timeSoFar() {
-            return (System.currentTimeMillis() - lastDispatchTimeMillis);
-        }
-
-        public void dispose() {
-            if (lastReportedStack != null) {
-                Log.warn("(hang #" + hangNumber + ") event dispatch thread unstuck after " + timeSoFar() + " ms.");
-            }
-        }
+      }
     }
+  }
 
-    private EventDispatchThreadHangMonitor() {
-        initTimer();
+  @Override
+  public void run() {
+    synchronized (dispatches) {
+      if (!dispatches.isEmpty()) {
+        // Only the most recent dispatch can be hung; nested dispatches
+        // by their nature cause the outer dispatch pump to be suspended.
+        dispatches.getLast().checkForHang();
+      }
     }
-
-    /**
-     * Sets up a timer to check for hangs frequently.
-     */
-    private void initTimer() {
-        final long initialDelayMs = 0;
-        final boolean isDaemon = true;
-        Timer timer = new Timer("EventDispatchThreadHangMonitor", isDaemon);
-        timer.schedule(new HangChecker(), initialDelayMs, CHECK_INTERVAL_MS);
-    }
-
-    private class HangChecker extends TimerTask {
-        @Override
-        public void run() {
-            synchronized (dispatches) {
-                if (dispatches.isEmpty() || !haveShownSomeComponent) {
-                    // Nothing to do.
-                    // We don't destroy the timer when there's nothing happening
-                    // because it would mean a lot more work on every single AWT
-                    // event that gets dispatched.
-                    return;
-                }
-                // Only the most recent dispatch can be hung; nested dispatches
-                // by their nature cause the outer dispatch pump to be suspended.
-                dispatches.getLast().checkForHang();
-            }
-        }
-    }
-
-    /**
-     * Sets up hang detection for the event dispatch thread.
-     */
-    public static void initMonitoring() {
-        Toolkit.getDefaultToolkit().getSystemEventQueue().push(INSTANCE);
-    }
-
-    /**
-     * Overrides EventQueue.dispatchEvent to call our pre and post hooks either
-     * side of the system's event dispatch code.
-     */
-    @Override
-    protected void dispatchEvent(AWTEvent event) {
-        try {
-            preDispatchEvent();
-            super.dispatchEvent(event);
-        } finally {
-            postDispatchEvent();
-            if (!haveShownSomeComponent && 
-                    event instanceof WindowEvent && event.getID() == WindowEvent.WINDOW_OPENED) {
-                haveShownSomeComponent = true;
-            }
-        }
-    }
-
-    private void debug(String which) {
-        if (false) {
-            for (int i = dispatches.size(); i >= 0; --i) {
-                System.out.print(' ');
-            }
-            System.out.println(which);
-        }
-    }
-
-    /**
-     * Starts tracking a dispatch.
-     */
-    private synchronized void preDispatchEvent() {
-        debug("pre");
-        synchronized (dispatches) {
-            dispatches.addLast(new DispatchInfo());
-        }
-    }
-
-    /**
-     * Stops tracking a dispatch.
-     */
-    private synchronized void postDispatchEvent() {
-        synchronized (dispatches) {
-            // We've finished the most nested dispatch, and don't need it any longer.
-            DispatchInfo justFinishedDispatch = dispatches.removeLast();
-            justFinishedDispatch.dispose();
-
-            // The other dispatches, which have been waiting, need to be credited extra time.
-            // We do this rather simplistically by pretending they've just been redispatched.
-            Thread currentEventDispatchThread = Thread.currentThread();
-            for (DispatchInfo dispatchInfo : dispatches) {
-                if (dispatchInfo.eventDispatchThread == currentEventDispatchThread) {
-                    dispatchInfo.lastDispatchTimeMillis = System.currentTimeMillis();
-                }
-            }
-        }
-        debug("post");
-    }
-
-    private static void checkForDeadlock() {
-        ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
-        long[] threadIds = threadBean.findMonitorDeadlockedThreads();
-        if (threadIds == null) {
-            return;
-        }
-        Log.warn("deadlock detected involving the following threads:");
-        ThreadInfo[] threadInfos = threadBean.getThreadInfo(threadIds, Integer.MAX_VALUE);
-        for (ThreadInfo info : threadInfos) {
-            Log.warn("Thread #" + info.getThreadId() + " " + info.getThreadName() + 
-                    " (" + info.getThreadState() + ") waiting on " + info.getLockName() + 
-                    " held by " + info.getLockOwnerName() + stackTraceToString(info.getStackTrace()));
-        }
-    }
-
-    private static String stackTraceToString(StackTraceElement[] stackTrace) {
-        StringBuilder result = new StringBuilder();
-        // We used to avoid showing any code above where this class gets
-        // involved in event dispatch, but that hides potentially useful
-        // information when dealing with modal dialogs. Maybe we should
-        // reinstate that, but search from the other end of the stack?
-        for (StackTraceElement stackTraceElement : stackTrace) {
-            String indentation = "    ";
-            result.append("\n" + indentation + stackTraceElement);
-        }
-        return result.toString();
-    }
-
-    private synchronized static int getNewHangNumber() {
-        return ++hangCount;
-    }
-
-    public static void main(String[] args) {
-        initMonitoring();
-        //special case for deadlock test
-        if (args.length > 0 && "deadlock".equals(args[0])) {
-            EventDispatchThreadHangMonitor.INSTANCE.haveShownSomeComponent = true;
-            Tests.runDeadlockTest();
-            return;
-        }
-        Tests.main(args);
-    }
-
-    private static class Tests {
-        public static void main(final String[] args) {
-
-            java.awt.EventQueue.invokeLater(new Runnable() {
-                public void run() {
-                    for (String arg : args) {
-                        final JFrame frame = new JFrame();
-                        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
-                        frame.setLocationRelativeTo(null);
-                        if (arg.equals("exception")) {
-                            runExceptionTest(frame);
-                        } else if (arg.equals("focus")) {
-                            runFocusTest(frame);
-                        } else if (arg.equals("modal-hang")) {
-                            runModalTest(frame, true);
-                        } else if (arg.equals("modal-no-hang")) {
-                            runModalTest(frame, false);
-                        } else {
-                            System.err.println("unknown regression test '" + arg + "'");
-                            System.exit(1);
-                        }
-                        frame.pack();
-                        frame.setVisible(true);
-                    }
-                }
-            });
-        }
-
-        private static void runDeadlockTest() {
-            class Locker {
-                private Locker locker;
-
-                public void setLocker(Locker locker) {
-                    this.locker = locker;
-                }
-
-                public synchronized void tryToDeadlock() {
-                    locker.toString();
-                }
-
-                public synchronized String toString() {
-                    try {
-                        Thread.sleep(50);
-                    } catch (InterruptedException e) {
-                        e.printStackTrace();
-                    }
-                    return super.toString();
-                }
-            }
-            final Locker one = new Locker();
-            final Locker two = new Locker();
-            one.setLocker(two);
-            two.setLocker(one);
-
-            //Deadlock expected here:
-            for (int i = 0; i < 100; i++) {
-                SwingUtilities.invokeLater(new Runnable() {
-                    public void run() {
-                        one.tryToDeadlock();
-                    }
-                });
-                two.tryToDeadlock();
-            }
-        }
-
-        // If we don't do our post-dispatch activity in a finally block, we'll
-        // report bogus hangs.
-        private static void runExceptionTest(final JFrame frame) {
-            JButton button = new JButton("Throw Exception");
-            button.addActionListener(new ActionListener() {
-                public void actionPerformed(ActionEvent e) {
-                    // This shouldn't cause us to report a hang.
-                    throw new RuntimeException("Nobody expects the Spanish Inquisition!");
-                }
-            });
-            frame.add(button);
-        }
-
-        // A demonstration of nested calls to dispatchEvent caused by SequencedEvent.
-        private static void runFocusTest(final JFrame frame) {
-            final JDialog dialog = new JDialog(frame, "Non-Modal Dialog");
-            dialog.add(new JLabel("Close me!"));
-            dialog.pack();
-            dialog.setLocationRelativeTo(frame);
-            dialog.addWindowFocusListener(new WindowAdapter() {
-                public void windowGainedFocus(WindowEvent e) {
-                    System.out.println("FocusTest.windowGainedFocus");
-                    // If you don't cope with nested calls to dispatchEvent, you won't detect this.
-                    // See java.awt.SequencedEvent for an example.
-                    sleep(2500);
-                }
-            });
-            JButton button = new JButton("Show Non-Modal Dialog");
-            button.addActionListener(new ActionListener() {
-                public void actionPerformed(ActionEvent e) {
-                    dialog.setVisible(true);
-                }
-            });
-            frame.add(button);
-        }
-
-        // A demonstration of the problems of dealing with modal dialogs.
-        private static void runModalTest(final JFrame frame, final boolean shouldSleep) {
-            System.out.println(shouldSleep ? "Expect hangs!" : "There should be no hangs...");
-            JButton button = new JButton("Show Modal Dialog");
-            button.addActionListener(new ActionListener() {
-                public void actionPerformed(ActionEvent e) {
-                    if (shouldSleep) {
-                        sleep(2500); // This is easy.
-                    }
-                    JDialog dialog = new JDialog(frame, "Modal dialog", true);
-                    dialog.setLayout(new FlowLayout());
-                    dialog.add(new JLabel("Close this dialog!"));
-                    final JLabel label = new JLabel(" ");
-                    dialog.add(label);
-                    dialog.pack();
-                    dialog.setLocation(frame.getX() - 100, frame.getY());
-
-                    // Make sure the new event pump has some work to do, each unit of which is insufficient to cause a hang.
-                    new Thread(new Runnable() {
-                        public void run() {
-                            for (int i = 0; i <= 100000; ++i) {
-                                final int value = i;
-                                EventQueue.invokeLater(new Runnable() {
-                                    public void run() {
-                                        label.setText(Integer.toString(value));
-                                    }
-                                });
-                            }
-                        }
-                    }).start();
-
-                    dialog.setVisible(true);
-
-                    if (shouldSleep) {
-                        sleep(2500); // If you don't distinguish different stack traces, you won't report this.
-                    }
-                }
-            });
-            frame.add(button);
-        }
-
-        private static void sleep(long ms) {
-            try {
-                System.out.println("Sleeping for " + ms + " ms on " + Thread.currentThread() + "...");
-                Thread.sleep(ms);
-                System.out.println("Finished sleeping...");
-            } catch (Exception ex) {
-                ex.printStackTrace();
-            }
-        }
-    }
-
-    private static class Log {
-        public static void warn(String str) {
-            System.out.println(str);
-        }
-    }
-}
+  }
+}
\ No newline at end of file
diff --git a/src/main/java/org/jdesktop/swinghelper/debug/LICENSE b/src/main/java/org/jdesktop/swinghelper/debug/LICENSE
new file mode 100644
index 0000000..de3016f
--- /dev/null
+++ b/src/main/java/org/jdesktop/swinghelper/debug/LICENSE
@@ -0,0 +1,13 @@
+  This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+  This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
\ No newline at end of file
diff --git a/src/main/java/org/jdesktop/swinghelper/debug/Tests.java b/src/main/java/org/jdesktop/swinghelper/debug/Tests.java
new file mode 100644
index 0000000..e9e314a
--- /dev/null
+++ b/src/main/java/org/jdesktop/swinghelper/debug/Tests.java
@@ -0,0 +1,190 @@
+package org.jdesktop.swinghelper.debug;
+
+import java.awt.EventQueue;
+import java.awt.FlowLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.SwingUtilities;
+
+final class Tests {
+
+  public static void main(String[] args) {
+    if (args.length == 0) {
+      return;
+    }
+    String mode = args[0];
+
+    new EventDispatchThreadHangMonitor().initMonitoring();
+
+    if ("deadlock".equals(mode)) {
+      runDeadlockTest();
+      return;
+    }
+
+    EventQueue.invokeLater(
+        () -> {
+          final JFrame frame = new JFrame();
+          frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+          frame.setLocationRelativeTo(null);
+          switch (mode) {
+            case "exception":
+              runExceptionTest(frame);
+              break;
+            case "focus":
+              runFocusTest(frame);
+              break;
+            case "modal-hang":
+              runModalTest(frame, true);
+              break;
+            case "modal-no-hang":
+              runModalTest(frame, false);
+              break;
+            default:
+              System.err.println("unknown regression test '" + mode + "'");
+              System.exit(1);
+          }
+          frame.pack();
+          frame.setVisible(true);
+        });
+  }
+
+  public static void runDeadlockTest() {
+    class Locker {
+
+      private Locker locker;
+
+      public void setLocker(Locker locker) {
+        this.locker = locker;
+      }
+
+      public synchronized void tryToDeadlock() {
+        String unused = locker.toString();
+      }
+
+      @Override
+      @SuppressWarnings("CatchAndPrintStackTrace")
+      public synchronized String toString() {
+        try {
+          Thread.sleep(50);
+        } catch (InterruptedException e) {
+          e.printStackTrace();
+        }
+        return super.toString();
+      }
+    }
+
+    final Locker one = new Locker();
+    final Locker two = new Locker();
+    one.setLocker(two);
+    two.setLocker(one);
+
+    // Deadlock expected here:
+    for (int i = 0; i < 100; i++) {
+      SwingUtilities.invokeLater(one::tryToDeadlock);
+      two.tryToDeadlock();
+    }
+  }
+
+  // If we don't do our post-dispatch activity in a finally block, we'll
+  // report bogus hangs.
+  private static void runExceptionTest(final JFrame frame) {
+    JButton button = new JButton("Throw Exception");
+    button.addActionListener(
+        new ActionListener() {
+          @Override
+          public void actionPerformed(ActionEvent e) {
+            // This shouldn't cause us to report a hang.
+            throw new IllegalStateException("Nobody expects the Spanish Inquisition!");
+          }
+        });
+    frame.add(button);
+  }
+
+  // A demonstration of nested calls to dispatchEvent caused by SequencedEvent.
+  private static void runFocusTest(final JFrame frame) {
+    final JDialog dialog = new JDialog(frame, "Non-Modal Dialog");
+    dialog.add(new JLabel("Close me!"));
+    dialog.pack();
+    dialog.setLocationRelativeTo(frame);
+    dialog.addWindowFocusListener(
+        new WindowAdapter() {
+          @Override
+          public void windowGainedFocus(WindowEvent e) {
+            System.out.println("FocusTest.windowGainedFocus");
+            // If you don't cope with nested calls to dispatchEvent, you won't detect this.
+            // See java.awt.SequencedEvent for an example.
+            sleep(2500);
+          }
+        });
+    JButton button = new JButton("Show Non-Modal Dialog");
+    button.addActionListener(
+        new ActionListener() {
+          @Override
+          public void actionPerformed(ActionEvent e) {
+            dialog.setVisible(true);
+          }
+        });
+    frame.add(button);
+  }
+
+  // A demonstration of the problems of dealing with modal dialogs.
+  private static void runModalTest(final JFrame frame, final boolean shouldSleep) {
+    System.out.println(shouldSleep ? "Expect hangs!" : "There should be no hangs...");
+    JButton button = new JButton("Show Modal Dialog");
+    button.addActionListener(
+        new ActionListener() {
+          @Override
+          public void actionPerformed(ActionEvent e) {
+            if (shouldSleep) {
+              sleep(2500); // This is easy.
+            }
+            JDialog dialog = new JDialog(frame, "Modal dialog", /* modal= */ true);
+            dialog.setLayout(new FlowLayout());
+            dialog.add(new JLabel("Close this dialog!"));
+            final JLabel label = new JLabel(" ");
+            dialog.add(label);
+            dialog.pack();
+            dialog.setLocation(frame.getX() - 100, frame.getY());
+
+            // Make sure the new event pump has some work to do, each unit of which is insufficient
+            // to cause a hang.
+            new Thread(
+                () -> {
+                  for (int i = 0; i <= 100000; ++i) {
+                    final int value = i;
+                    EventQueue.invokeLater(() -> label.setText(Integer.toString(value)));
+                  }
+                })
+                .start();
+
+            dialog.setVisible(true);
+
+            if (shouldSleep) {
+              sleep(
+                  2500); // If you don't distinguish different stack traces, you won't report this.
+            }
+          }
+        });
+    frame.add(button);
+  }
+
+  @SuppressWarnings("CatchAndPrintStackTrace")
+  private static void sleep(long ms) {
+    try {
+      System.out.println("Sleeping for " + ms + " ms on " + Thread.currentThread() + "...");
+      Thread.sleep(ms);
+      System.out.println("Finished sleeping...");
+    } catch (InterruptedException ex) {
+      ex.printStackTrace();
+    }
+  }
+
+  private Tests() {
+  }
+}
\ No newline at end of file