/*
* Copyright 2008-2014 by Emeric Vernat
*
* This file is part of Java Melody.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.bull.javamelody;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import javax.management.JMException;
import javax.management.MBeanServer;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import javax.servlet.http.HttpSession;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
/**
* Énumération des actions possibles dans l'IHM.
* @author Emeric Vernat
* @author <a href="mailto:davidkarlsen@gmail.com">David J. M. Karlsen (IBM heapdump support)<a>
*/
enum Action { // NOPMD
/**
* Test d'envoi du rapport pdf par mail.
*/
MAIL_TEST(""),
/**
* Réinitialisation d'un compteur non périodique.
*/
CLEAR_COUNTER("http"),
/**
* Garbage Collect.
*/
GC("systeminfo"),
/**
* Invalidations des sessions http.
*/
INVALIDATE_SESSIONS("systeminfo"),
/**
* Invalidation d'une session http.
*/
INVALIDATE_SESSION(""),
/**
* Invalidation de la session http courante.
*/
LOGOUT(""),
/**
* Heap dump.
*/
HEAP_DUMP("systeminfo"),
/**
* Purge le contenu de tous les caches (ie, for ALL_CACHE_MANAGERS {cacheManager.clearAll()})
*/
CLEAR_CACHES("caches"),
/**
* Purge le contenu d'un cache
*/
CLEAR_CACHE("caches"),
/**
* Tue un thread java.
*/
KILL_THREAD("threads"),
/**
* Met un job quartz en pause.
*/
PAUSE_JOB("jobs"),
/**
* Enlève la pause d'un job quartz.
*/
RESUME_JOB("jobs"),
/**
* Réinitialisation des hotspots.
*/
CLEAR_HOTSPOTS(""),
/**
* Purge les fichiers .rrd et .ser.gz obsolètes.
*/
PURGE_OBSOLETE_FILES("bottom");
static final String JAVA_VENDOR = System.getProperty("java.vendor");
/**
* Booléen selon que l'action 'Garbage collector' est possible.
*/
static final boolean GC_ENABLED = !ManagementFactory.getRuntimeMXBean().getInputArguments()
.contains("-XX:+DisableExplicitGC");
private static final String ALL = "all";
/**
* Nom du contexte dans lequel est exécutée l'action
* (servira dans l'url pour replacer la page html sur l'anchor de même nom)
*/
private final String contextName;
private Action(String contextName) {
this.contextName = contextName;
}
String getContextName(String counterName) {
if (this == CLEAR_COUNTER && !ALL.equalsIgnoreCase(counterName)) {
return counterName;
}
return contextName;
}
/**
* Convertit le code d'une action en énumération de l'action.
* @param action String
* @return Action
*/
static Action valueOfIgnoreCase(String action) {
return valueOf(action.toUpperCase(Locale.ENGLISH).trim());
}
/**
* Vérifie que le paramètre pour activer les actions systèmes est positionné.
*/
static void checkSystemActionsEnabled() {
if (!Parameters.isSystemActionsEnabled()) {
throw new IllegalStateException(I18N.getString("Actions_non_activees"));
}
}
// méthode conservée pour compatibilité ascendante
// CHECKSTYLE:OFF
String execute(Collector collector, CollectorServer collectorServer, String counterName, // NOPMD
String sessionId, String threadId, String jobId, String cacheId) throws IOException {
// CHECKSTYLE:ON
return execute(collector, collectorServer, null, counterName, sessionId, threadId, jobId,
cacheId);
}
/**
* Exécute l'action.
* @param collector Collector pour une réinitialisation et test de mail
* @param collectorServer Serveur de collecte pour test de mail (null s'il n'y en a pas)
* @param currentSession session http de l'utilisateur exécutant l'action (null sinon)
* @param counterName Nom du compteur pour une réinitialisation
* @param sessionId Identifiant de session pour invalidation (null sinon)
* @param threadId Identifiant du thread sous la forme pid_ip_id
* @param jobId Identifiant du job sous la forme pid_ip_id
* @param cacheId Identifiant du cache à vider
* @return Message de résultat
* @throws IOException e
* @since 1.49
*/
// CHECKSTYLE:OFF
String execute(Collector collector, CollectorServer collectorServer,
HttpSession currentSession, String counterName, // NOPMD
String sessionId, String threadId, String jobId, String cacheId) throws IOException {
// CHECKSTYLE:ON
String messageForReport;
switch (this) {
case CLEAR_COUNTER:
assert collector != null;
assert counterName != null;
messageForReport = clearCounter(collector, counterName);
break;
case MAIL_TEST:
assert collector != null;
messageForReport = mailTest(collector, collectorServer);
break;
case GC:
if (GC_ENABLED) {
// garbage collector
final long before = Runtime.getRuntime().totalMemory()
- Runtime.getRuntime().freeMemory();
gc();
final long after = Runtime.getRuntime().totalMemory()
- Runtime.getRuntime().freeMemory();
messageForReport = I18N.getFormattedString("ramasse_miette_execute",
(before - after) / 1024);
} else {
messageForReport = I18N.getString("ramasse_miette_desactive");
}
break;
case HEAP_DUMP:
if (JAVA_VENDOR.contains("IBM")) {
ibmHeapDump();
messageForReport = I18N.getString("heap_dump_genere_ibm");
} else {
// heap dump à générer dans le répertoire temporaire sur le serveur
// avec un suffixe contenant le host, la date et l'heure et avec une extension hprof
// (utiliser jvisualvm du jdk ou MAT d'eclipse en standalone ou en plugin)
final String heapDumpPath = heapDump().getPath();
messageForReport = I18N.getFormattedString("heap_dump_genere",
heapDumpPath.replace('\\', '/'));
}
break;
case INVALIDATE_SESSIONS:
// invalidation des sessions http
SessionListener.invalidateAllSessionsExceptCurrentSession(currentSession);
messageForReport = I18N.getString("sessions_http_invalidees");
break;
case INVALIDATE_SESSION:
// invalidation d'une session http
assert sessionId != null;
SessionListener.invalidateSession(sessionId);
messageForReport = I18N.getString("session_http_invalidee");
break;
case LOGOUT:
// invalidation de la session http courante
if (currentSession != null) {
SessionListener.invalidateSession(currentSession.getId());
}
messageForReport = I18N.getString("logged_out");
break;
case CLEAR_CACHES:
clearCaches();
messageForReport = I18N.getString("caches_purges");
break;
case CLEAR_CACHE:
clearCache(cacheId);
messageForReport = I18N.getFormattedString("cache_purge", cacheId);
break;
case KILL_THREAD:
assert threadId != null;
messageForReport = killThread(threadId);
break;
case PAUSE_JOB:
assert jobId != null;
messageForReport = pauseJob(jobId);
break;
case RESUME_JOB:
assert jobId != null;
messageForReport = resumeJob(jobId);
break;
case CLEAR_HOTSPOTS:
assert collector.getSamplingProfiler() != null;
collector.getSamplingProfiler().clear();
messageForReport = I18N.getString("hotspots_cleared");
break;
case PURGE_OBSOLETE_FILES:
assert collector != null;
collector.deleteObsoleteFiles();
messageForReport = I18N.getString("fichiers_obsoletes_purges") + '\n'
+ I18N.getString("Usage_disque") + ": "
+ (collector.getDiskUsage() / 1024 / 1024 + 1) + ' ' + I18N.getString("Mo");
break;
default:
throw new IllegalStateException(toString());
}
if (messageForReport != null) {
// log pour information en debug
LOG.debug("Action '" + this + "' executed. Result: "
+ messageForReport.replace('\n', ' '));
}
return messageForReport;
}
private String clearCounter(Collector collector, String counterName) {
String messageForReport;
if (ALL.equalsIgnoreCase(counterName)) {
for (final Counter counter : collector.getCounters()) {
collector.clearCounter(counter.getName());
}
messageForReport = I18N.getFormattedString("Toutes_statistiques_reinitialisees",
counterName);
} else {
// l'action Réinitialiser a été appelée pour un compteur
collector.clearCounter(counterName);
messageForReport = I18N.getFormattedString("Statistiques_reinitialisees", counterName);
}
return messageForReport;
}
private String mailTest(Collector collector, CollectorServer collectorServer) {
// note: a priori, inutile de traduire cela
if (!HtmlAbstractReport.isPdfEnabled()) {
throw new IllegalStateException("itext classes not found: add the itext dependency");
}
if (Parameters.getParameter(Parameter.MAIL_SESSION) == null) {
throw new IllegalStateException(
"mail-session has no value: add the mail-session parameter");
}
if (Parameters.getParameter(Parameter.ADMIN_EMAILS) == null) {
throw new IllegalStateException(
"admin-emails has no value: add the admin-emails parameter");
}
try {
if (collectorServer == null) {
// serveur local
new MailReport().sendReportMailForLocalServer(collector, Period.JOUR);
} else {
// serveur de collecte
new MailReport().sendReportMail(collector, true, collectorServer
.getJavaInformationsByApplication(collector.getApplication()), Period.JOUR);
}
} catch (final Exception e) {
throw new RuntimeException(e); // NOPMD
}
return "Mail sent with pdf report for the day to admins";
}
private File heapDump() throws IOException {
final boolean gcBeforeHeapDump = true;
try {
final MBeanServer platformMBeanServer = ManagementFactory.getPlatformMBeanServer();
final ObjectInstance instance = platformMBeanServer.getObjectInstance(new ObjectName(
"com.sun.management:type=HotSpotDiagnostic"));
final Object mxBean = platformMBeanServer.instantiate(instance.getClassName());
final Object vmOption = ((com.sun.management.HotSpotDiagnosticMXBean) mxBean)
.getVMOption("HeapDumpPath");
final String heapDumpPath;
if (vmOption == null) {
heapDumpPath = null;
} else {
heapDumpPath = ((com.sun.management.VMOption) vmOption).getValue();
}
final String path;
if (heapDumpPath == null || heapDumpPath.length() == 0) {
path = Parameters.TEMPORARY_DIRECTORY.getPath();
} else {
// -XX:HeapDumpPath=/tmp par exemple a été spécifié comme paramètre de VM.
// Dans ce cas, on prend en compte ce paramètre "standard" de la JVM Hotspot
final File file = new File(heapDumpPath);
if (file.exists()) {
if (file.isDirectory()) {
path = heapDumpPath;
} else {
path = file.getParent();
}
} else {
if (!file.mkdirs()) {
throw new IllegalStateException("Can't create directory " + file.getPath());
}
path = heapDumpPath;
}
}
final DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss",
Locale.getDefault());
final File heapDumpFile = new File(path, "heapdump-" + Parameters.getHostName() + '-'
+ PID.getPID() + '-' + dateFormat.format(new Date()) + ".hprof");
if (heapDumpFile.exists()) {
try {
// si le fichier existe déjà, un heap dump a déjà été généré dans la même seconde
// donc on attends 1 seconde pour créer le fichier avec un nom différent
Thread.sleep(1000);
} catch (final InterruptedException e) {
throw new IllegalStateException(e);
}
return heapDump();
}
((com.sun.management.HotSpotDiagnosticMXBean) mxBean).dumpHeap(heapDumpFile.getPath(),
gcBeforeHeapDump);
return heapDumpFile;
} catch (final JMException e) {
throw new IllegalStateException(e);
}
}
private void ibmHeapDump() {
try {
final Class<?> dumpClass = getClass().getClassLoader().loadClass("com.ibm.jvm.Dump"); // NOPMD
final Class<?>[] argTypes = null;
final Method dump = dumpClass.getMethod("HeapDump", argTypes);
final Object[] args = null;
dump.invoke(null, args);
} catch (final Exception e) {
throw new IllegalStateException(e);
}
}
// cette méthode doit s'appeler "gc" pour que findbugs ne fasse pas de warning
@SuppressWarnings("all")
private void gc() {
Runtime.getRuntime().gc();
}
@SuppressWarnings("unchecked")
private void clearCaches() {
final List<CacheManager> allCacheManagers = CacheManager.ALL_CACHE_MANAGERS;
for (final CacheManager cacheManager : allCacheManagers) {
cacheManager.clearAll();
}
}
@SuppressWarnings("unchecked")
private void clearCache(String cacheId) {
final List<CacheManager> allCacheManagers = CacheManager.ALL_CACHE_MANAGERS;
for (final CacheManager cacheManager : allCacheManagers) {
final Cache cache = cacheManager.getCache(cacheId);
if (cache != null) {
cache.removeAll();
}
}
}
private String killThread(String threadId) {
final String[] values = threadId.split("_");
if (values.length != 3) {
throw new IllegalArgumentException(threadId);
}
// rq : la syntaxe vérifiée ici doit être conforme à ThreadInformations.buildGlobalThreadId
if (values[0].equals(PID.getPID()) && values[1].equals(Parameters.getHostAddress())) {
final long myThreadId = Long.parseLong(values[2]);
final List<Thread> threads = JavaInformations.getThreadsFromThreadGroups();
for (final Thread thread : threads) {
if (thread.getId() == myThreadId) {
stopThread(thread);
return I18N.getFormattedString("Thread_tue", thread.getName());
}
}
return I18N.getString("Thread_non_trouve");
}
// cette action ne concernait pas cette JVM, donc on ne fait rien
return null;
}
@SuppressWarnings("deprecation")
private void stopThread(Thread thread) {
// I know that it is unsafe and the user has been warned
thread.stop();
}
private String pauseJob(String jobId) {
if (ALL.equalsIgnoreCase(jobId)) {
pauseAllJobs();
return I18N.getString("all_jobs_paused");
}
final String[] values = jobId.split("_");
if (values.length != 3) {
throw new IllegalArgumentException(jobId);
}
// rq : la syntaxe vérifiée ici doit être conforme à JobInformations.buildGlobalJobId
if (values[0].equals(PID.getPID()) && values[1].equals(Parameters.getHostAddress())) {
if (pauseJobById(Integer.parseInt(values[2]))) {
return I18N.getString("job_paused");
}
return I18N.getString("job_notfound");
}
// cette action ne concernait pas cette JVM, donc on ne fait rien
return null;
}
private boolean pauseJobById(int myJobId) {
try {
for (final Scheduler scheduler : JobInformations.getAllSchedulers()) {
for (final JobDetail jobDetail : JobInformations.getAllJobsOfScheduler(scheduler)) {
if (QuartzAdapter.getSingleton().getJobFullName(jobDetail).hashCode() == myJobId) {
QuartzAdapter.getSingleton().pauseJob(jobDetail, scheduler);
return true;
}
}
}
return false;
} catch (final Exception e) {
throw new IllegalStateException(e);
}
}
private void pauseAllJobs() {
try {
for (final Scheduler scheduler : JobInformations.getAllSchedulers()) {
scheduler.pauseAll();
}
} catch (final Exception e) {
throw new IllegalStateException(e);
}
}
private String resumeJob(String jobId) {
if (ALL.equalsIgnoreCase(jobId)) {
resumeAllJobs();
return I18N.getString("all_jobs_resumed");
}
final String[] values = jobId.split("_");
if (values.length != 3) {
throw new IllegalArgumentException(jobId);
}
// rq : la syntaxe vérifiée ici doit être conforme à JobInformations.buildGlobalJobId
if (values[0].equals(PID.getPID()) && values[1].equals(Parameters.getHostAddress())) {
if (resumeJobById(Integer.parseInt(values[2]))) {
return I18N.getString("job_resumed");
}
return I18N.getString("job_notfound");
}
// cette action ne concernait pas cette JVM, donc on ne fait rien
return null;
}
private boolean resumeJobById(int myJobId) {
try {
for (final Scheduler scheduler : JobInformations.getAllSchedulers()) {
for (final JobDetail jobDetail : JobInformations.getAllJobsOfScheduler(scheduler)) {
if (QuartzAdapter.getSingleton().getJobFullName(jobDetail).hashCode() == myJobId) {
QuartzAdapter.getSingleton().resumeJob(jobDetail, scheduler);
return true;
}
}
}
return false;
} catch (final Exception e) {
throw new IllegalStateException(e);
}
}
private void resumeAllJobs() {
try {
for (final Scheduler scheduler : JobInformations.getAllSchedulers()) {
scheduler.resumeAll();
}
} catch (final Exception e) {
throw new IllegalStateException(e);
}
}
}