package co.codewizards.cloudstore.ls.server.cproc; import static co.codewizards.cloudstore.core.oio.OioFileFactory.*; import static co.codewizards.cloudstore.core.util.AssertUtil.*; import static co.codewizards.cloudstore.core.util.IOUtil.*; import static co.codewizards.cloudstore.core.util.Util.*; import java.io.IOException; import java.lang.ProcessBuilder.Redirect; import java.net.MalformedURLException; import java.net.Socket; import java.net.URISyntaxException; import java.net.URL; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import co.codewizards.cloudstore.core.config.Config; import co.codewizards.cloudstore.core.io.TimeoutException; import co.codewizards.cloudstore.core.oio.File; import co.codewizards.cloudstore.ls.core.LocalServerPropertiesManager; import co.codewizards.cloudstore.ls.core.LsConfig; public class LocalServerProcessLauncher { private static final Logger logger = LoggerFactory.getLogger(LocalServerProcessLauncher.class); private static final String JAR_URL_PROTOCOL = "jar"; private static final String JAR_URL_PREFIX = JAR_URL_PROTOCOL + ':'; private static final String JAR_URL_CONTENT_PATH_SEPARATOR = "!/"; private static final String FILE_PROTOCOL = "file"; public LocalServerProcessLauncher() { } public boolean start() throws IOException { // Check the configuration 'localServerProcess.enabled'. if (! LsConfig.isLocalServerProcessEnabled()) return false; // Even though 'localServerProcess.enabled' is 'true', we also check for 'localServer.enabled'. // If the 'localServer.enabled' is 'false', waitUntilServerOnline() fails anyway, because the // LocalServer is not started inside the separate VM process. Hence, we don't launch the VM at all. if (! LsConfig.isLocalServerEnabled()) return false; final File javaExecutableFile = getJavaExecutableFile(); if (javaExecutableFile == null) return false; final File thisJarFile = getThisJarFile(); if (thisJarFile == null) return false; final List<String> command = new ArrayList<>(); command.add(javaExecutableFile.getPath()); populateJvmArguments(command); populateConfigSystemProperties(command); command.add("-jar"); command.add(thisJarFile.getPath()); logger.info("start: command={}", command); final ProcessBuilder pb = new ProcessBuilder(command); final File processRedirectInputFile = getProcessRedirectInputFile(); final File processRedirectOutputFile = getProcessRedirectOutputFile(); processRedirectInputFile.createNewFile(); // 0-byte-file pb.redirectInput(processRedirectInputFile.getIoFile()); pb.redirectOutput(processRedirectOutputFile.getIoFile()); pb.redirectError(processRedirectOutputFile.getIoFile()); final Process process = pb.start(); if (process == null) { logger.warn("start: process=null"); return false; } waitUntilServerOnline(); return true; } private void populateJvmArguments(final List<String> command) { String maxHeapSize = LsConfig.getLocalServerProcessMaxHeapSize(); if (maxHeapSize != null) { command.add("-Xmx" + maxHeapSize); // Warning: This might not be supported by the JVM! The -X... options are not standard. But what should we do instead?! } } private void populateConfigSystemProperties(final List<String> command) { for (final Map.Entry<Object, Object> me : System.getProperties().entrySet()) { final String k = me.getKey().toString(); final String v = me.getValue().toString(); if (k.startsWith(Config.SYSTEM_PROPERTY_PREFIX)) { final String arg = "-D" + k + "=" + v; command.add(arg); } } } private void waitUntilServerOnline() { final long startTimestamp = System.currentTimeMillis(); while (true) { final long timeoutMs = LsConfig.getLocalServerProcessStartTimeout(); final boolean timeout = System.currentTimeMillis() - startTimestamp > timeoutMs; LocalServerPropertiesManager.getInstance().clear(); final String baseUrlString = LocalServerPropertiesManager.getInstance().getBaseUrl(); if (baseUrlString != null) { final URL baseUrl; try { baseUrl = new URL(baseUrlString); } catch (MalformedURLException e) { throw new RuntimeException(e); } int port = baseUrl.getPort(); if (port < 0) port = baseUrl.getDefaultPort(); if (port < 0) port = 443; try { Socket socket = new Socket(baseUrl.getHost(), port); socket.close(); logger.info("waitUntilServerOnline: Connecting to " + baseUrl + " succeeded!"); return; } catch (IOException e) { if (timeout) logger.error("waitUntilServerOnline: Connecting to " + baseUrl + " failed (fatal): " + e, e); else logger.warn("waitUntilServerOnline: Connecting to " + baseUrl + " failed (retrying): " + e); } } if (timeout) throw new TimeoutException("LocalServer did not come online within timeout!"); try { Thread.sleep(500); } catch (InterruptedException e) { doNothing(); } } } /** * Gets the source file for system-in of the new process. * <p> * This file is created (with size 0) instead of the default behaviour {@link Redirect#PIPE PIPE}, * because we don't want the child-process to be linked with the current process. * * @return the source file for system-in of the new process. Never <code>null</code>. */ private File getProcessRedirectInputFile() { final File tempDir = getTempDir(); final DateFormat df = new SimpleDateFormat("YYYY-MM-dd-HH-mm-ss"); final String now = df.format(new Date()); final File file = tempDir.createFile(String.format("LocalServer.%s.in", now)).getAbsoluteFile(); logger.debug("getProcessRedirectInputFile: file='{}'", file); return file; } /** * Gets the destination file for system-out and system-error of the new process. * @return the destination file for system-out and system-error of the new process. Never <code>null</code>. */ private File getProcessRedirectOutputFile() { final File tempDir = getTempDir(); final DateFormat df = new SimpleDateFormat("YYYY-MM-dd-HH-mm-ss"); final String now = df.format(new Date()); final File file = tempDir.createFile(String.format("LocalServer.%s.out", now)).getAbsoluteFile(); logger.debug("getProcessRedirectOutputFile: file='{}'", file); return file; } private File getJavaExecutableFile() { final String javaHome = System.getProperty("java.home"); assertNotNull(javaHome, "javaHome"); File file = createFile(javaHome, "bin", "java").getAbsoluteFile(); if (file.isFile()) { logger.debug("getJavaExecutableFile: file='{}'", file); return file; } file = createFile(javaHome, "bin", "java.exe").getAbsoluteFile(); if (file.isFile()) { logger.debug("getJavaExecutableFile: file='{}'", file); return file; } logger.warn("getJavaExecutableFile: Could not locate 'java' executable!"); return null; } /** * Gets the JAR file containing this object's class. * @return the JAR file containing this object's class. <code>null</code>, if this class is not contained in a JAR. */ private File getThisJarFile() { // Should return an URL like this: // jar:file:/home/mn/.../co.codewizards.cloudstore.ls.server.cproc-0.9.7-SNAPSHOT.jar!/co/codewizards/cloudstore/ls/server/cproc/ final URL url = this.getClass().getResource(""); assertNotNull(url, "url"); final String urlString = url.toString(); logger.debug("getThisJarFile: url='{}'", urlString); if (! urlString.startsWith(JAR_URL_PREFIX)) { logger.warn("getThisJarFile: This class ({}) is not located in a JAR file! url='{}'", this.getClass().getName(), urlString); return null; } final int indexOfContentPathSeparator = urlString.indexOf(JAR_URL_CONTENT_PATH_SEPARATOR); if (indexOfContentPathSeparator < 0) throw new IllegalStateException(String.format("JAR-URL '%s' does not contain separator '%s'!", urlString, JAR_URL_CONTENT_PATH_SEPARATOR)); final String jarUrlString = urlString.substring(JAR_URL_PREFIX.length(), indexOfContentPathSeparator); logger.debug("getThisJarFile: url='{}'", urlString); final URL jarUrl; try { jarUrl = new URL(jarUrlString); } catch (MalformedURLException e) { throw new RuntimeException(e); } if (! FILE_PROTOCOL.equals(jarUrl.getProtocol())) throw new IllegalStateException(String.format("Illegal protocol ('%s' expected): %s", FILE_PROTOCOL, jarUrlString)); java.io.File f; try { f = new java.io.File(jarUrl.toURI()); } catch (URISyntaxException e) { throw new RuntimeException(e); } logger.debug("getThisJarFile: file='{}'", f); return createFile(f); } }