You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ProgramError.java 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. /*
  2. * Copyright (c) 2006-2014 DMDirc Developers
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining a copy
  5. * of this software and associated documentation files (the "Software"), to deal
  6. * in the Software without restriction, including without limitation the rights
  7. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. * copies of the Software, and to permit persons to whom the Software is
  9. * furnished to do so, subject to the following conditions:
  10. *
  11. * The above copyright notice and this permission notice shall be included in
  12. * all copies or substantial portions of the Software.
  13. *
  14. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  20. * SOFTWARE.
  21. */
  22. package com.dmdirc.logger;
  23. import com.dmdirc.config.IdentityManager;
  24. import com.dmdirc.ui.core.util.Info;
  25. import com.dmdirc.util.io.Downloader;
  26. import java.io.File;
  27. import java.io.FileOutputStream;
  28. import java.io.IOException;
  29. import java.io.OutputStream;
  30. import java.io.PrintWriter;
  31. import java.io.Serializable;
  32. import java.net.MalformedURLException;
  33. import java.text.DateFormat;
  34. import java.text.SimpleDateFormat;
  35. import java.util.ArrayList;
  36. import java.util.Arrays;
  37. import java.util.Date;
  38. import java.util.HashMap;
  39. import java.util.List;
  40. import java.util.Locale;
  41. import java.util.Map;
  42. import java.util.Objects;
  43. import java.util.concurrent.Semaphore;
  44. import java.util.concurrent.atomic.AtomicInteger;
  45. import javax.annotation.Nullable;
  46. import net.kencochrane.raven.Raven;
  47. import net.kencochrane.raven.RavenFactory;
  48. import net.kencochrane.raven.dsn.Dsn;
  49. import net.kencochrane.raven.event.Event;
  50. import net.kencochrane.raven.event.EventBuilder;
  51. import net.kencochrane.raven.event.interfaces.ExceptionInterface;
  52. /**
  53. * Stores a program error.
  54. */
  55. public final class ProgramError implements Serializable {
  56. /**
  57. * A version number for this class. It should be changed whenever the class
  58. * structure is changed (or anything else that would prevent serialized
  59. * objects being unserialized with the new class).
  60. */
  61. private static final long serialVersionUID = 3;
  62. /** Directory used to store errors. */
  63. private static File errorDir;
  64. /** Semaphore used to serialise write access. */
  65. private static final Semaphore WRITING_SEM = new Semaphore(1);
  66. public static final String SENTRY_DSN = "http://d53a31a3c53c4a4f91c5ff503e612677:e0a8aa1ecca14568a9f52d052ecf6a30@sentry.dmdirc.com/2";
  67. /** Error ID. */
  68. private final long id;
  69. /** Error icon. */
  70. private final ErrorLevel level;
  71. /** Error message. */
  72. private final String message;
  73. /** Underlying exception. */
  74. private final Throwable exception;
  75. /** Underlying details message. */
  76. private final String details;
  77. /** Date/time error first occurred. */
  78. private final Date firstDate;
  79. /** Date/time error last occurred. */
  80. private Date lastDate;
  81. /** Number of occurrences. */
  82. private AtomicInteger count;
  83. /** Error report Status. */
  84. private ErrorReportStatus reportStatus;
  85. /** Error fixed Status. */
  86. private ErrorFixedStatus fixedStatus;
  87. /**
  88. * Creates a new instance of ProgramError.
  89. *
  90. * @param id error id
  91. * @param level Error level
  92. * @param message Error message
  93. * @param exception The exception that caused the error, if any.
  94. * @param details The detailed cause of the error, if any.
  95. * @param date Error time and date
  96. */
  97. public ProgramError(final long id, final ErrorLevel level, final String message,
  98. @Nullable final Throwable exception,
  99. @Nullable final String details,
  100. final Date date) {
  101. if (id < 0) {
  102. throw new IllegalArgumentException("ID must be a positive integer: " + id);
  103. }
  104. if (level == null) {
  105. throw new IllegalArgumentException("Level cannot be null");
  106. }
  107. if (message == null || message.isEmpty()) {
  108. throw new IllegalArgumentException("Message cannot be null or an empty string");
  109. }
  110. if (date == null) {
  111. throw new IllegalArgumentException("date cannot be null");
  112. }
  113. this.id = id;
  114. this.level = level;
  115. this.message = message;
  116. this.exception = exception;
  117. this.details = details;
  118. this.firstDate = (Date) date.clone();
  119. this.lastDate = (Date) date.clone();
  120. this.count = new AtomicInteger(1);
  121. this.reportStatus = ErrorReportStatus.WAITING;
  122. this.fixedStatus = ErrorFixedStatus.UNKNOWN;
  123. }
  124. /**
  125. * Returns this errors level.
  126. *
  127. * @return Error level
  128. */
  129. public ErrorLevel getLevel() {
  130. return level;
  131. }
  132. /**
  133. * Returns this errors message.
  134. *
  135. * @return Error message
  136. */
  137. public String getMessage() {
  138. return message;
  139. }
  140. /**
  141. * Returns this errors trace.
  142. *
  143. * @return Error trace
  144. */
  145. public String[] getTrace() {
  146. return exception != null ? getTrace(exception) :
  147. message != null ? new String[] { message } :
  148. new String[0];
  149. }
  150. /**
  151. * Returns this errors time.
  152. *
  153. * @return Error time
  154. */
  155. public Date getDate() {
  156. return (Date) firstDate.clone();
  157. }
  158. /**
  159. * Returns the number of times this error has occurred.
  160. *
  161. * @return Error count
  162. */
  163. public int getCount() {
  164. return count.get();
  165. }
  166. /**
  167. * Returns the last time this error occurred.
  168. *
  169. * @return Last occurrence
  170. */
  171. public Date getLastDate() {
  172. return (Date) lastDate.clone();
  173. }
  174. /**
  175. * Returns the reportStatus of this error.
  176. *
  177. * @return Error reportStatus
  178. */
  179. public ErrorReportStatus getReportStatus() {
  180. return reportStatus;
  181. }
  182. /**
  183. * Returns the fixed status of this error.
  184. *
  185. * @return Error fixed status
  186. */
  187. public ErrorFixedStatus getFixedStatus() {
  188. return fixedStatus;
  189. }
  190. /**
  191. * Sets the report Status of this error.
  192. *
  193. * @param newStatus new ErrorReportStatus for the error
  194. */
  195. public void setReportStatus(final ErrorReportStatus newStatus) {
  196. if (newStatus != null && !reportStatus.equals(newStatus)) {
  197. reportStatus = newStatus;
  198. ErrorManager.getErrorManager().fireErrorStatusChanged(this);
  199. synchronized (this) {
  200. notifyAll();
  201. }
  202. }
  203. }
  204. /**
  205. * Sets the fixed status of this error.
  206. *
  207. * @param newStatus new ErrorFixedStatus for the error
  208. */
  209. public void setFixedStatus(final ErrorFixedStatus newStatus) {
  210. if (newStatus != null && !fixedStatus.equals(newStatus)) {
  211. fixedStatus = newStatus;
  212. ErrorManager.getErrorManager().fireErrorStatusChanged(this);
  213. synchronized (this) {
  214. notifyAll();
  215. }
  216. }
  217. }
  218. /**
  219. * Returns the ID of this error.
  220. *
  221. * @return Error ID
  222. */
  223. public long getID() {
  224. return id;
  225. }
  226. /**
  227. * Saves this error to disk.
  228. */
  229. public void save() {
  230. try (PrintWriter out = new PrintWriter(getErrorFile(), true)) {
  231. out.println("Date:" + getDate());
  232. out.println("Level: " + getLevel());
  233. out.println("Description: " + getMessage());
  234. out.println("Details:");
  235. for (String traceLine : getTrace()) {
  236. out.println('\t' + traceLine);
  237. }
  238. }
  239. }
  240. /**
  241. * Creates a new file for an error and returns the output stream.
  242. *
  243. * @return BufferedOutputStream to write to the error file
  244. */
  245. @SuppressWarnings("PMD.SystemPrintln")
  246. private OutputStream getErrorFile() {
  247. WRITING_SEM.acquireUninterruptibly();
  248. if (errorDir == null || !errorDir.exists()) {
  249. errorDir = new File(IdentityManager.getIdentityManager().getConfigurationDirectory() + "errors");
  250. if (!errorDir.exists()) {
  251. errorDir.mkdirs();
  252. }
  253. }
  254. final String logName = getDate().getTime() + "-" + getLevel();
  255. final File errorFile = new File(errorDir, logName + ".log");
  256. if (errorFile.exists()) {
  257. boolean rename = false;
  258. for (int i = 0; !rename; i++) {
  259. rename = errorFile.renameTo(new File(errorDir, logName + "-" + i + ".log"));
  260. }
  261. }
  262. try {
  263. errorFile.createNewFile();
  264. return new FileOutputStream(errorFile);
  265. } catch (IOException ex) {
  266. System.err.println("Error creating new file: ");
  267. ex.printStackTrace();
  268. return new NullOutputStream();
  269. } finally {
  270. WRITING_SEM.release();
  271. }
  272. }
  273. /**
  274. * Sends this error report to the DMDirc developers.
  275. */
  276. public void send() {
  277. sendToSentry();
  278. sendToLegacy();
  279. }
  280. private void sendToSentry() {
  281. final Raven raven = RavenFactory.ravenInstance(new Dsn(SENTRY_DSN));
  282. // record a simple message
  283. final EventBuilder eventBuilder = new EventBuilder()
  284. .setMessage(message)
  285. .setLevel(getSentryLevel())
  286. .setServerName("")
  287. .setTimestamp(firstDate)
  288. .addTag("version", getVersion())
  289. .addTag("version.major", getVersion().replaceAll("-.*", ""))
  290. .addTag("os.name", System.getProperty("os.name", "unknown"))
  291. .addTag("os.version", System.getProperty("os.version", "unknown"))
  292. .addTag("os.arch", System.getProperty("os.arch", "unknown"))
  293. .addTag("encoding", System.getProperty("file.encoding", "unknown"))
  294. .addTag("locale", Locale.getDefault().toString())
  295. .addTag("jvm.name", System.getProperty("java.vm.name", "unknown"))
  296. .addTag("jvm.vendor", System.getProperty("java.vm.vendor", "unknown"))
  297. .addTag("jvm.version", System.getProperty("java.version", "unknown"))
  298. .addTag("jvm.version.major", System.getProperty("java.version", "unknown").replaceAll("_.*", ""));
  299. if (exception != null) {
  300. eventBuilder.addSentryInterface(new ExceptionInterface(exception));
  301. }
  302. if (details != null) {
  303. eventBuilder.addExtra("details", details);
  304. }
  305. raven.sendEvent(eventBuilder.build());
  306. }
  307. private void sendToLegacy() {
  308. final Map<String, String> postData = new HashMap<>();
  309. List<String> response = new ArrayList<>();
  310. int tries = 0;
  311. String traceString = Arrays.toString(getTrace());
  312. if (traceString.isEmpty() || traceString.equals("[]")) {
  313. traceString = "[No Trace]";
  314. }
  315. postData.put("message", getMessage());
  316. postData.put("trace", traceString);
  317. postData.put("version", IdentityManager.getIdentityManager()
  318. .getGlobalConfiguration().getOption("version", "version"));
  319. setReportStatus(ErrorReportStatus.SENDING);
  320. do {
  321. if (tries != 0) {
  322. try {
  323. Thread.sleep(5000);
  324. } catch (InterruptedException ex) {
  325. //Ignore
  326. }
  327. }
  328. try {
  329. response = Downloader.getPage("http://www.dmdirc.com/error.php", postData);
  330. } catch (MalformedURLException ex) {
  331. //Ignore, wont happen
  332. } catch (IOException ex) {
  333. //Ignore being handled
  334. }
  335. tries++;
  336. } while ((response.isEmpty() || !response.get(response.size() - 1).
  337. equalsIgnoreCase("Error report submitted. Thank you."))
  338. && tries <= 5);
  339. checkResponses(response);
  340. }
  341. /**
  342. * Checks the responses and sets status accordingly.
  343. *
  344. * @param error Error to check response
  345. * @param response Response to check
  346. */
  347. private void checkResponses(final List<String> response) {
  348. if (!response.isEmpty() && response.get(response.size() - 1).
  349. equalsIgnoreCase("Error report submitted. Thank you.")) {
  350. setReportStatus(ErrorReportStatus.FINISHED);
  351. } else {
  352. setReportStatus(ErrorReportStatus.ERROR);
  353. return;
  354. }
  355. if (response.size() == 1) {
  356. setFixedStatus(ErrorFixedStatus.NEW);
  357. return;
  358. }
  359. final String responseToCheck = response.get(0);
  360. if (responseToCheck.matches(".*fixed.*")) {
  361. setFixedStatus(ErrorFixedStatus.FIXED);
  362. } else if (responseToCheck.matches(".*more recent version.*")) {
  363. setFixedStatus(ErrorFixedStatus.TOOOLD);
  364. } else if (responseToCheck.matches(".*invalid.*")) {
  365. setFixedStatus(ErrorFixedStatus.INVALID);
  366. } else if (responseToCheck.matches(".*previously.*")) {
  367. setFixedStatus(ErrorFixedStatus.KNOWN);
  368. } else {
  369. setFixedStatus(ErrorFixedStatus.NEW);
  370. }
  371. }
  372. /**
  373. * Determines whether or not the stack trace associated with this error
  374. * is from a valid source. A valid source is one that is within a DMDirc
  375. * package (com.dmdirc), and is not the DMDirc event queue.
  376. *
  377. * @return True if the source is valid, false otherwise
  378. */
  379. public boolean isValidSource() {
  380. final String line = getSourceLine();
  381. return line.startsWith("com.dmdirc")
  382. && !line.startsWith("com.dmdirc.addons.ui_swing.DMDircEventQueue");
  383. }
  384. /**
  385. * Returns the "source line" of this error, which is defined as the first
  386. * line starting with a DMDirc package name (com.dmdirc). If no such line
  387. * is found, returns the first line of the message.
  388. *
  389. * @return This error's source line
  390. */
  391. public String getSourceLine() {
  392. final String[] trace = getTrace();
  393. for (String line : trace) {
  394. if (line.startsWith("com.dmdirc")) {
  395. return line;
  396. }
  397. }
  398. return trace[0];
  399. }
  400. /**
  401. * Updates the last date this error occurred.
  402. */
  403. public void updateLastDate() {
  404. updateLastDate(new Date());
  405. }
  406. /**
  407. * Updates the last date this error occurred.
  408. *
  409. * @param date Date error occurred
  410. */
  411. public void updateLastDate(final Date date) {
  412. lastDate = date;
  413. count.getAndIncrement();
  414. ErrorManager.getErrorManager().fireErrorStatusChanged(this);
  415. synchronized (this) {
  416. notifyAll();
  417. }
  418. }
  419. /**
  420. * Retruns a human readable string describing the number of times
  421. * this error occurred and when these occurrences were.
  422. *
  423. * @return Occurrences description
  424. */
  425. public String occurrencesString() {
  426. final DateFormat format = new SimpleDateFormat("MMM dd hh:mm aa");
  427. if (count.get() == 1) {
  428. return "1 occurrence on " + format.format(getDate());
  429. } else {
  430. return count.get() + " occurrences between " + format.format(
  431. getDate()) + " and " + format.format(getLastDate()) + ".";
  432. }
  433. }
  434. /** {@inheritDoc} */
  435. @Override
  436. public String toString() {
  437. return "ID" + id + " Level: " + getLevel() + " Status: " + getReportStatus()
  438. + " Message: '" + getMessage() + "'";
  439. }
  440. /** {@inheritDoc} */
  441. @Override
  442. public boolean equals(final Object obj) {
  443. if (obj == null) {
  444. return false;
  445. }
  446. if (getClass() != obj.getClass()) {
  447. return false;
  448. }
  449. final ProgramError other = (ProgramError) obj;
  450. if (this.level != other.level) {
  451. return false;
  452. }
  453. if (!this.message.equals(other.message)) {
  454. return false;
  455. }
  456. if (!Objects.equals(this.exception, other.exception)) {
  457. return false;
  458. }
  459. if (!Objects.equals(this.details, other.details)) {
  460. return false;
  461. }
  462. return true;
  463. }
  464. /** {@inheritDoc} */
  465. @Override
  466. public int hashCode() {
  467. int hash = 7;
  468. hash = 67 * hash + this.level.hashCode();
  469. hash = 67 * hash + this.message.hashCode();
  470. hash = 67 * hash + (this.exception == null ? 1 : this.exception.hashCode());
  471. hash = 67 * hash + (this.details == null ? 1 : this.details.hashCode());
  472. return hash;
  473. }
  474. /**
  475. * Converts an exception into a string array.
  476. *
  477. * @param throwable Exception to convert
  478. * @since 0.6.3m1
  479. * @return Exception string array
  480. */
  481. private static String[] getTrace(final Throwable throwable) {
  482. String[] trace;
  483. if (throwable == null) {
  484. trace = new String[0];
  485. } else {
  486. final StackTraceElement[] traceElements = throwable.getStackTrace();
  487. trace = new String[traceElements.length + 1];
  488. trace[0] = throwable.toString();
  489. for (int i = 0; i < traceElements.length; i++) {
  490. trace[i + 1] = traceElements[i].toString();
  491. }
  492. if (throwable.getCause() != null) {
  493. final String[] causeTrace = getTrace(throwable.getCause());
  494. final String[] newTrace = new String[trace.length + causeTrace.length];
  495. trace[0] = "\nWhich caused: " + trace[0];
  496. System.arraycopy(causeTrace, 0, newTrace, 0, causeTrace.length);
  497. System.arraycopy(trace, 0, newTrace, causeTrace.length, trace.length);
  498. trace = newTrace;
  499. }
  500. }
  501. return trace;
  502. }
  503. private Event.Level getSentryLevel() {
  504. switch (level) {
  505. case FATAL:
  506. return Event.Level.FATAL;
  507. case HIGH:
  508. return Event.Level.ERROR;
  509. case MEDIUM:
  510. return Event.Level.WARNING;
  511. case LOW:
  512. return Event.Level.INFO;
  513. default:
  514. return Event.Level.INFO;
  515. }
  516. }
  517. private String getVersion() {
  518. return IdentityManager.getIdentityManager().getGlobalConfiguration()
  519. .getOption("version", "version");
  520. }
  521. }