001/*
002 * Copyright 2010-2015 Institut Pasteur.
003 * 
004 * This file is part of Icy.
005 * 
006 * Icy is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 * 
011 * Icy is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 * 
016 * You should have received a copy of the GNU General Public License
017 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
018 */
019package icy.system;
020
021import java.io.File;
022import java.lang.Thread.UncaughtExceptionHandler;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028
029import icy.gui.dialog.MessageDialog;
030import icy.gui.frame.progress.FailedAnnounceFrame;
031import icy.gui.plugin.PluginErrorReport;
032import icy.main.Icy;
033import icy.math.UnitUtil;
034import icy.network.NetworkUtil;
035import icy.plugin.PluginDescriptor;
036import icy.plugin.PluginDescriptor.PluginIdent;
037import icy.plugin.PluginLauncher;
038import icy.plugin.PluginLoader;
039import icy.plugin.interface_.PluginBundled;
040import icy.util.ClassUtil;
041import icy.util.StringUtil;
042
043/**
044 * @author Stephane
045 */
046public class IcyExceptionHandler implements UncaughtExceptionHandler
047{
048    private static final double ERROR_ANTISPAM_TIME = 15 * 1000;
049    private static IcyExceptionHandler exceptionHandler = new IcyExceptionHandler();
050    private static long lastErrorDialog = 0;
051    private static long lastErrorReport = 0;
052    private static Set<String> reportedPlugins = new HashSet<String>();
053
054    public static void init()
055    {
056        Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
057    }
058
059    /**
060     * Display the specified Throwable message in error output.
061     */
062    public static void showErrorMessage(Throwable t, boolean printStackTrace)
063    {
064        showErrorMessage(t, printStackTrace, true);
065    }
066
067    /**
068     * Display the specified Throwable message in console.<br>
069     * If <i>error</i> is true the message is considerer as an error and then written in error
070     * output.
071     */
072    public static void showErrorMessage(Throwable t, boolean printStackTrace, boolean error)
073    {
074        final String mess = getErrorMessage(t, printStackTrace);
075
076        if (!StringUtil.isEmpty(mess))
077        {
078            if (error)
079                System.err.println(mess);
080            else
081                System.out.println(mess);
082        }
083    }
084
085    /**
086     * Returns the formatted error message for the specified {@link Throwable}.<br>
087     * If <i>printStackTrace</i> is <code>true</code> the stack trace is also returned in the
088     * message.
089     */
090    public static String getErrorMessage(Throwable t, boolean printStackTrace)
091    {
092        String result = "";
093        Throwable throwable = t;
094
095        while (throwable != null)
096        {
097            result += throwable.toString() + "\n";
098
099            if (printStackTrace)
100            {
101                try
102                {
103                    // sometime 'getStackTrace()' throws a weird AbstractMethodError exception
104                    for (StackTraceElement element : throwable.getStackTrace())
105                        result += "\tat " + element.toString() + "\n";
106                }
107                catch (Throwable t2)
108                {
109                    result += "Error while trying to get exception stack trace...\n";
110                }
111            }
112
113            throwable = throwable.getCause();
114            if (throwable != null)
115                result += "Caused by :\n";
116        }
117
118        return result;
119    }
120
121    @Override
122    public void uncaughtException(Thread t, Throwable e)
123    {
124        handleException(t, e, true);
125    }
126
127    /**
128     * Handle the specified exception.<br>
129     * It actually display a message or report dialog depending the exception type.
130     */
131    private static void handleException(Thread thread, PluginDescriptor plugin, String devId, Throwable t,
132            boolean printStackStrace)
133    {
134        final long current = System.currentTimeMillis();
135        final String errMess = (t.getMessage() != null) ? t.getMessage() : "";
136
137        if (t instanceof IcyHandledException)
138        {
139            final String message = errMess + ((t.getCause() == null) ? "" : "\n" + t.getCause());
140
141            // handle HandledException differently
142            MessageDialog.showDialog(message, MessageDialog.ERROR_MESSAGE);
143            // update last error dialog time
144            lastErrorDialog = System.currentTimeMillis();
145
146            // don't need the antispam for the IcyHandledException
147            // if ((current - lastErrorDialog) > ERROR_ANTISPAM_TIME)
148            // {
149            // // handle HandledException differently
150            // MessageDialog.showDialog(message, MessageDialog.ERROR_MESSAGE);
151            // // update last error dialog time
152            // lastErrorDialog = System.currentTimeMillis();
153            // }
154            // else
155            // // spam --> write it in the console output instead
156            // System.err.println(message + " (spam protection)");
157        }
158        else
159        {
160            String message = "";
161
162            if (t instanceof OutOfMemoryError)
163            {
164                if (errMess.contains("Thread"))
165                {
166                    message = "Out of resource error: cannot create new thread.\n"
167                            + "You should report this error as something goes wrong here !";
168                }
169                else
170                {
171                    message = "The task could not be completed because there is not enough memory !\n"
172                            + "Try to use 'virtual mode' (image caching) or increase increase the 'Maximum Memory' parameter in Preferences.";
173                }
174            }
175
176            if (!StringUtil.isEmpty(message))
177                message += "\n";
178            message += getErrorMessage(t, printStackStrace);
179
180            // write message in console if wanted or if spam error message
181            if ((t instanceof OutOfMemoryError) || printStackStrace
182                    || ((current - lastErrorDialog) < ERROR_ANTISPAM_TIME))
183            {
184                if (plugin != null)
185                    System.err.println("An error occured while plugin '" + plugin.getName() + "' was running :");
186                else if (!StringUtil.isEmpty(devId))
187                    System.err.println("An error occured while a plugin was running :");
188
189                System.err.println(message);
190            }
191
192            // do report (anti spam protected)
193            if ((current - lastErrorDialog) > ERROR_ANTISPAM_TIME)
194            {
195                final String title = t.toString();
196
197                // handle the specific "not enough memory" differently
198                if ((t instanceof OutOfMemoryError) && (!errMess.contains("Thread")))
199                {
200                    if (!Icy.getMainInterface().isHeadLess())
201                        new FailedAnnounceFrame(
202                                "Not enough memory to complete the process ! Try to use 'Virtual Mode' (image caching) or increase the 'Maximum Memory' parameter in Preferences.",
203                                30);
204                }
205                else
206                    // just report the error
207                    PluginErrorReport.report(plugin, devId, title, message);
208
209                // update last error dialog time
210                lastErrorDialog = System.currentTimeMillis();
211            }
212        }
213    }
214
215    /**
216     * Handle the specified exception.<br>
217     * It actually display a message or report dialog depending the exception type.
218     */
219    public static void handleException(PluginDescriptor pluginDesc, Throwable t, boolean printStackStrace)
220    {
221        handleException(null, pluginDesc, null, t, printStackStrace);
222    }
223
224    /**
225     * Handle the specified exception.<br>
226     * It actually display a message or report dialog depending the exception type.
227     */
228    public static void handleException(String devId, Throwable t, boolean printStackStrace)
229    {
230        handleException(null, null, devId, t, printStackStrace);
231    }
232
233    /**
234     * Handle the specified exception.<br>
235     * Try to find the origin plugin which thrown the exception.
236     * It actually display a message or report dialog depending the exception type.
237     */
238    public static void handleException(Throwable t, boolean printStackStrace)
239    {
240        handleException((Thread) null, t, printStackStrace);
241    }
242
243    /**
244     * Handle the specified exception.<br>
245     * Try to find the origin plugin which thrown the exception.
246     * It actually display a message or report dialog depending the exception type.
247     */
248    private static void handleException(Thread thread, Throwable t, boolean printStackStrace)
249    {
250        Throwable throwable = t;
251        final List<PluginDescriptor> plugins = PluginLoader.getPlugins();
252
253        while (throwable != null)
254        {
255            StackTraceElement[] stackTrace;
256
257            try
258            {
259                // sometime 'getStackTrace()' throws a weird AbstractMethodError exception
260                stackTrace = throwable.getStackTrace();
261            }
262            catch (Throwable t2)
263            {
264                stackTrace = new StackTraceElement[0];
265            }
266
267            // search plugin class (start from the end of stack trace)
268            final PluginDescriptor plugin = findPluginFromStackTrace(plugins, stackTrace);
269
270            // plugin found --> show the plugin report frame
271            if (plugin != null)
272            {
273                // only send to last plugin raising the exception
274                handleException(thread, plugin, null, t, printStackStrace);
275                return;
276            }
277
278            // we did not find plugin class so we will search for plugin developer id
279            final String devId = findDevIdFromStackTrace(stackTrace);
280
281            if (devId != null)
282            {
283                handleException(thread, null, devId, t, printStackStrace);
284                return;
285            }
286
287            throwable = throwable.getCause();
288        }
289
290        // general exception (no plugin information found)
291        handleException(thread, null, null, t, printStackStrace);
292    }
293
294    /**
295     * @deprecated Use {@link #handleException(PluginDescriptor, Throwable, boolean)} instead.
296     */
297    @Deprecated
298    public static void handlePluginException(PluginDescriptor pluginDesc, Throwable t, boolean printStackStrace)
299    {
300        handleException(pluginDesc, t, printStackStrace);
301    }
302
303    private static PluginDescriptor findMatchingLocalPlugin(List<PluginDescriptor> plugins, String text)
304    {
305        String className = ClassUtil.getBaseClassName(text);
306
307        // get the JAR file of this class
308        final File file = ClassUtil.getFile(className);
309
310        // found ?
311        if (file != null)
312        {
313            // try to find plugin using the same JAR file (so
314            for (PluginDescriptor p : plugins)
315            {
316                final String jarFileName = p.getJarFilename();
317
318                if (!StringUtil.isEmpty(jarFileName))
319                {
320                    final File jarFile = new File(jarFileName);
321
322                    // matching jar file --> return plugin
323                    if (StringUtil.equals(file.getAbsolutePath(), jarFile.getAbsolutePath()))
324                        return p;
325                }
326            }
327        }
328
329        // not found with first method so now we try on the class name
330        while (!(StringUtil.equals(className, PluginLoader.PLUGIN_PACKAGE) || StringUtil.isEmpty(className)))
331        {
332            final PluginDescriptor plugin = findMatchingLocalPluginInternal(plugins, className);
333
334            if (plugin != null)
335                return plugin;
336
337            // not found --> we test with parent package
338            className = ClassUtil.getPackageName(className);
339        }
340
341        return null;
342    }
343
344    private static PluginDescriptor findMatchingLocalPluginInternal(List<PluginDescriptor> plugins, String text)
345    {
346        PluginDescriptor result = null;
347
348        for (PluginDescriptor plugin : plugins)
349        {
350            if (plugin.getClassName().startsWith(text))
351            {
352                if (result != null)
353                    return null;
354
355                result = plugin;
356            }
357        }
358
359        return result;
360    }
361
362    private static PluginDescriptor findPluginFromStackTrace(List<PluginDescriptor> plugins, StackTraceElement[] st)
363    {
364        for (StackTraceElement trace : st)
365        {
366            final String className = trace.getClassName();
367
368            // plugin class ?
369            if (className.startsWith(PluginLoader.PLUGIN_PACKAGE + "."))
370            {
371                // try to find a matching plugin
372                final PluginDescriptor plugin = findMatchingLocalPlugin(plugins, className);
373
374                // plugin found --> show the plugin report frame
375                if (plugin != null)
376                    return plugin;
377            }
378        }
379
380        return null;
381    }
382
383    private static String findDevIdFromStackTrace(StackTraceElement[] st)
384    {
385        // we did not find plugin class so we will search for plugin developer id
386        for (StackTraceElement trace : st)
387        {
388            final String className = trace.getClassName();
389
390            // plugin class ?
391            if (className.startsWith(PluginLoader.PLUGIN_PACKAGE + "."))
392                // use plugin developer id (only send to last plugin raising the exception)
393                return className.split("\\.")[1];
394        }
395
396        return null;
397    }
398
399    /**
400     * Report an error log from a given plugin or developer id to Icy web site.
401     * 
402     * @param plugin
403     *        The plugin responsible of the error or <code>null</code> if the error comes from the
404     *        application or if we are not able to get the plugin descriptor.
405     * @param devId
406     *        The developer id of the plugin responsible of the error when the plugin descriptor was
407     *        not found or <code>null</code> if the error comes from the application.
408     * @param errorLog
409     *        Error log to report.
410     */
411    public static void report(PluginDescriptor plugin, String devId, String errorLog)
412    {
413        final long current = System.currentTimeMillis();
414
415        // avoid report spam
416        if ((current - lastErrorReport) < ERROR_ANTISPAM_TIME)
417            return;
418        // we already reported error for this plugin --> avoid spaming
419        if ((plugin != null) && reportedPlugins.contains(plugin.getClassName()))
420            return;
421
422        // store last send time
423        lastErrorReport = current;
424
425        // TODO: switch to it when ready !
426        // WebInterface.reportError(plugin, devId, errorLog);
427
428        final String icyId;
429        final String javaId;
430        final String osId;
431        final String memory;
432        String pluginId;
433        String pluginDepsId;
434        final Map<String, String> values = new HashMap<String, String>();
435
436        values.put(NetworkUtil.ID_KERNELVERSION, Icy.version.toString());
437        values.put(NetworkUtil.ID_JAVANAME, SystemUtil.getJavaName());
438        values.put(NetworkUtil.ID_JAVAVERSION, SystemUtil.getJavaVersion());
439        values.put(NetworkUtil.ID_JAVABITS, Integer.toString(SystemUtil.getJavaArchDataModel()));
440        values.put(NetworkUtil.ID_OSNAME, SystemUtil.getOSName());
441        values.put(NetworkUtil.ID_OSVERSION, SystemUtil.getOSVersion());
442        values.put(NetworkUtil.ID_OSARCH, SystemUtil.getOSArch());
443
444        icyId = "Icy Version " + Icy.version + "\n";
445        javaId = SystemUtil.getJavaName() + " " + SystemUtil.getJavaVersion() + " (" + SystemUtil.getJavaArchDataModel()
446                + " bit)\n";
447        osId = "Running on " + SystemUtil.getOSName() + " " + SystemUtil.getOSVersion() + " (" + SystemUtil.getOSArch()
448                + ")\n";
449        memory = "Max java memory : " + UnitUtil.getBytesString(SystemUtil.getJavaMaxMemory()) + "\n";
450
451        if (plugin != null)
452        {
453            final String className = plugin.getClassName();
454
455            // we already reported error for this plugin --> avoid spaming
456            if (reportedPlugins.contains(className))
457                return;
458
459            reportedPlugins.add(className);
460
461            values.put(NetworkUtil.ID_PLUGINCLASSNAME, className);
462            values.put(NetworkUtil.ID_PLUGINVERSION, plugin.getVersion().toString());
463            pluginId = "Plugin " + plugin.toString();
464
465            // determine origin plugin
466            PluginDescriptor originPlugin = plugin;
467
468            // bundled plugin ?
469            if (plugin.isBundled())
470            {
471                try
472                {
473                    // get original plugin
474                    originPlugin = PluginLoader
475                            .getPlugin(((PluginBundled) PluginLauncher.create(plugin)).getMainPluginClassName());
476                    // add bundle info
477                    pluginId = "Bundled in " + originPlugin.toString();
478                }
479                catch (Throwable t)
480                {
481                    // miss bundle info
482                    pluginId = "Bundled plugin (could not retrieve origin plugin)";
483                }
484            }
485
486            pluginId += "\n\n";
487
488            if (originPlugin.getRequired().size() > 0)
489            {
490                pluginDepsId = "Dependances:\n";
491                for (PluginIdent ident : originPlugin.getRequired())
492                {
493                    final PluginDescriptor installed = PluginLoader.getPlugin(ident.getClassName());
494
495                    if (installed == null)
496                        pluginDepsId += "Class " + ident.getClassName() + " not found !\n";
497                    else
498                        pluginDepsId += "Plugin " + installed.toString() + " is correctly installed\n";
499                }
500                pluginDepsId += "\n";
501            }
502            else
503                pluginDepsId = "";
504        }
505        else
506        {
507            values.put(NetworkUtil.ID_PLUGINCLASSNAME, "");
508            values.put(NetworkUtil.ID_PLUGINVERSION, "");
509            pluginId = "";
510            pluginDepsId = "";
511        }
512
513        if (StringUtil.isEmpty(devId))
514            values.put(NetworkUtil.ID_DEVELOPERID, devId);
515        else
516            values.put(NetworkUtil.ID_DEVELOPERID, "");
517
518        values.put(NetworkUtil.ID_ERRORLOG, icyId + javaId + osId + memory + "\n" + pluginId + pluginDepsId + errorLog);
519
520        // send report
521        lastErrorReport = current;
522        NetworkUtil.report(values);
523    }
524
525    /**
526     * Report an error log from a given plugin to Icy web site.
527     * 
528     * @param plugin
529     *        The plugin responsible of the error or <code>null</code> if the error comes from the
530     *        application.
531     * @param errorLog
532     *        Error log to report.
533     */
534    public static void report(PluginDescriptor plugin, String errorLog)
535    {
536        report(plugin, null, errorLog);
537    }
538
539    /**
540     * Report an error log from the application to Icy web site.
541     * 
542     * @param errorLog
543     *        Error log to report.
544     */
545    public static void report(String errorLog)
546    {
547        report(null, null, errorLog);
548    }
549
550    /**
551     * Report an error log from a given plugin developer id to the Icy web site.
552     * 
553     * @param devId
554     *        The developer id of the plugin responsible of the error or <code>null</code> if the
555     *        error comes from the application.
556     * @param errorLog
557     *        Error log to report.
558     */
559    public static void report(String devId, String errorLog)
560    {
561        report(null, devId, errorLog);
562    }
563
564}