From 265804a750bcb27e487cf285e3b1527955cd0efd Mon Sep 17 00:00:00 2001
From: zzz <zzz@i2pmail.org>
Date: Mon, 6 Jun 2022 17:01:15 -0400
Subject: [PATCH] Console: Add deadlock detector

---
 .../i2p/router/web/ConfigServiceHandler.java  |   3 +
 .../net/i2p/router/web/DeadlockDetector.java  | 138 ++++++++++++++++++
 .../i2p/router/web/RouterConsoleRunner.java   |   1 +
 3 files changed, 142 insertions(+)
 create mode 100644 apps/routerconsole/java/src/net/i2p/router/web/DeadlockDetector.java

diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java
index 71a2e410b9..3f06e0d545 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java
@@ -277,6 +277,9 @@ public class ConfigServiceHandler extends FormHandler {
             }
             File wlog = wrapperLogFile(_context);
             addFormNotice(_t("Threads dumped to {0}", wlog.getAbsolutePath()));
+            boolean deadlock = DeadlockDetector.detect(_context);
+            if (deadlock)
+                addFormErrorNoEscape("Deadlock detected!<br><a href=\"/logs\">Please report using the information on the logs page!</a><br>After reporting, please restart your router!");
         } else if (_t("View console on startup").equals(_action)) {
             browseOnStartup(true);
             addFormNotice(_t("Console is to be shown on startup"));
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/DeadlockDetector.java b/apps/routerconsole/java/src/net/i2p/router/web/DeadlockDetector.java
new file mode 100644
index 0000000000..c32678b867
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/web/DeadlockDetector.java
@@ -0,0 +1,138 @@
+package net.i2p.router.web;
+
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadInfo;
+import java.lang.management.ThreadMXBean;
+
+import net.i2p.I2PAppContext;
+import net.i2p.util.Log;
+import net.i2p.util.SimpleTimer2;
+
+/**
+ *  Periodic check
+ *  ref: https://dzone.com/articles/how-detect-java-deadlocks
+ *
+ *  In routerconsole because java.lang.management is
+ *  not available in Android.
+ *
+ *  @since 0.9.55
+ */
+class DeadlockDetector extends SimpleTimer2.TimedEvent {
+
+    private final I2PAppContext _context;
+    private final Log _log;
+    private static final String PROP_INTERVAL = "router.deadlockDetectIntervalHours";
+    private static final long DEFAULT_INTERVAL = 24;
+
+    public DeadlockDetector(I2PAppContext ctx) {
+        super(ctx.simpleTimer2());
+        _context = ctx;
+        _log = _context.logManager().getLog(DeadlockDetector.class);
+        long interval = getInterval();
+        if (interval > 0)
+            schedule(interval);
+    }
+
+    private long getInterval() {
+        long rv = _context.getProperty(PROP_INTERVAL, DEFAULT_INTERVAL);
+        return rv * 60*60*1000L;
+    }
+
+    public void timeReached() {
+        long start = System.currentTimeMillis();
+        boolean detected = detect();
+        if (!detected) {
+            long time = System.currentTimeMillis() - start;
+            if (_log.shouldDebug())
+                _log.debug("No deadlocks detected, took " + time + "ms");
+            long interval = getInterval();
+            if (interval > 0)
+                schedule(interval);
+        }
+    }
+
+    private boolean detect() {
+        return detect(_context);
+    }
+
+    public static boolean detect(I2PAppContext ctx) {
+        try {
+            ThreadMXBean mxb = ManagementFactory.getThreadMXBean();
+            long[] ids = mxb.findDeadlockedThreads();
+            if (ids == null)
+                return false;
+            ThreadInfo[] infos;
+            try {
+                // java 10
+                //infos = mxb.getThreadInfo(ids, true, true, Integer.MAX_VALUE);
+                // java 6
+                infos = mxb.getThreadInfo(ids, true, true);
+            } catch (UnsupportedOperationException e) {
+                // won't throw
+                infos = mxb.getThreadInfo(ids, Integer.MAX_VALUE);
+            }
+            StringBuilder buf = new StringBuilder(2048);
+            buf.append("Deadlock detected, please report\n\n");
+            for (int i = 0; i < infos.length; i++) {
+                ThreadInfo info = infos[i];
+                if (info == null)
+                    continue;
+                buf.append("Thread ").append(i).append(':');
+                buf.append(info.toString());
+                StackTraceElement[] stes = info.getStackTrace();
+                buf.append("        Stack Trace:\n");
+                for (StackTraceElement ste : stes) {
+                    buf.append("        at ").append(ste.toString()).append('\n');
+                }
+                buf.append('\n');
+            }
+            buf.append("\nAfter reporting, please restart your router!\n");
+            Log log = ctx.logManager().getLog(DeadlockDetector.class);
+            log.log(Log.CRIT, buf.toString());
+        } catch (Throwable t) {
+            // class not found, unsupportedoperation, ...
+            Log log = ctx.logManager().getLog(DeadlockDetector.class);
+            log.warn("fail", t);
+            return false;
+        }
+        return true;
+    }
+
+/*
+    public static void main(String[] args) {
+        final Object o1 = new Object();
+        final Object o2 = new Object();
+        Thread t1 = new Thread(new Runnable() {
+            public void run() {
+                synchronized(o1) {
+                    try { Thread.sleep(1000); } catch (InterruptedException ie) {}
+                    // should hang here
+                    synchronized(o2) {
+                        System.out.println("Test fail");
+                    }
+                }
+            }
+        });
+        t1.start();
+        Thread t2 = new Thread(new Runnable() {
+            public void run() {
+                synchronized(o2) {
+                    // should hang here
+                    synchronized(o1) {
+                        System.out.println("Test fail");
+                    }
+                }
+            }
+        });
+        t2.start();
+        try { Thread.sleep(1000); } catch (InterruptedException ie) {}
+        long start = System.currentTimeMillis();
+        boolean yes = detect(I2PAppContext.getGlobalContext());
+        if (!yes)
+            System.out.println("Test fail");
+        long time = System.currentTimeMillis() - start;
+        System.out.println("Test took " + time + "ms");
+    }
+*/
+}
+
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
index 821cf0776d..eddae60e20 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
@@ -233,6 +233,7 @@ public class RouterConsoleRunner implements RouterApp {
         checkJavaVersion();
         startTrayApp();
         startConsole();
+        new DeadlockDetector(_context);
     }
 
     /** @since 0.9.4 */
-- 
GitLab