/** * Helios, OpenSource Monitoring * Brought to you by the Helios Development Group * * Copyright 2007, Helios Development Group and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. * */ package org.helios.gmx.util.jvmcontrol; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.InputStreamReader; import java.lang.management.ManagementFactory; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.TimeUnit; import org.helios.gmx.util.ByteCodeNet; /** * <p>Title: JVMLauncher</p> * <p>Description: </p> * <p>Company: Helios Development Group LLC</p> * @author Whitehead (nwhitehead AT heliosdev DOT org) * <p><code>org.helios.gmx.util.jvmcontrol.JVMLauncher</code></p> */ public class JVMLauncher { /** The process builder */ private final ProcessBuilder processBuilder; /** The main class */ protected volatile String mainClass = null; /** The main class arguments */ protected volatile String[] mainArgs = null; /** The configured Debug option */ protected volatile String debugOption = null; /** The configured JavaAgent option */ protected final List<String> javaAgent = new ArrayList<String>(); /** The launched JVM timeout */ protected long timeout = -1; /** Indicates if a shutdown hook should be registered to kill the process when this VM shuts down */ protected boolean shutdownHook = false; /** The classpath */ protected final StringBuilder classpath = new StringBuilder(); /** The system properties */ protected final List<String> sysProps = new ArrayList<String>(); /** Indicates if this JVM is running in Windows */ public static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase().contains("windows"); /** This JVM's java executable */ public static final String THIS_JAVA_EXEC = System.getProperty("java.home") + File.separator + "bin" + File.separator + (IS_WINDOWS ? "javaw.exe" : "java"); /** The Debug option template */ public static String DEBUG_OPTION = "-Xrunjdwp:transport=dt_socket,address=%s,server=%s,suspend=%s"; /** The eol character for this platform */ public static final String EOL = System.getProperty("line.separator", "\n"); /** The temp directory for this environment */ public static final File TMP_DIR = new File(System.getProperty("java.io.tmpdir")); /** The runtime name */ public static final String JVM_NAME = ManagementFactory.getRuntimeMXBean().getName(); /** A set of strings that when found in this MBean's RuntimeMXBean InputArguments mean the JVM is in debug mode */ public static final Set<String> DEBUG_ARGS = Collections.unmodifiableSet(new HashSet<String>(Arrays.asList( "-Xdebug", "-Xrunjdwp", "-agentlib" ))); /** * Creates a new JVMLauncher * @return a new JVMLauncher */ public static JVMLauncher newJVMLauncher() { JVMLauncher launcher = new JVMLauncher(); return launcher; } /** * Enables the debug agent on the JVM * @param port The port to listen on * @param server Indicates if the debug agent should listen as a server * @param suspend Indicates if the JVM should suspend until a debugger connects * @return the JVM launcher */ public JVMLauncher debug(int port, boolean server, boolean suspend) { debugOption = String.format(DEBUG_OPTION, port, server ? "y" : "n", suspend ? "y" : "n"); return this; } /** * Sets up a super basic JMX listener using sysprops. * Remoting is enabled, authentication and ssl are disabled. * @param port The port to listen on * @return this launcher. */ public JVMLauncher basicPortJmx(int port) { Properties p = new Properties(); p.put("com.sun.management.jmxremote", ""); p.put("com.sun.management.jmxremote.authenticate", "false"); p.put("com.sun.management.jmxremote.ssl", "false"); p.put("com.sun.management.jmxremote.port", "" + port); appendSysProps(p); return this; } /** * Configures a shutdown hook to kill the launched process on this vm shutdown * @return this launcher */ public JVMLauncher shutdownHook() { this.shutdownHook = true; return this; } /** * Configures the JVMLauncher for a Java Agent * @param agentJar The location of the agent jar * @param agentOptions The agent options * @return the JVM Launcher */ public JVMLauncher javaAgent(File agentJar, String...agentOptions) { if(agentJar==null) throw new IllegalArgumentException("The passed agent jar was null", new Throwable()); if(!agentJar.exists()) { throw new IllegalArgumentException("The specified agent jar [" + agentJar + "] does not exist", new Throwable()); } StringBuilder b = new StringBuilder("-javaagent:"); b.append(agentJar.getAbsolutePath()); if(agentOptions!=null && agentOptions.length>0) { b.append(":"); boolean atLeastOne = false; for(String option: agentOptions) { b.append(option.trim()).append(","); atLeastOne = true; } if(atLeastOne) { b.deleteCharAt(b.length()-1); } } javaAgent.add(b.toString()); return this; } /** * Configures the JVMLauncher for a Java Agent * @param agentJar The location of the agent jar * @param agentOptions The agent options * @return the JVM Launcher */ public JVMLauncher javaAgent(String agentJar, String...agentOptions) { if(agentJar==null) throw new IllegalArgumentException("The passed agent jar was null", new Throwable()); return javaAgent(new File(agentJar), agentOptions); } /** * Appends classpaths to the JVM's command line options * @param paths The paths to append to the classpath * @return this JVMLauncher */ public JVMLauncher appendClassPath(String...paths) { if(paths!=null) { for(String path: paths) { if(path==null || path.trim().length()<1) continue; classpath.append(File.pathSeparator).append(path.trim()); } } return this; } /** * Appends system property declarations to the JVM's command line options * @param sysProps system property name and value pairs, <code>=</code> separated * @return this JVMLauncher */ public JVMLauncher appendSysProps(String...sysProps) { if(sysProps!=null) { for(String sysProp: sysProps) { if(sysProp==null || sysProp.trim().length()<1) continue; this.sysProps.add("-D" + sysProp.trim()); } } return this; } /** * Appends system property declarations to the JVM's command line options * @param sysProps Properties to be appended to the JVM's command line options as "-D" options * @return this JVMLauncher */ public JVMLauncher appendSysProps(Properties sysProps) { if(sysProps!=null) { for(Map.Entry<Object, Object> entry: sysProps.entrySet()) { this.sysProps.add("-D" + entry.getKey().toString() + "=" + entry.getValue().toString()); } } return this; } /** * Causes the JVM to exit after the configured timeout * @param timeout The timeout period in ms. * @return this launcher */ public JVMLauncher timeout(long timeout) { return timeout(timeout, TimeUnit.MILLISECONDS); } /** * Causes the JVM to exit after the configured timeout * @param timeout The timeout period * @param unit The unit of time * @return this launcher */ public JVMLauncher timeout(long timeout, TimeUnit unit) { if(unit==null) unit = TimeUnit.MILLISECONDS; this.timeout = TimeUnit.MILLISECONDS.convert(timeout, unit); return this; } /** * Starts the JVM and returns a {@link LaunchedJVMProcess} that wraps the process * @return a {@link LaunchedJVMProcess} that wraps the process */ public LaunchedJVMProcess start() { Process process = null; if(timeout==-1 && mainClass==null) { timeout(0); } try { if(debugOption!=null) { processBuilder.command().add("-Xdebug"); processBuilder.command().add(debugOption); } String mainClassDir = writeMainClass(); classpath.append(File.pathSeparator).append(mainClassDir); processBuilder.command().add("-cp"); processBuilder.command().add(classpath.toString()); if(!sysProps.isEmpty()) { for(String sysProp: sysProps) { processBuilder.command().add(sysProp); } } if(timeout!=-1) { processBuilder.command().add(MainTimeoutAndExit.class.getName()); processBuilder.command().add("" + timeout); } if(mainClass!=null) { processBuilder.command().add(mainClass); if(mainArgs!=null) { for(String arg: mainArgs) { if(arg==null) continue; processBuilder.command().add(arg); } } } System.out.println("Starting:\n" + processBuilder.command()); String pid = null; process = processBuilder.start(); try { pid = getPid(process); if(shutdownHook) { final Process finalProcess = process; Runtime.getRuntime().addShutdownHook(new Thread("Shutdown Hook for Launched JVM [" + pid + "]"){ public void run() { try { finalProcess.destroy(); } catch (Throwable e) {} } }); } } catch (Exception e) { String errMsg = getError(process); process.destroy(); throw new RuntimeException("Failed to start JVM [" + errMsg + "]", e); } return LaunchedJVMProcess.newInstance("" + pid, process, processBuilder.command()); } catch (Exception e) { if(process!=null) { process.destroy(); } throw new RuntimeException("Failed to start process " + processBuilder.command().toString(), e); } } /** * Returns the PID for the passed process * @param process The process * @return the PID in string form */ protected String getPid(Process process) { InputStreamReader isr = null; BufferedReader br = null; try { isr = new InputStreamReader(process.getInputStream()); br = new BufferedReader(isr); String line = null; while(true) { line = br.readLine(); if(line.startsWith("MainTimeoutAndExit PID:")) { line = line.split(":")[1]; break; } } return line; } catch (Exception e) { throw new RuntimeException("Failed to get PID from process", e); } finally { if(br!=null) try { br.close(); } catch (Exception e) {} if(isr!=null) try { isr.close(); } catch (Exception e) {} } } /** * Reads the process error stream and returns it as a string. * @param process The process to read the error stream from * @return the error stream content */ protected String getError(Process process) { InputStreamReader isr = null; BufferedReader br = null; StringBuilder b = new StringBuilder("JVM Start Error:"); try { isr = new InputStreamReader(process.getErrorStream()); br = new BufferedReader(isr); String line = null; while((line = br.readLine())!=null) { b.append("\n").append(line); } return b.toString(); } catch (Exception e) { throw new RuntimeException("Failed to read process error stream", e); } finally { if(br!=null) try { br.close(); } catch (Exception e) {} if(isr!=null) try { isr.close(); } catch (Exception e) {} } } /** * Creates a new JVMLauncher */ private JVMLauncher() { processBuilder = new ProcessBuilder(THIS_JAVA_EXEC); } /** * Appends a main class and arguments * @param mainClass The main class to run * @param mainArgs The command line arguments to the main class * @return this JVMLauncher */ public JVMLauncher appendMain(String mainClass, Object...mainArgs) { this.mainClass = mainClass; List<String> args = new ArrayList<String>(); if(mainArgs!=null) { for(Object arg: mainArgs) { if(arg!=null) { args.add(arg.toString()); } } } this.mainArgs = args.toArray(new String[args.size()]); return this; } /** * @param args */ public static void main(String[] args) { //LaunchedJVMProcess jvm = JVMLauncher.newJVMLauncher().timeout(100000).debug(1889, true, true).start(); LaunchedJVMProcess jvm = JVMLauncher.newJVMLauncher().timeout(100000).start(); try { Thread.sleep(3000); } catch (Exception e) {} System.out.println(jvm); try { jvm.waitFor(); } catch (Exception e) {} } /** * Writes out a temporary file with the MainTimeoutAndExit class * @return the name of the directory with the root of the classpath */ protected String writeMainClass() { try { File jvmDir = new File(TMP_DIR + File.separator + JVM_NAME); if(jvmDir.exists()) { jvmDir.delete(); } jvmDir.mkdir(); jvmDir.deleteOnExit(); writeClassMap(jvmDir, ByteCodeNet.getClassBytes(MainTimeoutAndExit.class)); return jvmDir.getAbsolutePath(); } catch (Exception e) { throw new RuntimeException("Failed to write main class", e); } } protected void writeClassMap(File dir, Map<String, byte[]> classBytes) { for(Map.Entry<String, byte[]> entry: classBytes.entrySet()) { FileOutputStream fos = null; String className = entry.getKey(); try { StringBuilder classDirName = new StringBuilder(dir.getAbsolutePath()); String[] pkgs = className.split("/"); if(pkgs.length>1) { for(int i = 0; i < pkgs.length-1; i++) { classDirName.append(File.separator).append(pkgs[i]); } } File classDir = new File(classDirName.toString()); classDir.mkdirs(); File classFile = new File(classDir.getAbsolutePath() + File.separator + pkgs[pkgs.length-1] + ".class"); fos = new FileOutputStream(classFile); fos.write(entry.getValue()); fos.flush(); fos.close(); fos = null; System.out.println("Wrote [" + entry.getValue().length + "] bytes for class [" + className + "]"); } catch (Exception e) { throw new RuntimeException("Failed to write class bytes for [" + className + "]", e); } finally { if(fos!=null) { try { fos.close(); } catch (Exception e) {} } } } } }