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}