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}