1 // The following classes are from the Java Debugger Platform Architecture jpda
2 // The ship with SDK 1.3+ in tools.jar
3 import com.sun.jdi.VirtualMachine;
4 import com.sun.jdi.Bootstrap;
5 import com.sun.jdi.Method;
6 import com.sun.jdi.ObjectReference;
7 import com.sun.jdi.ReferenceType;
8 import com.sun.jdi.ThreadReference;
9 import com.sun.jdi.StackFrame;
10 import com.sun.jdi.connect.Connector;
11 import com.sun.jdi.connect.LaunchingConnector;
12 
13 import com.sun.jdi.event.Event;
14 import com.sun.jdi.event.MethodEntryEvent;
15 import com.sun.jdi.event.MethodExitEvent;
16 import com.sun.jdi.request.EventRequestManager;
17 import com.sun.jdi.request.MethodEntryRequest;
18 import com.sun.jdi.request.MethodExitRequest;
19 
20 // IO classes
21 import java.io.BufferedReader;
22 import java.io.FileReader;
23 import java.io.FileOutputStream;
24 import java.io.InputStreamReader;
25 import java.io.InputStream;
26 import java.io.IOException;
27 import java.io.PrintStream;
28 
29 // util classes
30 import java.util.ArrayList;
31 import java.util.Iterator;
32 import java.util.List;
33 import java.util.HashMap;
34 import java.util.Map;
35 import java.util.Set;
36 
37 /**
38  *  This class produces outlines of call sequences for other Java programs
39  *  by using debugger hooks.  It is useful for building UML sequence diagrams.
40  *  See the documentation for {@link #main} for usage.  See UML::Sequence
41  *  on CPAN for Perl scripts to make the diagrams.  In particular, see
42  *  genericseq.pl and UML::Sequence::JavaSeq.pm.
43  */
44 public class Seq {
45     VirtualMachine      vm;
46     Process             process;
47     SeqEventHandler     handler;
48     EventRequestManager requestManager;
49     MethodEntryRequest  initialEntryRequest;
50     MethodEntryRequest  regularEntryRequest;
51     MethodExitRequest   regularExitRequest;
52     static final int    INITIAL_EVENT_STATUS    = 1;
53     static final int    REGULAR_EVENT_STATUS    = 2;
54     int                 eventStatus             = INITIAL_EVENT_STATUS;
55     int                 indent                  = 0;
56     boolean             everythingIsInteresting = false;
57     HashMap             interestingMethods;
58     ArrayList           interestingClasses;
59     PrintStream         outputStream;
60     String              excludeFilter;
61 
62     // objects is keyed by object hash code storing object's number.
63     // Numbers are per class and are issued sequentially from 1 during
64     // constructor processing.
65     HashMap             objects                 = new HashMap();
66     // nextObjectNumber is keyed by class name storing the most recently
67     // used number.  Preincrement this to use it.
68     HashMap             nextObjectNumber        = new HashMap();
69 
70 
71     /**
72      *  Most callers will use only this method.
73      *  Builds a connection to the debugger, launches the supplied program,
74      *  directs production outpuer.
75      *  @param interestingMethodsFile name of a file listing methods or classes
76      *                                you want to see in your output
77      *  see <a href="Hello.methods">Hello.methods</a> for an example with
78      *  documenation
79      *  @param outputFileName name of a file where output will go, standard out
80      *                        can't be used, since the program you are tracing
81      *                        might be using it
82      *  @param args the java class to invoke and any command line arguments
83      *              it needs
84      */
Seq( String interestingMethodsFile, String outputFileName, String[] args )85     public Seq(
86         String   interestingMethodsFile,
87         String   outputFileName,
88         String[] args
89     ) throws Exception {
90         setupInterestingThings(interestingMethodsFile);
91         openOutput(outputFileName);
92 
93         LaunchingConnector  conn  = findConnector();
94         vm                        = conn.launch(getArgsMap(conn, args));
95         process                   = vm.process();
96         dumpProcessOutput(process);
97         handler                   = new SeqEventHandler(this);
98         requestManager            = vm.eventRequestManager();
99         initialEntryRequest       = requestManager.createMethodEntryRequest();
100 
101         // wait for a method in the starting class
102         initialEntryRequest.addClassFilter(args[0]);
103         initialEntryRequest.enable();
104     }
105 
setupInterestingThings(String file)106     private void setupInterestingThings(String file) throws IOException {
107         interestingMethods       = new HashMap();
108         HashMap        classHash = new HashMap();
109         interestingClasses       = new ArrayList();
110         FileReader     fr        = new FileReader(file);
111         BufferedReader reader    = new BufferedReader(fr);
112 
113         Integer dummyInt         = new Integer(1);
114         String line;
115         while ((line = reader.readLine()) != null) {
116             // skip comments and blanks
117             if (line.startsWith("#") || line.trim().length() == 0) {
118                 continue;
119             }
120             // turn off method name checking if file has an 'ALL' line
121             else if (line.equals("ALL")) {
122                 everythingIsInteresting = true;
123             }
124             // assume everything else is a method/class name
125             // There is no harm in putting extraneous things into the
126             // interestingMethods hash.  If everythingIsInteresting,
127             // the hash will be completely ignored.  In other cases,
128             // actual methods are looked up in the hash, if they are there
129             // they print, otherwise not.  No one cares if extra entries
130             // are there.
131             else {
132                 interestingMethods.put(line,                dummyInt);
133                 classHash         .put(grabClassName(line), dummyInt);
134             }
135         }
136         reader.close();
137         fr.close();
138 
139         // turn classHash into a List, this could be folded into
140         // switchToRegularStatus
141         Set      classKeys = classHash.keySet();
142         Iterator iter      = classKeys.iterator();
143         while (iter.hasNext()) {
144             interestingClasses.add(iter.next());
145         }
146     }
147 
grabClassName(String line)148     private String grabClassName(String line) {
149         // look for opening (
150         int parenPos = line.indexOf('(');
151         // there is one, this is a signature
152         if (parenPos >= 0) {
153             // remove arg list
154             String methodName = line.substring(0, parenPos);
155             // look for last dot
156             int lastDot = methodName.lastIndexOf('.');
157             // there is one, there is a package name
158             if (lastDot >= 0) {
159                 // remove the method name, leaving all packages and the class
160                 String className = methodName.substring(0, lastDot);
161                 return className;
162             }
163             else {
164                 return methodName;  // unlikely
165             }
166         }
167         // no opening paren, the whole line is a class name
168         else {
169             return line;
170         }
171     }
172 
openOutput(String file)173     private void openOutput(String file) throws IOException {
174         FileOutputStream fos = new FileOutputStream(file);
175         outputStream         = new PrintStream(fos);
176     }
177 
getVM()178     public VirtualMachine getVM() { return vm; }
179 
180     // sets the main attribute of the connection argument hash to the
181     // name of the program to trace, concatenated with its arguments,
182     // the list is delimited by spaces
getArgsMap(Connector conn, String[] args)183     private Map getArgsMap(Connector conn, String[] args) {
184         Map                argsMap = conn.defaultArguments();
185         Connector.Argument mainArg = (Connector.Argument)argsMap.get("main");
186 
187         StringBuffer       sb      = new StringBuffer();
188 
189         int argCount = args.length;
190         int maxArg   = argCount - 1;
191         for (int i = 0; i < argCount; i++) {
192             sb.append(args[i]);
193             if (i < maxArg) {
194                 sb.append(" ");
195             }
196         }
197 
198         mainArg.setValue(sb.toString());
199         return argsMap;
200     }
201 
202     // gains a valid LaunchingConnector reference by a name lookup
findConnector()203     public LaunchingConnector findConnector() {
204         List     connectors = Bootstrap.virtualMachineManager().allConnectors();
205         Iterator iter       = connectors.iterator();
206         while (iter.hasNext()) {
207             Connector conn = (Connector)iter.next();
208             if (conn.name().equals("com.sun.jdi.CommandLineLaunch")) {
209                 return (LaunchingConnector)conn;
210             }
211         }
212         return null;
213     }
214 
215     // when a method exits, adjusts the depth of the call sequence
216     // and restarts the virtual machine
methodExitEvent(MethodExitEvent event)217     public void methodExitEvent(MethodExitEvent event) {
218         indent--;
219         vm.resume();
220     }
221 
222     // the virtual machine is up, ask it to start
vmStartEvent(Event event)223     public void vmStartEvent(Event event) {
224         vm.resume();
225     }
226 
227     // If this is the first entry event, swithToRegularStatus.
228     // In all cases, print the method signature, if the
229     // user is interested in it.  Then increment the call sequence depth
230     // and restart the virtual machine.
methodEntryEvent(MethodEntryEvent event)231     public void methodEntryEvent(MethodEntryEvent event) {
232         if (eventStatus == INITIAL_EVENT_STATUS) {
233             switchToRegularStatus();
234         }
235 
236         Method method     = event.method();
237         String signature  = grabSignature(method);
238         String objectName = grabInstanceName(event, method);
239 
240         Object includeIt  = interestingMethods.get(signature);
241 
242         if (everythingIsInteresting || includeIt != null) {
243             outputStream.println(formIndentString() + objectName + signature);
244         }
245 
246         indent++;
247         vm.resume();
248     }
249 
250     // Returns the manufactured name of the instance which is operative
251     // in the current method (the one called this in that method).
252     // Maintains the list of objects by number using two hashes:
253     // objects and nextObjectNumber.  For example the second Roller object
254     // used in a program will yield roller2.
grabInstanceName(MethodEntryEvent event, Method method)255     private String grabInstanceName(MethodEntryEvent event, Method method) {
256         ObjectReference thisRef    = grabStackTopReference(event);
257 
258         if (thisRef == null) { // the top method is static => no this instance
259             return "";
260         }
261 
262         String          type       = thisRef.referenceType().name();
263         Integer         thisCode   = new Integer(thisRef.hashCode());
264         Integer         countI     = null;
265 
266         if (method.isConstructor()) {  // store the hash code
267             countI     = (Integer)nextObjectNumber.get(type);
268             int count  = 0;
269             if (countI == null) { count = 1;                     }
270             else                { count = countI.intValue() + 1; }
271 
272             countI     = new Integer(count);
273             nextObjectNumber.put(type, countI);
274 
275             objects.put(thisCode, countI);
276         }
277         else {  // regular instance method (not constructor, not static)
278             countI = (Integer)objects.get(thisCode);
279         }
280         return lcfirst(type) + countI.toString() + ":";
281     }
282 
283     // Examines the current stack frame returning the ObjectReference
284     // of the instance operative in the method on top of the stack.
285     // Caller can fish in the returned reference for the type name of
286     // the operative instance
grabStackTopReference(MethodEntryEvent event)287     private ObjectReference grabStackTopReference(MethodEntryEvent event) {
288         ThreadReference thread     = event.thread();
289         StackFrame      frame      = null;
290         try {
291             frame  = thread.frame(0);
292         } catch (Exception e) { }
293         // com.sun.jdi.IncompatibleThreadStateException
294 
295         return frame.thisObject();
296     }
297 
298     // This should be part of java.lang.String.  It takes a String and
299     // returns it with the first character in lower case Roller becomes roller.
lcfirst(String in)300     private static String lcfirst(String in) {
301         String first = in.substring(0, 1);
302         String rest  = in.substring(1);
303         return first.toLowerCase() + rest;
304     }
305 
306     // turn off initial entry request
307     // make new entry and exit requests for each class the user want to see
switchToRegularStatus()308     private void switchToRegularStatus() {
309         eventStatus          = REGULAR_EVENT_STATUS;
310         initialEntryRequest.disable();
311 
312         Iterator       iter  = interestingClasses.iterator();
313         if (iter.hasNext()) {
314             while (iter.hasNext()) {  // make one filter for each class
315                 String className = (String)iter.next();
316 
317                 MethodEntryRequest  entryRequest;
318                 MethodExitRequest   exitRequest;
319 
320                 entryRequest     = requestManager.createMethodEntryRequest();
321                 exitRequest      = requestManager.createMethodExitRequest();
322 
323                 entryRequest.addClassFilter(className);
324                 exitRequest .addClassFilter(className);
325 
326                 entryRequest.enable();
327                 exitRequest .enable();
328 
329             }
330         }
331         else { // no classes were named, here comes the flood
332             MethodEntryRequest  entryRequest;
333             MethodExitRequest   exitRequest;
334 
335             entryRequest     = requestManager.createMethodEntryRequest();
336             exitRequest      = requestManager.createMethodExitRequest();
337 
338             entryRequest.enable();
339             exitRequest .enable();
340         }
341     }
342 
343     // builds an official signature like
344     // com.company.package.ClassName.method(java.lang.String[], float)
345     // uses assembleArgs to make the argument list
grabSignature(Method method)346     private String grabSignature(Method method) {
347         return method.declaringType().name()
348                + "." + method.name() + "("
349                + assembleArgs(method) + ")";
350     }
351 
352     // gives a string which can be printed before the signature to
353     // show the current call sequence depth visually
formIndentString()354     private String formIndentString() {
355         StringBuffer sb = new StringBuffer();
356         for (int i = 0; i < indent; i++) {
357             sb.append("  ");
358         }
359         return sb.toString();
360     }
361 
362     // asks the method for its types, then assembles them for proper printing
assembleArgs(Method method)363     private String assembleArgs(Method method) {
364         List         argTypes = method.argumentTypeNames();
365         Iterator     iter     = argTypes.iterator();
366         StringBuffer sb       = new StringBuffer();
367         while (iter.hasNext()) {
368             sb.append(iter.next());
369             if (iter.hasNext()) {
370                 sb.append(", ");
371             }
372         }
373         return sb.toString();
374     }
375 
376     // the debugger must have a way to expell error message, lest it die
377     // from full buffers, this method arranges that
dumpProcessOutput(Process proc)378     private void dumpProcessOutput(Process proc) {
379         dumpOutput(proc.getErrorStream());
380         dumpOutput(proc.getInputStream());
381     }
382 
383     // spawns a thread so dumpStream can run concurrently with other threads
dumpOutput(final InputStream stream)384     private void dumpOutput(final InputStream stream) {
385         Thread thread = new Thread() {
386             public void run() {
387                 try {
388                     dumpStream(stream);
389                 }
390                 catch (Exception e) {
391                     System.err.println("dump failed for " + stream);
392                 }
393             }
394         };
395         thread.setPriority(Thread.MAX_PRIORITY - 1);
396         thread.start();
397     }
398 
399     // continually issues blocking reads stdin, or stderr from the virtual
400     // machines process
401     // prints the result to standard err.
dumpStream(InputStream stream)402     private void dumpStream(InputStream stream) throws IOException {
403         BufferedReader in = new BufferedReader(new InputStreamReader(stream));
404         String line;
405         while ((line = in.readLine()) != null) {
406             System.err.println(line);
407         }
408     }
409 
printUsage()410     public static void printUsage() {
411         System.err.println("usage: java Seq methods_file output_file"
412             + " class [args...]");
413     }
414 
415     /**
416      *  This is meant to be used, as shown in {@link #printUsage} above.
417      *  @param args <br>method_file<br>output_file<br>class
418      *  <br>[args_for_class...]
419      */
main(String[] args)420     public static void main(String[] args) throws Exception {
421         if (args.length < 3) {
422             printUsage();
423             System.exit(1);
424         }
425         String[] passThroughArgs = new String[args.length - 2];
426         for (int i = 2; i < args.length; i++) {
427             passThroughArgs[i - 2] = args[i];
428         }
429         Seq s = new Seq(args[0], args[1], passThroughArgs);
430     }
431 }
432