001/*
002 *  Copyright 2021 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.runtime.plugins.admin.jvmstatus;
017
018import java.lang.management.LockInfo;
019import java.lang.management.ManagementFactory;
020import java.lang.management.MonitorInfo;
021import java.lang.management.ThreadMXBean;
022import java.time.ZonedDateTime;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.regex.Matcher;
029import java.util.regex.Pattern;
030import java.util.stream.Collectors;
031
032import org.apache.avalon.framework.component.Component;
033
034import org.ametys.core.ui.Callable;
035import org.ametys.core.util.DateUtils;
036
037/**
038 * Component used by the admin thread tool
039 */
040public class ThreadInfo implements Component
041{
042    /** Avalon role */
043    public static final String ROLE = ThreadInfo.class.getName();
044    
045    private Pattern _groupPattern = Pattern.compile("^(.*)-\\d+$");
046
047    /**
048     * Create a threaddump
049     * @return The new threaddump
050     */
051    @Callable(rights = "REPOSITORY_Rights_Access", context = "/admin")
052    public Map<String, Object> getThreadDump()
053    {
054        StringBuffer threadDump = new StringBuffer (System.lineSeparator ());
055        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean ();
056        for (java.lang.management.ThreadInfo threadInfo : threadMXBean.dumpAllThreads (true, true))
057        {
058            threadDump.append (_threadInfoToString(threadInfo));
059        }
060        return Map.of(
061            "date", DateUtils.zonedDateTimeToString(ZonedDateTime.now()),
062            "data", threadDump.toString().replaceAll("\r", "")
063        );
064    }
065    
066    /**
067     * Dump all JVM threads.
068     * @return a JSON-oriented thread dump
069     */
070    @Callable(rights = "REPOSITORY_Rights_Access", context = "/admin")
071    public List<Map<String, Object>> dumpAllThreads()
072    {
073        List<Map<String, Object>> result = new ArrayList<>();
074        
075        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
076        java.lang.management.ThreadInfo[] threads = threadMXBean.dumpAllThreads(true, true);
077        
078        for (java.lang.management.ThreadInfo thread : threads)
079        {
080            Map<String, Object> info = new HashMap<>();
081            
082            String name = thread.getThreadName();
083            String group = "-";
084            Matcher matcher = _groupPattern.matcher(name);
085            if (matcher.matches())
086            {
087                group = matcher.group(1);
088            }
089            
090            info.put("name", name);
091            info.put("group", group);
092            info.put("state", thread.getThreadState());
093            info.put("cpu", threadMXBean.getThreadCpuTime(thread.getThreadId()));
094            info.put("blocked-time", thread.getBlockedTime());
095            info.put("waited-time", thread.getWaitedTime());
096            info.put("lock-owner", thread.getLockOwnerName());
097            
098            String trace = Arrays.stream(thread.getStackTrace())
099                                       .map(el -> _printStackTraceElement(el))
100                                       .collect(Collectors.joining("\n"));
101            info.put("stack-trace", trace);
102            info.put("stack-ametys", Arrays.stream(thread.getStackTrace()).anyMatch(el -> el.getClassName().contains("org.ametys")));
103            info.put("stack-size", thread.getStackTrace().length);
104            
105            result.add(info);
106        }
107        
108        return result;
109    }
110    
111    private String _printStackTraceElement(StackTraceElement el)
112    {
113        return el.getClassName() + "." + el.getMethodName() + "(" + (el.isNativeMethod() ? "Native Method)" : (el.getFileName() != null && el.getLineNumber() >= 0 ? el.getFileName() + ":" + el.getLineNumber() + ")" : (el.getFileName() != null ?  "" + el.getFileName() + ")" : "Unknown Source)")));
114    }
115    
116    // Duplicated from java.lang.management.ThreadInfo#toString removing the MAX_FRAMES limit
117    private String _threadInfoToString(java.lang.management.ThreadInfo threadInfo)
118    {
119        StringBuilder sb = new StringBuilder("\"" + threadInfo.getThreadName() + "\"" + (threadInfo.isDaemon() ? " daemon" : "") + " prio=" + threadInfo.getPriority() + " Id=" + threadInfo.getThreadId() + " " + threadInfo.getThreadState());
120        if (threadInfo.getLockName() != null)
121        {
122            sb.append(" on " + threadInfo.getLockName());
123        }
124        if (threadInfo.getLockOwnerName() != null)
125        {
126            sb.append(" owned by \"" + threadInfo.getLockOwnerName() + "\" Id=" + threadInfo.getLockOwnerId());
127        }
128        if (threadInfo.isSuspended())
129        {
130            sb.append(" (suspended)");
131        }
132        if (threadInfo.isInNative())
133        {
134            sb.append(" (in native)");
135        }
136        sb.append('\n');
137        
138        StackTraceElement[] stackTrace = threadInfo.getStackTrace();
139        
140        int i = 0;
141        for (; i < stackTrace.length; i++)
142        {
143            StackTraceElement ste = stackTrace[i];
144            sb.append("\tat " + ste.toString());
145            sb.append('\n');
146            if (i == 0 && threadInfo.getLockInfo() != null)
147            {
148                Thread.State ts = threadInfo.getThreadState();
149                switch (ts)
150                {
151                    case BLOCKED:
152                        sb.append("\t-  blocked on " + threadInfo.getLockInfo());
153                        sb.append('\n');
154                        break;
155                    case WAITING:
156                        sb.append("\t-  waiting on " + threadInfo.getLockInfo());
157                        sb.append('\n');
158                        break;
159                    case TIMED_WAITING:
160                        sb.append("\t-  waiting on " + threadInfo.getLockInfo());
161                        sb.append('\n');
162                        break;
163                    default:
164                }
165            }
166
167            for (MonitorInfo mi : threadInfo.getLockedMonitors())
168            {
169                if (mi.getLockedStackDepth() == i)
170                {
171                    sb.append("\t-  locked " + mi);
172                    sb.append('\n');
173                }
174            }
175        }
176
177        LockInfo[] locks = threadInfo.getLockedSynchronizers();
178        if (locks.length > 0)
179        {
180            sb.append("\n\tNumber of locked synchronizers = " + locks.length);
181            sb.append('\n');
182            for (LockInfo li : locks)
183            {
184                sb.append("\t- " + li);
185                sb.append('\n');
186            }
187        }
188        sb.append('\n');
189        return sb.toString();
190    }
191}