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 (asm, bcel, ...)
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 ASM
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 ASM implementation</h2>
76 * When using the default AspectWerkz layer 1 ASM 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
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
157
158
159 if (!hasCanRedefineClass() || (System.getProperty(CL_BOOTCLASSPATH_FORCE_PROPERTY) != null)) {
160 String bootDir = System.getProperty(CL_BOOTCLASSPATH_FORCE_PROPERTY, CL_BOOTCLASSPATH_FORCE_DEFAULT);
161 if (System.getProperty(CL_BOOTCLASSPATH_FORCE_PROPERTY) != null) {
162 System.out.println("HotSwap deactivated, using bootclasspath: " + bootDir);
163 } else {
164 System.out.println("HotSwap not supported by this java version, using bootclasspath: " + bootDir);
165 }
166 ClassLoaderPatcher.patchClassLoader(clp, bootDir);
167 BootClasspathStarter starter = new BootClasspathStarter(options, mainArgs, bootDir);
168 try {
169 process = starter.launchVM();
170 } catch (IOException e) {
171 System.err.println("failed to launch process :" + starter.getCommandLine());
172 e.printStackTrace();
173 return -1;
174 }
175
176
177
178 redirectStdoutStreams();
179 } else {
180
181 JDWPStarter starter = new JDWPStarter(options, mainArgs, "dt_socket", "9300");
182 try {
183 process = starter.launchVM();
184 } catch (IOException e) {
185 System.err.println("failed to launch process :" + starter.getCommandLine());
186 e.printStackTrace();
187 return -1;
188 }
189
190
191
192 redirectStdoutStreams();
193
194
195 int secondsToWait = 0;
196 try {
197 secondsToWait = Integer.parseInt(System.getProperty(CONNECTION_WAIT_PROPERTY, "0"));
198 } catch (NumberFormatException nfe) {
199 ;
200 }
201 VirtualMachine vm = ClassLoaderPatcher.hotswapClassLoader(
202 clp,
203 starter.getTransport(),
204 starter.getAddress(),
205 secondsToWait
206 );
207 if (vm == null) {
208 process.destroy();
209 } else {
210 vm.resume();
211 vm.dispose();
212 }
213 }
214
215
216 redirectOtherStreams();
217
218
219 Thread shutdownHook = new Thread() {
220 public void run() {
221 shutdown();
222 }
223 };
224 try {
225 Runtime.getRuntime().addShutdownHook(shutdownHook);
226 int exitCode = process.waitFor();
227 executeShutdownHook = false;
228 return exitCode;
229 } catch (Exception e) {
230 executeShutdownHook = false;
231 e.printStackTrace();
232 return -1;
233 }
234 }
235
236 /***
237 * shutdown target VM (used by shutdown hook of lauching VM)
238 */
239 private void shutdown() {
240 if (executeShutdownHook) {
241 process.destroy();
242 }
243 try {
244 outThread.join();
245 errThread.join();
246 } catch (InterruptedException e) {
247 ;
248 }
249 }
250
251 /***
252 * Set up stream redirection in target VM for stdout
253 */
254 private void redirectStdoutStreams() {
255 outThread = new StreamRedirectThread("out.redirect", process.getInputStream(), System.out);
256 outThread.start();
257 }
258
259 /***
260 * Set up stream redirection in target VM for stderr and stdin
261 */
262 private void redirectOtherStreams() {
263 inThread = new StreamRedirectThread("in.redirect", System.in, process.getOutputStream());
264 inThread.setDaemon(true);
265 errThread = new StreamRedirectThread("err.redirect", process.getErrorStream(), System.err);
266 inThread.start();
267 errThread.start();
268 }
269
270 public static void main(String[] args) {
271 System.exit((new ProcessStarter()).run(args));
272 }
273
274 private static String escapeWhiteSpace(String s) {
275 if (s.indexOf(' ') > 0) {
276 StringBuffer sb = new StringBuffer();
277 StringTokenizer st = new StringTokenizer(s, " ", true);
278 String current = null;
279 while (st.hasMoreTokens()) {
280 current = st.nextToken();
281 if (" ".equals(current)) {
282 sb.append("// ");
283 } else {
284 sb.append(current);
285 }
286 }
287 return sb.toString();
288 } else {
289 return s;
290 }
291 }
292
293 /***
294 * Remove first and last " or ' if any
295 *
296 * @param s string to handle
297 * @return s whitout first and last " or ' if any
298 */
299 public static String removeEmbracingQuotes(String s) {
300 if ((s.length() >= 2) && (s.charAt(0) == '"') && (s.charAt(s.length() - 1) == '"')) {
301 return s.substring(1, s.length() - 1);
302 } else if ((s.length() >= 2) && (s.charAt(0) == '\'') && (s.charAt(s.length() - 1) == '\'')) {
303 return s.substring(1, s.length() - 1);
304 } else {
305 return s;
306 }
307 }
308
309 /***
310 * Analyse the args[] as a java command line
311 *
312 * @param args
313 * @return String[] [0]:jvm options except -cp|-classpath, [1]:classpath without -cp, [2]: mainClass + mainOptions
314 */
315 public String[] parseJavaCommandLine(String[] args) {
316 StringBuffer optionsArgB = new StringBuffer();
317 StringBuffer cpOptionsArgB = new StringBuffer();
318 StringBuffer mainArgB = new StringBuffer();
319 String previous = null;
320 boolean foundMain = false;
321 for (int i = 0; i < args.length; i++) {
322
323 if (args[i].startsWith("-") && !foundMain) {
324 if (!("-cp".equals(args[i])) && !("-classpath").equals(args[i])) {
325 optionsArgB.append(args[i]).append(" ");
326 }
327 } else if (!foundMain && ("-cp".equals(previous) || "-classpath".equals(previous))) {
328 if (cpOptionsArgB.length() > 0) {
329 cpOptionsArgB.append(
330 (System.getProperty("os.name", "").toLowerCase().indexOf("windows") >= 0)
331 ? ";"
332 : ":"
333 );
334 }
335 cpOptionsArgB.append(removeEmbracingQuotes(args[i]));
336 } else {
337 foundMain = true;
338 mainArgB.append(args[i]).append(" ");
339 }
340 previous = args[i];
341 }
342
343
344
345
346
347
348
349
350
351
352
353
354
355