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.io.File;
26  import java.io.FileInputStream;
27  import java.io.FileOutputStream;
28  import java.io.IOException;
29  import java.io.ObjectInputStream;
30  import java.io.ObjectOutputStream;
31  import java.net.MalformedURLException;
32  import java.net.URL;
33  import java.net.URLClassLoader;
34  import java.util.ArrayList;
35  import java.util.HashMap;
36  import java.util.LinkedList;
37  import java.util.List;
38  import java.util.Map;
39  
40  import com.sun.jdi.Bootstrap;
41  import com.sun.jdi.VirtualMachine;
42  import com.sun.jdi.connect.AttachingConnector;
43  import com.sun.jdi.connect.Connector;
44  import com.sun.jdi.connect.IllegalConnectorArgumentsException;
45  import com.sun.jdi.connect.LaunchingConnector;
46  import com.sun.jdi.connect.VMStartException;
47  import com.sun.jdi.connect.Connector.Argument;
48  
49  public class Main {
50      private static final String PROGRAM_NAME = "JSeq";
51      private static final String PROGRAM_VERSION = "0.5.SNAPSHOT";
52      private static final String[] STANDARD_EXCLUDES =
53              { "java.*", "javax.*", "sun.*", "com.sun.*", "junit.*" };
54      private ActivationList rootActivations = new ActivationList();
55      private ConnectorType connectorType = null;
56      private String attachAddress = null;
57      private String classname = null;
58      private String arguments = null;
59      private String classpath = null;
60      private String readFilename = null;
61      private String saveFilename = null;
62      private String outFilename = null;
63      private Formatter formatter = FormatterRegistry.getInstance().get("svg");
64      private boolean quiet = false;
65      private boolean trace = true;
66      private String startMethod = null;
67      private List<String> includePatterns = new LinkedList<String>();
68      private List<String> excludePatterns = new LinkedList<String>();
69      private boolean stdExcludes = true;
70      private boolean shouldRun = true;
71  
72      public static void main(String[] args) {
73          try {
74              final Main main = new Main(args);
75              if (main.shouldRun) {
76                  // Make sure that generateSequenceDiagram is always called at
77                  // the end. This way, it is possible to attach to a long-running
78                  // process and use Ctrl-C to stop JSeq and still get a diagram.
79                  Runtime.getRuntime().addShutdownHook(new Thread() {
80                      public void run() {
81                          try {
82                              main.generateSequenceDiagram();
83                          } catch (Exception e) {
84                              e.printStackTrace();
85                          }
86                      }
87                  });
88                  main.traceProgram();
89                  // Here is main.generateSequenceDiagram called by the shutdown
90                  // hook.
91              }
92          } catch (IllegalArgumentException e) {
93              System.err.println(e.getMessage());
94              System.err.println(getUsage());
95          } catch (Exception e) {
96              e.printStackTrace();
97          }
98      }
99  
100     private Main(String[] args) {
101         int inx;
102         if (args.length == 0) {
103             throw new IllegalArgumentException("No arguments given");
104         }
105         for (inx = 0; inx < args.length; ++inx) {
106             String arg = args[inx];
107             if (arg.charAt(0) != '-') {
108                 break;
109             }
110             if (arg.equals("-connector")) {
111                 connectorType = ConnectorType.valueOf(args[++inx]);
112             } else if (arg.equals("-attach")) {
113                 attachAddress = args[++inx];
114             } else if (arg.equals("-read")) {
115                 readFilename = args[++inx];
116             } else if (arg.equals("-classpath") || arg.equals("-cp")) {
117                 classpath = args[++inx];
118             } else if (arg.equals("-save")) {
119                 saveFilename = args[++inx];
120             } else if (arg.equals("-out")) {
121                 outFilename = args[++inx];
122             } else if (arg.equals("-format")) {
123                 formatter = FormatterRegistry.getInstance().get(args[++inx]);
124             } else if (arg.equals("-quiet")) {
125                 quiet = true;
126             } else if (arg.equals("-start")) {
127                 startMethod = args[++inx];
128             } else if (arg.equals("-include")) {
129                 includePatterns.add(args[++inx]);
130             } else if (arg.equals("-exclude")) {
131                 excludePatterns.add(args[++inx]);
132             } else if (arg.equals("-nostdexcludes")) {
133                 stdExcludes = false;
134             } else if (arg.equals("-notrace")) {
135                 trace = false;
136             } else if (arg.equals("-version")) {
137                 System.out.println(getVersion());
138                 shouldRun = false;
139             } else {
140                 throw new IllegalArgumentException("Illegal option: " +
141                         args[inx]);
142             }
143         }
144         if (inx < args.length) {
145             classname = args[inx];
146             StringBuffer sb = new StringBuffer();
147             for (++inx; inx < args.length; ++inx) {
148                 sb.append(args[inx]);
149                 sb.append(' ');
150             }
151             arguments = sb.toString();
152         }
153 
154         if (stdExcludes) {
155             excludePatterns = addStandardExcludes(excludePatterns);
156         }
157 
158         if (connectorType == null) {
159             if (attachAddress == null) {
160                 connectorType = ConnectorType.LAUNCHING;
161             } else {
162                 connectorType = ConnectorType.SOCKET;
163             }
164         }
165     }
166 
167     private static List<String> addStandardExcludes(
168             List<String> originalExcludes) {
169         List<String> excludes = new LinkedList<String>(originalExcludes);
170         for (String standardExclude : STANDARD_EXCLUDES) {
171             excludes.add(standardExclude);
172         }
173         return excludes;
174     }
175 
176     private void traceProgram() throws IOException, ClassNotFoundException {
177         if (readFilename != null) {
178             readActivationList(readFilename);
179         } else if (attachAddress != null) {
180             attachProgram();
181         } else {
182             runProgram();
183         }
184     }
185 
186     private void generateSequenceDiagram() throws IOException, FormatException {
187         if (saveFilename != null) {
188             saveActivationList(rootActivations, saveFilename);
189         }
190         if (!quiet) {
191             ActivationList filteredActivations =
192                     filterActivations(rootActivations);
193             Diagram diagram = formatter.format(filteredActivations);
194             if (outFilename == null) {
195                 System.out.println(diagram);
196             } else {
197                 File file = new File(outFilename);
198                 diagram.save(file);
199             }
200         }
201     }
202 
203     private ActivationList filterActivations(ActivationList activationList) {
204         ActivationList filteredActivations = activationList;
205         if (startMethod != null) {
206             filteredActivations =
207                     filteredActivations.find(new MethodFilter(startMethod));
208         }
209         for (String pattern : excludePatterns) {
210             ClassExclusionFilter classExclusionFilter =
211                     new ClassExclusionFilter(pattern);
212             filteredActivations =
213                     filteredActivations.filter(classExclusionFilter);
214         }
215         filteredActivations =
216                 filteredActivations.filter(new ConstructorFilter(
217                         getClassLoader()));
218         filteredActivations = filteredActivations.collapseRepetitions();
219 
220         return filteredActivations;
221     }
222 
223     private ClassLoader getClassLoader() {
224         URLClassLoader classLoader =
225                 new URLClassLoader(getClasspathURLs(), ClassLoader
226                         .getSystemClassLoader());
227         return classLoader;
228     }
229 
230     private URL[] getClasspathURLs() {
231         URL[] classpathURLs = new URL[0];
232         if (classpath != null) {
233             try {
234                 String[] classpathEntries = classpath.split("path.separator");
235                 classpathURLs = new URL[classpathEntries.length];
236                 for (int i = 0; i < classpathURLs.length; i++) {
237                     classpathURLs[i] =
238                             new File(classpathEntries[i]).toURI().toURL();
239                 }
240             } catch (MalformedURLException e) {
241                 System.err.println(e);
242             }
243         }
244         return classpathURLs;
245     }
246 
247     private void readActivationList(String filename) throws IOException,
248             ClassNotFoundException {
249         ObjectInputStream in =
250                 new ObjectInputStream(new FileInputStream(filename));
251         rootActivations = (ActivationList) in.readObject();
252         in.close();
253     }
254 
255     private void saveActivationList(ActivationList activationList,
256             String filename) throws IOException {
257         ObjectOutputStream out =
258                 new ObjectOutputStream(new FileOutputStream(filename));
259         out.writeObject(activationList);
260         out.close();
261     }
262 
263     private void attachProgram() {
264         ProgramRunner runner =
265                 new ProgramRunner(rootActivations, attachAddress,
266                         includePatterns, excludePatterns, startMethod, trace);
267         runner.runProgram(connectorType);
268     }
269 
270     private void runProgram() {
271         ProgramRunner runner =
272                 new ProgramRunner(rootActivations, classname, arguments,
273                         classpath, includePatterns, excludePatterns,
274                         startMethod, trace);
275         runner.runProgram(connectorType);
276     }
277 
278     public static String getUsage() {
279         return "Usage: jseq [-options] <class> [args...]\n"
280                 + "\t(to execute a class)\n"
281                 + "    or jseq [-options] -attach <address>\n"
282                 + "\t(to attach to a running VM at the specified address)\n"
283                 + "    or jseq [-options] -read <filename>\n"
284                 + "\t(to generate output from a previously saved run)\n"
285                 + "\n"
286                 + "Options for running a program:\n"
287                 + "\t[-classpath <path>]\tto set classpath\n"
288                 + "\t[-cp <path>]\tsame as -classpath\n"
289                 + "\t[-save <filename>]\tto save the program run in a file\n"
290                 + "\n"
291                 + "Options for attaching to a program:\n"
292                 + "\t[-connector {SOCKET,SHARED_MEMORY}]\tto choose JDI connector\n"
293                 + "\n"
294                 + "Options for generating sequence diagrams:\n"
295                 + "\t[-out <filename>]\tto save diagram in a file\n"
296                 + "\t[-format {text,png,sdedit,svg,argouml}]\tto specify format of output\n"
297                 + "\t[-quiet]\tto not generate any output\n"
298                 + "\t[-start <methodname>]\tto specify start method in diagram\n"
299                 + "\t[-include <class regexp>]\tto include only some classes in diagram\n"
300                 + "\t[-exclude <class regexp>]\tto exclude some classes from diagram\n"
301                 + "\t[-nostdexcludes]\tto not exclude java.*, javax.*, etc\n"
302                 + "\n" + "Other options:\n"
303                 + "\t[-notrace]\tto turn off tracing of method entries, etc.\n"
304                 + "\t[-version]\tto print version information and exit";
305     }
306 
307     public static String getVersion() {
308         return PROGRAM_NAME + " " + PROGRAM_VERSION;
309     }
310 
311     private static class ProgramRunner {
312         private ActivationList rootActivations;
313         private String classname;
314         private String arguments;
315         private String classpath;
316         private String attachAddress;
317         private String startMethod;
318         private boolean trace;
319         private VirtualMachine vm;
320         private Thread errThread;
321         private Thread outThread;
322         private List<String> includes;
323         private List<String> excludes;
324         private EventThread eventThread;
325 
326         public ProgramRunner(ActivationList rootActivations,
327                 String attachAddress, List<String> includes,
328                 List<String> excludes, String startMethod, boolean trace) {
329             this.rootActivations = rootActivations;
330             this.attachAddress = attachAddress;
331             this.includes = includes;
332             this.excludes = excludes;
333             this.startMethod = startMethod;
334             this.trace = trace;
335             this.classpath = System.getProperty("java.class.path");
336         }
337 
338         public ProgramRunner(ActivationList rootActivations, String classname,
339                 String arguments, String classpath, List<String> includes,
340                 List<String> excludes, String startMethod, boolean trace) {
341             this.rootActivations = rootActivations;
342             this.classname = classname;
343             this.arguments = arguments;
344             this.includes = includes;
345             this.excludes = excludes;
346             this.startMethod = startMethod;
347             this.trace = trace;
348             if (classpath == null) {
349                 this.classpath = System.getProperty("java.class.path");
350             } else {
351                 this.classpath = classpath;
352             }
353         }
354 
355         public void runProgram(ConnectorType connectorType) {
356             if (attachAddress == null) {
357                 vm = launchTarget(connectorType, classname + " " + arguments);
358                 redirectOutput();
359             } else {
360                 vm = attachTarget(connectorType, attachAddress);
361             }
362             List<String> emptyStringList = new ArrayList<String>();
363             eventThread =
364                     new EventThread(vm, rootActivations, includes, excludes,
365                             emptyStringList, trace);
366             eventThread.setEventRequests(startMethod);
367             eventThread.start();
368             vm.resume();
369 
370             try {
371                 eventThread.join();
372                 if (attachAddress == null) {
373                     errThread.join();
374                     outThread.join();
375                 }
376             } catch (InterruptedException exc) {
377                 // Ignore
378             }
379         }
380 
381         private VirtualMachine attachTarget(ConnectorType connectorType,
382                 String attachAddress) {
383             AttachingConnector connector =
384                     (AttachingConnector) findConnectorByType(connectorType);
385             Map<String, String> arguments = new HashMap<String, String>();
386             switch (connectorType) {
387             case SOCKET:
388                 String[] hostAndPort = attachAddress.split(":");
389                 if (hostAndPort.length != 2) {
390                     throw new IllegalArgumentException(
391                             "Attach address should be formatted as 'hostname:port'");
392                 }
393                 arguments.put("hostname", hostAndPort[0]);
394                 arguments.put("port", hostAndPort[1]);
395                 break;
396             case SHARED_MEMORY:
397                 arguments.put("name", attachAddress);
398                 break;
399             default:
400                 throw new IllegalArgumentException(
401                         "Unexpected connector type: " + connectorType);
402             }
403             Map<String, Argument> connectorArguments =
404                     getConnectorArgs(connector, arguments);
405             try {
406                 return connector.attach(connectorArguments);
407             } catch (IOException e) {
408                 throw new Error("Unable to launch target VM: " + e);
409             } catch (IllegalConnectorArgumentsException e) {
410                 throw new Error("Internal error: " + e);
411             }
412         }
413 
414         private VirtualMachine launchTarget(ConnectorType connectorType,
415                 String mainArgs) {
416             LaunchingConnector connector =
417                     (LaunchingConnector) findConnectorByType(connectorType);
418             Map<String, String> arguments = new HashMap<String, String>();
419             switch (connectorType) {
420             case LAUNCHING:
421                 arguments.put("main", mainArgs);
422                 break;
423             default:
424                 throw new IllegalArgumentException(
425                         "Unexpected connector type: " + connectorType);
426             }
427             Map<String, Argument> connectorArguments =
428                     getConnectorArgs(connector, arguments);
429             Connector.StringArgument options =
430                     (Connector.StringArgument) connectorArguments
431                             .get("options");
432             options.setValue(options.value() + "-classpath " + classpath);
433             try {
434                 return connector.launch(connectorArguments);
435             } catch (IOException exc) {
436                 throw new Error("Unable to launch target VM: " + exc);
437             } catch (IllegalConnectorArgumentsException exc) {
438                 throw new Error("Internal error: " + exc);
439             } catch (VMStartException exc) {
440                 throw new Error("Target VM failed to initialize: " +
441                         exc.getMessage());
442             }
443         }
444 
445         private Connector findConnectorByType(ConnectorType connectorType) {
446             Connector foundConnector = null;
447             String name = connectorType.getName();
448             List<Connector> connectors =
449                     Bootstrap.virtualMachineManager().allConnectors();
450             for (Connector connector : connectors) {
451                 if (connector.name().equals(name)) {
452                     foundConnector = connector;
453                     break;
454                 }
455             }
456             if (foundConnector == null) {
457                 throw new Error("Connector not found: " + name);
458             }
459             return foundConnector;
460         }
461 
462         private Map<String, Argument> getConnectorArgs(Connector connector,
463                 Map<String, String> arguments) {
464             Map<String, Argument> allArguments = connector.defaultArguments();
465             for (String argumentName : arguments.keySet()) {
466                 String argumentValue = arguments.get(argumentName);
467                 Connector.Argument argument = allArguments.get(argumentName);
468                 if (argument == null) {
469                     throw new IllegalArgumentException(
470                             "Unknown argument name '" + argumentName +
471                                     "' for " + connector);
472                 }
473                 argument.setValue(argumentValue);
474             }
475             return allArguments;
476         }
477 
478         private void redirectOutput() {
479             Process process = vm.process();
480             errThread =
481                     new StreamRedirectThread("error reader", process
482                             .getErrorStream(), System.err);
483             outThread =
484                     new StreamRedirectThread("output reader", process
485                             .getInputStream(), System.out);
486             errThread.start();
487             outThread.start();
488         }
489     }
490 
491     private static enum ConnectorType {
492         SOCKET("com.sun.jdi.SocketAttach", true),
493         SHARED_MEMORY("com.sun.jdi.SharedMemoryAttach", true),
494         LAUNCHING("com.sun.jdi.CommandLineLaunch", false);
495 
496         private String name;
497         private boolean attaching;
498 
499         private ConnectorType(String name, boolean attaching) {
500             this.name = name;
501             this.attaching = attaching;
502         }
503 
504         public String getName() {
505             return name;
506         }
507 
508         public boolean isAttaching() {
509             return attaching;
510         }
511     }
512 }