Forging a stack trace in Java
Asked Answered
L

3

12

When you use RMI in Java the remote stack trace of an exception will be prepended when you receive it, somewhat like this:

ERROR Client received error when doing stuff:
myapp.FooBarException: bla
 at server.myMethod()
 at rmi.callHandler() // and now, on the next line comes the client
 at rmi.sendCall();
 at client.doServerMethod()
 at Thread.run()

How is that kind of stacktrace "forgery" done?


What do I want it for (apart from just being iterested)? Well, it would help me if I could do this:

outer() {

  thread = new Thread(...
      inner();
      // inner() throws
      // RuntimeException
      //  at inner();
      //  at Runnable.run();
      //  at Thread.run();
      //  at outer();
      //  at lalalala();
      //  ...

  ).start();

  thread.join();

}

And make it so that an exception thrown in inner() would have outer() (and methods lower down the chain) in the stacktrace as well, for logging purposes.

Letaletch answered 7/6, 2011 at 19:1 Comment(0)
C
15

It is kind of easy:

Throwable has methods getStackTrace() and setStackTrace().

From one of my projects (non open-source, but maybe I'll some day open the remote call engine):

    /**
     * Setzt den Stack-Trace zusammen. Das untere Ende (tiefer in der
     * Aufrufhierarchie, am Anfang des Arrays/der Ausgabe) ist das,
     * welches im Throwable schon drin ist, das obere Ende wird aus
     * dem aktuellen Stack genommen. Dazwischen
     * kommt ein "Remote-Aufruf-Markierer".
     */

Translated for your convenience:

Merges the stack trace. The lower end (deeper in the call hierarchy, at the end of the array/the output) is what already is in the stack, the upper end will be taken from the current stack. Between them we will put an Remote call marker.

    private void mergeStackTraces(Throwable error)
    {
        StackTraceElement[] currentStack =
            new Throwable().getStackTrace();
        int currentStackLimit = 5; // TODO: raussuchen
        StackTraceElement[] oldStack =
            error.getStackTrace();
        StackTraceElement[] zusammen =
            new StackTraceElement[currentStack.length - currentStackLimit +
                                  oldStack.length + 1];
        System.arraycopy(oldStack, 0, zusammen, 0, oldStack.length);
        zusammen[oldStack.length] =
            new StackTraceElement("══════════════════════════",
                                  "<remote call %" +callID+ ">",
                                  "", -3);
        System.arraycopy(currentStack, currentStackLimit,
                         zusammen, oldStack.length+1,
                         currentStack.length - currentStackLimit);
        error.setStackTrace(zusammen);
    }

(On the server side, I'm already cutting off the parts of the stack trace which do not relate to the method call itself, i.e. everything related to the message handling.)

This results in a combined stack trace like this:

java.lang.SecurityException: Das Passwort für Nutzer »Paul« ist falsch.
        at de.fencing_game.db.userdb.Db4oUserDB.login(Db4oUserDB.java:304)
        at de.fencing_game.server.impl.StandardServers$SSServer$1.run(StandardServers.java:316)
        at de.fencing_game.server.impl.StandardServers$SSServer$1.run(StandardServers.java:314)
        at java.security.AccessController.doPrivileged(Native Method)
        at de.fencing_game.server.impl.StandardServers$SSServer.login(StandardServers.java:313)
        at de.fencing_game.transport.server.ServerTransport$ConnectionInfo$4.login(ServerTransport.java:460)
        at ══════════════════════════.<remote call %2>()
        at $Proxy1.login(Unknown Source)
        at de.fencing_game.gui.basics.LoginUtils.login(LoginUtils.java:80)
        at de.fencing_game.gui.Lobby.connectTo(Lobby.java:302)
        at de.fencing_game.gui.Lobby$20.run(Lobby.java:849)
        at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:226)
        at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:647)
        at java.awt.EventQueue.access$000(EventQueue.java:96)
        at java.awt.EventQueue$1.run(EventQueue.java:608)
        at java.awt.EventQueue$1.run(EventQueue.java:606)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.security.AccessControlContext$1.doIntersectionPrivilege(AccessControlContext.java:105)
        at java.awt.EventQueue.dispatchEvent(EventQueue.java:617)
        at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:275)
        at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:200)
        at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:190)
        at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:185)
        at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:177)
        at java.awt.EventDispatchThread.run(EventDispatchThread.java:138)

I suppose the RMI system does something quite similar (just without the ══════════════════════════).


Edit: For your usecase, you would have to save the stack trace of the outer thread when the inner thread is started, then in the run method catch the exception and append the outer stack trace to the stack trace of the inner exception. I would really recommend putting some type of separator, though.

Condolence answered 7/6, 2011 at 19:6 Comment(3)
I would like the inner exception to have the stacktrace of the outer appended, even if it's caught by something deep in the inner thread. I guess that's not possible, since I can't apply a custom exception.Letaletch
Your catching code has to do this (or the exception must do this itself before throwing, overriding fillInStackTrace or doing this in the constructor). Neither of this will work for unknown exceptions thrown and caught by unknown code.Daman
I like that you have 3 languages in one code snippet - English, German and Java :)Brotherhood
C
2

You create a customized exception that extracts the stacktrace from one exception and adds it to another via setStackTrace().

It's useful for doing things like this or maintaining a stacktrace when you don't want to maintain a hard reference to the caused by exception. This is handy when passing exception info from server to client where the root cause exception classes may not be present, thus causing serialization issues.

Counterreply answered 7/6, 2011 at 19:10 Comment(0)
A
2

I would like to suggest an alternative solution, which isn't what the OP asked for, but may be better for some people that have a very similar question. Like me.

I suggest creating a throwable in outer and add the throwable from inner as the cause of the throwable from outer. This will also capture the point at which a thread switch was made... which may be useful to avoid confusion about the stack trace. Furthermore, thread information can be stored in this throwable created in outer that may help even more.

Here's some code.

public class StackCaptor {
    public static Runnable capture(Runnable runnable) {
        // Capture the stack
        final Throwable prison = new Throwable();
        // Wrap run method to create a new throwable representing the creator of the original Runnable.
        return new Runnable() {
            @Override
            public void run() {
                try {
                    runnable.run();
                } catch (Throwable originalThrowable) {
                    RuntimeException callingThreadsException = new RuntimeException(originalThrowable);
                    callingThreadsException.setStackTrace(prison.getStackTrace());
                    throw callingThreadsException;
                }
            }
        };
    }
}

Then use the code like this:

// This code has not be compiled or tested... You may need to use your
// smarts to get it working, but it should give you an idea.
public void outer() {
    Thread thread = new Thread(StackCaptor.capture(new Runnable() {
        public void run() { throw new RuntimeException("my ex"); }
    }));
    thread.start();
}
Althaalthea answered 16/2, 2018 at 0:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.