View Javadoc

1   /***************************************************************************************
2    * Copyright (c) Jonas Bonér, Alexandre Vasseur. All rights reserved.                 *
3    * http://aspectwerkz.codehaus.org                                                    *
4    * ---------------------------------------------------------------------------------- *
5    * The software in this package is published under the terms of the LGPL license      *
6    * a copy of which has been included with this distribution in the license.txt file.  *
7    **************************************************************************************/
8   package org.codehaus.aspectwerkz.hook;
9   
10  import com.sun.jdi.VirtualMachine;
11  
12  import java.io.File;
13  import java.io.IOException;
14  import java.util.StringTokenizer;
15  
16  /***
17   * ProcessStarter uses JPDA JDI api to start a VM with a runtime modified java.lang.ClassLoader, or transparently use a
18   * Xbootclasspath style (java 1.3 detected or forced) <p/>
19   * <p>
20   * <h2>Important note</h2>
21   * Due to a JPDA issue in LauchingConnector, this implementation is based on Process forking. If Xbootclasspath is not
22   * used the target VM is started with JDWP options <i>transport=dt_socket,address=9300 </i> unless other specified.
23   * <br/>It is possible after the short startup sequence to attach a debugger or any other JPDA attaching connector. It
24   * has been validated against a WebLogic 7 startup and is the <i>must use </i> implementation.
25   * </p>
26   * <p/>
27   * <p>
28   * <h2>Implementation Note</h2>
29   * See http://java.sun.com/products/jpda/ <br/>See http://java.sun.com/j2se/1.4.1/docs/guide/jpda/jdi/index.html <br/>
30   * </p>
31   * <p/><p/>For java 1.3, it launch the target VM using a modified java.lang.ClassLoader by generating it and putting it
32   * in the bootstrap classpath of the target VM. The java 1.3 version should only be run for experimentation since it
33   * breaks the Java 2 Runtime Environment binary code license by overriding a class of rt.jar
34   * </p>
35   * <p/><p/>For java 1.4, it hotswaps java.lang.ClassLoader with a runtime patched version, wich is compatible with the
36   * Java 2 Runtime Environment binary code license. For JVM not supporting the class hotswapping, the same mechanism as
37   * for java 1.3 is used.
38   * </p>
39   * <p/>
40   * <p>
41   * <h2>Usage</h2>
42   * Use it as a replacement of "java" :<br/><code>java [target jvm option] [target classpath]
43   * targetMainClass [targetMainClass args]</code>
44   * <br/>should be called like: <br/><code>java [jvm option] [classpath]
45   * org.codehaus.aspectwerkz.hook.ProcessStarter [target jvm option] [target classpath] targetMainClass [targetMainClass
46   * args]</code>
47   * <br/><b>[classpath] must contain %JAVA_HOME%/tools.jar for HotSwap support </b> <br/>[target jvm option] can contain
48   * JDWP options, transport and address are preserved if specified.
49   * </p>
50   * <p/>
51   * <p>
52   * <h2>Options</h2>
53   * [classpath] must contain %JAVA_HOME%/tools.jar and the jar you want for bytecode modification (bcel, javassist, asm...)
54   * <br/>The java.lang.ClassLoader is patched using the <code>-Daspectwerkz.classloader.clpreprocessor=...</code> in
55   * [jvm option]. Specify the FQN of your implementation of hook.ClassLoaderPreProcessor. See {@link
56   * org.codehaus.aspectwerkz.hook.ClassLoaderPreProcessor} If not given, the default AspectWerkz layer 1 Javassist
57   * implementation hook.impl.* is used, which is equivalent to
58   * <code>-Daspectwerkz.classloader.clpreprocessor=org.codehaus.aspectwerkz.hook.impl.ClassLoaderPreProcessorImpl</code>
59   * <br/>Use -Daspectwerkz.classloader.wait=2 in [jvm option] to force a pause of 2 seconds between process fork and JPDA
60   * connection for HotSwap. Defaults to no wait.
61   * </p>
62   * <p/>
63   * <p>
64   * <h2>Disabling HotSwap</h2>
65   * You disable HotSwap and thus force the use of -Xbootclasspath (like in java 1.3 mode) and specify the directory where
66   * the modified class loader bytecode will be stored using in [jvm option]
67   * <code>-Daspectwerkz.classloader.clbootclasspath=...</code>. Specify the directory where you want the patched
68   * java.lang.ClassLoader to be stored. Default is "./_boot". The directory is created if needed (with the subdirectories
69   * corresponding to package names). <br/>The directory is <b>automatically </b> incorporated in the -Xbootclasspath
70   * option of [target jvm option]. <br/>You shoud use this option mainly for debuging purpose, or if you need to start
71   * different jvm with different classloader preprocessor implementations.
72   * </p>
73   * <p/>
74   * <p>
75   * <h2>Option for AspectWerkz layer 1 Javassist implementation</h2>
76   * When using the default AspectWerkz layer 1 Javassist implementation
77   * <code>org.codehaus.aspectwerkz.hook.impl.ClassLoaderPreProcessorImpl</code>, java.lang.ClassLoader is modified to
78   * call a class preprocessor at each class load (except for class loaded by the bootstrap classloader). <br/>The
79   * effective class preprocessor is defined with <code>-Daspectwerkz.classloader.preprocessor=...</code> in [target jvm
80   * option]. Specify the FQN of your implementation of org.codehaus.aspectwerkz.hook.ClassPreProcessor interface. <br/>If
81   * this parameter is not given, the default AspectWerkz layer 2
82   * org.codehaus.aspectwerkz.transform.AspectWerkzPreProcessor is used. <br/>
83   * </p>
84   * 
85   * @author <a href="mailto:alex@gnilux.com">Alexandre Vasseur </a>
86   */
87  public class ProcessStarter {
88      /***
89       * option for classloader preprocessor target
90       */
91      final static String CL_PRE_PROCESSOR_CLASSNAME_PROPERTY = "aspectwerkz.classloader.clpreprocessor";
92  
93      /***
94       * default dir when -Xbootclasspath is forced or used (java 1.3)
95       */
96      private final static String CL_BOOTCLASSPATH_FORCE_DEFAULT = "." + File.separatorChar + "_boot";
97  
98      /***
99       * option for target dir when -Xbootclasspath is forced or used (java 1.3)
100      */
101     private final static String CL_BOOTCLASSPATH_FORCE_PROPERTY = "aspectwerkz.classloader.clbootclasspath";
102 
103     /***
104      * option for seconds to wait before connecting
105      */
106     private final static String CONNECTION_WAIT_PROPERTY = "aspectwerkz.classloader.wait";
107 
108     /***
109      * target process
110      */
111     private Process process = null;
112 
113     /***
114      * used if target VM exits before launching VM
115      */
116     private boolean executeShutdownHook = true;
117 
118     /***
119      * thread to redirect streams of target VM in launching VM
120      */
121     private Thread inThread;
122 
123     /***
124      * thread to redirect streams of target VM in launching VM
125      */
126     private Thread outThread;
127 
128     /***
129      * thread to redirect streams of target VM in launching VM
130      */
131     private Thread errThread;
132 
133     /***
134      * Test if current java installation supports HotSwap
135      */
136     private static boolean hasCanRedefineClass() {
137         try {
138             VirtualMachine.class.getMethod("canRedefineClasses", new Class[] {});
139         } catch (NoSuchMethodException e) {
140             return false;
141         }
142         return true;
143     }
144 
145     private int run(String[] args) {
146         // retrieve options and main
147         String[] javaArgs = parseJavaCommandLine(args);
148         String optionArgs = javaArgs[0];
149         String cpArgs = javaArgs[1];
150         String mainArgs = javaArgs[2];
151         String options = optionArgs + " -cp " + cpArgs;
152         String clp = System.getProperty(
153             CL_PRE_PROCESSOR_CLASSNAME_PROPERTY,
154             "org.codehaus.aspectwerkz.hook.impl.ClassLoaderPreProcessorImpl");
155 
156         // if java version does not support method "VirtualMachine.canRedefineClass"
157         // or if bootclasspath is forced, transform optionsArg
158         if (!hasCanRedefineClass() || (System.getProperty(CL_BOOTCLASSPATH_FORCE_PROPERTY) != null)) {
159             String bootDir = System.getProperty(CL_BOOTCLASSPATH_FORCE_PROPERTY, CL_BOOTCLASSPATH_FORCE_DEFAULT);
160             if (System.getProperty(CL_BOOTCLASSPATH_FORCE_PROPERTY) != null) {
161                 System.out.println("HotSwap deactivated, using bootclasspath: " + bootDir);
162             } else {
163                 System.out.println("HotSwap not supported by this java version, using bootclasspath: " + bootDir);
164             }
165             ClassLoaderPatcher.patchClassLoader(clp, bootDir);
166             BootClasspathStarter starter = new BootClasspathStarter(options, mainArgs, bootDir);
167             try {
168                 process = starter.launchVM();
169             } catch (IOException e) {
170                 System.err.println("failed to launch process :" + starter.getCommandLine());
171                 e.printStackTrace();
172                 return -1;
173             }
174 
175             // attach stdout VM streams to this streams
176             // this is needed early to support -verbose:class like options
177             redirectStdoutStreams();
178         } else {
179             // lauch VM in suspend mode
180             JDWPStarter starter = new JDWPStarter(options, mainArgs, "dt_socket", "9300");
181             try {
182                 process = starter.launchVM();
183             } catch (IOException e) {
184                 System.err.println("failed to launch process :" + starter.getCommandLine());
185                 e.printStackTrace();
186                 return -1;
187             }
188 
189             // attach stdout VM streams to this streams
190             // this is needed early to support -verbose:class like options
191             redirectStdoutStreams();
192 
193             // override class loader in VM thru an attaching connector
194             int secondsToWait = 0;
195             try {
196                 secondsToWait = Integer.parseInt(System.getProperty(CONNECTION_WAIT_PROPERTY, "0"));
197             } catch (NumberFormatException nfe) {
198                 ;
199             }
200             VirtualMachine vm = ClassLoaderPatcher.hotswapClassLoader(
201                 clp,
202                 starter.getTransport(),
203                 starter.getAddress(),
204                 secondsToWait);
205             if (vm == null) {
206                 process.destroy();
207             } else {
208                 vm.resume();
209                 vm.dispose();
210             }
211         }
212 
213         // attach VM other streams to this streams
214         redirectOtherStreams();
215 
216         // add a shutdown hook to "this" to shutdown VM
217         Thread shutdownHook = new Thread() {
218             public void run() {
219                 shutdown();
220             }
221         };
222         try {
223             Runtime.getRuntime().addShutdownHook(shutdownHook);
224             int exitCode = process.waitFor();
225             executeShutdownHook = false;
226             return exitCode;
227         } catch (Exception e) {
228             executeShutdownHook = false;
229             e.printStackTrace();
230             return -1;
231         }
232     }
233 
234     /***
235      * shutdown target VM (used by shutdown hook of lauching VM)
236      */
237     private void shutdown() {
238         if (executeShutdownHook) {
239             process.destroy();
240         }
241         try {
242             outThread.join();
243             errThread.join();
244         } catch (InterruptedException e) {
245             ;
246         }
247     }
248 
249     /***
250      * Set up stream redirection in target VM for stdout
251      */
252     private void redirectStdoutStreams() {
253         outThread = new StreamRedirectThread("out.redirect", process.getInputStream(), System.out);
254         outThread.start();
255     }
256 
257     /***
258      * Set up stream redirection in target VM for stderr and stdin
259      */
260     private void redirectOtherStreams() {
261         inThread = new StreamRedirectThread("in.redirect", System.in, process.getOutputStream());
262         inThread.setDaemon(true);
263         errThread = new StreamRedirectThread("err.redirect", process.getErrorStream(), System.err);
264         inThread.start();
265         errThread.start();
266     }
267 
268     public static void main(String[] args) {
269         System.exit((new ProcessStarter()).run(args));
270     }
271 
272     private static String escapeWhiteSpace(String s) {
273         if (s.indexOf(' ') > 0) {
274             StringBuffer sb = new StringBuffer();
275             StringTokenizer st = new StringTokenizer(s, " ", true);
276             String current = null;
277             while (st.hasMoreTokens()) {
278                 current = st.nextToken();
279                 if (" ".equals(current)) {
280                     sb.append("// ");
281                 } else {
282                     sb.append(current);
283                 }
284             }
285             return sb.toString();
286         } else {
287             return s;
288         }
289     }
290 
291     /***
292      * Remove first and last " or ' if any
293      * 
294      * @param s string to handle
295      * @return s whitout first and last " or ' if any
296      */
297     public static String removeEmbracingQuotes(String s) {
298         if ((s.length() >= 2) && (s.charAt(0) == '"') && (s.charAt(s.length() - 1) == '"')) {
299             return s.substring(1, s.length() - 1);
300         } else if ((s.length() >= 2) && (s.charAt(0) == '\'') && (s.charAt(s.length() - 1) == '\'')) {
301             return s.substring(1, s.length() - 1);
302         } else {
303             return s;
304         }
305     }
306 
307     /***
308      * Analyse the args[] as a java command line
309      * 
310      * @param args
311      * @return String[] [0]:jvm options except -cp|-classpath, [1]:classpath without -cp, [2]: mainClass + mainOptions
312      */
313     public String[] parseJavaCommandLine(String[] args) {
314         StringBuffer optionsArgB = new StringBuffer();
315         StringBuffer cpOptionsArgB = new StringBuffer();
316         StringBuffer mainArgB = new StringBuffer();
317         String previous = null;
318         boolean foundMain = false;
319         for (int i = 0; i < args.length; i++) {
320             //System.out.println("" + i + " " + args[i]);
321             if (args[i].startsWith("-") && !foundMain) {
322                 if (!("-cp".equals(args[i])) && !("-classpath").equals(args[i])) {
323                     optionsArgB.append(args[i]).append(" ");
324                 }
325             } else if (!foundMain && ("-cp".equals(previous) || "-classpath".equals(previous))) {
326                 if (cpOptionsArgB.length() > 0) {
327                     cpOptionsArgB.append((System.getProperty("os.name", "").toLowerCase().indexOf("windows") >= 0)
328                         ? ";"
329                         : ":");
330                 }
331                 cpOptionsArgB.append(removeEmbracingQuotes(args[i]));
332             } else {
333                 foundMain = true;
334                 mainArgB.append(args[i]).append(" ");
335             }
336             previous = args[i];
337         }
338 
339         // restore quote around classpath or escape whitespace depending on win*/*nix
340         StringBuffer classPath = new StringBuffer();
341         if (System.getProperty("os.name", "").toLowerCase().indexOf("windows") >= 0) {
342             classPath = classPath.append("\"").append(cpOptionsArgB.toString()).append("\"");
343         } else {
344             classPath = classPath.append(escapeWhiteSpace(cpOptionsArgB.toString()));
345         }
346         String[] res = new String[] {
347             optionsArgB.toString(), classPath.toString(), mainArgB.toString()
348         };
349         return res;
350     }
351 }