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}