/*
* Copyright 2010-2015 Institut Pasteur.
*
* This file is part of Icy.
*
* Icy is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Icy 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Icy. If not, see <http://www.gnu.org/licenses/>.
*/
package icy.system;
import icy.gui.dialog.MessageDialog;
import icy.gui.frame.progress.FailedAnnounceFrame;
import icy.gui.plugin.PluginErrorReport;
import icy.main.Icy;
import icy.math.UnitUtil;
import icy.network.NetworkUtil;
import icy.plugin.PluginDescriptor;
import icy.plugin.PluginDescriptor.PluginIdent;
import icy.plugin.PluginLauncher;
import icy.plugin.PluginLoader;
import icy.plugin.interface_.PluginBundled;
import icy.util.ClassUtil;
import icy.util.StringUtil;
import java.io.File;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author Stephane
*/
public class IcyExceptionHandler implements UncaughtExceptionHandler
{
private static final double ERROR_ANTISPAM_TIME = 15 * 1000;
private static IcyExceptionHandler exceptionHandler = new IcyExceptionHandler();
private static long lastErrorDialog = 0;
private static long lastReport = 0;
private static Set<String> reportedPlugin = new HashSet<String>();
public static void init()
{
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
}
/**
* Display the specified Throwable message in error output.
*/
public static void showErrorMessage(Throwable t, boolean printStackTrace)
{
showErrorMessage(t, printStackTrace, true);
}
/**
* Display the specified Throwable message in console.<br>
* If <i>error</i> is true the message is considerer as an error and then written in error
* output.
*/
public static void showErrorMessage(Throwable t, boolean printStackTrace, boolean error)
{
final String mess = getErrorMessage(t, printStackTrace);
if (!StringUtil.isEmpty(mess))
{
if (error)
System.err.println(mess);
else
System.out.println(mess);
}
}
/**
* Returns the formatted error message for the specified {@link Throwable}.<br>
* If <i>printStackTrace</i> is <code>true</code> the stack trace is also returned in the
* message.
*/
public static String getErrorMessage(Throwable t, boolean printStackTrace)
{
String result = "";
Throwable throwable = t;
while (throwable != null)
{
result += throwable.toString() + "\n";
if (printStackTrace)
{
try
{
// sometime 'getStackTrace()' throws a weird AbstractMethodError exception
for (StackTraceElement element : throwable.getStackTrace())
result += "\tat " + element.toString() + "\n";
}
catch (Throwable t2)
{
result += "Error while trying to get exception stack trace...\n";
}
}
throwable = throwable.getCause();
if (throwable != null)
result += "Caused by :\n";
}
return result;
}
@Override
public void uncaughtException(Thread t, Throwable e)
{
handleException(t, e, true);
}
/**
* Handle the specified exception.<br>
* It actually display a message or report dialog depending the exception type.
*/
private static void handleException(Thread thread, PluginDescriptor plugin, String devId, Throwable t,
boolean printStackStrace)
{
final long current = System.currentTimeMillis();
final String errMess = (t.getMessage() != null) ? t.getMessage() : "";
if (t instanceof IcyHandledException)
{
final String message = errMess + ((t.getCause() == null) ? "" : "\n" + t.getCause());
// handle HandledException differently
MessageDialog.showDialog(message, MessageDialog.ERROR_MESSAGE);
// update last error dialog time
lastErrorDialog = System.currentTimeMillis();
// don't need the antispam for the IcyHandledException
// if ((current - lastErrorDialog) > ERROR_ANTISPAM_TIME)
// {
// // handle HandledException differently
// MessageDialog.showDialog(message, MessageDialog.ERROR_MESSAGE);
// // update last error dialog time
// lastErrorDialog = System.currentTimeMillis();
// }
// else
// // spam --> write it in the console output instead
// System.err.println(message + " (spam protection)");
}
else
{
String message = "";
if (t instanceof OutOfMemoryError)
{
if (errMess.contains("Thread"))
{
message = "Out of resource error: cannot create new thread.\n"
+ "You should report this error as something goes wrong here !";
}
else
{
message = "The task could not be completed because there is not enough memory !\n"
+ "Try to increase the Maximum Memory parameter in Preferences.";
}
}
if (!StringUtil.isEmpty(message))
message += "\n";
message += getErrorMessage(t, printStackStrace);
// write message in console if wanted or if spam error message
if ((t instanceof OutOfMemoryError) || printStackStrace
|| ((current - lastErrorDialog) < ERROR_ANTISPAM_TIME))
{
if (plugin != null)
System.err.println("An error occured while plugin '" + plugin.getName() + "' was running :");
else if (!StringUtil.isEmpty(devId))
System.err.println("An error occured while a plugin was running :");
System.err.println(message);
}
// do report (anti spam protected)
if ((current - lastErrorDialog) > ERROR_ANTISPAM_TIME)
{
final String title = t.toString();
// handle the specific "not enough memory" differently
if ((t instanceof OutOfMemoryError) && (!errMess.contains("Thread")))
{
if (!Icy.getMainInterface().isHeadLess())
new FailedAnnounceFrame(
"Not enough memory to complete the process ! Try to increase the 'Max Memory' parameter in Preferences.",
30);
}
else
// just report the error
PluginErrorReport.report(plugin, devId, title, message);
// update last error dialog time
lastErrorDialog = System.currentTimeMillis();
}
}
}
/**
* Handle the specified exception.<br>
* It actually display a message or report dialog depending the exception type.
*/
public static void handleException(PluginDescriptor pluginDesc, Throwable t, boolean printStackStrace)
{
handleException(null, pluginDesc, null, t, printStackStrace);
}
/**
* Handle the specified exception.<br>
* It actually display a message or report dialog depending the exception type.
*/
public static void handleException(String devId, Throwable t, boolean printStackStrace)
{
handleException(null, null, devId, t, printStackStrace);
}
/**
* Handle the specified exception.<br>
* Try to find the origin plugin which thrown the exception.
* It actually display a message or report dialog depending the exception type.
*/
public static void handleException(Throwable t, boolean printStackStrace)
{
handleException((Thread) null, t, printStackStrace);
}
/**
* Handle the specified exception.<br>
* Try to find the origin plugin which thrown the exception.
* It actually display a message or report dialog depending the exception type.
*/
private static void handleException(Thread thread, Throwable t, boolean printStackStrace)
{
Throwable throwable = t;
final List<PluginDescriptor> plugins = PluginLoader.getPlugins();
while (throwable != null)
{
StackTraceElement[] stackTrace;
try
{
// sometime 'getStackTrace()' throws a weird AbstractMethodError exception
stackTrace = throwable.getStackTrace();
}
catch (Throwable t2)
{
stackTrace = new StackTraceElement[0];
}
// search plugin class (start from the end of stack trace)
final PluginDescriptor plugin = findPluginFromStackTrace(plugins, stackTrace);
// plugin found --> show the plugin report frame
if (plugin != null)
{
// only send to last plugin raising the exception
handleException(thread, plugin, null, t, printStackStrace);
return;
}
// we did not find plugin class so we will search for plugin developer id
final String devId = findDevIdFromStackTrace(stackTrace);
if (devId != null)
{
handleException(thread, null, devId, t, printStackStrace);
return;
}
throwable = throwable.getCause();
}
// general exception (no plugin information found)
handleException(thread, null, null, t, printStackStrace);
}
/**
* @deprecated Use {@link #handleException(PluginDescriptor, Throwable, boolean)} instead.
*/
@Deprecated
public static void handlePluginException(PluginDescriptor pluginDesc, Throwable t, boolean printStackStrace)
{
handleException(pluginDesc, t, printStackStrace);
}
private static PluginDescriptor findMatchingLocalPlugin(List<PluginDescriptor> plugins, String text)
{
String className = ClassUtil.getBaseClassName(text);
// get the JAR file of this class
final File file = ClassUtil.getFile(className);
// found ?
if (file != null)
{
// try to find plugin using the same JAR file (so
for (PluginDescriptor p : plugins)
{
final String jarFileName = p.getJarFilename();
if (!StringUtil.isEmpty(jarFileName))
{
final File jarFile = new File(jarFileName);
// matching jar file --> return plugin
if (StringUtil.equals(file.getAbsolutePath(), jarFile.getAbsolutePath()))
return p;
}
}
}
// not found with first method so now we try on the class name
while (!(StringUtil.equals(className, PluginLoader.PLUGIN_PACKAGE) || StringUtil.isEmpty(className)))
{
final PluginDescriptor plugin = findMatchingLocalPluginInternal(plugins, className);
if (plugin != null)
return plugin;
// not found --> we test with parent package
className = ClassUtil.getPackageName(className);
}
return null;
}
private static PluginDescriptor findMatchingLocalPluginInternal(List<PluginDescriptor> plugins, String text)
{
PluginDescriptor result = null;
for (PluginDescriptor plugin : plugins)
{
if (plugin.getClassName().startsWith(text))
{
if (result != null)
return null;
result = plugin;
}
}
return result;
}
private static PluginDescriptor findPluginFromStackTrace(List<PluginDescriptor> plugins, StackTraceElement[] st)
{
for (StackTraceElement trace : st)
{
final String className = trace.getClassName();
// plugin class ?
if (className.startsWith(PluginLoader.PLUGIN_PACKAGE + "."))
{
// try to find a matching plugin
final PluginDescriptor plugin = findMatchingLocalPlugin(plugins, className);
// plugin found --> show the plugin report frame
if (plugin != null)
return plugin;
}
}
return null;
}
private static String findDevIdFromStackTrace(StackTraceElement[] st)
{
// we did not find plugin class so we will search for plugin developer id
for (StackTraceElement trace : st)
{
final String className = trace.getClassName();
// plugin class ?
if (className.startsWith(PluginLoader.PLUGIN_PACKAGE + "."))
// use plugin developer id (only send to last plugin raising the exception)
return className.split("\\.")[1];
}
return null;
}
/**
* Report an error log from a given plugin or developer id to Icy web site.
*
* @param plugin
* The plugin responsible of the error or <code>null</code> if the error comes from the
* application or if we are not able to get the plugin descriptor.
* @param devId
* The developer id of the plugin responsible of the error when the plugin descriptor was
* not found or <code>null</code> if the error comes from the application.
* @param errorLog
* Error log to report.
*/
public static void report(PluginDescriptor plugin, String devId, String errorLog)
{
final long current = System.currentTimeMillis();
// avoid report spam
if ((current - lastReport) < ERROR_ANTISPAM_TIME)
return;
final String icyId;
final String javaId;
final String osId;
final String memory;
String pluginId;
String pluginDepsId;
final Map<String, String> values = new HashMap<String, String>();
values.put(NetworkUtil.ID_KERNELVERSION, Icy.version.toString());
values.put(NetworkUtil.ID_JAVANAME, SystemUtil.getJavaName());
values.put(NetworkUtil.ID_JAVAVERSION, SystemUtil.getJavaVersion());
values.put(NetworkUtil.ID_JAVABITS, Integer.toString(SystemUtil.getJavaArchDataModel()));
values.put(NetworkUtil.ID_OSNAME, SystemUtil.getOSName());
values.put(NetworkUtil.ID_OSVERSION, SystemUtil.getOSVersion());
values.put(NetworkUtil.ID_OSARCH, SystemUtil.getOSArch());
icyId = "Icy Version " + Icy.version + "\n";
javaId = SystemUtil.getJavaName() + " " + SystemUtil.getJavaVersion() + " ("
+ SystemUtil.getJavaArchDataModel() + " bit)\n";
osId = "Running on " + SystemUtil.getOSName() + " " + SystemUtil.getOSVersion() + " (" + SystemUtil.getOSArch()
+ ")\n";
memory = "Max java memory : " + UnitUtil.getBytesString(SystemUtil.getJavaMaxMemory()) + "\n";
if (plugin != null)
{
final String className = plugin.getClassName();
// we already reported error for this plugin --> avoid spaming
if (reportedPlugin.contains(className))
return;
reportedPlugin.add(className);
values.put(NetworkUtil.ID_PLUGINCLASSNAME, className);
values.put(NetworkUtil.ID_PLUGINVERSION, plugin.getVersion().toString());
pluginId = "Plugin " + plugin.toString();
// determine origin plugin
PluginDescriptor originPlugin = plugin;
// bundled plugin ?
if (plugin.isBundled())
{
try
{
// get original plugin
originPlugin = PluginLoader.getPlugin(((PluginBundled) PluginLauncher.create(plugin))
.getMainPluginClassName());
// add bundle info
pluginId = "Bundled in " + originPlugin.toString();
}
catch (Throwable t)
{
// miss bundle info
pluginId = "Bundled plugin (could not retrieve origin plugin)";
}
}
pluginId += "\n\n";
if (originPlugin.getRequired().size() > 0)
{
pluginDepsId = "Dependances:\n";
for (PluginIdent ident : originPlugin.getRequired())
{
final PluginDescriptor installed = PluginLoader.getPlugin(ident.getClassName());
if (installed == null)
pluginDepsId += "Class " + ident.getClassName() + " not found !\n";
else
pluginDepsId += "Plugin " + installed.toString() + " is correctly installed\n";
}
pluginDepsId += "\n";
}
else
pluginDepsId = "";
}
else
{
values.put(NetworkUtil.ID_PLUGINCLASSNAME, "");
values.put(NetworkUtil.ID_PLUGINVERSION, "");
pluginId = "";
pluginDepsId = "";
}
if (StringUtil.isEmpty(devId))
values.put(NetworkUtil.ID_DEVELOPERID, devId);
else
values.put(NetworkUtil.ID_DEVELOPERID, "");
values.put(NetworkUtil.ID_ERRORLOG, icyId + javaId + osId + memory + "\n" + pluginId + pluginDepsId + errorLog);
// send report
lastReport = current;
NetworkUtil.report(values);
}
/**
* Report an error log from a given plugin to Icy web site.
*
* @param plugin
* The plugin responsible of the error or <code>null</code> if the error comes from the
* application.
* @param errorLog
* Error log to report.
*/
public static void report(PluginDescriptor plugin, String errorLog)
{
report(plugin, null, errorLog);
}
/**
* Report an error log from the application to Icy web site.
*
* @param errorLog
* Error log to report.
*/
public static void report(String errorLog)
{
report(null, null, errorLog);
}
/**
* Report an error log from a given plugin developer id to the Icy web site.
*
* @param devId
* The developer id of the plugin responsible of the error or <code>null</code> if the
* error comes from the application.
* @param errorLog
* Error log to report.
*/
public static void report(String devId, String errorLog)
{
report(null, devId, errorLog);
}
}