001: /**************************************************************************************
002: * Copyright (c) Jonas BonŽr, Alexandre Vasseur. All rights reserved. *
003: * http://aspectwerkz.codehaus.org *
004: * ---------------------------------------------------------------------------------- *
005: * The software in this package is published under the terms of the LGPL license *
006: * a copy of which has been included with this distribution in the license.txt file. *
007: **************************************************************************************/package org.codehaus.aspectwerkz.hook;
008:
009: import com.sun.jdi.VirtualMachine;
010:
011: import java.io.File;
012: import java.io.IOException;
013: import java.util.StringTokenizer;
014:
015: /**
016: * ProcessStarter uses JPDA JDI api to start a VM with a runtime modified java.lang.ClassLoader, or transparently use a
017: * Xbootclasspath style (java 1.3 detected or forced) <p/>
018: * <p/>
019: * <h2>Important note</h2>
020: * Due to a JPDA issue in LauchingConnector, this implementation is based on Process forking. If Xbootclasspath is not
021: * used the target VM is started with JDWP options <i>transport=dt_socket,address=9300 </i> unless other specified.
022: * <br/>It is possible after the short startup sequence to attach a debugger or any other JPDA attaching connector. It
023: * has been validated against a WebLogic 7 startup and is the <i>must use </i> implementation.
024: * </p>
025: * <p/>
026: * <p/>
027: * <h2>Implementation Note</h2>
028: * See http://java.sun.com/products/jpda/ <br/>See http://java.sun.com/j2se/1.4.1/docs/guide/jpda/jdi/index.html <br/>
029: * </p>
030: * <p/><p/>For java 1.3, it launch the target VM using a modified java.lang.ClassLoader by generating it and putting it
031: * in the bootstrap classpath of the target VM. The java 1.3 version should only be run for experimentation since it
032: * breaks the Java 2 Runtime Environment binary code license by overriding a class of rt.jar
033: * </p>
034: * <p/><p/>For java 1.4, it hotswaps java.lang.ClassLoader with a runtime patched version, wich is compatible with the
035: * Java 2 Runtime Environment binary code license. For JVM not supporting the class hotswapping, the same mechanism as
036: * for java 1.3 is used.
037: * </p>
038: * <p/>
039: * <p/>
040: * <h2>Usage</h2>
041: * Use it as a replacement of "java" :<br/><code>java [target jvm option] [target classpath]
042: * targetMainClass [targetMainClass args]</code>
043: * <br/>should be called like: <br/><code>java [jvm option] [classpath]
044: * org.codehaus.aspectwerkz.hook.ProcessStarter [target jvm option] [target classpath] targetMainClass [targetMainClass
045: * args]</code>
046: * <br/><b>[classpath] must contain %JAVA_HOME%/tools.jar for HotSwap support </b> <br/>[target jvm option] can contain
047: * JDWP options, transport and address are preserved if specified.
048: * </p>
049: * <p/>
050: * <p/>
051: * <h2>Options</h2>
052: * [classpath] must contain %JAVA_HOME%/tools.jar and the jar you want for bytecode modification (asm, bcel, ...)
053: * <br/>The java.lang.ClassLoader is patched using the <code>-Daspectwerkz.classloader.clpreprocessor=...</code> in
054: * [jvm option]. Specify the FQN of your implementation of hook.ClassLoaderPreProcessor. See {@link
055: * org.codehaus.aspectwerkz.hook.ClassLoaderPreProcessor} If not given, the default AspectWerkz layer 1 ASM
056: * implementation hook.impl.* is used, which is equivalent to
057: * <code>-Daspectwerkz.classloader.clpreprocessor=org.codehaus.aspectwerkz.hook.impl.ClassLoaderPreProcessorImpl</code>
058: * <br/>Use -Daspectwerkz.classloader.wait=2 in [jvm option] to force a pause of 2 seconds between process fork and JPDA
059: * connection for HotSwap. Defaults to no wait.
060: * </p>
061: * <p/>
062: * <p/>
063: * <h2>Disabling HotSwap</h2>
064: * You disable HotSwap and thus force the use of -Xbootclasspath (like in java 1.3 mode) and specify the directory where
065: * the modified class loader bytecode will be stored using in [jvm option]
066: * <code>-Daspectwerkz.classloader.clbootclasspath=...</code>. Specify the directory where you want the patched
067: * java.lang.ClassLoader to be stored. Default is "./_boot". The directory is created if needed (with the subdirectories
068: * corresponding to package names). <br/>The directory is <b>automatically </b> incorporated in the -Xbootclasspath
069: * option of [target jvm option]. <br/>You shoud use this option mainly for debuging purpose, or if you need to start
070: * different jvm with different classloader preprocessor implementations.
071: * </p>
072: * <p/>
073: * <p/>
074: * <h2>Option for AspectWerkz layer 1 ASM implementation</h2>
075: * When using the default AspectWerkz layer 1 ASM implementation
076: * <code>org.codehaus.aspectwerkz.hook.impl.ClassLoaderPreProcessorImpl</code>, java.lang.ClassLoader is modified to
077: * call a class preprocessor at each class load (except for class loaded by the bootstrap classloader). <br/>The
078: * effective class preprocessor is defined with <code>-Daspectwerkz.classloader.preprocessor=...</code> in [target jvm
079: * option]. Specify the FQN of your implementation of org.codehaus.aspectwerkz.hook.ClassPreProcessor interface. <br/>If
080: * this parameter is not given, the default AspectWerkz layer 2
081: * org.codehaus.aspectwerkz.transform.AspectWerkzPreProcessor is used. <br/>
082: * </p>
083: *
084: * @author <a href="mailto:alex@gnilux.com">Alexandre Vasseur </a>
085: */
086: public class ProcessStarter {
087: /**
088: * option for classloader preprocessor target
089: */
090: final static String CL_PRE_PROCESSOR_CLASSNAME_PROPERTY = "aspectwerkz.classloader.clpreprocessor";
091:
092: /**
093: * default dir when -Xbootclasspath is forced or used (java 1.3)
094: */
095: private final static String CL_BOOTCLASSPATH_FORCE_DEFAULT = "."
096: + File.separatorChar + "_boot";
097:
098: /**
099: * 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",
139: new Class[] {});
140: } catch (NoSuchMethodException e) {
141: return false;
142: }
143: return true;
144: }
145:
146: private int run(String[] args) {
147: // retrieve options and main
148: String[] javaArgs = parseJavaCommandLine(args);
149: String optionArgs = javaArgs[0];
150: String cpArgs = javaArgs[1];
151: String mainArgs = javaArgs[2];
152: String options = optionArgs + " -cp " + cpArgs;
153: String clp = System
154: .getProperty(CL_PRE_PROCESSOR_CLASSNAME_PROPERTY,
155: "org.codehaus.aspectwerkz.hook.impl.ClassLoaderPreProcessorImpl");
156:
157: // if java version does not support method "VirtualMachine.canRedefineClass"
158: // or if bootclasspath is forced, transform optionsArg
159: if (!hasCanRedefineClass()
160: || (System.getProperty(CL_BOOTCLASSPATH_FORCE_PROPERTY) != null)) {
161: String bootDir = System.getProperty(
162: CL_BOOTCLASSPATH_FORCE_PROPERTY,
163: CL_BOOTCLASSPATH_FORCE_DEFAULT);
164: if (System.getProperty(CL_BOOTCLASSPATH_FORCE_PROPERTY) != null) {
165: System.out
166: .println("HotSwap deactivated, using bootclasspath: "
167: + bootDir);
168: } else {
169: System.out
170: .println("HotSwap not supported by this java version, using bootclasspath: "
171: + bootDir);
172: }
173: ClassLoaderPatcher.patchClassLoader(clp, bootDir);
174: BootClasspathStarter starter = new BootClasspathStarter(
175: options, mainArgs, bootDir);
176: try {
177: process = starter.launchVM();
178: } catch (IOException e) {
179: System.err.println("failed to launch process :"
180: + starter.getCommandLine());
181: e.printStackTrace();
182: return -1;
183: }
184:
185: // attach stdout VM streams to this streams
186: // this is needed early to support -verbose:class like options
187: redirectStdoutStreams();
188: } else {
189: // lauch VM in suspend mode
190: JDWPStarter starter = new JDWPStarter(options, mainArgs,
191: "dt_socket", "9300");
192: try {
193: process = starter.launchVM();
194: } catch (IOException e) {
195: System.err.println("failed to launch process :"
196: + starter.getCommandLine());
197: e.printStackTrace();
198: return -1;
199: }
200:
201: // attach stdout VM streams to this streams
202: // this is needed early to support -verbose:class like options
203: redirectStdoutStreams();
204:
205: // override class loader in VM thru an attaching connector
206: int secondsToWait = 0;
207: try {
208: secondsToWait = Integer.parseInt(System.getProperty(
209: CONNECTION_WAIT_PROPERTY, "0"));
210: } catch (NumberFormatException nfe) {
211: ;
212: }
213: VirtualMachine vm = ClassLoaderPatcher.hotswapClassLoader(
214: clp, starter.getTransport(), starter.getAddress(),
215: secondsToWait);
216: if (vm == null) {
217: process.destroy();
218: } else {
219: vm.resume();
220: vm.dispose();
221: }
222: }
223:
224: // attach VM other streams to this streams
225: redirectOtherStreams();
226:
227: // add a shutdown hook to "this" to shutdown VM
228: Thread shutdownHook = new Thread() {
229: public void run() {
230: shutdown();
231: }
232: };
233: try {
234: Runtime.getRuntime().addShutdownHook(shutdownHook);
235: int exitCode = process.waitFor();
236: executeShutdownHook = false;
237: return exitCode;
238: } catch (Exception e) {
239: executeShutdownHook = false;
240: e.printStackTrace();
241: return -1;
242: }
243: }
244:
245: /**
246: * shutdown target VM (used by shutdown hook of lauching VM)
247: */
248: private void shutdown() {
249: if (executeShutdownHook) {
250: process.destroy();
251: }
252: try {
253: outThread.join();
254: errThread.join();
255: } catch (InterruptedException e) {
256: ;
257: }
258: }
259:
260: /**
261: * Set up stream redirection in target VM for stdout
262: */
263: private void redirectStdoutStreams() {
264: outThread = new StreamRedirectThread("out.redirect", process
265: .getInputStream(), System.out);
266: outThread.start();
267: }
268:
269: /**
270: * Set up stream redirection in target VM for stderr and stdin
271: */
272: private void redirectOtherStreams() {
273: inThread = new StreamRedirectThread("in.redirect", System.in,
274: process.getOutputStream());
275: inThread.setDaemon(true);
276: errThread = new StreamRedirectThread("err.redirect", process
277: .getErrorStream(), System.err);
278: inThread.start();
279: errThread.start();
280: }
281:
282: public static void main(String[] args) {
283: System.exit((new ProcessStarter()).run(args));
284: }
285:
286: private static String escapeWhiteSpace(String s) {
287: if (s.indexOf(' ') > 0) {
288: StringBuffer sb = new StringBuffer();
289: StringTokenizer st = new StringTokenizer(s, " ", true);
290: String current = null;
291: while (st.hasMoreTokens()) {
292: current = st.nextToken();
293: if (" ".equals(current)) {
294: sb.append("\\ ");
295: } else {
296: sb.append(current);
297: }
298: }
299: return sb.toString();
300: } else {
301: return s;
302: }
303: }
304:
305: /**
306: * Remove first and last " or ' if any
307: *
308: * @param s string to handle
309: * @return s whitout first and last " or ' if any
310: */
311: public static String removeEmbracingQuotes(String s) {
312: if ((s.length() >= 2) && (s.charAt(0) == '"')
313: && (s.charAt(s.length() - 1) == '"')) {
314: return s.substring(1, s.length() - 1);
315: } else if ((s.length() >= 2) && (s.charAt(0) == '\'')
316: && (s.charAt(s.length() - 1) == '\'')) {
317: return s.substring(1, s.length() - 1);
318: } else {
319: return s;
320: }
321: }
322:
323: /**
324: * Analyse the args[] as a java command line
325: *
326: * @param args
327: * @return String[] [0]:jvm options except -cp|-classpath, [1]:classpath without -cp, [2]: mainClass + mainOptions
328: */
329: public String[] parseJavaCommandLine(String[] args) {
330: StringBuffer optionsArgB = new StringBuffer();
331: StringBuffer cpOptionsArgB = new StringBuffer();
332: StringBuffer mainArgB = new StringBuffer();
333: String previous = null;
334: boolean foundMain = false;
335: for (int i = 0; i < args.length; i++) {
336: //System.out.println("" + i + " " + args[i]);
337: if (args[i].startsWith("-") && !foundMain) {
338: if (!("-cp".equals(args[i]))
339: && !("-classpath").equals(args[i])) {
340: optionsArgB.append(args[i]).append(" ");
341: }
342: } else if (!foundMain
343: && ("-cp".equals(previous) || "-classpath"
344: .equals(previous))) {
345: if (cpOptionsArgB.length() > 0) {
346: cpOptionsArgB
347: .append((System.getProperty("os.name", "")
348: .toLowerCase().indexOf("windows") >= 0) ? ";"
349: : ":");
350: }
351: cpOptionsArgB.append(removeEmbracingQuotes(args[i]));
352: } else {
353: foundMain = true;
354: mainArgB.append(args[i]).append(" ");
355: }
356: previous = args[i];
357: }
358:
359: // restore quote around classpath or escape whitespace depending on win*/*nix
360: StringBuffer classPath = new StringBuffer();
361: if (System.getProperty("os.name", "").toLowerCase().indexOf(
362: "windows") >= 0) {
363: classPath = classPath.append("\"").append(
364: cpOptionsArgB.toString()).append("\"");
365: } else {
366: classPath = classPath.append(escapeWhiteSpace(cpOptionsArgB
367: .toString()));
368: }
369: String[] res = new String[] { optionsArgB.toString(),
370: classPath.toString(), mainArgB.toString() };
371: return res;
372: }
373: }
|