If it is too hard to get stacktraces from users using JWS, try redirecting System.out to a log file, or even redirecting it to a server that logs it all.
Code:
/**
* An OutputStream that writes to multiple other OutputStreams.
*/
public class MultiplexOutputStream extends OutputStream {
private final OutputStream[] streams;
public MultiplexOutputStream (OutputStream... streams) {
super();
if (streams == null) throw new IllegalArgumentException("streams cannot be null.");
this.streams = streams;
}
public void write (int b) throws IOException {
for (int i = 0; i < streams.length; i++)
streams[i].write(b);
}
public void write (byte[] b, int off, int len) throws IOException {
for (int i = 0; i < streams.length; i++)
streams[i].write(b, off, len);
}
}
.
.
.
PrintStream out = new PrintStream(new MultiplexOutputStream(System.out, new FileOutputStream(logFile)), true);
System.setOut(out);
System.setErr(out);
System.getProperty("user.home") is a good place to put the file.
Here is a bit of code that uses java.util.Logging to log to a server...
Code:
/**
* Buffers until a newline is received.
*/
public abstract class LineOutputStream extends OutputStream {
private final StringBuilder buffer = new StringBuilder();
public void write (int b) throws IOException {
char c = (char)b;
if (c == '\n') {
write(buffer.toString());
buffer.setLength(0);
} else
buffer.append(c);
}
public void close () throws IOException {
flush();
}
public void flush () throws IOException {
if (buffer.length() > 0) write('\n');
}
abstract public void write (String line);
}
.
.
.
Thread thread = new Thread("Logger") {
public void run () {
try {
final Logger logger = Logger.getAnonymousLogger();
Handler handler = new SocketHandler("someServerIPorName", 54321) {
protected void reportError (String msg, Exception ex, int code) {
logger.removeHandler(this);
System.out.println("Logging shutdown.");
}
};
handler.setFormatter(new SimpleFormatter());
logger.addHandler(handler);
logger.setUseParentHandlers(false);
PrintStream out = new PrintStream(new MultiplexOutputStream(System.out, new FileOutputStream(logFile),
new LineOutputStream() {
public void write (String line) {
logger.info(line);
}
}), true);
System.setOut(out);
System.setErr(out);
BufferedReader reader = new BufferedReader(new FileReader(logFile));
while (true) {
String line = reader.readLine();
if (line == null) break;
logger.info(line);
}
reader.close();
} catch (IOException ex) {
Log.debug("Unable to log.");
}
}
};
thread.setPriority(Thread.NORM_PRIORITY - 1);
thread.setDaemon(true);
thread.start();
And here is the server that would log to...
Code:
public class LogServer {
public static void main (String args[]) throws Exception {
new File("remote").mkdir();
int count = 0;
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 500, 10, TimeUnit.SECONDS, new LinkedBlockingQueue());
threadPool.prestartAllCoreThreads();
ServerSocket serverSocket = ServerSocketFactory.getDefault().createServerSocket(54321);
System.out.println("LogServer running on port: 54321");
while (true) {
try {
final Socket socket = serverSocket.accept();
socket.setSoTimeout(60 * 60 * 1000);
if (socket.getRemoteSocketAddress().toString().contains("yourExternalIP")) {
System.out.println("Ignoring localhost.");
socket.close();
continue;
}
if (threadPool.getActiveCount() < threadPool.getMaximumPoolSize()) {
final int id = ++count;
final BufferedWriter writer = new BufferedWriter(new FileWriter("remote/" + id + ".log"));
threadPool.execute(new Runnable() {
public void run () {
try {
String message = "--- Connected to: " + socket.getRemoteSocketAddress() + " at " + new Date() + " ---";
System.out.println(id + " " + message);
writer.write(message + "\n");
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "US-ASCII"));
while (true) {
String line = reader.readLine();
if (line == null) break;
writer.write(line + "\n");
writer.flush();
System.out.println(id + " | " + line);
}
} catch (Throwable ex) {
// ex.printStackTrace();
if (ex.getMessage().contains("Read timed out")) System.out.println(id + " | Socket timed out.");
} finally {
try {
System.out.println(id + " --- Closed at " + new Date() + " ---");
writer.write("--- Closed at " + new Date() + " ---\n");
writer.close();
} catch (IOException ignored) {
}
try {
socket.close();
} catch (IOException ignored) {
}
}
}
});
} else {
System.out.println(threadPool.getActiveCount() + " threads are busy!");
try {
if (socket != null) socket.close();
} catch (IOException ignored) {
}
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
All this code is not as refined as I usually like my code, but it is a debugging tool, not for production, and it does the job. Each time someone runs your game it logs it to a new file on the server and also dumps it to System.out so you can watch in real time. There are probably some errors that it won't catch if the failure happens before the logging code, but it is neat to (ab)use users to test on a wide variety of machines.

You might have your game download a simple file from a webserver that contains a 1 or a 0, and only connect to the LogServer if the value is 1. Since you probably don't want to log all the time, this way you can turn on/off logging for all your users, without having to distribute a new version of the game. The code for this is simple:
Code:
URLConnection conn = null;
BufferedReader reader = null;
try {
conn = new URL("http://someDomain/someFile").openConnection();
conn.setConnectTimeout(16000);
conn.setReadTimeout(16000);
conn.setDoOutput(true);
reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
if (reader.readLine().equals("1")) initializeRemoteLogger();
} catch (Throwable ignored) {
} finally {
try {
if (reader != null) reader.close();
if (conn != null) conn.getOutputStream().close();
} catch (Throwable ignored) {
}
}
Chances are you don't have a domain name pointing to a server to host your LogServer and file containing 1 or 0. You can use DynDNS:
https://www.dyndns.com/
Sign up then go to My Hosts and create a host. This will give you a domain like somegame.dnsalias.com that you set in DynDNS to point to your external IP. This way you can run your LogServer, etc on your own box and use somegame.dnsalias.com in your game.
One last tip: When I write my log messages, I like to write the time since the app started rather than the time on that machine. This way it is easier to see how long users spent on each screen, etc.