001/*
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2026, QOS.ch. All rights reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under
006 * either the terms of the Eclipse Public License v2.0 as published by
007 * the Eclipse Foundation
008 *
009 *   or (per the licensee's choosing)
010 *
011 * under the terms of the GNU Lesser General Public License version 2.1
012 * as published by the Free Software Foundation.
013 */
014package ch.qos.logback.classic.net;
015
016import java.io.IOException;
017import java.net.ServerSocket;
018import java.net.Socket;
019import java.util.ArrayList;
020import java.util.List;
021import java.util.concurrent.CountDownLatch;
022
023import javax.net.ServerSocketFactory;
024
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027
028import ch.qos.logback.classic.LoggerContext;
029import ch.qos.logback.classic.joran.JoranConfigurator;
030import ch.qos.logback.core.joran.spi.JoranException;
031
032/**
033 * A simple {@link SocketNode} based server.
034 * 
035 * <pre>
036 *      &lt;b&gt;Usage:&lt;/b&gt; java ch.qos.logback.classic.net.SimpleSocketServer port configFile
037 * </pre>
038 * 
039 * where <em>port</em> is a port number where the server listens and
040 * <em>configFile</em> is an XML configuration file fed to
041 * {@link JoranConfigurator}.
042 * 
043 * </pre>
044 * 
045 * @author Ceki G&uuml;lc&uuml;
046 * @author S&eacute;bastien Pennec
047 * 
048 * @since 0.8.4
049 */
050public class SimpleSocketServer extends Thread {
051
052    Logger logger = LoggerFactory.getLogger(SimpleSocketServer.class);
053
054    private final int port;
055    private final LoggerContext lc;
056    private boolean closed = false;
057    private ServerSocket serverSocket;
058    private List<SocketNode> socketNodeList = new ArrayList<SocketNode>();
059
060    // used for testing purposes
061    private CountDownLatch latch;
062
063    public static void main(String argv[]) throws Exception {
064        doMain(SimpleSocketServer.class, argv);
065    }
066
067    protected static void doMain(Class<? extends SimpleSocketServer> serverClass, String argv[]) throws Exception {
068        int port = -1;
069        if (argv.length == 2) {
070            port = parsePortNumber(argv[0]);
071        } else {
072            usage("Wrong number of arguments.");
073        }
074
075        String configFile = argv[1];
076        LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
077        configureLC(lc, configFile);
078
079        SimpleSocketServer sss = new SimpleSocketServer(lc, port);
080
081        // start the server in a separate thread
082        sss.start();
083    }
084
085    public SimpleSocketServer(LoggerContext lc, int port) {
086        this.lc = lc;
087        this.port = port;
088    }
089
090    public void run() {
091
092        final String oldThreadName = Thread.currentThread().getName();
093
094        try {
095
096            final String newThreadName = getServerThreadName();
097            Thread.currentThread().setName(newThreadName);
098
099            logger.info("Listening on port " + port);
100            serverSocket = getServerSocketFactory().createServerSocket(port);
101            while (!closed) {
102                logger.info("Waiting to accept a new client.");
103                signalAlmostReadiness();
104                Socket socket = serverSocket.accept();
105                logger.info("Connected to client at " + socket.getInetAddress());
106                logger.info("Starting new socket node.");
107                SocketNode newSocketNode = new SocketNode(this, socket, lc);
108                synchronized (socketNodeList) {
109                    socketNodeList.add(newSocketNode);
110                }
111                final String clientThreadName = getClientThreadName(socket);
112                new Thread(newSocketNode, clientThreadName).start();
113            }
114        } catch (Exception e) {
115            if (closed) {
116                logger.info("Exception in run method for a closed server. This is normal.");
117            } else {
118                logger.error("Unexpected failure in run method", e);
119            }
120        }
121
122        finally {
123            Thread.currentThread().setName(oldThreadName);
124        }
125    }
126
127    /**
128     * Returns the name given to the server thread.
129     */
130    protected String getServerThreadName() {
131        return String.format("Logback %s (port %d)", getClass().getSimpleName(), port);
132    }
133
134    /**
135     * Returns a name to identify each client thread.
136     */
137    protected String getClientThreadName(Socket socket) {
138        return String.format("Logback SocketNode (client: %s)", socket.getRemoteSocketAddress());
139    }
140
141    /**
142     * Gets the platform default {@link ServerSocketFactory}.
143     * <p>
144     * Subclasses may override to provide a custom server socket factory.
145     */
146    protected ServerSocketFactory getServerSocketFactory() {
147        return ServerSocketFactory.getDefault();
148    }
149
150    /**
151     * Signal another thread that we have established a connection This is useful
152     * for testing purposes.
153     */
154    void signalAlmostReadiness() {
155        if (latch != null && latch.getCount() != 0) {
156            // System.out.println("signalAlmostReadiness() with latch "+latch);
157            latch.countDown();
158        }
159    }
160
161    /**
162     * Used for testing purposes
163     * 
164     * @param latch
165     */
166    void setLatch(CountDownLatch latch) {
167        this.latch = latch;
168    }
169
170    /**
171     * Used for testing purposes
172     */
173    public CountDownLatch getLatch() {
174        return latch;
175    }
176
177    public boolean isClosed() {
178        return closed;
179    }
180
181    public void close() {
182        closed = true;
183        if (serverSocket != null) {
184            try {
185                serverSocket.close();
186            } catch (IOException e) {
187                logger.error("Failed to close serverSocket", e);
188            } finally {
189                serverSocket = null;
190            }
191        }
192
193        logger.info("closing this server");
194        synchronized (socketNodeList) {
195            for (SocketNode sn : socketNodeList) {
196                sn.close();
197            }
198        }
199        if (socketNodeList.size() != 0) {
200            logger.warn("Was expecting a 0-sized socketNodeList after server shutdown");
201        }
202
203    }
204
205    public void socketNodeClosing(SocketNode sn) {
206        logger.debug("Removing {}", sn);
207
208        // don't allow simultaneous access to the socketNodeList
209        // (e.g. removal whole iterating on the list causes
210        // java.util.ConcurrentModificationException)
211        synchronized (socketNodeList) {
212            socketNodeList.remove(sn);
213        }
214    }
215
216    static void usage(String msg) {
217        System.err.println(msg);
218        System.err.println("Usage: java " + SimpleSocketServer.class.getName() + " port configFile");
219        System.exit(1);
220    }
221
222    static int parsePortNumber(String portStr) {
223        try {
224            return Integer.parseInt(portStr);
225        } catch (java.lang.NumberFormatException e) {
226            e.printStackTrace();
227            usage("Could not interpret port number [" + portStr + "].");
228            // we won't get here
229            return -1;
230        }
231    }
232
233    static public void configureLC(LoggerContext lc, String configFile) throws JoranException {
234        JoranConfigurator configurator = new JoranConfigurator();
235        lc.reset();
236        configurator.setContext(lc);
237        configurator.doConfigure(configFile);
238    }
239}