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 (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         // 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
    157         // if java version does not support method "VirtualMachine.canRedefineClass"
    158         // or if bootclasspath is forced, transform optionsArg
    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             // attach stdout VM streams to this streams
    177             // this is needed early to support -verbose:class like options
    178             redirectStdoutStreams();
    179         } else {
    180             // lauch VM in suspend mode
    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             // attach stdout VM streams to this streams
    191             // this is needed early to support -verbose:class like options
    192             redirectStdoutStreams();
    193
    194             // override class loader in VM thru an attaching connector
    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         // attach VM other streams to this streams
    216         redirectOtherStreams();
    217
    218         // add a shutdown hook to "this" to shutdown VM
    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             //System.out.println("" + i + " " + args[i]);
    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         // restore quote around classpath or escape whitespace depending on win*/*nix
    344         StringBuffer classPath = new StringBuffer();
    345         if (System.getProperty("os.name", "").toLowerCase().indexOf("windows") >= 0) {
    346             classPath = classPath.append("\"").append(cpOptionsArgB.toString()).append("\"");
    347         } else {
    348             classPath = classPath.append(escapeWhiteSpace(cpOptionsArgB.toString()));
    349         }
    350         String[] res = new String[]{
    351             optionsArgB.toString(), classPath.toString(), mainArgB.toString()
    352         };
    353         return res;
    354     }
    355 }