PageRenderTime 39ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/illacommon/src/main/java/illarion/common/bug/CrashReporter.java

http://github.com/Illarion-eV/Illarion-Java
Java | 422 lines | 239 code | 58 blank | 125 comment | 40 complexity | ca16d20e1a3c2c8c03c89098bf303951 MD5 | raw file
Possible License(s): GPL-3.0
  1. /*
  2. * This file is part of the Illarion project.
  3. *
  4. * Copyright © 2015 - Illarion e.V.
  5. *
  6. * Illarion is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * Illarion is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. */
  16. package illarion.common.bug;
  17. import biz.futureware.mantis.rpc.soap.client.*;
  18. import illarion.common.config.Config;
  19. import illarion.common.util.AppIdent;
  20. import illarion.common.util.DirectoryManager;
  21. import illarion.common.util.DirectoryManager.Directory;
  22. import illarion.common.util.MessageSource;
  23. import org.jetbrains.annotations.Contract;
  24. import org.slf4j.Logger;
  25. import org.slf4j.LoggerFactory;
  26. import javax.annotation.Nonnull;
  27. import javax.annotation.Nullable;
  28. import javax.xml.rpc.ServiceException;
  29. import java.io.IOException;
  30. import java.math.BigInteger;
  31. import java.net.MalformedURLException;
  32. import java.net.URL;
  33. import java.nio.file.Files;
  34. import java.rmi.RemoteException;
  35. import java.util.Collection;
  36. import java.util.concurrent.CountDownLatch;
  37. /**
  38. * This class stores the crash reporter itself. It holds all settings done to
  39. * the reporter and handles sending the crash reports as well as showing the
  40. * required dialogs.
  41. *
  42. * @author Martin Karing <nitram@illarion.org>
  43. */
  44. public final class CrashReporter {
  45. /**
  46. * This is the key used in the configuration to store and read the settings
  47. * for the reporting system.
  48. */
  49. public static final String CFG_KEY = "errorReport"; //$NON-NLS-1$
  50. /**
  51. * This constant is used as mode value in case the crash reporter is
  52. * supposed to send the crash reports every time.
  53. */
  54. public static final int MODE_ALWAYS = 1;
  55. /**
  56. * This constant is used as mode value in case the crash reporter is
  57. * supposed to ask the user if the message shall be send to the server or
  58. * not.
  59. */
  60. public static final int MODE_ASK = 0;
  61. /**
  62. * This constant is used as mode value in case the crash reporter is
  63. * supposed to discard the crash report.
  64. */
  65. public static final int MODE_NEVER = 2;
  66. /**
  67. * This URL is the URL of the server that is supposed to receive the crash
  68. * data using a HTTP POST request.
  69. */
  70. @Nullable
  71. private static final URL CRASH_SERVER;
  72. /**
  73. * The singleton instance of this class.
  74. */
  75. @Nonnull
  76. private static final CrashReporter INSTANCE = new CrashReporter();
  77. /**
  78. * The logger instance that takes care for the logging output of this class.
  79. */
  80. @Nonnull
  81. private static final Logger log = LoggerFactory.getLogger(CrashReporter.class);
  82. static {
  83. URL result = null;
  84. try {
  85. result = new URL("https://illarion.org/mantis/api/soap/mantisconnect.php"); //$NON-NLS-1$
  86. } catch (@Nonnull MalformedURLException e) {
  87. log.warn("Preparing the crash report target URL failed. Crash reporter not functional."); //$NON-NLS-1$
  88. }
  89. CRASH_SERVER = result;
  90. }
  91. /**
  92. * The configuration handler that is used for the settings of this class.
  93. */
  94. @Nullable
  95. private Config cfg;
  96. /**
  97. * The currently displayed report dialog is displayed in this class.
  98. */
  99. @Nullable
  100. private ReportDialog dialog;
  101. /**
  102. * This is the source of the messages that are displayed in the crash report
  103. * dialog.
  104. */
  105. @Nullable
  106. private MessageSource messages;
  107. /**
  108. * This value stores the currently set mode.
  109. */
  110. private int mode;
  111. /**
  112. * This is the factory that is used to create report dialogs.
  113. */
  114. @Nullable
  115. private ReportDialogFactory dialogFactory;
  116. @Nullable
  117. private CountDownLatch crashReportDoneLatch;
  118. /**
  119. * Private constructor of the crash reporter that prepares all the required
  120. * data.
  121. */
  122. private CrashReporter() {
  123. mode = MODE_ASK;
  124. }
  125. /**
  126. * Get the singleton instance of this class.
  127. *
  128. * @return the singleton instance of this class
  129. */
  130. @Nonnull
  131. @Contract(pure = true)
  132. public static CrashReporter getInstance() {
  133. return INSTANCE;
  134. }
  135. /**
  136. * Set the instance of the factory that is used to create a report dialog.
  137. *
  138. * @param dialogFactory the dialog factory
  139. */
  140. public void setDialogFactory(@Nullable ReportDialogFactory dialogFactory) {
  141. this.dialogFactory = dialogFactory;
  142. }
  143. /**
  144. * Report a crash to the Illarion Server in case the application is supposed
  145. * to do so.
  146. *
  147. * @param crash the data about the crash
  148. */
  149. public void reportCrash(@Nonnull CrashData crash) {
  150. reportCrash(crash, false);
  151. }
  152. /**
  153. * Report a crash to the Illarion Server in case the application is supposed
  154. * to do so.
  155. *
  156. * @param crash the data about the crash
  157. * @param ownThread {@code true} in case the crash report is supposed
  158. * to be started in a additional thread
  159. */
  160. public void reportCrash(@Nonnull CrashData crash, boolean ownThread) {
  161. if (ownThread) {
  162. new Thread(() -> {
  163. reportCrash(crash, false);
  164. }).start();
  165. }
  166. if ("NoClassDefFoundError".equals(crash.getExceptionName())) {
  167. try {
  168. Files.createFile(
  169. DirectoryManager.getInstance().resolveFile(Directory.Data, "corrupted"));
  170. } catch (@Nonnull IOException e) {
  171. log.error("Failed to mark data as corrupted.");
  172. }
  173. }
  174. waitForReport();
  175. switch (mode) {
  176. case MODE_ALWAYS:
  177. sendCrashData(crash);
  178. break;
  179. case MODE_NEVER:
  180. return;
  181. default:
  182. crashReportDoneLatch = new CountDownLatch(1);
  183. dialog = dialogFactory.createDialog();
  184. dialog.setCrashData(crash);
  185. dialog.setMessageSource(messages);
  186. dialog.showDialog();
  187. int result = dialog.getResult();
  188. switch (result) {
  189. case ReportDialog.SEND_ALWAYS:
  190. setMode(MODE_ALWAYS);
  191. if (cfg != null) {
  192. cfg.set(CFG_KEY, MODE_ALWAYS);
  193. }
  194. sendCrashData(crash);
  195. break;
  196. case ReportDialog.SEND_ONCE:
  197. sendCrashData(crash);
  198. break;
  199. case ReportDialog.SEND_NEVER:
  200. setMode(MODE_NEVER);
  201. if (cfg != null) {
  202. cfg.set(CFG_KEY, MODE_NEVER);
  203. }
  204. break;
  205. default:
  206. break;
  207. }
  208. crashReportDoneLatch.countDown();
  209. crashReportDoneLatch = null;
  210. dialog = null;
  211. break;
  212. }
  213. }
  214. /**
  215. * Set the configuration that is used for this crash reporter.
  216. *
  217. * @param config the new configuration
  218. */
  219. public void setConfig(@Nullable Config config) {
  220. cfg = config;
  221. if (config != null) {
  222. setMode(config.getInteger(CFG_KEY));
  223. }
  224. }
  225. /**
  226. * Set the message source that supplies the messages for the dialog. In case
  227. * this is set to {@code null} its impossible to display a window
  228. * asking the user if the error report shall be send or not. In this case no
  229. * report message will be send.
  230. *
  231. * @param source the new source of messages
  232. */
  233. public void setMessageSource(MessageSource source) {
  234. messages = source;
  235. }
  236. /**
  237. * This function blocks the current thread from execution in case the crash
  238. * reporter is currently showing a crash report or is sending the
  239. * information on a crash to the server.
  240. */
  241. public void waitForReport() {
  242. CountDownLatch localLatch = crashReportDoneLatch;
  243. if (localLatch != null) {
  244. try {
  245. localLatch.await();
  246. } catch (InterruptedException e) {
  247. log.debug("Wait for report was interrupted!", e);
  248. // Thread interrupted. Just exit the function
  249. }
  250. }
  251. }
  252. private static final ObjectRef REPRODUCIBILITY_NA_NUM = new ObjectRef(BigInteger.valueOf(100), null);
  253. private static final ObjectRef SEVERITY_CRASH_NUM = new ObjectRef(BigInteger.valueOf(70), null);
  254. private static final ObjectRef PRIORITY_HIGH_NUM = new ObjectRef(BigInteger.valueOf(40), null);
  255. private static final String CATEGORY = "Automatic";
  256. /**
  257. * Send the data of the crash to the Illarion server.
  258. *
  259. * @param data the data that was collected about the crash
  260. */
  261. private static void sendCrashData(@Nonnull CrashData data) {
  262. if (CRASH_SERVER == null) {
  263. return;
  264. }
  265. try {
  266. MantisConnector connector = new MantisConnector();
  267. ProjectData selectedProject = connector.getProject(data.getMantisProject());
  268. if (selectedProject == null) {
  269. log.error("Failed to find {} project.", data.getMantisProject());
  270. return;
  271. }
  272. AppIdent application = data.getApplicationIdentifier();
  273. String summery = data.getExceptionName() + " in Thread " + data.getThreadName();
  274. String exceptionDescription = "Exception: " + data.getExceptionName() + "\nBacktrace:\n" +
  275. data.getStackBacktrace() + "\nDescription: " + data.getDescription();
  276. String description = "Application:" + application.getApplicationIdentifier() +
  277. (application.getCommitCount() > 0 ? " (DEV)" : "") +
  278. "\nThread: " + data.getThreadName() +
  279. '\n' + exceptionDescription;
  280. @Nullable IssueData similarIssue = null;
  281. @Nullable IssueData possibleDuplicateIssue = null;
  282. @Nullable IssueData duplicateIssue = null;
  283. FilterData filter = connector.getFilter(selectedProject);
  284. Collection<IssueHeaderData> headers = connector.getIssueHeaders(selectedProject, filter);
  285. for (@Nonnull IssueHeaderData header : headers) {
  286. if (!CATEGORY.equals(header.getCategory())) {
  287. continue;
  288. }
  289. if (!saveString(header.getSummary()).equals(summery)) {
  290. continue;
  291. }
  292. @Nonnull IssueData checkedIssue = connector.getIssue(header);
  293. if (!saveString(checkedIssue.getDescription()).endsWith(exceptionDescription)) {
  294. continue;
  295. }
  296. similarIssue = checkedIssue;
  297. if (!saveString(checkedIssue.getVersion()).equals(application.getApplicationRootVersion())) {
  298. continue;
  299. }
  300. if (!saveString(checkedIssue.getOs()).equals(System.getProperty("os.name"))) {
  301. continue;
  302. }
  303. if (!saveString(checkedIssue.getOs_build()).equals(System.getProperty("os.version"))) {
  304. continue;
  305. }
  306. possibleDuplicateIssue = checkedIssue;
  307. if (saveString(checkedIssue.getDescription()).equals(description)) {
  308. duplicateIssue = checkedIssue;
  309. break;
  310. }
  311. }
  312. if (duplicateIssue != null) {
  313. connector.addNote(duplicateIssue, "Same problem occurred again.");
  314. } else if (possibleDuplicateIssue != null) {
  315. connector.addNote(possibleDuplicateIssue, "A problem that is by all means very similar occurred:\n" +
  316. description + "\nOperating System: " +
  317. System.getProperty("os.name") + ' ' +
  318. System.getProperty("os.version"));
  319. } else {
  320. IssueData issue = new IssueData();
  321. issue.setCategory(CATEGORY);
  322. issue.setSummary(summery);
  323. issue.setDescription(description);
  324. issue.setVersion(application.getApplicationRootVersion());
  325. issue.setOs(System.getProperty("os.name"));
  326. issue.setOs_build(System.getProperty("os.version"));
  327. issue.setReproducibility(REPRODUCIBILITY_NA_NUM);
  328. issue.setSeverity(SEVERITY_CRASH_NUM);
  329. issue.setPriority(PRIORITY_HIGH_NUM);
  330. BigInteger id = connector.addIssue(selectedProject, issue);
  331. log.info("Added new Issue #{}", id);
  332. if (similarIssue != null) {
  333. connector.addNote(issue, "Similar issue was found at #" + similarIssue.getId());
  334. connector.addRelation(issue, similarIssue);
  335. }
  336. }
  337. } catch (ServiceException | RemoteException e) {
  338. log.error("Failed to send error reporting data.", e);
  339. }
  340. }
  341. @Nonnull
  342. @Contract(pure = true)
  343. private static String saveString(@Nullable String input) {
  344. if (input == null) {
  345. return "";
  346. }
  347. return input;
  348. }
  349. /**
  350. * Set a new value for the mode of this crash reporter. The legal values for
  351. * this mode are {@link #MODE_ALWAYS}, {@link #MODE_ASK} and
  352. * {@link #MODE_NEVER}.
  353. *
  354. * @param newMode the new mode value
  355. * @throws IllegalArgumentException in case the invalid mode value is chosen
  356. */
  357. public void setMode(int newMode) {
  358. if ((newMode != MODE_ALWAYS) && (newMode != MODE_ASK) && (newMode != MODE_NEVER)) {
  359. mode = MODE_ASK;
  360. return;
  361. }
  362. mode = newMode;
  363. }
  364. }