001package co.codewizards.cloudstore.ls.server;
002
003import static co.codewizards.cloudstore.core.oio.OioFileFactory.*;
004import static co.codewizards.cloudstore.core.util.DebugUtil.*;
005import static java.util.Objects.*;
006
007import java.io.IOException;
008import java.util.HashMap;
009import java.util.Map;
010import java.util.Timer;
011import java.util.TimerTask;
012
013import org.eclipse.jetty.server.Connector;
014import org.eclipse.jetty.server.HttpConfiguration;
015import org.eclipse.jetty.server.HttpConnectionFactory;
016import org.eclipse.jetty.server.Server;
017import org.eclipse.jetty.server.ServerConnector;
018import org.eclipse.jetty.servlet.ServletContextHandler;
019import org.eclipse.jetty.servlet.ServletHolder;
020import org.eclipse.jetty.util.component.AbstractLifeCycle;
021import org.eclipse.jetty.util.component.LifeCycle;
022import org.eclipse.jetty.util.thread.QueuedThreadPool;
023import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
024import org.glassfish.jersey.server.ResourceConfig;
025import org.glassfish.jersey.servlet.ServletContainer;
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028
029import co.codewizards.cloudstore.core.auth.BouncyCastleRegistrationUtil;
030import co.codewizards.cloudstore.core.config.ConfigDir;
031import co.codewizards.cloudstore.core.config.ConfigImpl;
032import co.codewizards.cloudstore.core.io.LockFile;
033import co.codewizards.cloudstore.core.io.LockFileFactory;
034import co.codewizards.cloudstore.core.io.TimeoutException;
035import co.codewizards.cloudstore.core.oio.File;
036import co.codewizards.cloudstore.ls.core.LocalServerPropertiesManager;
037import co.codewizards.cloudstore.ls.core.LsConfig;
038import co.codewizards.cloudstore.ls.rest.server.LocalServerRest;
039import co.codewizards.cloudstore.ls.rest.server.auth.AuthManager;
040
041public class LocalServer {
042        public static final String CONFIG_KEY_PORT = "localServer.port";
043        private static final int RANDOM_PORT = 0;
044        private static final int DEFAULT_PORT = RANDOM_PORT;
045
046        private static final Logger logger = LoggerFactory.getLogger(LocalServer.class);
047
048        private Server server;
049        private int port = -1;
050
051        private File localServerRunningFile;
052        private LockFile localServerRunningLockFile;
053        private boolean localServerStopFileEnabled;
054        private File localServerStopFile;
055        private final Timer localServerStopFileTimer = new Timer("localServerStopFileTimer", true);
056        private TimerTask localServerStopFileTimerTask;
057
058        private static final Map<String, LocalServer> localServerRunningFile2LocalServer_running = new HashMap<>();
059
060        public LocalServer() {
061                BouncyCastleRegistrationUtil.registerBouncyCastleIfNeeded();
062        }
063
064        public File getLocalServerRunningFile() {
065                if (localServerRunningFile == null) {
066                        try {
067                                localServerRunningFile = createFile(ConfigDir.getInstance().getFile(), "localServerRunning.lock").getCanonicalFile();
068                        } catch (IOException e) {
069                                throw new RuntimeException(e);
070                        }
071                }
072                return localServerRunningFile;
073        }
074
075        public File getLocalServerStopFile() {
076                if (localServerStopFile == null)
077                        localServerStopFile = createFile(ConfigDir.getInstance().getFile(), "localServerRunning.deleteToStop");
078
079                return localServerStopFile;
080        }
081
082        public boolean isLocalServerStopFileEnabled() {
083                return localServerStopFileEnabled;
084        }
085        public void setLocalServerStopFileEnabled(boolean localServerStopFileEnabled) {
086                this.localServerStopFileEnabled = localServerStopFileEnabled;
087        }
088
089        /**
090         * Starts this instance of {@code LocalServer}, if no other instance is running on this computer.
091         * @return <code>true</code>, if neither this nor any other {@code LocalServer} is running on this computer, yet, and this
092         * instance could thus be started. <code>false</code>, if this instance or another instance was already started before.
093         * @throws RuntimeException in case starting the server fails for an unexpected reason.
094         */
095        public boolean start() {
096                logMemoryStats(logger);
097                if (! LsConfig.isLocalServerEnabled())
098                        return false;
099
100                LockFile _localServerRunningLockFile = null;
101                try {
102                        final Server s;
103                        synchronized (localServerRunningFile2LocalServer_running) {
104                                final File localServerRunningFile = getLocalServerRunningFile();
105                                final String localServerRunningFilePath = localServerRunningFile.getPath();
106
107                                try {
108                                        _localServerRunningLockFile = LockFileFactory.getInstance().acquire(localServerRunningFile, 5000);
109                                } catch (TimeoutException x) {
110                                        return false;
111                                }
112
113                                if (localServerRunningFile2LocalServer_running.containsKey(localServerRunningFilePath))
114                                        return false;
115
116                                // We now hold both the computer-wide LockFile and the JVM-wide synchronization, hence it's safe to write all the fields.
117                                localServerRunningLockFile = _localServerRunningLockFile;
118                                server = s = createServer();
119                                createLocalServerStopFileTimerTask();
120
121                                localServerRunningFile2LocalServer_running.put(localServerRunningFilePath, this);
122
123                                // Then we hook the lifecycle-listener in order to transfer the locking responsibility out of this method.
124                                s.addLifeCycleListener(new AbstractLifeCycle.AbstractLifeCycleListener() {
125                                        @Override
126                                        public void lifeCycleFailure(LifeCycle event, Throwable cause) {
127                                                onStopOrFailure();
128                                        }
129                                        @Override
130                                        public void lifeCycleStopped(LifeCycle event) {
131                                                onStopOrFailure();
132                                        }
133                                });
134
135                                // The listener is hooked and thus this method's finally block is not responsible for unlocking, anymore!
136                                // onStopOrFailure() now has the duty of unlocking instead!
137                                _localServerRunningLockFile = null;
138                        }
139
140                        // Start outside of synchronized block to make sure, any listeners don't get stuck in a deadlock.
141                        s.start();
142
143                        writeLocalServerProperties();
144
145//                      waitForServerToGetReady(); // seems not to be necessary => start() seems to block until the REST app is ready => commented out.
146
147                        return true;
148                } catch (final RuntimeException x) {
149                        throw x;
150                } catch (final Exception x) {
151                        throw new RuntimeException(x);
152                } finally {
153                        if (_localServerRunningLockFile != null)
154                                _localServerRunningLockFile.release();
155                }
156        }
157
158        private void createLocalServerStopFileTimerTask() {
159                if (! isLocalServerStopFileEnabled())
160                        return;
161
162                final File localServerStopFile = getLocalServerStopFile();
163                try {
164                        localServerStopFile.createNewFile();
165
166                        if (! localServerStopFile.exists())
167                                throw new IOException("File not created!");
168
169                } catch (Exception e) {
170                        throw new RuntimeException("Failed to create file: " + localServerStopFile);
171                }
172
173                synchronized (localServerStopFileTimer) {
174                        cancelLocalServerStopFileTimerTask();
175
176                        localServerStopFileTimerTask = new TimerTask() {
177                                @Override
178                                public void run() {
179                                        if (localServerStopFile.exists()) {
180                                                logger.debug("localServerStopFileTimerTask.run: file '{}' exists => nothing to do.", localServerStopFile);
181                                                return;
182                                        }
183                                        logger.info("localServerStopFileTimerTask.run: file '{}' does not exist => stopping server!", localServerStopFile);
184
185                                        stop();
186                                        System.exit(0);
187                                }
188                        };
189
190                        final long period = 5000L;
191                        localServerStopFileTimer.schedule(localServerStopFileTimerTask, period, period);
192                }
193        }
194
195        private void cancelLocalServerStopFileTimerTask() {
196                synchronized (localServerStopFileTimer) {
197                        if (localServerStopFileTimerTask != null) {
198                                localServerStopFileTimerTask.cancel();
199                                localServerStopFileTimerTask = null;
200                        }
201                }
202        }
203
204//      private void waitForServerToGetReady() throws TimeoutException { // seems not to be necessary => start() seems to block until the REST app is ready => commented out.
205//              final int localPort = getLocalPort();
206//              if (localPort < 0)
207//                      return;
208//
209//              final long timeoutMillis = 60000L;
210//              final long begin = System.currentTimeMillis();
211//              while (true) {
212//                      try {
213//                              final Socket socket = new Socket("127.0.0.1", localPort);
214//                              socket.close();
215//                              return; // success!
216//                      } catch (final Exception x) {
217//                              try { Thread.sleep(1000); } catch (final InterruptedException ie) { }
218//                      }
219//
220//                      if (System.currentTimeMillis() - begin > timeoutMillis)
221//                              throw new TimeoutException("Server did not start within timeout (ms): " + timeoutMillis);
222//              }
223//      }
224
225        private void onStopOrFailure() {
226                cancelLocalServerStopFileTimerTask();
227
228                synchronized (localServerRunningFile2LocalServer_running) {
229                        final File localServerRunningFile = getLocalServerRunningFile();
230                        final String localServerRunningFilePath = localServerRunningFile.getPath();
231
232                        if (localServerRunningFile2LocalServer_running.get(localServerRunningFilePath) == this) {
233                                localServerRunningFile2LocalServer_running.remove(localServerRunningFilePath);
234                                server = null;
235                        }
236
237                        final File localServerStopFile = getLocalServerStopFile();
238                        if (localServerStopFile.exists()) {
239                                localServerStopFile.delete();
240                                if (localServerStopFile.exists())
241                                        logger.warn("onStopOrFailure: Failed to delete file: {}", localServerStopFile);
242                                else
243                                        logger.info("onStopOrFailure: Successfully deleted file: {}", localServerStopFile);
244                        }
245                        else
246                                logger.info("onStopOrFailure: File did not exist (could not delete): {}", localServerStopFile);
247
248                        if (localServerRunningLockFile != null) {
249                                localServerRunningLockFile.release();
250                                localServerRunningLockFile = null;
251                        }
252                }
253        }
254
255        private void writeLocalServerProperties() throws IOException {
256                final int localPort = getLocalPort();
257                if (localPort < 0)
258                        return;
259
260                final LocalServerPropertiesManager localServerPropertiesManager = LocalServerPropertiesManager.getInstance();
261                localServerPropertiesManager.setPort(localPort);
262                localServerPropertiesManager.setPassword(new String(AuthManager.getInstance().getCurrentPassword()));
263                localServerPropertiesManager.writeLocalServerProperties();
264        }
265
266        public void stop() {
267                final Server s = getServer();
268                if (s != null) {
269                        try {
270                                s.stop();
271                        } catch (final Exception e) {
272                                throw new RuntimeException();
273                        }
274                }
275        }
276
277        public Server getServer() {
278                synchronized (localServerRunningFile2LocalServer_running) {
279                        return server;
280                }
281        }
282
283        public int getLocalPort() {
284                final Server server = getServer();
285                if (server == null)
286                        return -1;
287
288                final Connector[] connectors = server.getConnectors();
289                if (connectors.length != 1)
290                        throw new IllegalStateException("connectors.length != 1");
291
292                return ((ServerConnector) connectors[0]).getLocalPort();
293        }
294
295        public int getPort() {
296                synchronized (localServerRunningFile2LocalServer_running) {
297                        if (port < 0) {
298                                port = ConfigImpl.getInstance().getPropertyAsInt(CONFIG_KEY_PORT, DEFAULT_PORT);
299                                if (port < 0 || port > 65535) {
300                                        logger.warn("Config key '{}' is set to the value '{}' which is out of range for a port number. Falling back to default port {} ({} meaning a random port).",
301                                                        CONFIG_KEY_PORT, port, DEFAULT_PORT, RANDOM_PORT);
302                                        port = DEFAULT_PORT;
303                                }
304                        }
305                        return port;
306                }
307        }
308
309        public void setPort(final int port) {
310                synchronized (localServerRunningFile2LocalServer_running) {
311                        assertNotRunning();
312                        this.port = port;
313                }
314        }
315
316        private boolean isRunning() {
317                return getServer() != null;
318        }
319
320        private void assertNotRunning() {
321                if (isRunning())
322                        throw new IllegalStateException("Server is already running.");
323        }
324
325        private Server createServer() {
326                final QueuedThreadPool threadPool = new QueuedThreadPool();
327                threadPool.setMaxThreads(500);
328
329                final Server server = new Server(threadPool);
330                server.addBean(new ScheduledExecutorScheduler());
331
332                final ServerConnector http = createHttpServerConnector(server);
333        server.addConnector(http);
334
335                server.setHandler(createServletContextHandler());
336                server.setDumpAfterStart(false);
337                server.setDumpBeforeStop(false);
338                server.setStopAtShutdown(true);
339
340                return server;
341        }
342
343        private ServerConnector createHttpServerConnector(Server server) {
344                final HttpConfiguration http_config = createHttpConfigurationForHTTP();
345
346        final ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(http_config));
347        http.setHost("127.0.0.1");
348        http.setPort(getPort());
349        http.setIdleTimeout(30000);
350
351        return http;
352        }
353
354        private HttpConfiguration createHttpConfigurationForHTTP() {
355                final HttpConfiguration http_config = new HttpConfiguration();
356//              http_config.setSecureScheme("https");
357//              http_config.setSecurePort(getSecurePort());
358                http_config.setOutputBufferSize(32768);
359                http_config.setRequestHeaderSize(8192);
360                http_config.setResponseHeaderSize(8192);
361                http_config.setSendServerVersion(true);
362                http_config.setSendDateHeader(false);
363                return http_config;
364        }
365
366        private ServletContextHandler createServletContextHandler() {
367                final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
368                context.setContextPath("/");
369                final ServletContainer servletContainer = new ServletContainer(requireNonNull(createResourceConfig(), "createResourceConfig()"));
370                context.addServlet(new ServletHolder(servletContainer), "/*");
371//              context.addFilter(GzipFilter.class, "/*", EnumSet.allOf(DispatcherType.class)); // Does not work :-( Using GZip...Interceptor instead ;-)
372                return context;
373        }
374
375        /**
376         * Creates the actual REST application.
377         * @return the actual REST application. Must not be <code>null</code>.
378         */
379        protected ResourceConfig createResourceConfig() {
380                return new LocalServerRest();
381        }
382}