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}