package co.codewizards.cloudstore.ls.server; import static co.codewizards.cloudstore.core.oio.OioFileFactory.*; import static co.codewizards.cloudstore.core.util.DebugUtil.*; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.util.component.AbstractLifeCycle; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.servlet.ServletContainer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import co.codewizards.cloudstore.core.auth.BouncyCastleRegistrationUtil; import co.codewizards.cloudstore.core.config.ConfigDir; import co.codewizards.cloudstore.core.config.ConfigImpl; import co.codewizards.cloudstore.core.io.LockFile; import co.codewizards.cloudstore.core.io.LockFileFactory; import co.codewizards.cloudstore.core.io.TimeoutException; import co.codewizards.cloudstore.core.oio.File; import co.codewizards.cloudstore.core.util.AssertUtil; import co.codewizards.cloudstore.ls.core.LocalServerPropertiesManager; import co.codewizards.cloudstore.ls.core.LsConfig; import co.codewizards.cloudstore.ls.rest.server.LocalServerRest; import co.codewizards.cloudstore.ls.rest.server.auth.AuthManager; public class LocalServer { public static final String CONFIG_KEY_PORT = "localServer.port"; private static final int RANDOM_PORT = 0; private static final int DEFAULT_PORT = RANDOM_PORT; private static final Logger logger = LoggerFactory.getLogger(LocalServer.class); private Server server; private int port = -1; private File localServerRunningFile; private LockFile localServerRunningLockFile; private boolean localServerStopFileEnabled; private File localServerStopFile; private final Timer localServerStopFileTimer = new Timer("localServerStopFileTimer", true); private TimerTask localServerStopFileTimerTask; private static final Map<String, LocalServer> localServerRunningFile2LocalServer_running = new HashMap<>(); public LocalServer() { BouncyCastleRegistrationUtil.registerBouncyCastleIfNeeded(); } public File getLocalServerRunningFile() { if (localServerRunningFile == null) { try { localServerRunningFile = createFile(ConfigDir.getInstance().getFile(), "localServerRunning.lock").getCanonicalFile(); } catch (IOException e) { throw new RuntimeException(e); } } return localServerRunningFile; } public File getLocalServerStopFile() { if (localServerStopFile == null) localServerStopFile = createFile(ConfigDir.getInstance().getFile(), "localServerRunning.deleteToStop"); return localServerStopFile; } public boolean isLocalServerStopFileEnabled() { return localServerStopFileEnabled; } public void setLocalServerStopFileEnabled(boolean localServerStopFileEnabled) { this.localServerStopFileEnabled = localServerStopFileEnabled; } /** * Starts this instance of {@code LocalServer}, if no other instance is running on this computer. * @return <code>true</code>, if neither this nor any other {@code LocalServer} is running on this computer, yet, and this * instance could thus be started. <code>false</code>, if this instance or another instance was already started before. * @throws RuntimeException in case starting the server fails for an unexpected reason. */ public boolean start() { logMemoryStats(logger); if (! LsConfig.isLocalServerEnabled()) return false; LockFile _localServerRunningLockFile = null; try { final Server s; synchronized (localServerRunningFile2LocalServer_running) { final File localServerRunningFile = getLocalServerRunningFile(); final String localServerRunningFilePath = localServerRunningFile.getPath(); try { _localServerRunningLockFile = LockFileFactory.getInstance().acquire(localServerRunningFile, 5000); } catch (TimeoutException x) { return false; } if (localServerRunningFile2LocalServer_running.containsKey(localServerRunningFilePath)) return false; // We now hold both the computer-wide LockFile and the JVM-wide synchronization, hence it's safe to write all the fields. localServerRunningLockFile = _localServerRunningLockFile; server = s = createServer(); createLocalServerStopFileTimerTask(); localServerRunningFile2LocalServer_running.put(localServerRunningFilePath, this); // Then we hook the lifecycle-listener in order to transfer the locking responsibility out of this method. s.addLifeCycleListener(new AbstractLifeCycle.AbstractLifeCycleListener() { @Override public void lifeCycleFailure(LifeCycle event, Throwable cause) { onStopOrFailure(); } @Override public void lifeCycleStopped(LifeCycle event) { onStopOrFailure(); } }); // The listener is hooked and thus this method's finally block is not responsible for unlocking, anymore! // onStopOrFailure() now has the duty of unlocking instead! _localServerRunningLockFile = null; } // Start outside of synchronized block to make sure, any listeners don't get stuck in a deadlock. s.start(); writeLocalServerProperties(); // waitForServerToGetReady(); // seems not to be necessary => start() seems to block until the REST app is ready => commented out. return true; } catch (final RuntimeException x) { throw x; } catch (final Exception x) { throw new RuntimeException(x); } finally { if (_localServerRunningLockFile != null) _localServerRunningLockFile.release(); } } private void createLocalServerStopFileTimerTask() { if (! isLocalServerStopFileEnabled()) return; final File localServerStopFile = getLocalServerStopFile(); try { localServerStopFile.createNewFile(); if (! localServerStopFile.exists()) throw new IOException("File not created!"); } catch (Exception e) { throw new RuntimeException("Failed to create file: " + localServerStopFile); } synchronized (localServerStopFileTimer) { cancelLocalServerStopFileTimerTask(); localServerStopFileTimerTask = new TimerTask() { @Override public void run() { if (localServerStopFile.exists()) { logger.debug("localServerStopFileTimerTask.run: file '{}' exists => nothing to do.", localServerStopFile); return; } logger.info("localServerStopFileTimerTask.run: file '{}' does not exist => stopping server!", localServerStopFile); stop(); System.exit(0); } }; final long period = 5000L; localServerStopFileTimer.schedule(localServerStopFileTimerTask, period, period); } } private void cancelLocalServerStopFileTimerTask() { synchronized (localServerStopFileTimer) { if (localServerStopFileTimerTask != null) { localServerStopFileTimerTask.cancel(); localServerStopFileTimerTask = null; } } } // private void waitForServerToGetReady() throws TimeoutException { // seems not to be necessary => start() seems to block until the REST app is ready => commented out. // final int localPort = getLocalPort(); // if (localPort < 0) // return; // // final long timeoutMillis = 60000L; // final long begin = System.currentTimeMillis(); // while (true) { // try { // final Socket socket = new Socket("127.0.0.1", localPort); // socket.close(); // return; // success! // } catch (final Exception x) { // try { Thread.sleep(1000); } catch (final InterruptedException ie) { } // } // // if (System.currentTimeMillis() - begin > timeoutMillis) // throw new TimeoutException("Server did not start within timeout (ms): " + timeoutMillis); // } // } private void onStopOrFailure() { cancelLocalServerStopFileTimerTask(); synchronized (localServerRunningFile2LocalServer_running) { final File localServerRunningFile = getLocalServerRunningFile(); final String localServerRunningFilePath = localServerRunningFile.getPath(); if (localServerRunningFile2LocalServer_running.get(localServerRunningFilePath) == this) { localServerRunningFile2LocalServer_running.remove(localServerRunningFilePath); server = null; } final File localServerStopFile = getLocalServerStopFile(); if (localServerStopFile.exists()) { localServerStopFile.delete(); if (localServerStopFile.exists()) logger.warn("onStopOrFailure: Failed to delete file: {}", localServerStopFile); else logger.info("onStopOrFailure: Successfully deleted file: {}", localServerStopFile); } else logger.info("onStopOrFailure: File did not exist (could not delete): {}", localServerStopFile); if (localServerRunningLockFile != null) { localServerRunningLockFile.release(); localServerRunningLockFile = null; } } } private void writeLocalServerProperties() throws IOException { final int localPort = getLocalPort(); if (localPort < 0) return; final LocalServerPropertiesManager localServerPropertiesManager = LocalServerPropertiesManager.getInstance(); localServerPropertiesManager.setPort(localPort); localServerPropertiesManager.setPassword(AuthManager.getInstance().getCurrentPassword()); localServerPropertiesManager.writeLocalServerProperties(); } public void stop() { final Server s = getServer(); if (s != null) { try { s.stop(); } catch (final Exception e) { throw new RuntimeException(); } } } public Server getServer() { synchronized (localServerRunningFile2LocalServer_running) { return server; } } public int getLocalPort() { final Server server = getServer(); if (server == null) return -1; final Connector[] connectors = server.getConnectors(); if (connectors.length != 1) throw new IllegalStateException("connectors.length != 1"); return ((ServerConnector) connectors[0]).getLocalPort(); } public int getPort() { synchronized (localServerRunningFile2LocalServer_running) { if (port < 0) { port = ConfigImpl.getInstance().getPropertyAsInt(CONFIG_KEY_PORT, DEFAULT_PORT); if (port < 0 || port > 65535) { 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).", CONFIG_KEY_PORT, port, DEFAULT_PORT, RANDOM_PORT); port = DEFAULT_PORT; } } return port; } } public void setPort(final int port) { synchronized (localServerRunningFile2LocalServer_running) { assertNotRunning(); this.port = port; } } private boolean isRunning() { return getServer() != null; } private void assertNotRunning() { if (isRunning()) throw new IllegalStateException("Server is already running."); } private Server createServer() { final QueuedThreadPool threadPool = new QueuedThreadPool(); threadPool.setMaxThreads(500); final Server server = new Server(threadPool); server.addBean(new ScheduledExecutorScheduler()); final ServerConnector http = createHttpServerConnector(server); server.addConnector(http); server.setHandler(createServletContextHandler()); server.setDumpAfterStart(false); server.setDumpBeforeStop(false); server.setStopAtShutdown(true); return server; } private ServerConnector createHttpServerConnector(Server server) { final HttpConfiguration http_config = createHttpConfigurationForHTTP(); final ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(http_config)); http.setHost("127.0.0.1"); http.setPort(getPort()); http.setIdleTimeout(30000); return http; } private HttpConfiguration createHttpConfigurationForHTTP() { final HttpConfiguration http_config = new HttpConfiguration(); // http_config.setSecureScheme("https"); // http_config.setSecurePort(getSecurePort()); http_config.setOutputBufferSize(32768); http_config.setRequestHeaderSize(8192); http_config.setResponseHeaderSize(8192); http_config.setSendServerVersion(true); http_config.setSendDateHeader(false); return http_config; } private ServletContextHandler createServletContextHandler() { final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); context.setContextPath("/"); final ServletContainer servletContainer = new ServletContainer(AssertUtil.assertNotNull(createResourceConfig(), "createResourceConfig()")); context.addServlet(new ServletHolder(servletContainer), "/*"); // context.addFilter(GzipFilter.class, "/*", EnumSet.allOf(DispatcherType.class)); // Does not work :-( Using GZip...Interceptor instead ;-) return context; } /** * Creates the actual REST application. * @return the actual REST application. Must not be <code>null</code>. */ protected ResourceConfig createResourceConfig() { return new LocalServerRest(); } }