/** * ThreaddumpServlet * Copyright 03.07.2015 by Michael Peter Christen, @0rb1t3r * * 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 program in the file lgpl21.txt * If not, see <http://www.gnu.org/licenses/>. */ package org.loklak.api.admin; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.lang.Thread.State; import java.lang.management.ManagementFactory; import java.lang.management.OperatingSystemMXBean; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.SortedSet; import java.util.TreeSet; import java.util.regex.Pattern; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.WriteListener; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.util.log.Log; import org.loklak.Caretaker; import org.loklak.data.DAO; import org.loklak.http.RemoteAccess; import org.loklak.server.FileHandler; import org.loklak.server.Query; import org.loklak.tools.UTF8; public class ThreaddumpServlet extends HttpServlet { private static final long serialVersionUID = -7095346222464124198L; private static final String multiDumpFilter = ".*((java.net.DatagramSocket.receive)|(java.lang.Thread.getAllStackTraces)|(java.net.SocketInputStream.read)|(java.net.ServerSocket.accept)|(java.net.Socket.connect)).*"; private static final Pattern multiDumpFilterPattern = Pattern.compile(multiDumpFilter); private static ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); private static OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); private static final Thread.State[] ORDERED_STATES = new Thread.State[]{ Thread.State.BLOCKED, Thread.State.RUNNABLE, Thread.State.TIMED_WAITING, Thread.State.WAITING, Thread.State.NEW, Thread.State.TERMINATED}; @Override protected void doPost(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } @Override protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { Query post = RemoteAccess.evaluate(request); String servlet = post.isLocalhostAccess() ? post.get("servlet", "") : ""; if (servlet.length() > 0) { try { final Class<?> servletClass = ClassLoader.getSystemClassLoader().loadClass("org.loklak.api.server." + servlet); final Method getMethod = servletClass.getDeclaredMethod("doGet", HttpServletRequest.class, HttpServletResponse.class); final Thread servletThread = new Thread(new Runnable() { public void run() { try { getMethod.invoke(servletClass.newInstance(), request, new DummyResponse()); } catch (IllegalArgumentException | InvocationTargetException | InstantiationException | IllegalAccessException e) { Log.getLog().warn(e); } } }); servletThread.start(); } catch (ClassNotFoundException | NoSuchMethodException | SecurityException e) { Log.getLog().warn(e); } long sleep = post.get("sleep", 0L); if (sleep > 0) try {Thread.sleep(sleep);} catch (InterruptedException e) {} } int multi = post.isLocalhostAccess() ? post.get("multi", post.get("count", 0)) : 0; final StringBuilder buffer = new StringBuilder(1000); // Thread dump final Date dt = new Date(); Runtime runtime = Runtime.getRuntime(); int keylen = 30; bufferappend(buffer, "************* Start Thread Dump " + dt + " *******************"); bufferappend(buffer, ""); bufferappend(buffer, keylen, "Assigned Memory", runtime.maxMemory()); bufferappend(buffer, keylen, "Used Memory", runtime.totalMemory() - runtime.freeMemory()); bufferappend(buffer, keylen, "Available Memory", runtime.maxMemory() - runtime.totalMemory() + runtime.freeMemory()); bufferappend(buffer, keylen, "Cores", runtime.availableProcessors()); bufferappend(buffer, keylen, "Active Thread Count", Thread.activeCount()); bufferappend(buffer, keylen, "Total Started Thread Count", threadBean.getTotalStartedThreadCount()); bufferappend(buffer, keylen, "Peak Thread Count", threadBean.getPeakThreadCount()); bufferappend(buffer, keylen, "System Load Average", osBean.getSystemLoadAverage()); long runtimeseconds = (System.currentTimeMillis() - Caretaker.startupTime) / 1000; int runtimeminutes = (int) (runtimeseconds / 60); runtimeseconds = runtimeseconds % 60; int runtimehours = runtimeminutes / 60; runtimeminutes = runtimeminutes % 60; bufferappend(buffer, keylen, "Runtime", runtimehours + "h " + runtimeminutes + "m " + runtimeseconds + "s"); long timetorestartseconds = (Caretaker.upgradeTime - System.currentTimeMillis()) / 1000; int timetorestartminutes = (int) (timetorestartseconds / 60); timetorestartseconds = timetorestartseconds % 60; int timetorestarthours = timetorestartminutes / 60; timetorestartminutes = timetorestartminutes % 60; bufferappend(buffer, keylen, "Time To Restart", timetorestarthours + "h " + timetorestartminutes + "m " + timetorestartseconds + "s"); // print system beans for (Method method : osBean.getClass().getDeclaredMethods()) try { method.setAccessible(true); if (method.getName().startsWith("get") && Modifier.isPublic(method.getModifiers())) { bufferappend(buffer, keylen, method.getName(), method.invoke(osBean)); } } catch (Throwable e) {} bufferappend(buffer, ""); bufferappend(buffer, ""); if (multi > 0) { // generate multiple dumps final Map<String, Integer> dumps = new HashMap<String, Integer>(); for (int i = 0; i < multi; i++) { try { ThreadDump dump = new ThreadDump(ThreadDump.getAllStackTraces(), Thread.State.RUNNABLE); for (final Map.Entry<StackTrace, SortedSet<String>> e: dump.entrySet()) { if (multiDumpFilterPattern.matcher(e.getKey().text).matches()) continue; Integer c = dumps.get(e.getKey().text); if (c == null) dumps.put(e.getKey().text, Integer.valueOf(e.getValue().size())); else { c = Integer.valueOf(c.intValue() + e.getValue().size()); dumps.put(e.getKey().text, c); } } } catch (final OutOfMemoryError e) { break; } } // write dumps while (!dumps.isEmpty()) { final Map.Entry<String, Integer> e = removeMax(dumps); bufferappend(buffer, "Occurrences: " + e.getValue()); bufferappend(buffer, e.getKey()); bufferappend(buffer, ""); } bufferappend(buffer, ""); } else { // generate a single thread dump final Map<Thread, StackTraceElement[]> stackTraces = ThreadDump.getAllStackTraces(); // write those ordered into the stackTrace list for (Thread.State state: ORDERED_STATES) new ThreadDump(stackTraces, state).appendStackTraces(buffer, state); } ThreadMXBean threadbean = ManagementFactory.getThreadMXBean(); bufferappend(buffer, ""); bufferappend(buffer, "THREAD LIST FROM ThreadMXBean, " + threadbean.getThreadCount() + " threads:"); bufferappend(buffer, ""); ThreadInfo[] threadinfo = threadbean.dumpAllThreads(true, true); for (ThreadInfo ti: threadinfo) { bufferappend(buffer, ti.getThreadName()); } bufferappend(buffer, ""); bufferappend(buffer, "ELASTICSEARCH ClUSTER STATS"); bufferappend(buffer, DAO.clusterStats()); bufferappend(buffer, ""); bufferappend(buffer, "ELASTICSEARCH PENDING ClUSTER TASKS"); bufferappend(buffer, DAO.pendingClusterTasks()); if (post.isLocalhostAccess()) { // this can reveal private data, so keep it on localhost access only bufferappend(buffer, ""); bufferappend(buffer, "ELASTICSEARCH NODE SETTINGS"); bufferappend(buffer, DAO.nodeSettings().toString()); } FileHandler.setCaching(response, 10); post.setResponse(response, "text/plain"); response.getOutputStream().write(UTF8.getBytes(buffer.toString())); post.finalize(); } private static class StackTrace { private String text; private StackTrace(final String text) { this.text = text; } @Override public boolean equals(final Object a) { return (a != null && a instanceof StackTrace && this.text.equals(((StackTrace) a).text)); } @Override public int hashCode() { return this.text.hashCode(); } @Override public String toString() { return this.text; } } private static Map.Entry<String, Integer> removeMax(final Map<String, Integer> result) { Map.Entry<String, Integer> max = null; for (final Map.Entry<String, Integer> e: result.entrySet()) { if (max == null || e.getValue().intValue() > max.getValue().intValue()) { max = e; } } result.remove(max.getKey()); return max; } private static void bufferappend(final StringBuilder buffer, int keylen, final String key, Object value) { if (value instanceof Double) bufferappend(buffer, keylen, key, ((Double) value).toString()); else if (value instanceof Number) bufferappend(buffer, keylen, key, ((Number) value).longValue()); else bufferappend(buffer, keylen, key, value.toString()); } private static final DecimalFormat cardinalFormatter = new DecimalFormat("###,###,###,###,###"); private static void bufferappend(final StringBuilder buffer, int keylen, final String key, long value) { bufferappend(buffer, keylen, key, cardinalFormatter.format(value)); } private static void bufferappend(final StringBuilder buffer, int keylen, final String key, String value) { String a = key; while (a.length() < keylen) a += " "; a += "="; for (int i = value.length(); i < 20; i++) a += " "; a += value; bufferappend(buffer, a); } private static void bufferappend(final StringBuilder buffer, final String a) { buffer.append(a); buffer.append('\n'); } private static class ThreadDump extends HashMap<StackTrace, SortedSet<String>> implements Map<StackTrace, SortedSet<String>> { private static final long serialVersionUID = -5587850671040354397L; private static Map<Thread, StackTraceElement[]> getAllStackTraces() { return Thread.getAllStackTraces(); } private ThreadDump( final Map<Thread, StackTraceElement[]> stackTraces, final Thread.State stateIn) { super(); Thread thread; // collect single dumps for (final Map.Entry<Thread, StackTraceElement[]> entry: stackTraces.entrySet()) { thread = entry.getKey(); final StackTraceElement[] stackTraceElements = entry.getValue(); StackTraceElement ste; String tracename = ""; final State threadState = thread.getState(); final ThreadInfo info = threadBean.getThreadInfo(thread.getId()); if (threadState != null && info != null && (stateIn == null || stateIn.equals(threadState)) && stackTraceElements.length > 0) { final StringBuilder sb = new StringBuilder(3000); final String threadtitle = tracename + "THREAD: " + thread.getName() + " " + (thread.isDaemon()?"daemon":"") + " id=" + thread.getId() + " " + threadState.toString() + (info.getLockOwnerId() >= 0 ? " lock owner =" + info.getLockOwnerId() : ""); boolean cutcore = true; for (int i = 0; i < stackTraceElements.length; i++) { ste = stackTraceElements[i]; String className = ste.getClassName(); String classString = ste.toString(); if (cutcore && (className.startsWith("java.") || className.startsWith("sun."))) { sb.setLength(0); bufferappend(sb, tracename + "at " + classString); } else { cutcore = false; bufferappend(sb, tracename + "at " + classString); } } final StackTrace stackTrace = new StackTrace(sb.toString()); SortedSet<String> threads = get(stackTrace); if (threads == null) { threads = new TreeSet<String>(); put(stackTrace, threads); } threads.add(threadtitle); } } } private void appendStackTraces( final StringBuilder buffer, final Thread.State stateIn) { bufferappend(buffer, "THREADS WITH STATES: " + stateIn.toString()); bufferappend(buffer, ""); // write dumps for (final Map.Entry<StackTrace, SortedSet<String>> entry: entrySet()) { final SortedSet<String> threads = entry.getValue(); for (final String t: threads) bufferappend(buffer, t); bufferappend(buffer, entry.getKey().text); bufferappend(buffer, ""); } bufferappend(buffer, ""); } } private static class DummyResponse implements HttpServletResponse { @Override public void flushBuffer() throws IOException {} @Override public int getBufferSize() {return 2048;} @Override public String getCharacterEncoding() {return "UTF-8";} @Override public String getContentType() {return "text/plain";} @Override public Locale getLocale() {return Locale.ENGLISH;} @Override public boolean isCommitted() {return true;} @Override public void reset() {} @Override public void resetBuffer() {} @Override public void setBufferSize(int arg0) {} @Override public void setCharacterEncoding(String arg0) {} @Override public void setContentLength(int arg0) {} @Override public void setContentLengthLong(long arg0) {} @Override public void setContentType(String arg0) {} @Override public void setLocale(Locale arg0) {} @Override public void addCookie(Cookie arg0) {} @Override public void addDateHeader(String arg0, long arg1) {} @Override public void addHeader(String arg0, String arg1) {} @Override public void addIntHeader(String arg0, int arg1) {} @Override public boolean containsHeader(String arg0) {return true;} @Override public String encodeRedirectURL(String arg0) {return arg0;} @Deprecated @Override public String encodeRedirectUrl(String arg0) {return arg0;} @Override public String encodeURL(String arg0) {return arg0;} @Deprecated @Override public String encodeUrl(String arg0) {return arg0;} @Override public String getHeader(String arg0) {return "";} @Override public Collection<String> getHeaderNames() {return new ArrayList<String>(0);} @Override public Collection<String> getHeaders(String arg0) {return new ArrayList<String>(0);} @Override public int getStatus() {return 200;} @Override public void sendError(int arg0) throws IOException {} @Override public void sendError(int arg0, String arg1) throws IOException {} @Override public void sendRedirect(String arg0) throws IOException {} @Override public void setDateHeader(String arg0, long arg1) {} @Override public void setHeader(String arg0, String arg1) {} @Override public void setIntHeader(String arg0, int arg1) {} @Override public void setStatus(int arg0) {} @Deprecated @Override public void setStatus(int arg0, String arg1) {} @Override public PrintWriter getWriter() throws IOException {return new PrintWriter(new OutputStreamWriter(getOutputStream(), "UTF-8"));} @Override public ServletOutputStream getOutputStream() throws IOException {return new ServletOutputStream(){ public void write(int aByte) throws IOException {} public boolean isReady() { return true; } public void setWriteListener(WriteListener arg0) {} };} } }