001package co.codewizards.cloudstore.ls.server.cproc; 002 003import static co.codewizards.cloudstore.core.oio.OioFileFactory.*; 004import static co.codewizards.cloudstore.core.util.IOUtil.*; 005import static co.codewizards.cloudstore.core.util.Util.*; 006import static java.util.Objects.*; 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 List<String> additionalVmArgs = LsConfig.getLocalServerProcessVmArgs(); 096 command.addAll(additionalVmArgs); 097 } 098 099 private void populateConfigSystemProperties(final List<String> command) { 100 for (final Map.Entry<Object, Object> me : System.getProperties().entrySet()) { 101 final String k = me.getKey().toString(); 102 final String v = me.getValue().toString(); 103 104 if (k.startsWith(Config.SYSTEM_PROPERTY_PREFIX)) { 105 final String arg = "-D" + k + "=" + v; 106 command.add(arg); 107 } 108 } 109 } 110 111 private void waitUntilServerOnline() { 112 final long startTimestamp = System.currentTimeMillis(); 113 while (true) { 114 final long timeoutMs = LsConfig.getLocalServerProcessStartTimeout(); 115 final boolean timeout = System.currentTimeMillis() - startTimestamp > timeoutMs; 116 117 LocalServerPropertiesManager.getInstance().clear(); 118 final String baseUrlString = LocalServerPropertiesManager.getInstance().getBaseUrl(); 119 if (baseUrlString != null) { 120 final URL baseUrl; 121 try { 122 baseUrl = new URL(baseUrlString); 123 } catch (MalformedURLException e) { 124 throw new RuntimeException(e); 125 } 126 127 int port = baseUrl.getPort(); 128 if (port < 0) 129 port = baseUrl.getDefaultPort(); 130 131 if (port < 0) 132 port = 443; 133 134 try { 135 Socket socket = new Socket(baseUrl.getHost(), port); 136 socket.close(); 137 logger.info("waitUntilServerOnline: Connecting to " + baseUrl + " succeeded!"); 138 return; 139 } catch (IOException e) { 140 if (timeout) 141 logger.error("waitUntilServerOnline: Connecting to " + baseUrl + " failed (fatal): " + e, e); 142 else 143 logger.warn("waitUntilServerOnline: Connecting to " + baseUrl + " failed (retrying): " + e); 144 } 145 } 146 147 if (timeout) 148 throw new TimeoutException("LocalServer did not come online within timeout!"); 149 150 try { Thread.sleep(500); } catch (InterruptedException e) { doNothing(); } 151 } 152 } 153 154 /** 155 * Gets the source file for system-in of the new process. 156 * <p> 157 * This file is created (with size 0) instead of the default behaviour {@link Redirect#PIPE PIPE}, 158 * because we don't want the child-process to be linked with the current process. 159 * 160 * @return the source file for system-in of the new process. Never <code>null</code>. 161 */ 162 private File getProcessRedirectInputFile() { 163 final File tempDir = getTempDir(); 164 final DateFormat df = new SimpleDateFormat("YYYY-MM-dd-HH-mm-ss"); 165 final String now = df.format(new Date()); 166 final File file = tempDir.createFile(String.format("LocalServer.%s.in", now)).getAbsoluteFile(); 167 logger.debug("getProcessRedirectInputFile: file='{}'", file); 168 return file; 169 } 170 171 /** 172 * Gets the destination file for system-out and system-error of the new process. 173 * @return the destination file for system-out and system-error of the new process. Never <code>null</code>. 174 */ 175 private File getProcessRedirectOutputFile() { 176 final File tempDir = getTempDir(); 177 final DateFormat df = new SimpleDateFormat("YYYY-MM-dd-HH-mm-ss"); 178 final String now = df.format(new Date()); 179 final File file = tempDir.createFile(String.format("LocalServer.%s.out", now)).getAbsoluteFile(); 180 logger.debug("getProcessRedirectOutputFile: file='{}'", file); 181 return file; 182 } 183 184 private File getJavaExecutableFile() { 185 final String javaHome = System.getProperty("java.home"); 186 requireNonNull(javaHome, "javaHome"); 187 188 File file = createFile(javaHome, "bin", "java").getAbsoluteFile(); 189 if (file.isFile()) { 190 logger.debug("getJavaExecutableFile: file='{}'", file); 191 return file; 192 } 193 194 file = createFile(javaHome, "bin", "java.exe").getAbsoluteFile(); 195 if (file.isFile()) { 196 logger.debug("getJavaExecutableFile: file='{}'", file); 197 return file; 198 } 199 200 logger.warn("getJavaExecutableFile: Could not locate 'java' executable!"); 201 return null; 202 } 203 204 /** 205 * Gets the JAR file containing this object's class. 206 * @return the JAR file containing this object's class. <code>null</code>, if this class is not contained in a JAR. 207 */ 208 private File getThisJarFile() { 209 // Should return an URL like this: 210 // jar:file:/home/mn/.../co.codewizards.cloudstore.ls.server.cproc-0.9.7-SNAPSHOT.jar!/co/codewizards/cloudstore/ls/server/cproc/ 211 final URL url = this.getClass().getResource(""); 212 requireNonNull(url, "url"); 213 214 final String urlString = url.toString(); 215 logger.debug("getThisJarFile: url='{}'", urlString); 216 217 if (! urlString.startsWith(JAR_URL_PREFIX)) { 218 logger.warn("getThisJarFile: This class ({}) is not located in a JAR file! url='{}'", 219 this.getClass().getName(), urlString); 220 221 return null; 222 } 223 224 final int indexOfContentPathSeparator = urlString.indexOf(JAR_URL_CONTENT_PATH_SEPARATOR); 225 if (indexOfContentPathSeparator < 0) 226 throw new IllegalStateException(String.format("JAR-URL '%s' does not contain separator '%s'!", 227 urlString, JAR_URL_CONTENT_PATH_SEPARATOR)); 228 229 final String jarUrlString = urlString.substring(JAR_URL_PREFIX.length(), indexOfContentPathSeparator); 230 logger.debug("getThisJarFile: url='{}'", urlString); 231 232 final URL jarUrl; 233 try { 234 jarUrl = new URL(jarUrlString); 235 } catch (MalformedURLException e) { 236 throw new RuntimeException(e); 237 } 238 239 if (! FILE_PROTOCOL.equals(jarUrl.getProtocol())) 240 throw new IllegalStateException(String.format("Illegal protocol ('%s' expected): %s", 241 FILE_PROTOCOL, jarUrlString)); 242 243 java.io.File f; 244 try { 245 f = new java.io.File(jarUrl.toURI()); 246 } catch (URISyntaxException e) { 247 throw new RuntimeException(e); 248 } 249 250 logger.debug("getThisJarFile: file='{}'", f); 251 return createFile(f); 252 } 253}