View Javadoc

1   /*
2    * Copyright (c) 2003-2008, by Henrik Arro and Contributors
3    *
4    * This file is part of JSeq, a tool to automatically create
5    * sequence diagrams by tracing program execution.
6    *
7    * See <http://jseq.sourceforge.net> for more information.
8    *
9    * JSeq is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation, either version 3 of
12   * the License, or (at your option) any later version.
13   *
14   * JSeq is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17   * GNU Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public License
20   * along with JSeq. If not, see <http://www.gnu.org/licenses/>.
21   */
22  
23  package th.co.edge.jseq;
24  
25  import java.util.HashMap;
26  import java.util.List;
27  import java.util.Map;
28  
29  import com.sun.jdi.AbsentInformationException;
30  import com.sun.jdi.IncompatibleThreadStateException;
31  import com.sun.jdi.Location;
32  import com.sun.jdi.Method;
33  import com.sun.jdi.ReferenceType;
34  import com.sun.jdi.ThreadReference;
35  import com.sun.jdi.VMDisconnectedException;
36  import com.sun.jdi.VirtualMachine;
37  import com.sun.jdi.event.BreakpointEvent;
38  import com.sun.jdi.event.ClassPrepareEvent;
39  import com.sun.jdi.event.Event;
40  import com.sun.jdi.event.EventIterator;
41  import com.sun.jdi.event.EventQueue;
42  import com.sun.jdi.event.EventSet;
43  import com.sun.jdi.event.ExceptionEvent;
44  import com.sun.jdi.event.LocatableEvent;
45  import com.sun.jdi.event.MethodEntryEvent;
46  import com.sun.jdi.event.MethodExitEvent;
47  import com.sun.jdi.event.StepEvent;
48  import com.sun.jdi.event.ThreadDeathEvent;
49  import com.sun.jdi.event.VMDeathEvent;
50  import com.sun.jdi.event.VMDisconnectEvent;
51  import com.sun.jdi.event.VMStartEvent;
52  import com.sun.jdi.request.BreakpointRequest;
53  import com.sun.jdi.request.ClassPrepareRequest;
54  import com.sun.jdi.request.EventRequest;
55  import com.sun.jdi.request.EventRequestManager;
56  import com.sun.jdi.request.ExceptionRequest;
57  import com.sun.jdi.request.MethodEntryRequest;
58  import com.sun.jdi.request.MethodExitRequest;
59  import com.sun.jdi.request.StepRequest;
60  import com.sun.jdi.request.ThreadDeathRequest;
61  
62  /**
63   * An <code>EventThred</code> traces the execution of a Java process, keeping
64   * track of all method calls as <code>Activation</code>s in an
65   * <code>ActivationList</code> containing all root activations, that is, all
66   * method calls that occur "magically" from outside the traced methods, for
67   * example from the main method. Each Java thread is traced as a separate root
68   * activation.
69   */
70  public class EventThread extends Thread {
71  
72      private final VirtualMachine vm;
73      private final ActivationList rootActivations;
74      private final List<String> includes;
75      private final List<String> excludes;
76      private final List<String> boundaryMethods;
77      private final boolean trace;
78  
79      private String startClassName;
80      private String startMethodName;
81  
82      private boolean connected = true;
83      private boolean vmDied = true;
84  
85      private Map<ThreadReference, ThreadTrace> traceMap =
86              new HashMap<ThreadReference, ThreadTrace>();
87  
88      /**
89       * Creates a new <code>EventThread</code> that traces a given JDI
90       * <code>VirtualMachine</code>, filling in a list of root activations.
91       *
92       * <p>
93       * A list of included and excluded method names determine which methods to
94       * trace. If the list of included method names is empty, all methods except
95       * the excluded ones are traced, and correspondingly if the list of excluded
96       * method name is empty. If both lists are non-empty, only the methods in
97       * the included list are traced, except the ones in the excluded list.
98       * Method names in the included and excluded lists can either be a fully
99       * qualified method name, or a simple wild-card expression starting or
100      * ending with "*".
101      *
102      * <p>
103      * For example, if the included list contains "foo.Bar.*", and the excluded
104      * list is empty, all methods in the class <code>foo.Bar</code> will be
105      * traced. If the included list is empty and the excluded list contains
106      * "java.*", everything but methods in the <code>java</code> package will
107      * be traced. Finally, if the included list contains "foo.Bar.*" and the
108      * excluded list contains "foo.Bar.baz", all methods in the class
109      * <code>foo.Bar</code> will be traced, except for the method
110      * <code>baz</code>.
111      *
112      * <p>
113      * It is also possible to specify a list of boundary methods. A <it>bounday
114      * method</it> is a method where tracing stops at entry to the method and
115      * is resumed when the method exits. This list should contain fully
116      * qualified method names, no wild-cards are accepted.
117      *
118      * <p>
119      * For example, if the list of boundary methods contains "foo.Bar.baz",
120      * tracing will include the call to <code>baz</code>, but no methods
121      * called by <code>baz</code>. Normal tracing is resumed after
122      * <code>baz</code> returns.
123      *
124      * @param vm
125      *            the JDI <code>VirtualMachine</code>, a representation of
126      *            the Java process to trace
127      * @param rootActivations
128      *            the list of root activations that will be filled in during the
129      *            program trace
130      * @param includes
131      *            a list of method name pattern that will be included in the
132      *            program trace, where name patterns may start or end with the
133      *            wild-card "*"
134      * @param excludes
135      *            a list of method name patterns that will be excluded in the
136      *            program trace, where name patterns may start or end with the
137      *            wild-card "*"
138      * @param boundaryMethods
139      *            a list of boundary methods, methods where tracing will stop
140      *            during the execution of the method
141      * @param trace
142      *            if <code>true</code> the program trace (method entry and
143      *            exit, for example) will be echoed to <code>System.out</code>
144      */
145     public EventThread(VirtualMachine vm, ActivationList rootActivations,
146             List<String> includes, List<String> excludes,
147             List<String> boundaryMethods, boolean trace) {
148         super("event-handler");
149         this.vm = vm;
150         this.rootActivations = rootActivations;
151         this.includes = includes;
152         this.excludes = excludes;
153         this.trace = trace;
154         this.boundaryMethods = boundaryMethods;
155     }
156 
157     /**
158      * Handles all events generated by JDI, running for as long as the Java
159      * process is active, or until an attached process disconnects the event
160      * thread.
161      */
162     @Override
163     public void run() {
164         EventQueue queue = vm.eventQueue();
165         while (connected) {
166             try {
167                 EventSet eventSet = queue.remove();
168                 EventIterator it = eventSet.eventIterator();
169                 while (it.hasNext()) {
170                     Event event = it.nextEvent();
171                     handleEvent(event);
172                 }
173                 eventSet.resume();
174             } catch (InterruptedException exc) {
175                 System.err.println(exc);
176             } catch (VMDisconnectedException discExc) {
177                 handleDisconnectedException();
178                 break;
179             } catch (NoSuchMethodException e) {
180                 e.printStackTrace();
181             }
182         }
183     }
184 
185     /**
186      * Creates all JDI event requests, that is, defines the set of events that
187      * will be handled by this <code>EventThread</code>.
188      *
189      * @param startMethod
190      *            the fully qualified name of the method where tracing should
191      *            start, or <code>null</code> if tracing should start
192      *            immediately
193      */
194     public void setEventRequests(String startMethod) {
195         EventRequestManager mgr = vm.eventRequestManager();
196 
197         if (startMethod != null) {
198             setStartMethodBreakpoints(startMethod);
199         } else {
200 
201             // I really, really, want to refactor this clumsy implementation by
202             // calling a helper method, but this is difficult since the
203             // different request types do not implement a common interface
204             // defining the addClassFilter and addClassExclusionFilter methods.
205 
206             if (includes.isEmpty()) {
207                 MethodEntryRequest menr = mgr.createMethodEntryRequest();
208                 for (String excludePattern : excludes) {
209                     menr.addClassExclusionFilter(excludePattern);
210                 }
211                 enableRequest(menr);
212             } else {
213                 for (String includePattern : includes) {
214                     MethodEntryRequest menr = mgr.createMethodEntryRequest();
215                     menr.addClassFilter(includePattern);
216                     for (String excludePattern : excludes) {
217                         menr.addClassExclusionFilter(excludePattern);
218                     }
219                     enableRequest(menr);
220                 }
221             }
222 
223             if (includes.isEmpty()) {
224                 MethodExitRequest mexr = mgr.createMethodExitRequest();
225                 for (String excludePattern : excludes) {
226                     mexr.addClassExclusionFilter(excludePattern);
227                 }
228                 enableRequest(mexr);
229             } else {
230                 for (String includePattern : includes) {
231                     MethodExitRequest mexr = mgr.createMethodExitRequest();
232                     mexr.addClassFilter(includePattern);
233                     for (String excludePattern : excludes) {
234                         mexr.addClassExclusionFilter(excludePattern);
235                     }
236                     enableRequest(mexr);
237                 }
238             }
239 
240             if (includes.isEmpty()) {
241                 ExceptionRequest exr =
242                         mgr.createExceptionRequest(null, true, true);
243                 for (String excludePattern : excludes) {
244                     exr.addClassExclusionFilter(excludePattern);
245                 }
246                 enableRequest(exr);
247             } else {
248                 for (String includePattern : includes) {
249                     ExceptionRequest exr =
250                             mgr.createExceptionRequest(null, true, true);
251                     exr.addClassFilter(includePattern);
252                     for (String excludePattern : excludes) {
253                         exr.addClassExclusionFilter(excludePattern);
254                     }
255                     enableRequest(exr);
256                 }
257             }
258 
259             ThreadDeathRequest tdr = mgr.createThreadDeathRequest();
260             enableRequest(tdr);
261         }
262     }
263 
264     private void enableRequest(EventRequest request) {
265         request.setSuspendPolicy(EventRequest.SUSPEND_ALL);
266         request.enable();
267     }
268 
269     private void setStartMethodBreakpoints(String qualifiedMethodName) {
270         int lastDot = qualifiedMethodName.lastIndexOf('.');
271         if (lastDot < 0) {
272             String message =
273                     "Not a qualified method name: \"" + qualifiedMethodName +
274                             "\"";
275             throw new IllegalArgumentException(message);
276         }
277         startClassName = qualifiedMethodName.substring(0, lastDot);
278         startMethodName = qualifiedMethodName.substring(lastDot + 1);
279 
280         List<ReferenceType> startClasses = vm.classesByName(startClassName);
281         boolean methodFound = false;
282         for (ReferenceType refType : startClasses) {
283             List<Method> methods = refType.methodsByName(startMethodName);
284             if (!methods.isEmpty()) {
285                 setMethodBreakpoints(methods);
286                 methodFound = true;
287             }
288         }
289         if (!methodFound) {
290             System.err.println("Start method not found: " +
291                     qualifiedMethodName +
292                     ". Waiting to see if the class will be loaded.");
293             EventRequestManager mgr = vm.eventRequestManager();
294             ClassPrepareRequest cpr = mgr.createClassPrepareRequest();
295             enableRequest(cpr);
296         }
297     }
298 
299     private void setMethodBreakpoints(List<Method> methods) {
300         EventRequestManager mgr = vm.eventRequestManager();
301         for (Method method : methods) {
302             try {
303                 Location location = method.allLineLocations().get(0);
304                 BreakpointRequest bpr = mgr.createBreakpointRequest(location);
305                 enableRequest(bpr);
306             } catch (AbsentInformationException e) {
307                 e.printStackTrace();
308             }
309         }
310     }
311 
312     private void deleteAllEventRequests() {
313         EventRequestManager mgr = vm.eventRequestManager();
314         mgr.deleteEventRequests(mgr.methodEntryRequests());
315         mgr.deleteEventRequests(mgr.methodExitRequests());
316         mgr.deleteEventRequests(mgr.exceptionRequests());
317     }
318 
319     private class ThreadTrace {
320         private final ThreadReference thread;
321 
322         private Activation currentActivation = null;
323 
324         private String currentBoundaryMethod;
325 
326         private boolean stopWhenActivationDone = false;
327 
328         public ThreadTrace(ThreadReference thread) {
329             this.thread = thread;
330             trace("====== " + thread.name() + " ======");
331         }
332 
333         private void classPrepareEvent(ClassPrepareEvent event)
334                 throws NoSuchMethodException {
335             if (event.referenceType().name().equals(startClassName)) {
336                 List<Method> methods =
337                         event.referenceType().methodsByName(startMethodName);
338                 if (methods.isEmpty()) {
339                     throw new NoSuchMethodException(startClassName + "." +
340                             startMethodName);
341                 }
342                 setMethodBreakpoints(methods);
343             }
344         }
345 
346         private void methodEntryEvent(MethodEntryEvent event) {
347             String className = getClassName(event);
348             Method method = event.method();
349             List<String> argumentTypeNames = event.method().argumentTypeNames();
350             if (isBoundaryMethod(event)) {
351                 deleteAllEventRequests();
352                 currentBoundaryMethod = className + "." + method.name();
353                 EventRequestManager mgr = vm.eventRequestManager();
354                 MethodExitRequest methodExitRequest =
355                         mgr.createMethodExitRequest();
356                 enableRequest(methodExitRequest);
357             }
358             methodEntry(className, method, argumentTypeNames);
359         }
360 
361         private boolean isBoundaryMethod(MethodEntryEvent event) {
362             String className = getClassName(event);
363             String methodName = event.method().name();
364             String qualifiedMethodName = className + "." + methodName;
365             for (String boundaryMethod : boundaryMethods) {
366                 if (qualifiedMethodName.equals(boundaryMethod)) {
367                     return true;
368                 }
369             }
370             return false;
371         }
372 
373         private void methodEntry(String className, Method method,
374                 List<String> argumentTypeNames) {
375             trace(className + "." + method.name() + " " + argumentTypeNames);
376             int frameCount = -1;
377             try {
378                 frameCount = thread.frameCount();
379             } catch (IncompatibleThreadStateException e) {
380                 e.printStackTrace();
381                 // Ignore -- I THINK this exception cannot occur here...
382             }
383             Activation activation =
384                     new Activation(currentActivation, className, method,
385                             frameCount);
386             if (currentActivation == null) {
387                 rootActivations.add(activation);
388             }
389             currentActivation = activation;
390         }
391 
392         private void methodExitEvent(MethodExitEvent event) {
393             if (currentActivation == null) {
394                 return;
395             }
396 
397             String className = getClassName(event);
398             String methodName = event.method().name();
399             String qualifiedMethodName = className + "." + methodName;
400 
401             if (className.equals(currentActivation.getClassName()) &&
402                     methodName.equals(currentActivation.getMethod().name())) {
403                 currentActivation = currentActivation.getParent();
404             }
405             if (currentActivation == null && stopWhenActivationDone) {
406                 deleteAllEventRequests();
407             }
408             if (currentBoundaryMethod != null &&
409                     currentBoundaryMethod.equals(qualifiedMethodName)) {
410                 setEventRequests(null);
411             }
412         }
413 
414         private void exceptionEvent(ExceptionEvent event) {
415             EventRequestManager mgr = vm.eventRequestManager();
416             StepRequest request =
417                     mgr.createStepRequest(thread, StepRequest.STEP_MIN,
418                             StepRequest.STEP_INTO);
419             request.addCountFilter(1);
420             enableRequest(request);
421         }
422 
423         private void stepEvent(StepEvent event) {
424             EventRequestManager mgr = vm.eventRequestManager();
425             mgr.deleteEventRequest(event.request());
426             // Find the activation that catches the exception.
427             int frameCount = -1;
428             try {
429                 frameCount = thread.frameCount();
430             } catch (IncompatibleThreadStateException e) {
431                 System.err.println(e);
432             }
433             while (currentActivation.getParent() != null) {
434                 if (currentActivation.getFrameCount() == frameCount) {
435                     break;
436                 }
437                 currentActivation = currentActivation.getParent();
438             }
439         }
440 
441         private void breakpointEvent(BreakpointEvent event) {
442             String methodName = event.location().method().name();
443             if (!methodName.equals(startMethodName)) {
444                 String message =
445                         "Unexpected breakpoint. Should be in method " +
446                                 startMethodName + ", but was in method " +
447                                 methodName + ". Event=" + event;
448                 throw new IllegalArgumentException(message);
449             }
450             methodEntry(startClassName, event.location().method(), event
451                     .location().method().argumentTypeNames());
452             EventRequestManager mgr = vm.eventRequestManager();
453             mgr.deleteEventRequest(event.request());
454             setEventRequests(null);
455             stopWhenActivationDone = true;
456         }
457 
458         private void threadDeathEvent(ThreadDeathEvent event) {
459             trace("====== " + thread.name() + " end ======");
460         }
461 
462         private String getClassName(LocatableEvent event) {
463             String className = getDeclaringType(event).name();
464             try {
465                 className = getInstanceType(event).name();
466             } catch (IncompatibleThreadStateException itse) {
467                 System.err.println("cannot identify instance type for " +
468                         "method call: " + event.location().method().name() +
469                         "; using declaring type: " + className);
470             }
471             return className;
472         }
473 
474         private ReferenceType getDeclaringType(LocatableEvent event) {
475             return event.location().method().declaringType();
476         }
477 
478         private ReferenceType getInstanceType(LocatableEvent event)
479                 throws IncompatibleThreadStateException {
480             try {
481                 return event.thread().frame(0).thisObject().referenceType();
482             } catch (Exception e) {
483                 throw new IncompatibleThreadStateException(e.getMessage());
484             }
485         }
486     }
487 
488     private ThreadTrace threadTrace(ThreadReference thread) {
489         ThreadTrace threadTrace = traceMap.get(thread);
490         if (threadTrace == null) {
491             threadTrace = new ThreadTrace(thread);
492             traceMap.put(thread, threadTrace);
493         }
494         return threadTrace;
495     }
496 
497     private void handleEvent(Event event) throws NoSuchMethodException {
498         if (event instanceof ClassPrepareEvent) {
499             classPrepareEvent((ClassPrepareEvent) event);
500         } else if (event instanceof MethodEntryEvent) {
501             methodEntryEvent((MethodEntryEvent) event);
502         } else if (event instanceof MethodExitEvent) {
503             methodExitEvent((MethodExitEvent) event);
504         } else if (event instanceof ExceptionEvent) {
505             exceptionEvent((ExceptionEvent) event);
506         } else if (event instanceof StepEvent) {
507             stepEvent((StepEvent) event);
508         } else if (event instanceof BreakpointEvent) {
509             breakpointEvent((BreakpointEvent) event);
510         } else if (event instanceof ThreadDeathEvent) {
511             threadDeathEvent((ThreadDeathEvent) event);
512         } else if (event instanceof VMStartEvent) {
513             vmStartEvent((VMStartEvent) event);
514         } else if (event instanceof VMDeathEvent) {
515             vmDeathEvent((VMDeathEvent) event);
516         } else if (event instanceof VMDisconnectEvent) {
517             vmDisconnectEvent((VMDisconnectEvent) event);
518         } else {
519             throw new Error("Internal error: Unexpected event type");
520         }
521     }
522 
523     private synchronized void handleDisconnectedException() {
524         EventQueue queue = vm.eventQueue();
525         while (connected) {
526             try {
527                 EventSet eventSet = queue.remove();
528                 EventIterator iter = eventSet.eventIterator();
529                 while (iter.hasNext()) {
530                     Event event = iter.nextEvent();
531                     if (event instanceof VMDeathEvent) {
532                         vmDeathEvent((VMDeathEvent) event);
533                     } else if (event instanceof VMDisconnectEvent) {
534                         vmDisconnectEvent((VMDisconnectEvent) event);
535                     }
536                 }
537                 eventSet.resume();
538             } catch (InterruptedException e) {
539                 System.err.println(e);
540             }
541         }
542     }
543 
544     private void vmStartEvent(VMStartEvent event) {
545         trace("-- VM Started --");
546     }
547 
548     private void classPrepareEvent(ClassPrepareEvent event)
549             throws NoSuchMethodException {
550         threadTrace(event.thread()).classPrepareEvent(event);
551     }
552 
553     private void methodEntryEvent(MethodEntryEvent event) {
554         threadTrace(event.thread()).methodEntryEvent(event);
555     }
556 
557     private void methodExitEvent(MethodExitEvent event) {
558         threadTrace(event.thread()).methodExitEvent(event);
559     }
560 
561     private void stepEvent(StepEvent event) {
562         threadTrace(event.thread()).stepEvent(event);
563     }
564 
565     private void breakpointEvent(BreakpointEvent event) {
566         threadTrace(event.thread()).breakpointEvent(event);
567     }
568 
569     private void threadDeathEvent(ThreadDeathEvent event) {
570         ThreadTrace threadTrace = traceMap.get(event.thread());
571         if (threadTrace != null) {
572             threadTrace.threadDeathEvent(event);
573         }
574     }
575 
576     private void exceptionEvent(ExceptionEvent event) {
577         ThreadTrace threadTrace = traceMap.get(event.thread());
578         if (threadTrace != null) {
579             threadTrace.exceptionEvent(event);
580         }
581     }
582 
583     private void vmDeathEvent(VMDeathEvent event) {
584         vmDied = true;
585         trace("-- The application exited --");
586     }
587 
588     private void vmDisconnectEvent(VMDisconnectEvent event) {
589         connected = false;
590         if (!vmDied) {
591             trace("-- The application has been disconnected --");
592         }
593     }
594 
595     private void trace(String s) {
596         if (trace) {
597             System.err.println(s);
598         }
599     }
600 }