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}