Reporting "Crash To Desktop" Failures – code inside

Hi all,

This is something I’ve just written for HeroDex and while its still a bit rough-and-ready seems to have promise as a way to handle crashes in as user-friendly a way as possible and to extract that vital stack trace and system information without teaching tech to non-techies.

The general idea is that an error is detected and a window is displayed showing data collected about the machine its running on and the description and stack trace of the error. The text from that can be copied by the user (and is already placed in the clipboard for them) and additionally there is a “Send Report” button. The button posts the report to a website where you can then do whatever you like with it, a very simple example is provided that just stores it into a database. For HeroDex we send crash reports to the Zero Separation website and a separate database there rather than placing any load at all onto the main HeroDex website and database. You could remove the upload, have it store the data a different way, or do whatever you liked with your own version.

Step one:
Create a file CrashReport.java
[java]
package net.herodex.client.main;

import com.jme3.app.Application;
import com.jme3.renderer.Caps;
import com.zeroseparation.protocol.Definitions;
import java.awt.BorderLayout;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.HeadlessException;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.GL11;

/**
*

  • @author Tim
    */
    public class CrashReport extends JFrame {

    private static final Logger logger = Logger.getLogger(CrashReport.class.getName());

    public CrashReport(final Application app, String player, String errorMsg, Throwable error) throws HeadlessException {

     try  {
    
         setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
         setVisible(true);
         setTitle("HeroDex Crash Report");
         setLayout(new GridBagLayout());
         setAlwaysOnTop(true);
    
         GridBagConstraints gbc = new GridBagConstraints();
         JTextArea header = new JTextArea("YOUR GAME HERE has failed, and would like to send a crash report to YOUR NAME HERE.\n"
                 + "All the information that will be sent can be reviewed below and has also been copied into the clipboard.\n"
                 + "You can either copy it into a bug report or press the Send button to have it sent automatically.");
         header.setFont(header.getFont().deriveFont(Font.BOLD));
         header.setEnabled(false);
         add(header, gbc);
    
         gbc = new GridBagConstraints();
         gbc.gridy = 1;
         gbc.fill = GridBagConstraints.BOTH;
         gbc.weightx = 1;
         gbc.weighty = 1;
    
         JTextArea crashReportText = new JTextArea();
         crashReportText.setEditable(false);
         add(new JScrollPane(crashReportText), gbc);
    
         StringBuilder builder = new StringBuilder();
         builder.append("Crash for user: ").append(player == null? "Not logged in yet": player).append("\n");
         builder.append("\n");
         builder.append("System Details\n");
         builder.append("OsName: ").append(System.getProperty("os.name")).append("\n");
         builder.append("OsArchitecture: ").append(System.getProperty("os.arch")).append("\n");
         builder.append("OsVersion: ").append(System.getProperty("os.version")).append("\n");
         builder.append("JavaVersion: ").append(System.getProperty("java.version")).append("\n");
         builder.append("JavaVender: ").append(System.getProperty("java.vendor")).append("\n");
         builder.append("DisplayAdaptor: ").append(Display.getAdapter()).append("\n");
         builder.append("DisplayVersion: ").append(Display.getVersion()).append("\n");
         try {
             builder.append("Vendor: ").append(GL11.glGetString(GL11.GL_VENDOR)).append("\n");
             builder.append("OpenGLVersion: ").append(GL11.glGetString(GL11.GL_VERSION)).append("\n");
             builder.append("Renderer: ").append(GL11.glGetString(GL11.GL_RENDERER)).append("\n");
         } catch (Exception ex) {
             builder.append("Could not read GL11 data\n");
         }
         builder.append("\n");
         builder.append("Gfx Capabilities:");
         int count = 0;
         for (Caps c : app.getRenderer().getCaps()) {
             builder.append(" ").append(c.toString());
             if (++count >= 5) {
                 count = 0;
                 builder.append("\n");
             }
         }
         builder.append("\n\n");
         builder.append("Protocol Version: ").append(Definitions.PROTOCOL_VERSION).append("\n");
         builder.append("\n");
         builder.append("ErrMsg: ").append(errorMsg).append("\n");
         StringWriter sw = new StringWriter();
         error.printStackTrace(new PrintWriter(sw));
         builder.append("Exception: ").append(sw).append("\n");
         final String report = builder.toString();
         crashReportText.setText(report);
         StringSelection selection = new StringSelection(report);
         Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, null);
    
         gbc = new GridBagConstraints();
         gbc.gridy = 2;
         final JButton button = new JButton("Send Report");
         add(button, gbc);
    
         button.addActionListener(new ActionListener() {
             public void actionPerformed(ActionEvent e) {
                 try {
                        // Construct data
                        String data = URLEncoder.encode("crash_report", "UTF-8") + "=" + URLEncoder.encode(report, "UTF-8");
    
                        // Send data
                        URL url = new URL("YOUR CRASH REPORT PAGE URL");
                        URLConnection conn = url.openConnection();
                        conn.setDoOutput(true);
                        OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream());
                        wr.write(data);
                        wr.flush();
    
                        // Get the response
                        BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                        StringBuilder responseBuilder = new StringBuilder();
                        
                        String line;
                        while ((line = rd.readLine()) != null) {
                            responseBuilder.append(line).append("\n");
                        }
                        wr.close();
                        rd.close();
                        
                        JOptionPane.showMessageDialog(button, responseBuilder.toString().split("\n"), "Response from server", JOptionPane.INFORMATION_MESSAGE);
                        button.setEnabled(false);
                    } catch (Exception ex) {
                        logger.log(Level.SEVERE, "Failed to send crash report to server", ex);
                        JOptionPane.showMessageDialog(button, ex, "Failed to send crash report to server", JOptionPane.INFORMATION_MESSAGE);
                    }
             }
         });
    
         addWindowListener(new WindowAdapter() {
    
             @Override
             public void windowClosed(WindowEvent e) {
                 app.stop();
             }
             
         });
         
     } catch (Exception ex) {
         logger.log(Level.SEVERE, "Unable to construct CrashReport screen ", ex);
     }
     pack();
    

    }
    }
    [/java]

In order to call this when needed (you could always add it other places as well if you have anything else causing you to exit the application) add the following code to your SImpleApplication instance:
[java]

@Override
public void handleError(final String errMsg, final Throwable t) {
    
    logger.log(Level.SEVERE, "Unhandled error "+errMsg, t);
    try {
        SwingUtilities.invokeAndWait(new Runnable() {

            public void run() {
               
                new CrashReport(Main.this, PUT WHATEVER YOU LIKE TO IDENTIFY THIS USER/INSTANCE/WHATEVER HERE, errMsg, t).setVisible(true);
            }
        });
    } catch (InterruptedException ex) {
        logger.log(Level.SEVERE, null, ex);
    } catch (InvocationTargetException ex) {
        logger.log(Level.SEVERE, null, ex);
    }
    // Note we call Display.destroy not App.stop() as App.stop() causes the entire
    // running program to exit, destroying our crash report window in the process!
    // Calling Display.destroy() is vital as otherwise fullscreen mode under certain
    // OS effectively locks up on a crash as the fullscreen stays open but blank
    // while the error popup opens behind it an cannot be accessed...and you also
    // cannot now switch out of the application in order to kill it!
    Display.destroy();
}

[/java]

In order to be able to upload the report to your website create the following table in a mysql database:
[java]
CREATE TABLE crash_reports (
id int(10) unsigned NOT NULL auto_increment,
timestamp timestamp NOT NULL default CURRENT_TIMESTAMP,
report text NOT NULL,
PRIMARY KEY (id)
) ENGINE=MyISAM AUTO_INCREMENT=6 DEFAULT CHARSET=latin1
[/java]

You need a php capable web server, create the following php file:

[java]<?php
$sqldb = mysql_connect(“localhost”, “YOUR USERNAME”, “YOUR PASSWORD”);
mysql_select_db(“YOUR DATABASE”, $sqldb);

$report = $_POST["crash_report"];
$result = mysql_query("insert into crash_reports (report) values ('".mysql_real_escape_string($report)."')");
if ($result) {
    echo "Thank you, your report has been received and recorded";
} else {
    echo "There was an error, please report the crash manually so that we can fix it";
}

?>
[/java]

You can test the file by navigating to the page directly and seeing an empty report appear in the log. To then test the whole system simply add a
[java]
throw new RuntimeException();
[/java]

From any suitable point (for example an update() method in an app state) in order to trigger a crash. You will see a report that looks like:

Crash for user: ZS Enigma

System Details
OsName: Windows 7
OsArchitecture: x86
OsVersion: 6.1
JavaVersion: 1.7.0_01
JavaVender: Oracle Corporation
DisplayAdaptor: aticfx64
DisplayVersion: null
Vendor: Could not read GL11 data

Gfx Capabilities: FrameBuffer FrameBufferMRT FrameBufferMultisample TextureMultisample OpenGL20
OpenGL21 OpenGL30 OpenGL31 OpenGL32 ARBprogram
GLSL100 GLSL110 GLSL120 GLSL130 GLSL140
GLSL150 VertexTextureFetch TextureArray TextureBuffer FloatTexture
FloatColorBuffer FloatDepthBuffer PackedFloatTexture SharedExponentTexture PackedFloatColorBuffer
TextureCompressionLATC NonPowerOfTwoTextures MeshInstancing VertexBufferArray Multisample
PackedDepthStencilBuffer

Protocol Version: 20

ErrMsg: Uncaught exception thrown in Thread[LWJGL Renderer Thread,5,main]
Exception: java.lang.RuntimeException
at net.herodex.client.main.state.HeroState.update(HeroState.java:1034)
at com.jme3.app.state.AppStateManager.update(AppStateManager.java:254)
at com.jme3.app.SimpleApplication.update(SimpleApplication.java:238)
at com.jme3.system.lwjgl.LwjglAbstractDisplay.runLoop(LwjglAbstractDisplay.java:151)
at com.jme3.system.lwjgl.LwjglDisplay.runLoop(LwjglDisplay.java:185)
at com.jme3.system.lwjgl.LwjglAbstractDisplay.run(LwjglAbstractDisplay.java:228)
at java.lang.Thread.run(Thread.java:722)

All feedback welcome, in particular if anyone knows a more reliable way than the calls to GL11 (which seem to not work more often than they work) to get graphics card information that would be great.

There is nothing here about processing/handling the reports. You are on your own there but if anyone wants to provide any utilities that would be cool :slight_smile:

If anyone spots any flaws or can suggest other information to gather (and knows how to gather it!) then again all suggestions are welcome!

Thanks,
Zarch

4 Likes

Don’t worry about the protocol version stuff, thats specific to herodex - replace it with whatever you like to identify your version of your app.

Wow nice. We should add this by default with an option to copy-paste for the forum :wink:

Just a quick note… I know you’re using mysql_escape_string, but you can use PDO to do a properly parameterized SQL statement. I’m not sure if the upcoming deprecation of mysql-specific functionality covers the escape string function, but that could also be an issue.

@sbook I agree with you, however for this I just went with quick and dirty and guaranteed to work on 99.999999% of php installations no matter how old, so far as I know mysql_real_escape_string is enough to prevent SQL injection attacks.

I’m sort of thinking of this as a base to build from (either communally or individually) rather than a finished product that someone can just flick a switch and turn on in their app.

@normen I did wonder about that myself - we would need to think about how it works though, I guess we could make the network upload section an option that you turn on by providing your server settings…it also uses Swing so would be Desktop specific, something similar would need doing for the Android side.

Yeah, for desktop I didn’t even think about auto-sending, we could do that via PHP on the jme server side though. I just meant a text area that shows this info and asks the user to definitely post that when posting in the forum :wink:

Yes! this is great, it should absolutely be in the SDK also. Normen, do you have an idea of where it is suited or shall I go digging in the source?
It would be nice if there was some detection of SDK crashing when it restarted (some pid/lock file or something) to recover the last log. Could help when tracking down those darn linux bugs.

http://hub.jmonkeyengine.org/forum/topic/will-the-stable-release-give-more-support-to-linux/#post-185781
http://hub.jmonkeyengine.org/forum/topic/bug-jme-3-0-beta-sdk-crashes-on-linux/#post-193207

No it won’t cause those are JVM/system bugs, it just bails or freezes. Any normal engine issue is caught by the “Error in Scene” window that sometimes goes wild when theres altering messages (it only compares with the last error to determine if it displayed the current error already).

Yes you are right :facepalm:
It would ofc be possible to detect unclean shutdown and include the GL-info in the report but that is of limited value I would guess. I’ll keep holding my breath for a renderer based on “the competitor” to LWJGL to solve both linux and OSX problems :slight_smile:

yeh, there would be super useful if it was automatically included with our apps, and by default was sent to the jME server, with the ability to change the server it is sent to if people want to send it to their own one.

Well if somebody wants to hack this up with a small PHP script alongside of it, you’re welcome to. One issue we have is that plain java apps are rejected e.g. by Cloudflare to protect servers from bots, one has to set the http agent to something else than the default to get through. I could also build this into the SDK then for the “warning sign” crashes if it can be adapted.

1 Like

@wezrule - how would sending it to the JME server be used/processed/etc ? Crashes like this are usually going to be specific to the game experiencing the crashes.

I guess you could split them by application name and then automatically store them on the server and allow certain users to then see those apps - it seems like a fairly big can of worms though.

That’s why I was thinking it is better to have the “send to server” button not appear unless a server has been configured but then allow people to set up their own servers easily and provide tools for them. In particular I have no time to do it but putting a text search index onto that column and then providing php pages to analyse the data would be great. I even considered splitting the data into columns and sending/storing the data that way but this seemed more flexible and easier to get started with. It answers the core need which is getting crash reports from less techy users to me with as little pain as possible.

We’d also need the normal “Do you want to send anonymous usage data to the server” or else we’ll end up with 30 threads asking why jME needs access to the network and how come we’re all hackers trying to steal people’s code.

As for the HTTP user-agent business, that’s trivial to set. I don’t think NetBeans has an HTTPClient dependency built-in though, so we’d need to add that on our own.

Actually that’s already covered sbook. The window that pops up explains what’s going on and they can see the data being sent and press a button to send it :slight_smile:

I was thinking more along the lines of the popup you get when you first start NetBeans or Eclipse… That said, I suspect we’d have a higher rate of allowing this information to get through if people got to use the SDK a bit and interact with the community before having to make a decision about that.

I agree that people are more likely to click yes after using it for a while :slight_smile:

This isn’t really anonymous usage statistics though, it’s non-anonymous crash reporting. (In the case of herodex I include the username, even without that things like the IP etc could potentially be recorded even if the basic version here doesn’t).