/* * Copyright 2007-2010 Sun Microsystems, Inc. * * This file is part of Project Darkstar Server. * * Project Darkstar Server is free software: you can redistribute it * and/or modify it under the terms of the GNU General Public License * version 2 as published by the Free Software Foundation and * distributed hereunder to you. * * Project Darkstar Server 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * -- */ package com.sun.sgs.system; import java.io.Closeable; import java.io.File; import java.io.OutputStream; import java.io.BufferedOutputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.FileNotFoundException; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; import java.util.List; import java.util.ArrayList; import java.util.jar.JarFile; import java.util.Scanner; /** * Bootstraps and launches a Project Darkstar server. */ public final class Boot { private static final Logger logger = Logger.getLogger(Boot.class.getName()); private static volatile Process sgsProcess = null; /** * This class should not be instantiated. */ private Boot() { } /** * Main-line method that bootstraps startup of a Project Darkstar server. * <p> * If a single argument is given on the command line, the value of * the argument is assumed to be a filename. This file is used to * specify a set of configuration properties required in order to locate * the required components to startup an application in a Project * Darkstar container. If no argument is given on the command line, * the filename is assumed to be at the location specified by the system * resource {@value BootEnvironment#SGS_BOOT}. * <p> * The properties included in the configuration file must conform to * the rules allowed by {@link SubstitutionProperties}. * * @param args optional filename of configuration file * @throws Exception if there is any problem booting up */ public static void main(String[] args) throws Exception { if (args.length > 1) { logger.log(Level.SEVERE, "Invalid number of arguments"); throw new IllegalArgumentException("Invalid number of arguments"); } //load properties from configuration file SubstitutionProperties properties = null; if (args.length == 0) { properties = BootEnvironment.loadProperties(null); } else { properties = BootEnvironment.loadProperties(args[0]); } //get the java executable and verify version String javaHome = properties.getProperty(BootEnvironment.JAVA_HOME); if (System.getProperty("java.home").equals(javaHome) && System.getProperty("java.version").startsWith("1.5.")) { logger.log(Level.SEVERE, "Project Darkstar Server requires Java 6 or higher"); throw new IllegalStateException( "Project Darkstar Server requires Java 6 or higher"); } String javaCmd = javaHome + File.separator + "bin" + File.separator + "java"; //build the command ExtJarGraph extGraph = new ExtJarGraph(); List<String> executeCmd = new ArrayList<String>(); executeCmd.add(javaCmd); executeCmd.add("-classpath"); executeCmd.add(bootClassPath(properties, extGraph)); executeCmd.add("-Djava.library.path=" + bootNativePath(properties)); executeCmd.add("-Djava.util.logging.config.class=" + BootEnvironment.DEFAULT_SGS_LOGGING_CLASS); executeCmd.add("-Djava.util.logging.config.file=" + properties.getProperty(BootEnvironment.SGS_LOGGING)); String extPropertiesFile = extGraph.getPropertiesFile(); if (extPropertiesFile != null) { executeCmd.add("-Dcom.sun.sgs.ext.properties=" + extPropertiesFile); } // command-line properties for JMX management executeCmd.add("-Dcom.sun.management.jmxremote.port=" + properties.getProperty(BootEnvironment.JMX_PORT)); if (!properties. getProperty(BootEnvironment.DISABLE_JMX_SECURITY).equals("false")) { executeCmd.add("-Dcom.sun.management.jmxremote.authenticate=false"); executeCmd.add("-Dcom.sun.management.jmxremote.ssl=false"); } for (String i : bootCommandLineProps(properties)) { executeCmd.add(i); } for (String j : bootJavaOpts(properties)) { executeCmd.add(j); } executeCmd.add(BootEnvironment.KERNEL_CLASS); executeCmd.add(properties.getProperty(BootEnvironment.SGS_PROPERTIES)); logger.log(Level.CONFIG, "Execute path = " + executeCmd); //build the process ProcessBuilder pb = new ProcessBuilder(executeCmd); pb.directory( new File(properties.getProperty(BootEnvironment.SGS_HOME))); pb.redirectErrorStream(true); //get the output stream OutputStream output = System.out; String logFile = properties.getProperty(BootEnvironment.SGS_OUTPUT); if (logFile != null) { try { //attempt to create any necessary parent directories //for the log file File log = new File(logFile); File parentDir = log.getParentFile(); if (parentDir != null && !parentDir.exists() && !parentDir.mkdirs()) { logger.log(Level.SEVERE, "Unable to create log directory : " + parentDir); throw new IOException("Unable to create log directory : " + parentDir); } //create a stream for the log file output = new BufferedOutputStream( new FileOutputStream(logFile)); logger.log(Level.INFO, "Redirecting log output to: " + logFile); } catch (FileNotFoundException e) { logger.log(Level.SEVERE, "Unable to open log file", e); throw e; } } // install a handler to cleanup when the process is killed directly Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { if (sgsProcess != null) { sgsProcess.destroy(); closeStream(sgsProcess.getOutputStream()); closeStream(sgsProcess.getInputStream()); closeStream(sgsProcess.getErrorStream()); } } private void closeStream(Closeable c) { try { c.close(); } catch (IOException ioe) { } } }); //run the process try { sgsProcess = pb.start(); new Thread(new StreamPipe(sgsProcess.getInputStream(), output)).start(); sgsProcess.waitFor(); } catch (IOException e) { logger.log(Level.SEVERE, "Unable to start process", e); throw e; } catch (InterruptedException i) { logger.log(Level.WARNING, "Thread interrupted", i); } finally { if (sgsProcess != null) { sgsProcess.destroy(); } output.close(); } } /** * Constructs a classpath to be used when running the Project Darkstar * kernel. The classpath consists of any jar files that live directly * in the {@code $SGS_HOME/lib} * directory. It also recursively includes jar files from the * {@code $SGS_DEPLOY} and {@code SGS_EXT} directories. <p> * * Additionally, files included in the path from the {@code $SGS_HOME/lib} * directory are filtered based on the value of {@code $BDB_TYPE}. * <ul> * <li>If the value of {@code $BDB_TYPE} is equal to {@code db}, any jar * files in {@code $SGS_HOME/lib} that begin with "je-" are excluded * from the path.</li> * <li>If the value of {@code $BDB_TYPE} is equal to {@code je}, any jar * files in {@code $SGS_HOME/lib} that begin with "db-" are excluded * from the path.</li> * <li>If the value of {@code $BDB_TYPE} is equal to anything else, any jar * files in {@code SGS_HOME/lib} that being with "db-" OR "je-" are * excluded from the path.</li> * </ul> * * @param env environment with SGS_HOME set * @param extGraph collection for the extension jar files * @return classpath to use to run the kernel */ private static String bootClassPath(Properties env, ExtJarGraph extGraph) { //determine SGS_HOME String sgsHome = env.getProperty(BootEnvironment.SGS_HOME); if (sgsHome == null) { return ""; } //locate SGS_HOME directory File sgsHomeDir = new File(sgsHome); if (!sgsHomeDir.isDirectory()) { return ""; } StringBuilder buf = new StringBuilder(); //determine BDB_TYPE String bdbType = env.getProperty(BootEnvironment.BDB_TYPE); String filter = "^(db-|je-).*"; if ("db".equals(bdbType)) { filter = "^(je-).*"; } else if ("je".equals(bdbType)) { filter = "^(db-).*"; } //add jars from SGS_HOME/lib, excluding the filtered bdb jar(s) File sgsLibDir = new File(sgsHome + File.separator + "lib"); for (File sgsJar : sgsLibDir.listFiles()) { if (sgsJar.isFile() && sgsJar.getName().endsWith(".jar") && !sgsJar.getName().matches(filter)) { if (buf.length() != 0) { buf.append(File.pathSeparator + sgsJar.getAbsolutePath()); } else { buf.append(sgsJar.getAbsolutePath()); } } } //recursively add jars from SGS_DEPLOY File sgsDeployDir = new File( env.getProperty(BootEnvironment.SGS_DEPLOY)); List<File> jars = new ArrayList<File>(); int appPropsFound = appJars(sgsDeployDir, jars); if (appPropsFound == 0) { logger.log(Level.WARNING, "No application jar found with a " + BootEnvironment.DEFAULT_APP_PROPERTIES + " configuration file in the " + sgsDeployDir + " directory"); } if (appPropsFound > 1) { logger.log(Level.SEVERE, "Multiple application jars " + "found with a " + BootEnvironment.DEFAULT_APP_PROPERTIES + " configuration file in the " + sgsDeployDir + " directory"); throw new IllegalStateException("Multiple application jars " + "found with a " + BootEnvironment.DEFAULT_APP_PROPERTIES + " configuration file in the " + sgsDeployDir + " directory"); } for (File jar : jars) { if (buf.length() != 0) { buf.append(File.pathSeparator + jar.getAbsolutePath()); } else { buf.append(jar.getAbsolutePath()); } } //recursively add jars from SGS_EXT File sgsExtDir = new File(env.getProperty(BootEnvironment.SGS_EXT)); List<String> extJarNames = new ArrayList<String>(); extJars(sgsExtDir, extJarNames, extGraph); for (String jarName : extJarNames) { if (buf.length() != 0) { buf.append(File.pathSeparator + jarName); } else { buf.append(jarName); } } //include the additional classpath if specified String addPath = env.getProperty(BootEnvironment.CUSTOM_CLASSPATH_ADD); if (addPath != null) { if (buf.length() != 0) { buf.append(File.pathSeparator + addPath); } else { buf.append(addPath); } } return buf.toString(); } /** * Constructs a path to be used as the {@code java.library.path} * when running the Project Darkstar kernel. The path combines the string * specified by the {@code $BDB_NATIVES } property with the string specified * by the {@code $CUSTOM_NATIVES} property. Additionally, if the * {@code BDB_TYPE} property is not set to {@code db}, only * the {@code $CUSTOM_NATIVES} property is used for the path. * * @param env the environment * @return path to use as the {@code java.library.path} in the kernel */ private static String bootNativePath(Properties env) { String type = env.getProperty(BootEnvironment.BDB_TYPE); String bdb = env.getProperty(BootEnvironment.BDB_NATIVES); String custom = env.getProperty(BootEnvironment.CUSTOM_NATIVES); StringBuilder buf = new StringBuilder(); if (type.equals("db")) { buf.append(bdb); if (custom != null && !custom.equals("")) { buf.append(File.pathSeparator + custom); } } else { if (custom != null && !custom.equals("")) { buf.append(custom); } } return buf.toString(); } /** * Constructs a set of additional command line properties that are to * be used when running the Project Darkstar kernel. Specifically, this * method specifies a value for the property * {@code com.sun.sgs.impl.service.data.store.db.environment.class} in * order to specify the bdb flavor that is being used. It is dependent * on the value of the {@code $BDB_TYPE} environment property. * * <ul> * <li>If the value of {@code $BDB_TYPE} is equal to {@code db}, then * {@code com.sun.sgs.impl.service.data.store.db.bdb.BdbEnvironment} is * used.</li> * <li>If the value of {@code $BDB_TYPE} is equal to {@code je}, then * {@code com.sun.sgs.impl.service.data.store.db.je.JeEnvironment} is * used.</li> * <li>If the value of {@code BDB_TYPE} is equal to anything else, no * value is specified.</li> * </ul> * * @param env the environment * @return additional set of properties to be passed to the command line */ private static List<String> bootCommandLineProps(Properties env) { List<String> props = new ArrayList<String>(); String type = env.getProperty(BootEnvironment.BDB_TYPE); String line = "-Dcom.sun.sgs.impl.service.data.store.db.environment.class"; if (type.equals("db")) { props.add(line + "=" + "com.sun.sgs.impl.service.data.store.db.bdb." + "BdbEnvironment"); } else if (type.equals("je")) { props.add(line + "=" + "com.sun.sgs.impl.service.data.store.db.je." + "JeEnvironment"); } return props; } /** * Splits the {@code $JAVA_OPTS} configuration property into a list * of {@code String} objects consumable by a {@link ProcessBuilder}. * <p> * The split operation will break down the property specified by * {@code $JAVA_OPTS} into tokens delimited by whitespace. Additionally, * quoted strings that include whitespace will be treated as a single token. * * @param env the environment * @return a list of {@code String} objects that represent the individual * components of the {@code JAVA_OPTS} configuration property * @throws IllegalArgumentException if the {@code JAVA_OPTS} configuration * property has an invalid format */ private static List<String> bootJavaOpts(Properties env) { String javaOpts = env.getProperty(BootEnvironment.JAVA_OPTS, ""); Scanner s = new Scanner(javaOpts); List<String> realTokens = new ArrayList<String>(); while (s.hasNext()) { if (s.hasNext("\\\".*")) { String nextToken = s.findInLine("\\\".*?\\\""); if (nextToken == null) { throw new IllegalArgumentException( "Invalid " + BootEnvironment.JAVA_OPTS + " format"); } else { realTokens.add( nextToken.substring(1, nextToken.length() - 1)); } } else { realTokens.add(s.next()); } } return realTokens; } /** * Helper method that recursively searches the given directory and adds * any jar files found to the jars list. * * @param directory directory to search for jar files * @param jars list of Files to add any jar files found * @return the number of jar files found that have a * {@link BootEnvironment.DEFAULT_APP_PROPERTIES} file in them */ private static int appJars(File directory, List<File> jars) { int appPropsFound = 0; if (directory.isDirectory() && directory.canRead()) { for (File f : directory.listFiles()) { if (f.isFile() && f.getName().endsWith(".jar")) { try { JarFile jar = new JarFile(f); jars.add(f); if (jar.getJarEntry( BootEnvironment.DEFAULT_APP_PROPERTIES) != null) { appPropsFound++; } } catch (IOException e) { //not a jar file, log and ignore logger.log(Level.WARNING, "File " + f.getAbsolutePath() + " is not a jar file"); } } else if (f.isDirectory()) { appPropsFound += appJars(f, jars); } } } return appPropsFound; } /** * Helper method that recursively searches the given directory for * extension jar files, adding them to a list and separate graph of * discovered extensions. * * @param directory the directory to search for jar files * @param jarFileNames list of file names for the discovered, valid jars * @param graph a collection of discovered extension jar files */ private static void extJars(File directory, List<String> jarFileNames, ExtJarGraph graph) { if (directory.isDirectory() && directory.canRead()) { for (File f : directory.listFiles()) { if (f.isFile() && f.getName().endsWith(".jar")) { try { graph.addJarFile(new JarFile(f)); jarFileNames.add(f.getAbsolutePath()); } catch (IOException e) { //not a jar file, log and ignore logger.log(Level.WARNING, "Extension file " + f.getAbsolutePath() + " is not a jar file"); } } else if (f.isDirectory()) { extJars(f, jarFileNames, graph); } } } } }