001/* 002 * Copyright 2012-2016 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.devtools.livereload; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.io.OutputStream; 022import java.net.ServerSocket; 023import java.net.Socket; 024import java.net.SocketTimeoutException; 025import java.util.ArrayList; 026import java.util.List; 027import java.util.concurrent.ExecutorService; 028import java.util.concurrent.Executors; 029import java.util.concurrent.ThreadFactory; 030import java.util.concurrent.TimeUnit; 031import java.util.concurrent.atomic.AtomicInteger; 032 033import org.apache.commons.logging.Log; 034import org.apache.commons.logging.LogFactory; 035 036import org.springframework.util.Assert; 037 038/** 039 * A <a href="http://livereload.com">livereload</a> server. 040 * 041 * @author Phillip Webb 042 * @since 1.3.0 043 * @see <a href="http://livereload.com">livereload.com</a> 044 */ 045public class LiveReloadServer { 046 047 /** 048 * The default live reload server port. 049 */ 050 public static final int DEFAULT_PORT = 35729; 051 052 private static final Log logger = LogFactory.getLog(LiveReloadServer.class); 053 054 private static final int READ_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(4); 055 056 private final ExecutorService executor = Executors 057 .newCachedThreadPool(new WorkerThreadFactory()); 058 059 private final List<Connection> connections = new ArrayList<Connection>(); 060 061 private final Object monitor = new Object(); 062 063 private final int port; 064 065 private final ThreadFactory threadFactory; 066 067 private ServerSocket serverSocket; 068 069 private Thread listenThread; 070 071 /** 072 * Create a new {@link LiveReloadServer} listening on the default port. 073 */ 074 public LiveReloadServer() { 075 this(DEFAULT_PORT); 076 } 077 078 /** 079 * Create a new {@link LiveReloadServer} listening on the default port with a specific 080 * {@link ThreadFactory}. 081 * @param threadFactory the thread factory 082 */ 083 public LiveReloadServer(ThreadFactory threadFactory) { 084 this(DEFAULT_PORT, threadFactory); 085 } 086 087 /** 088 * Create a new {@link LiveReloadServer} listening on the specified port. 089 * @param port the listen port 090 */ 091 public LiveReloadServer(int port) { 092 this(port, new ThreadFactory() { 093 094 @Override 095 public Thread newThread(Runnable runnable) { 096 return new Thread(runnable); 097 } 098 099 }); 100 } 101 102 /** 103 * Create a new {@link LiveReloadServer} listening on the specified port with a 104 * specific {@link ThreadFactory}. 105 * @param port the listen port 106 * @param threadFactory the thread factory 107 */ 108 public LiveReloadServer(int port, ThreadFactory threadFactory) { 109 this.port = port; 110 this.threadFactory = threadFactory; 111 } 112 113 /** 114 * Start the livereload server and accept incoming connections. 115 * @throws IOException in case of I/O errors 116 */ 117 public void start() throws IOException { 118 synchronized (this.monitor) { 119 Assert.state(!isStarted(), "Server already started"); 120 logger.debug("Starting live reload server on port " + this.port); 121 this.serverSocket = new ServerSocket(this.port); 122 this.listenThread = this.threadFactory.newThread(new Runnable() { 123 124 @Override 125 public void run() { 126 acceptConnections(); 127 } 128 129 }); 130 this.listenThread.setDaemon(true); 131 this.listenThread.setName("Live Reload Server"); 132 this.listenThread.start(); 133 } 134 } 135 136 /** 137 * Return if the server has been started. 138 * @return {@code true} if the server is running 139 */ 140 public boolean isStarted() { 141 synchronized (this.monitor) { 142 return this.listenThread != null; 143 } 144 } 145 146 /** 147 * Return the port that the server is listening on. 148 * @return the server port 149 */ 150 public int getPort() { 151 return this.port; 152 } 153 154 private void acceptConnections() { 155 do { 156 try { 157 Socket socket = this.serverSocket.accept(); 158 socket.setSoTimeout(READ_TIMEOUT); 159 this.executor.execute(new ConnectionHandler(socket)); 160 } 161 catch (SocketTimeoutException ex) { 162 // Ignore 163 } 164 catch (Exception ex) { 165 if (logger.isDebugEnabled()) { 166 logger.debug("LiveReload server error", ex); 167 } 168 } 169 } 170 while (!this.serverSocket.isClosed()); 171 } 172 173 /** 174 * Gracefully stop the livereload server. 175 * @throws IOException in case of I/O errors 176 */ 177 public void stop() throws IOException { 178 synchronized (this.monitor) { 179 if (this.listenThread != null) { 180 closeAllConnections(); 181 try { 182 this.executor.shutdown(); 183 this.executor.awaitTermination(1, TimeUnit.MINUTES); 184 } 185 catch (InterruptedException ex) { 186 Thread.currentThread().interrupt(); 187 } 188 this.serverSocket.close(); 189 try { 190 this.listenThread.join(); 191 } 192 catch (InterruptedException ex) { 193 Thread.currentThread().interrupt(); 194 } 195 this.listenThread = null; 196 this.serverSocket = null; 197 } 198 } 199 } 200 201 private void closeAllConnections() throws IOException { 202 synchronized (this.connections) { 203 for (Connection connection : this.connections) { 204 connection.close(); 205 } 206 } 207 } 208 209 /** 210 * Trigger livereload of all connected clients. 211 */ 212 public void triggerReload() { 213 synchronized (this.monitor) { 214 synchronized (this.connections) { 215 for (Connection connection : this.connections) { 216 try { 217 connection.triggerReload(); 218 } 219 catch (Exception ex) { 220 logger.debug("Unable to send reload message", ex); 221 } 222 } 223 } 224 } 225 } 226 227 private void addConnection(Connection connection) { 228 synchronized (this.connections) { 229 this.connections.add(connection); 230 } 231 } 232 233 private void removeConnection(Connection connection) { 234 synchronized (this.connections) { 235 this.connections.remove(connection); 236 } 237 } 238 239 /** 240 * Factory method used to create the {@link Connection}. 241 * @param socket the source socket 242 * @param inputStream the socket input stream 243 * @param outputStream the socket output stream 244 * @return a connection 245 * @throws IOException in case of I/O errors 246 */ 247 protected Connection createConnection(Socket socket, InputStream inputStream, 248 OutputStream outputStream) throws IOException { 249 return new Connection(socket, inputStream, outputStream); 250 } 251 252 /** 253 * {@link Runnable} to handle a single connection. 254 * 255 * @see Connection 256 */ 257 private class ConnectionHandler implements Runnable { 258 259 private final Socket socket; 260 261 private final InputStream inputStream; 262 263 ConnectionHandler(Socket socket) throws IOException { 264 this.socket = socket; 265 this.inputStream = socket.getInputStream(); 266 } 267 268 @Override 269 public void run() { 270 try { 271 handle(); 272 } 273 catch (ConnectionClosedException ex) { 274 logger.debug("LiveReload connection closed"); 275 } 276 catch (Exception ex) { 277 if (logger.isDebugEnabled()) { 278 logger.debug("LiveReload error", ex); 279 } 280 } 281 } 282 283 private void handle() throws Exception { 284 try { 285 try { 286 OutputStream outputStream = this.socket.getOutputStream(); 287 try { 288 Connection connection = createConnection(this.socket, 289 this.inputStream, outputStream); 290 runConnection(connection); 291 } 292 finally { 293 outputStream.close(); 294 } 295 } 296 finally { 297 this.inputStream.close(); 298 } 299 } 300 finally { 301 this.socket.close(); 302 } 303 } 304 305 private void runConnection(Connection connection) throws IOException, Exception { 306 try { 307 addConnection(connection); 308 connection.run(); 309 } 310 finally { 311 removeConnection(connection); 312 } 313 } 314 315 } 316 317 /** 318 * {@link ThreadFactory} to create the worker threads. 319 */ 320 private static class WorkerThreadFactory implements ThreadFactory { 321 322 private final AtomicInteger threadNumber = new AtomicInteger(1); 323 324 @Override 325 public Thread newThread(Runnable r) { 326 Thread thread = new Thread(r); 327 thread.setDaemon(true); 328 thread.setName("Live Reload #" + this.threadNumber.getAndIncrement()); 329 return thread; 330 } 331 332 } 333 334}