001/* 002 * Copyright 2012-2017 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<>(); 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, Thread::new); 093 } 094 095 /** 096 * Create a new {@link LiveReloadServer} listening on the specified port with a 097 * specific {@link ThreadFactory}. 098 * @param port the listen port 099 * @param threadFactory the thread factory 100 */ 101 public LiveReloadServer(int port, ThreadFactory threadFactory) { 102 this.port = port; 103 this.threadFactory = threadFactory; 104 } 105 106 /** 107 * Start the livereload server and accept incoming connections. 108 * @return the port on which the server is listening 109 * @throws IOException in case of I/O errors 110 */ 111 public int start() throws IOException { 112 synchronized (this.monitor) { 113 Assert.state(!isStarted(), "Server already started"); 114 logger.debug("Starting live reload server on port " + this.port); 115 this.serverSocket = new ServerSocket(this.port); 116 int localPort = this.serverSocket.getLocalPort(); 117 this.listenThread = this.threadFactory.newThread(this::acceptConnections); 118 this.listenThread.setDaemon(true); 119 this.listenThread.setName("Live Reload Server"); 120 this.listenThread.start(); 121 return localPort; 122 } 123 } 124 125 /** 126 * Return if the server has been started. 127 * @return {@code true} if the server is running 128 */ 129 public boolean isStarted() { 130 synchronized (this.monitor) { 131 return this.listenThread != null; 132 } 133 } 134 135 /** 136 * Return the port that the server is listening on. 137 * @return the server port 138 */ 139 public int getPort() { 140 return this.port; 141 } 142 143 private void acceptConnections() { 144 do { 145 try { 146 Socket socket = this.serverSocket.accept(); 147 socket.setSoTimeout(READ_TIMEOUT); 148 this.executor.execute(new ConnectionHandler(socket)); 149 } 150 catch (SocketTimeoutException ex) { 151 // Ignore 152 } 153 catch (Exception ex) { 154 if (logger.isDebugEnabled()) { 155 logger.debug("LiveReload server error", ex); 156 } 157 } 158 } 159 while (!this.serverSocket.isClosed()); 160 } 161 162 /** 163 * Gracefully stop the livereload server. 164 * @throws IOException in case of I/O errors 165 */ 166 public void stop() throws IOException { 167 synchronized (this.monitor) { 168 if (this.listenThread != null) { 169 closeAllConnections(); 170 try { 171 this.executor.shutdown(); 172 this.executor.awaitTermination(1, TimeUnit.MINUTES); 173 } 174 catch (InterruptedException ex) { 175 Thread.currentThread().interrupt(); 176 } 177 this.serverSocket.close(); 178 try { 179 this.listenThread.join(); 180 } 181 catch (InterruptedException ex) { 182 Thread.currentThread().interrupt(); 183 } 184 this.listenThread = null; 185 this.serverSocket = null; 186 } 187 } 188 } 189 190 private void closeAllConnections() throws IOException { 191 synchronized (this.connections) { 192 for (Connection connection : this.connections) { 193 connection.close(); 194 } 195 } 196 } 197 198 /** 199 * Trigger livereload of all connected clients. 200 */ 201 public void triggerReload() { 202 synchronized (this.monitor) { 203 synchronized (this.connections) { 204 for (Connection connection : this.connections) { 205 try { 206 connection.triggerReload(); 207 } 208 catch (Exception ex) { 209 logger.debug("Unable to send reload message", ex); 210 } 211 } 212 } 213 } 214 } 215 216 private void addConnection(Connection connection) { 217 synchronized (this.connections) { 218 this.connections.add(connection); 219 } 220 } 221 222 private void removeConnection(Connection connection) { 223 synchronized (this.connections) { 224 this.connections.remove(connection); 225 } 226 } 227 228 /** 229 * Factory method used to create the {@link Connection}. 230 * @param socket the source socket 231 * @param inputStream the socket input stream 232 * @param outputStream the socket output stream 233 * @return a connection 234 * @throws IOException in case of I/O errors 235 */ 236 protected Connection createConnection(Socket socket, InputStream inputStream, 237 OutputStream outputStream) throws IOException { 238 return new Connection(socket, inputStream, outputStream); 239 } 240 241 /** 242 * {@link Runnable} to handle a single connection. 243 * 244 * @see Connection 245 */ 246 private class ConnectionHandler implements Runnable { 247 248 private final Socket socket; 249 250 private final InputStream inputStream; 251 252 ConnectionHandler(Socket socket) throws IOException { 253 this.socket = socket; 254 this.inputStream = socket.getInputStream(); 255 } 256 257 @Override 258 public void run() { 259 try { 260 handle(); 261 } 262 catch (ConnectionClosedException ex) { 263 logger.debug("LiveReload connection closed"); 264 } 265 catch (Exception ex) { 266 if (logger.isDebugEnabled()) { 267 logger.debug("LiveReload error", ex); 268 } 269 } 270 } 271 272 private void handle() throws Exception { 273 try { 274 try (OutputStream outputStream = this.socket.getOutputStream()) { 275 Connection connection = createConnection(this.socket, 276 this.inputStream, outputStream); 277 runConnection(connection); 278 } 279 finally { 280 this.inputStream.close(); 281 } 282 } 283 finally { 284 this.socket.close(); 285 } 286 } 287 288 private void runConnection(Connection connection) throws IOException, Exception { 289 try { 290 addConnection(connection); 291 connection.run(); 292 } 293 finally { 294 removeConnection(connection); 295 } 296 } 297 298 } 299 300 /** 301 * {@link ThreadFactory} to create the worker threads. 302 */ 303 private static class WorkerThreadFactory implements ThreadFactory { 304 305 private final AtomicInteger threadNumber = new AtomicInteger(1); 306 307 @Override 308 public Thread newThread(Runnable r) { 309 Thread thread = new Thread(r); 310 thread.setDaemon(true); 311 thread.setName("Live Reload #" + this.threadNumber.getAndIncrement()); 312 return thread; 313 } 314 315 } 316 317}