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}