package com.limegroup.gnutella.bugs; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.SwingUtilities; import com.limegroup.gnutella.gui.GUIMediator; import com.limegroup.gnutella.gui.MessageService; import com.limegroup.gnutella.gui.MultiLineLabel; import com.limegroup.gnutella.settings.BugSettings; import com.limegroup.gnutella.util.CommonUtils; import com.limegroup.gnutella.util.FileUtils; import com.limegroup.gnutella.util.IOUtils; import com.limegroup.gnutella.util.ProcessingQueue; import com.limegroup.gnutella.version.Version; import com.limegroup.gnutella.version.VersionFormatException; /** * Interface for reporting bugs. * This can do any of the following: * - Send the bug directly to the servlet * - Allow the bug to be reviewed before sending * - Allow the user to copy the bug & email it if sending fails. * - Supress the bug entirely */ public final class BugManager { /** * The instance of BugManager -- follows a singleton pattern. */ private static final BugManager INSTANCE = new BugManager(); /** * The error title */ private static final String TITLE = GUIMediator.getStringResource("ERROR_INTERNAL_TITLE"); /** * The queue that processes processes the bugs. */ private final ProcessingQueue BUGS_QUEUE = new ProcessingQueue("BugProcessor", false); /** * A mapping of stack traces (String) to next allowed time (long) * that the bug can be reported. * * Used only if reporting the bug to the servlet. */ private final Map BUG_TIMES = Collections.synchronizedMap(new HashMap()); /** * A lock to be used when writing to the logfile, if the log is to be * recorded locally. */ private final Object WRITE_LOCK = new Object(); /** * A separator between bug reports. */ private static final byte[] SEPARATOR = "-----------------\n".getBytes(); /** * The next time we're allowed to send any bug. * * Used only if reporting the bug to the servlet. */ private volatile long _nextAllowedTime = 0; /** * The number of bug dialogs currently showing. */ private volatile int _dialogsShowing = 0; /** * The maximum number of dialogs we're allowed to show. */ private final static int MAX_DIALOGS = 3; /** * Whether or not we have dirty data after the last save. */ private boolean dirty = false; public static BugManager instance() { return INSTANCE; } /** * Private to ensure that only this class can construct a * <tt>BugManager</tt>, thereby ensuring that only one instance is created. */ private BugManager() { loadOldBugs(); } /** * Shuts down the BugManager. */ public void shutdown() { writeBugsToDisk(); } /** * Handles a single bug report. * If bug is a ThreadDeath, rethrows it. * If the user wants to ignore all bugs, this effectively does nothing. * The the server told us to stop reporting this (or any) bug(s) for * awhile, this effectively does nothing. * Otherwise, it will either send the bug directly to the servlet * or ask the user to review it before sending. */ public void handleBug(Throwable bug, String threadName, String detail) { if( bug instanceof ThreadDeath ) // must rethrow. throw (ThreadDeath)bug; // Try to dispatch the bug to a friendly handler. if(bug instanceof IOException && IOUtils.handleException((IOException)bug, null)) return; // handled already. bug.printStackTrace(); // Build the LocalClientInfo out of the info ... LocalClientInfo info = new LocalClientInfo(bug, threadName, detail, false); if( BugSettings.LOG_BUGS_LOCALLY.getValue() ) logBugLocally(info); boolean sent = false; // never ignore bugs or auto-send when developing. if(!CommonUtils.isTestingVersion()) { if( BugSettings.IGNORE_ALL_BUGS.getValue() ) return; // ignore. // If we have already sent information about this bug, leave. if( !shouldInform(info) ) return; // ignore. // If the user wants to automatically send to the servlet, do so. // Otherwise, display it for review. if( BugSettings.USE_BUG_SERVLET.getValue() && isSendableVersion() ) { sent = true; sendToServlet(info); } } if (!sent && _dialogsShowing < MAX_DIALOGS ) reviewBug(info); } /** * Logs the bug report to a local file. * If the file reaches a certain size it is erased. */ private void logBugLocally(LocalClientInfo info) { File f = BugSettings.BUG_LOG_FILE.getValue(); FileUtils.setWriteable(f); OutputStream os = null; try { synchronized(WRITE_LOCK) { if ( f.length() > BugSettings.MAX_BUGFILE_SIZE.getValue() ) f.delete(); os = new BufferedOutputStream( new FileOutputStream(f.getPath(), true)); os.write((new Date().toString() + "\n").getBytes()); os.write(info.toBugReport().getBytes()); os.write(SEPARATOR); os.flush(); } } catch(IOException ignored) { } finally { if( os != null ) try { os.close(); } catch(IOException ignored) {} } } /** * Loads bugs from disk. */ private void loadOldBugs() { ObjectInputStream in = null; File f = BugSettings.BUG_INFO_FILE.getValue(); try { // Purposely not a ConverterObjectInputStream -- // we never want to read old version's bug info. in = new ObjectInputStream( new BufferedInputStream( new FileInputStream(f))); String version = (String)in.readObject(); long nextTime = in.readLong(); Map bugs = (Map)in.readObject(); // Only load them if we're continuing to use the same version // This way bugs for newer versions get reported. // We could check to make sure this is a newer version, // but it's not all that necessary. if( version.equals(CommonUtils.getLimeWireVersion()) ) { _nextAllowedTime = nextTime; long now = System.currentTimeMillis(); Iterator i = bugs.entrySet().iterator(); // Only insert those whose times haven't expired. while(i.hasNext()) { Map.Entry entry = (Map.Entry)i.next(); Long allowed = (Long)entry.getValue(); if( allowed != null && now < allowed.longValue() ) { BUG_TIMES.put(entry.getKey(), allowed); } } } else { // Otherwise, we're using a different version than the last time. // Unset 'discard all bugs'. if(BugSettings.IGNORE_ALL_BUGS.getValue()) { BugSettings.IGNORE_ALL_BUGS.setValue(false); BugSettings.USE_BUG_SERVLET.setValue(false); } } } catch(Throwable t) { // ignore errors from disk. } finally { if(in != null) try { in.close(); } catch(Throwable t) {} } } /** * Write bugs out to disk. */ private void writeBugsToDisk() { synchronized(WRITE_LOCK) { if(!dirty) return; ObjectOutputStream out = null; try { File f = BugSettings.BUG_INFO_FILE.getValue(); out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(f))); String version = CommonUtils.getLimeWireVersion(); out.writeObject(version); out.writeLong(_nextAllowedTime); out.writeObject(BUG_TIMES); out.flush(); } catch(Exception e) { // oh well, no biggie if we couldn't write to disk. } finally { if(out != null) try { out.close(); } catch(IOException e) {} } dirty = false; } } /** * Determines if the bug has already been reported enough. * If it has, this returns false. Otherwise (if the bug should * be reported) this returns true. */ private boolean shouldInform(LocalClientInfo info) { long now = System.currentTimeMillis(); // If we aren't allowed to report a bug, exit. if( now < _nextAllowedTime ) return false; Long allowed = (Long)BUG_TIMES.get(info.getParsedBug()); return allowed == null || now >= allowed.longValue(); } /** * Determines if we're allowed to send a bug report. */ private boolean isSendableVersion() { Version myVersion; Version lastVersion; try { myVersion = new Version(CommonUtils.getLimeWireVersion()); lastVersion = new Version(BugSettings.LAST_ACCEPTABLE_VERSION.getValue()); } catch(VersionFormatException vfe) { return false; } return myVersion.compareTo(lastVersion) >= 0; } /** * Displays a message to the user informing them an internal error * has occurred. The user is asked to click 'send' to send the bug * report to the servlet and has the option to review the bug * before it is sent. */ private void reviewBug(final LocalClientInfo info) { _dialogsShowing++; final JDialog DIALOG = new JDialog(GUIMediator.getAppFrame(), TITLE, true); final Dimension DIALOG_DIMENSION = new Dimension(100, 300); DIALOG.setSize(DIALOG_DIMENSION); JPanel mainPanel = new JPanel(); mainPanel.setBorder(BorderFactory.createEmptyBorder(10,10,10,10)); mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); boolean sendable = isSendableVersion(); MultiLineLabel label; if(sendable) label = new MultiLineLabel(GUIMediator.getStringResource("ERROR_INTERNAL_SERVLET"), 400); else label = new MultiLineLabel(GUIMediator.getStringResource("ERROR_INTERNAL_OLD"), 400); JPanel labelPanel = new JPanel(); labelPanel.setLayout(new BoxLayout(labelPanel, BoxLayout.X_AXIS)); labelPanel.add(Box.createHorizontalGlue()); labelPanel.add(label); JPanel buttonPanel = new JPanel(); JButton sendButton = new JButton(GUIMediator.getStringResource("ERROR_INTERNAL_SEND")); sendButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { sendToServlet(info); DIALOG.dispose(); _dialogsShowing--; } }); JButton reviewButton = new JButton(GUIMediator.getStringResource("ERROR_INTERNAL_REVIEW")); reviewButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { JTextArea textArea = new JTextArea(info.toBugReport()); textArea.setColumns(50); textArea.setEditable(false); textArea.setCaretPosition(0); JScrollPane scroller = new JScrollPane(textArea); scroller.setBorder(BorderFactory.createEtchedBorder()); scroller.setPreferredSize( new Dimension(500, 200) ); MessageService.instance().showMessage(scroller); } }); JButton discardButton = new JButton(GUIMediator.getStringResource("ERROR_INTERNAL_DISCARD")); discardButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { DIALOG.dispose(); _dialogsShowing--; } }); if(sendable) buttonPanel.add(sendButton); buttonPanel.add(reviewButton); buttonPanel.add(discardButton); JPanel optionsPanel = new JPanel(); JPanel innerPanel = new JPanel(); ButtonGroup bg = new ButtonGroup(); innerPanel.setLayout( new BoxLayout(innerPanel, BoxLayout.Y_AXIS)); optionsPanel.setLayout(new BorderLayout()); final JRadioButton alwaysSend = new JRadioButton(GUIMediator.getStringResource("ERROR_INTERNAL_ALWAYS_SEND")); final JRadioButton alwaysReview = new JRadioButton(GUIMediator.getStringResource("ERROR_INTERNAL_ALWAYS_REVIEW")); final JRadioButton alwaysDiscard = new JRadioButton(GUIMediator.getStringResource("ERROR_INTERNAL_ALWAYS_DISCARD")); innerPanel.add(Box.createVerticalStrut(6)); if(!CommonUtils.isTestingVersion()) { if(sendable) innerPanel.add(alwaysSend); innerPanel.add(alwaysReview); innerPanel.add(alwaysDiscard); } innerPanel.add(Box.createVerticalStrut(6)); optionsPanel.add( innerPanel, BorderLayout.WEST ); bg.add(alwaysSend); bg.add(alwaysReview); bg.add(alwaysDiscard); bg.setSelected(alwaysReview.getModel(), true); ActionListener alwaysListener = new ActionListener() { public void actionPerformed(ActionEvent e) { if( e.getSource() == alwaysSend ) { BugSettings.IGNORE_ALL_BUGS.setValue(false); BugSettings.USE_BUG_SERVLET.setValue(true); } else if (e.getSource() == alwaysReview ) { BugSettings.IGNORE_ALL_BUGS.setValue(false); BugSettings.USE_BUG_SERVLET.setValue(false); } else if( e.getSource() == alwaysDiscard ) { BugSettings.IGNORE_ALL_BUGS.setValue(true); } } }; alwaysSend.addActionListener(alwaysListener); alwaysReview.addActionListener(alwaysListener); alwaysDiscard.addActionListener(alwaysListener); mainPanel.add(labelPanel); mainPanel.add(optionsPanel); mainPanel.add(buttonPanel); DIALOG.getContentPane().add(mainPanel); DIALOG.pack(); if(GUIMediator.isAppVisible()) DIALOG.setLocationRelativeTo(MessageService.getParentComponent()); else { Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); Dimension dialogSize = DIALOG.getSize(); DIALOG.setLocation((screenSize.width - dialogSize.width)/2, (screenSize.height - dialogSize.height)/2); } try { DIALOG.setVisible(true); } catch(InternalError ie) { //happens occasionally, ignore. } catch(ArrayIndexOutOfBoundsException npe) { //happens occasionally, ignore. } } /** * Displays a message to the user informing them an internal error * has occurred and the send to the servlet has failed, asking * the user to email the bug to us. */ private void servletSendFailed(final LocalClientInfo info) { _dialogsShowing++; final JDialog DIALOG = new JDialog(GUIMediator.getAppFrame(), TITLE, true); final Dimension DIALOG_DIMENSION = new Dimension(350, 300); final Dimension ERROR_DIMENSION = new Dimension(300, 200); DIALOG.setSize(DIALOG_DIMENSION); JPanel mainPanel = new JPanel(); mainPanel.setBorder(BorderFactory.createEmptyBorder(10,10,10,10)); mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); MultiLineLabel label = new MultiLineLabel(GUIMediator.getStringResource("ERROR_INTERNAL_SERVLET_FAILED"), 400); JPanel labelPanel = new JPanel(); JPanel innerPanel = new JPanel(); labelPanel.setLayout(new BoxLayout(labelPanel, BoxLayout.X_AXIS)); innerPanel.setLayout(new BoxLayout(innerPanel, BoxLayout.Y_AXIS)); innerPanel.add(label); innerPanel.add(Box.createVerticalStrut(6)); labelPanel.add(innerPanel); labelPanel.add(Box.createHorizontalGlue()); // Add 'FILES IN CURRENT DIRECTORY [text] // SIZE: 0' // So that the script processing the emails still // works correctly. [It uses the info as markers // of when to stop reading -- if it wasn't present // it failed processing the email correctly.] String bugInfo = info.toBugReport().trim() + "\n\n" + "FILES IN CURRENT DIRECTORY NOT LISTED.\n" + "SIZE: 0"; final JTextArea textArea = new JTextArea(bugInfo); textArea.selectAll(); textArea.copy(); textArea.setColumns(50); textArea.setEditable(false); JScrollPane scroller = new JScrollPane(textArea); scroller.setBorder(BorderFactory.createEtchedBorder()); scroller.setPreferredSize(ERROR_DIMENSION); JPanel buttonPanel = new JPanel(); JButton copyButton = new JButton(GUIMediator.getStringResource("ERROR_INTERNAL_COPY")); copyButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { textArea.selectAll(); textArea.copy(); textArea.setCaretPosition(0); } }); JButton quitButton = new JButton(GUIMediator.getStringResource("ERROR_INTERNAL_OK")); quitButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ DIALOG.dispose(); _dialogsShowing--; } }); buttonPanel.add(copyButton); buttonPanel.add(quitButton); mainPanel.add(labelPanel); mainPanel.add(scroller); mainPanel.add(buttonPanel); DIALOG.getContentPane().add(mainPanel); try { DIALOG.pack(); } catch(OutOfMemoryError oome) { // we couldn't put this dialog together, discard it entirely. return; } if(GUIMediator.isAppVisible()) DIALOG.setLocationRelativeTo(MessageService.getParentComponent()); else { Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); Dimension dialogSize = DIALOG.getSize(); DIALOG.setLocation((screenSize.width - dialogSize.width)/2, (screenSize.height - dialogSize.height)/2); } DIALOG.show(); } /** * Sends the bug to the servlet and updates the next allowed times * that this bug (or any bug) can be sent. * This is done in another thread so the current thread does not block * while connecting and transferring information to/from the servlet. * If the send failed, displays another message asking the user to email * the error. */ private void sendToServlet(final LocalClientInfo info) { BUGS_QUEUE.add(new ServletSender(info)); } /** * Sends a single bug report. */ private class ServletSender implements Runnable { final LocalClientInfo INFO; ServletSender(LocalClientInfo info) { INFO = info; } public void run() { // Send this bug to the servlet & store its response. // THIS CALL BLOCKS. RemoteClientInfo remoteInfo = new ServletAccessor().getRemoteBugInfo(INFO); if( remoteInfo == null ) { // could not connect SwingUtilities.invokeLater( new Runnable() { public void run() { servletSendFailed(INFO); } }); return; } long now = System.currentTimeMillis(); long thisNextTime = remoteInfo.getNextThisBugTime(); long anyNextTime = remoteInfo.getNextAnyBugTime(); synchronized(WRITE_LOCK) { if( anyNextTime != 0 ) { _nextAllowedTime = now + thisNextTime; dirty = true; } if( thisNextTime != 0 ) { BUG_TIMES.put(INFO.getParsedBug(), new Long(now + thisNextTime)); dirty = true; } writeBugsToDisk(); } } } }