001package co.codewizards.cloudstore.ls.server.cproc; 002 003import static co.codewizards.cloudstore.core.oio.OioFileFactory.*; 004import static co.codewizards.cloudstore.core.util.AssertUtil.*; 005import static co.codewizards.cloudstore.core.util.IOUtil.*; 006import static co.codewizards.cloudstore.core.util.Util.*; 007 008import java.io.IOException; 009import java.lang.ProcessBuilder.Redirect; 010import java.net.MalformedURLException; 011import java.net.Socket; 012import java.net.URISyntaxException; 013import java.net.URL; 014import java.text.DateFormat; 015import java.text.SimpleDateFormat; 016import java.util.ArrayList; 017import java.util.Date; 018import java.util.List; 019import java.util.Map; 020 021import org.slf4j.Logger; 022import org.slf4j.LoggerFactory; 023 024import co.codewizards.cloudstore.core.config.Config; 025import co.codewizards.cloudstore.core.io.TimeoutException; 026import co.codewizards.cloudstore.core.oio.File; 027import co.codewizards.cloudstore.ls.core.LocalServerPropertiesManager; 028import co.codewizards.cloudstore.ls.core.LsConfig; 029 030public class LocalServerProcessLauncher { 031 private static final Logger logger = LoggerFactory.getLogger(LocalServerProcessLauncher.class); 032 private static final String JAR_URL_PROTOCOL = "jar"; 033 private static final String JAR_URL_PREFIX = JAR_URL_PROTOCOL + ':'; 034 private static final String JAR_URL_CONTENT_PATH_SEPARATOR = "!/"; 035 private static final String FILE_PROTOCOL = "file"; 036 037 public LocalServerProcessLauncher() { 038 } 039 040 public boolean start() throws IOException { 041 // Check the configuration 'localServerProcess.enabled'. 042 if (! LsConfig.isLocalServerProcessEnabled()) 043 return false; 044 045 // Even though 'localServerProcess.enabled' is 'true', we also check for 'localServer.enabled'. 046 // If the 'localServer.enabled' is 'false', waitUntilServerOnline() fails anyway, because the 047 // LocalServer is not started inside the separate VM process. Hence, we don't launch the VM at all. 048 if (! LsConfig.isLocalServerEnabled()) 049 return false; 050 051 final File javaExecutableFile = getJavaExecutableFile(); 052 if (javaExecutableFile == null) 053 return false; 054 055 final File thisJarFile = getThisJarFile(); 056 if (thisJarFile == null) 057 return false; 058 059 final List<String> command = new ArrayList<>(); 060 command.add(javaExecutableFile.getPath()); 061 062 populateJvmArguments(command); 063 populateConfigSystemProperties(command); 064 065 command.add("-jar"); 066 command.add(thisJarFile.getPath()); 067 068 logger.info("start: command={}", command); 069 070 final ProcessBuilder pb = new ProcessBuilder(command); 071 072 final File processRedirectInputFile = getProcessRedirectInputFile(); 073 final File processRedirectOutputFile = getProcessRedirectOutputFile(); 074 processRedirectInputFile.createNewFile(); // 0-byte-file 075 076 pb.redirectInput(processRedirectInputFile.getIoFile()); 077 pb.redirectOutput(processRedirectOutputFile.getIoFile()); 078 pb.redirectError(processRedirectOutputFile.getIoFile()); 079 080 final Process process = pb.start(); 081 if (process == null) { 082 logger.warn("start: process=null"); 083 return false; 084 } 085 086 waitUntilServerOnline(); 087 return true; 088 } 089 090 private void populateJvmArguments(final List<String> command) { 091 String maxHeapSize = LsConfig.getLocalServerProcessMaxHeapSize(); 092 if (maxHeapSize != null) { 093 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?! 094 } 095 } 096 097 private void populateConfigSystemProperties(final List<String> command) { 098 for (final Map.Entry<Object, Object> me : System.getProperties().entrySet()) { 099 final String k = me.getKey().toString(); 100 final String v = me.getValue().toString(); 101 102 if (k.startsWith(Config.SYSTEM_PROPERTY_PREFIX)) { 103 final String arg = "-D" + k + "=" + v; 104 command.add(arg); 105 } 106 } 107 } 108 109 private void waitUntilServerOnline() { 110 final long startTimestamp = System.currentTimeMillis(); 111 while (true) { 112 final long timeoutMs = LsConfig.getLocalServerProcessStartTimeout(); 113 final boolean timeout = System.currentTimeMillis() - startTimestamp > timeoutMs; 114 115 LocalServerPropertiesManager.getInstance().clear(); 116 final String baseUrlString = LocalServerPropertiesManager.getInstance().getBaseUrl(); 117 if (baseUrlString != null) { 118 final URL baseUrl; 119 try { 120 baseUrl = new URL(baseUrlString); 121 } catch (MalformedURLException e) { 122 throw new RuntimeException(e); 123 } 124 125 int port = baseUrl.getPort(); 126 if (port < 0) 127 port = baseUrl.getDefaultPort(); 128 129 if (port < 0) 130 port = 443; 131 132 try { 133 Socket socket = new Socket(baseUrl.getHost(), port); 134 socket.close(); 135 logger.info("waitUntilServerOnline: Connecting to " + baseUrl + " succeeded!"); 136 return; 137 } catch (IOException e) { 138 if (timeout) 139 logger.error("waitUntilServerOnline: Connecting to " + baseUrl + " failed (fatal): " + e, e); 140 else 141 logger.warn("waitUntilServerOnline: Connecting to " + baseUrl + " failed (retrying): " + e); 142 } 143 } 144 145 if (timeout) 146 throw new TimeoutException("LocalServer did not come online within timeout!"); 147 148 try { Thread.sleep(500); } catch (InterruptedException e) { doNothing(); } 149 } 150 } 151 152 /** 153 * Gets the source file for system-in of the new process. 154 * <p> 155 * This file is created (with size 0) instead of the default behaviour {@link Redirect#PIPE PIPE}, 156 * because we don't want the child-process to be linked with the current process. 157 * 158 * @return the source file for system-in of the new process. Never <code>null</code>. 159 */ 160 private File getProcessRedirectInputFile() { 161 final File tempDir = getTempDir(); 162 final DateFormat df = new SimpleDateFormat("YYYY-MM-dd-HH-mm-ss"); 163 final String now = df.format(new Date()); 164 final File file = tempDir.createFile(String.format("LocalServer.%s.in", now)).getAbsoluteFile(); 165 logger.debug("getProcessRedirectInputFile: file='{}'", file); 166 return file; 167 } 168 169 /** 170 * Gets the destination file for system-out and system-error of the new process. 171 * @return the destination file for system-out and system-error of the new process. Never <code>null</code>. 172 */ 173 private File getProcessRedirectOutputFile() { 174 final File tempDir = getTempDir(); 175 final DateFormat df = new SimpleDateFormat("YYYY-MM-dd-HH-mm-ss"); 176 final String now = df.format(new Date()); 177 final File file = tempDir.createFile(String.format("LocalServer.%s.out", now)).getAbsoluteFile(); 178 logger.debug("getProcessRedirectOutputFile: file='{}'", file); 179 return file; 180 } 181 182 private File getJavaExecutableFile() { 183 final String javaHome = System.getProperty("java.home"); 184 assertNotNull(javaHome, "javaHome"); 185 186 File file = createFile(javaHome, "bin", "java").getAbsoluteFile(); 187 if (file.isFile()) { 188 logger.debug("getJavaExecutableFile: file='{}'", file); 189 return file; 190 } 191 192 file = createFile(javaHome, "bin", "java.exe").getAbsoluteFile(); 193 if (file.isFile()) { 194 logger.debug("getJavaExecutableFile: file='{}'", file); 195 return file; 196 } 197 198 logger.warn("getJavaExecutableFile: Could not locate 'java' executable!"); 199 return null; 200 } 201 202 /** 203 * Gets the JAR file containing this object's class. 204 * @return the JAR file containing this object's class. <code>null</code>, if this class is not contained in a JAR. 205 */ 206 private File getThisJarFile() { 207 // Should return an URL like this: 208 // jar:file:/home/mn/.../co.codewizards.cloudstore.ls.server.cproc-0.9.7-SNAPSHOT.jar!/co/codewizards/cloudstore/ls/server/cproc/ 209 final URL url = this.getClass().getResource(""); 210 assertNotNull(url, "url"); 211 212 final String urlString = url.toString(); 213 logger.debug("getThisJarFile: url='{}'", urlString); 214 215 if (! urlString.startsWith(JAR_URL_PREFIX)) { 216 logger.warn("getThisJarFile: This class ({}) is not located in a JAR file! url='{}'", 217 this.getClass().getName(), urlString); 218 219 return null; 220 } 221 222 final int indexOfContentPathSeparator = urlString.indexOf(JAR_URL_CONTENT_PATH_SEPARATOR); 223 if (indexOfContentPathSeparator < 0) 224 throw new IllegalStateException(String.format("JAR-URL '%s' does not contain separator '%s'!", 225 urlString, JAR_URL_CONTENT_PATH_SEPARATOR)); 226 227 final String jarUrlString = urlString.substring(JAR_URL_PREFIX.length(), indexOfContentPathSeparator); 228 logger.debug("getThisJarFile: url='{}'", urlString); 229 230 final URL jarUrl; 231 try { 232 jarUrl = new URL(jarUrlString); 233 } catch (MalformedURLException e) { 234 throw new RuntimeException(e); 235 } 236 237 if (! FILE_PROTOCOL.equals(jarUrl.getProtocol())) 238 throw new IllegalStateException(String.format("Illegal protocol ('%s' expected): %s", 239 FILE_PROTOCOL, jarUrlString)); 240 241 java.io.File f; 242 try { 243 f = new java.io.File(jarUrl.toURI()); 244 } catch (URISyntaxException e) { 245 throw new RuntimeException(e); 246 } 247 248 logger.debug("getThisJarFile: file='{}'", f); 249 return createFile(f); 250 } 251}