001package jmri.jmris;
002
003import java.io.DataInputStream;
004import java.io.DataOutputStream;
005import java.io.IOException;
006import java.net.ServerSocket;
007import java.net.Socket;
008import java.util.ArrayList;
009import java.util.HashMap;
010
011import jmri.InstanceManager;
012import jmri.ShutDownTask;
013import jmri.util.zeroconf.ZeroConfService;
014import org.slf4j.Logger;
015import org.slf4j.LoggerFactory;
016
017/**
018 * This is the main JMRI Server implementation.
019 *
020 * It starts a thread for each client.
021 *
022 */
023public class JmriServer {
024
025    protected int portNo = 3000; // Port to listen to for new clients.
026    protected int timeout = 0; // Timeout in milliseconds (0 = no timeout).
027    protected ServerSocket connectSocket;
028    protected ZeroConfService service = null;
029    protected ShutDownTask shutDownTask = null;
030    private Thread listenThread = null;
031    protected ArrayList<ClientListener> connectedClientThreads = new ArrayList<>();
032
033    // Create a new server using the default port
034    public JmriServer() {
035        this(3000);
036    }
037
038    // Create a new server using a given port and no timeout
039    public JmriServer(int port) {
040        this(port, 0);
041    }
042
043    // Create a new server using a given port with a timeout
044    // A timeout of 0 is infinite
045    public JmriServer(int port, int timeout) {
046        super();
047        // Try registering the server on the given port
048        try {
049            this.connectSocket = new ServerSocket(port);
050        } catch (IOException e) {
051            log.error("Failed to connect to port {}", port);
052        }
053        this.portNo = port;
054        this.timeout = timeout;
055    }
056
057    // Maintain a vector of connected clients
058    // Add a new client
059    private synchronized void addClient(ClientListener client) {
060        if (!connectedClientThreads.contains(client)) {
061            connectedClientThreads.add(client);
062            client.start();
063        }
064    }
065
066    //Remove a client
067    private synchronized void removeClient(ClientListener client) {
068        if (connectedClientThreads.contains(client)) {
069            client.stop(this);
070            connectedClientThreads.remove(client);
071        }
072    }
073
074    public void start() {
075        /* Start the server thread */
076        if (this.listenThread == null) {
077            this.listenThread = jmri.util.ThreadingUtil.newThread(new NewClientListener(connectSocket));
078            this.listenThread.start();
079            this.advertise();
080        }
081        if (this.shutDownTask != null) {
082            InstanceManager.getDefault(jmri.ShutDownManager.class).register(this.shutDownTask);
083        }
084    }
085
086    // Advertise the service with ZeroConf
087    protected void advertise() {
088        this.advertise("_jmri._tcp.local.");
089    }
090
091    protected void advertise(String type) {
092        this.advertise(type, new HashMap<>());
093    }
094
095    protected void advertise(String type, HashMap<String, String> properties) {
096        if (this.service == null) {
097            this.service = ZeroConfService.create(type, this.portNo, properties);
098        }
099        this.service.publish();
100    }
101
102    public void stop() {
103        this.connectedClientThreads.forEach((client) -> {
104            client.stop(this);
105        });
106        this.listenThread = null;
107        this.service.stop();
108        if (this.shutDownTask != null) {
109            InstanceManager.getDefault(jmri.ShutDownManager.class).deregister(this.shutDownTask);
110        }
111    }
112
113    // Internal thread to listen for new connections
114    class NewClientListener implements Runnable {
115
116        ServerSocket listenSocket = null;
117        boolean running = true;
118
119        public NewClientListener(ServerSocket socket) {
120
121            listenSocket = socket;
122        }
123
124        @Override
125        public void run() {
126            // Listen for connection requests
127            try {
128                while (running) {
129                    Socket clientSocket = listenSocket.accept();
130                    clientSocket.setSoTimeout(timeout);
131                    log.debug(" Client Connected from IP {} port {}", clientSocket.getInetAddress(), clientSocket.getPort());
132                    addClient(new ClientListener(clientSocket));
133                }
134            } catch (IOException e) {
135                log.error("IOException while Listening for clients");
136            }
137        }
138
139        public void stop() {
140            //super.stop();
141            running = false;
142            try {
143                listenSocket.close();
144                log.debug("Listen Socket closed");
145            } catch (IOException e) {
146                log.error("socket in ThreadedServer won't close");
147            }
148        }
149    } // end of NewClientListener class
150
151    // Internal class to handle a client
152    protected class ClientListener implements Runnable {
153
154        Socket clientSocket = null;
155        DataInputStream inStream = null;
156        DataOutputStream outStream = null;
157        Thread clientThread = null;
158
159        public ClientListener(Socket socket) {
160            log.debug("Starting new Client");
161            clientSocket = socket;
162            try {
163                inStream = new DataInputStream(clientSocket.getInputStream());
164                outStream = new DataOutputStream(clientSocket.getOutputStream());
165            } catch (IOException e) {
166                log.error("Error obtaining I/O Stream from socket.");
167            }
168        }
169
170        public void start() {
171            clientThread = jmri.util.ThreadingUtil.newThread(this);
172            clientThread.start();
173        }
174
175        public void stop(JmriServer server) {
176            try {
177                server.stopClient(inStream, outStream);
178                clientSocket.close();
179            } catch (IOException e) {
180                // silently ignore, since we may be reacting to a closed socket
181            }
182            clientThread = null;
183        }
184
185        @Override
186        public void run() {
187            // handle a client.
188            try {
189                handleClient(inStream, outStream);
190            } catch (IOException ex) {
191                // When we get an IO exception here, we're done
192                log.debug("Server Exiting");
193                // Unregister with the server
194                removeClient(this);
195            } catch (java.lang.NullPointerException ex) {
196                // When we get an IO exception here, we're done with this client
197                log.debug("Client Disconnect", ex);
198                // Unregister with the server
199                removeClient(this);
200            }
201        }
202    } // end of ClientListener class.
203
204    // Handle communication to a client through inStream and outStream
205    public void handleClient(DataInputStream inStream, DataOutputStream outStream) throws IOException {
206        // Listen for commands from the client until the connection closes
207        byte cmd[] = new byte[100];
208        int count;
209        while (true) {
210            // Read the command from the client
211            count = inStream.read(cmd);
212            // Echo the input back to the client
213            if (count != 0) {
214                outStream.write(cmd);
215            }
216        }
217    }
218
219    // Send a stop message to the client if applicable
220    public void stopClient(DataInputStream inStream, DataOutputStream outStream) throws IOException {
221        outStream.writeBytes("");
222    }
223    private final static Logger log = LoggerFactory.getLogger(JmriServer.class);
224}