← Back to team overview

apport-hackers team mailing list archive

[Merge] lp:~mdz/apport/java into lp:apport

 

Matt Zimmerman has proposed merging lp:~mdz/apport/java into lp:apport.

Requested reviews:
  Apport upstream developers (apport-hackers)
Related bugs:
  #548877 Trap uncaught exceptions in Java programs
  https://bugs.launchpad.net/bugs/548877


This branch provides the basic components for handling uncaught Java exceptions, and capturing problem reports for them.

It includes:
- the com.ubuntu.apport.ApportUncaughtExceptionHandler class, which can be set as the uncaught exception handler for a Java program (perhaps eventually by default)
- a java_uncaught_exception script, which receives data from ApportUncaughtExceptionHandler and creates a problem report
- a simple "crash" class for testing purposes
- java/README, which explains the basic architecture
- test/java, which should generate crash reports if everything is working (not a proper test suite yet)

I had to hack setup.py a bit to build the Java parts, and am not sure whether I did that in the right way. Thanks for reviewing.
-- 
https://code.launchpad.net/~mdz/apport/java/+merge/42517
Your team Apport upstream developers is requested to review the proposed merge of lp:~mdz/apport/java into lp:apport.
=== added file 'data/java_uncaught_exception'
--- data/java_uncaught_exception	1970-01-01 00:00:00 +0000
+++ data/java_uncaught_exception	2010-12-02 17:37:53 +0000
@@ -0,0 +1,78 @@
+#!/usr/bin/python
+
+#
+# Receive details from ApportUncaughtExceptionHandler, generate and save a
+# problem report
+#
+# Copyright 2010 Matt Zimmerman <mdz@xxxxxxxxxx>
+#
+
+import sys
+
+#from apport import unicode_gettext as _
+
+def make_title(report):
+	lines = report['StackTrace'].split('\n')
+	message = lines[0].strip()
+	stackframe = lines[1].strip()
+	return '%s in %s' % (message, stackframe)
+
+def main():
+	from apport.packaging_impl import impl as packaging
+	if not packaging.enabled():
+		return -1
+
+	# read from the JVM process a sequence of key, value delimited by null
+	# bytes
+	items = sys.stdin.read().split('\0')
+	d = dict()
+	while items:
+		key = items.pop(0)
+		if not items: break
+		value = items.pop(0)
+		d[key] = value
+
+	# create report
+	import apport.report
+	import os
+
+	report = apport.report.Report(type='Crash')
+	# assume our parent is the JVM process
+	report.pid = os.getppid()
+
+	report.add_os_info()
+	report.add_proc_info()
+	# these aren't relevant because the crash was in bytecode
+	del report['ProcMaps']
+	del report['ProcStatus']
+	report.add_user_info()
+
+	# add in data which was fed to us from the JVM process
+	for key, value in d.items():
+		report[key] = value
+
+	# Add an ExecutablePath pointing to the file where the main class resides
+	if 'MainClassUrl' in report:
+		import urlparse
+		url = report['MainClassUrl']
+
+		scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
+
+		if scheme == 'jar':
+			# path is then a URL to the jar file
+			scheme, netloc, path, params, query, fragment = urlparse.urlparse(path)
+			if '!/' in path:
+				path = path.split('!/', 1)[0]
+
+		if scheme == 'file':
+			report['ExecutablePath'] = path
+		else:
+			# Program at some non-file URL crashed. Give up.
+			return
+
+	report['Title'] = make_title(report)
+
+	report.write(open(apport.fileutils.make_report_path(report), 'w'))
+
+if __name__ == '__main__':
+	main()

=== added directory 'java'
=== added file 'java/README'
--- java/README	1970-01-01 00:00:00 +0000
+++ java/README	2010-12-02 17:37:53 +0000
@@ -0,0 +1,5 @@
+apport.jar contains the necessary class(es) for trapping uncaught Java
+exceptions.  crash.class and crash.jar are used only by the test suite.
+
+The crash handler, when invoked, opens a pipe to the java_uncaught_exception
+script, and feeds it key/value pairs containing the relevant JVM state.

=== added file 'java/TODO'
--- java/TODO	1970-01-01 00:00:00 +0000
+++ java/TODO	2010-12-02 17:37:53 +0000
@@ -0,0 +1,3 @@
+Make the test suite work
+Make "setup.py clean" clean the Java bits
+Enable the exception handler by default when apport is enabled

=== added directory 'java/com'
=== added directory 'java/com/ubuntu'
=== added directory 'java/com/ubuntu/apport'
=== added file 'java/com/ubuntu/apport/ApportUncaughtExceptionHandler.java'
--- java/com/ubuntu/apport/ApportUncaughtExceptionHandler.java	1970-01-01 00:00:00 +0000
+++ java/com/ubuntu/apport/ApportUncaughtExceptionHandler.java	2010-12-02 17:37:53 +0000
@@ -0,0 +1,98 @@
+package com.ubuntu.apport;
+
+/*
+ * Apport handler for uncaught Java exceptions
+ * 
+ * Copyright 2010 Matt Zimmerman <mdz@xxxxxxxxxx>
+ */
+
+import java.io.*;
+import java.util.HashMap;
+
+public class ApportUncaughtExceptionHandler
+	implements java.lang.Thread.UncaughtExceptionHandler {
+
+	/* Write out an apport problem report with details of the
+     * exception, then print it in the usual canonical format */
+	public void uncaughtException(Thread t, Throwable e) {
+		//System.out.println("uncaughtException");
+		if (e instanceof ThreadDeath)
+			return;
+
+		HashMap problemReport = getProblemReport(t, e);
+		//System.out.println("got problem report");
+
+		try {
+		 	Process p = new ProcessBuilder("/usr/share/apport/java_uncaught_exception").start();
+			//System.out.println("started process");
+
+			OutputStream os = p.getOutputStream();
+			writeProblemReport(os, problemReport);
+			//System.out.println("wrote problem report");
+
+			os.close();
+
+			try {
+				p.waitFor();
+			} catch (InterruptedException ignore) {
+				// ignored
+			}
+
+		} catch (java.io.IOException ioe) {
+			// ignored
+		}
+
+        System.err.print("Exception in thread \""
+                         + t.getName() + "\" ");
+        e.printStackTrace(System.err);
+	}
+
+	public HashMap getProblemReport(Thread t, Throwable e) {
+		HashMap problemReport = new HashMap();
+		
+		StringWriter sw = new StringWriter();
+		PrintWriter pw = new PrintWriter(sw);
+		e.printStackTrace(pw);
+		problemReport.put("StackTrace", sw.toString());
+
+		problemReport.put("MainClassUrl", mainClassUrl(e));
+
+		return problemReport;
+	}
+
+	public void writeProblemReport(OutputStream os, HashMap pr)
+		throws IOException {
+
+		StringWriter sw = new StringWriter();
+		for(Object o : pr.keySet()) {
+			String key = (String)o;
+			String value = (String)pr.get(o);
+			sw.write(key);
+			sw.write("\0");
+			sw.write(value);
+			sw.write("\0");
+		}
+		os.write(sw.toString().getBytes());
+	}
+
+	public static String mainClassUrl(Throwable e) {
+		StackTraceElement[] stacktrace = e.getStackTrace();
+		String className = stacktrace[stacktrace.length-1].getClassName();
+
+		if (!className.startsWith("/")) {
+			className = "/" + className;
+		}
+		className = className.replace('.', '/');
+		className = className + ".class";
+
+		java.net.URL classUrl =
+			new ApportUncaughtExceptionHandler().getClass().getResource(className);
+
+		return classUrl.toString();
+    }
+
+	/* Install this handler as the default uncaught exception handler */
+	public static void install() {
+		Thread.setDefaultUncaughtExceptionHandler(new ApportUncaughtExceptionHandler());
+	}
+}

=== added file 'java/crash.java'
--- java/crash.java	1970-01-01 00:00:00 +0000
+++ java/crash.java	2010-12-02 17:37:53 +0000
@@ -0,0 +1,8 @@
+import com.ubuntu.apport.*;
+
+class crash {
+    public static void main(String[] args) {
+        com.ubuntu.apport.ApportUncaughtExceptionHandler.install();
+        throw new RuntimeException("Can't catch this");
+    }
+}

=== modified file 'setup.py'
--- setup.py	2009-11-24 12:24:07 +0000
+++ setup.py	2010-12-02 17:37:53 +0000
@@ -4,6 +4,57 @@
 import os.path, shutil, sys
 from distutils.version import StrictVersion
 
+# Add Java build step
+from distutils.command.build import build
+from distutils.core import Command
+import subprocess
+
+class build_java_subdir(Command):
+    description = 'Compile java components of Apport'
+
+    user_options = []
+
+    def initialize_options(self):
+        pass
+
+    def finalize_options(self):
+        pass
+
+    def run(self):
+        oldwd = os.getcwd()
+        os.chdir('java')
+
+        try:
+            subprocess.check_call(['javac'] + glob('com/ubuntu/apport/*.java'))
+            subprocess.check_call(['jar','cvf', 'apport.jar'] + 
+                                glob('com/ubuntu/apport/*.class'))
+            subprocess.check_call(['javac','crash.java'])
+            subprocess.check_call(['jar','cvf', 'crash.jar', 'crash.class'])
+        except OSError, e:
+            if e.errno == os.errno.ENOENT:
+                print "Java prerequisites are not installed; skipping"
+            else:
+                raise e
+        finally:
+            os.chdir(oldwd)
+
+have_java = False
+try:
+    with open('/dev/null', 'w') as null:
+        subprocess.check_call(['javac', '-version'], stderr=null, stdout=null)
+    have_java = True
+
+except OSError, e:
+    have_java = False
+
+optional_data_files = []
+cmdclass = {}
+
+if have_java:
+    build.sub_commands.append(('build_java_subdir', None))
+    optional_data_files.append(('share/java', ['java/apport.jar']))
+    cmdclass.update({'build_java_subdir' : build_java_subdir})
+
 try:
     import DistUtilsExtra.auto
 except ImportError:
@@ -41,6 +92,7 @@
                   ('share/apport', glob('kde/*.ui')), #TODO: use pykdeuic modules
                   ('share/apport/testsuite/', glob('test/*')),
                   ('share/doc/apport/', glob('doc/*.txt')),
-                  ('lib/pm-utils/sleep.d/', glob('pm-utils/sleep.d/*'))
-                  ],
+                  ('lib/pm-utils/sleep.d/', glob('pm-utils/sleep.d/*')),
+                  ] + optional_data_files,
+    cmdclass=cmdclass
 )

=== added file 'test/java'
--- test/java	1970-01-01 00:00:00 +0000
+++ test/java	2010-12-02 17:37:53 +0000
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+set -e
+
+mydir=$(dirname $0)
+srcdir=$mydir/..
+
+# XXX - this is a mess in so many ways -mdz
+# This only works if /usr/share/apport/java_uncaught_exception is installed
+# ...and it's run from the source tree
+# It also dumps crash reports in /var/crash
+# ...and doesn't even check if it worked!
+
+# Should produce a crash report for $srcdir/java/crash.class
+java -classpath $srcdir/java/apport.jar:$srcdir/java crash || true
+
+# Should produce a crash report for $srcdir/java/crash.jar
+java -classpath $srcdir/java/apport.jar:$srcdir/java/crash.jar crash || true


Follow ups