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