001package jmri.jmrix.dccpp.dccppovertcp;
002
003import java.io.FileInputStream;
004import java.io.FileNotFoundException;
005import java.io.FileOutputStream;
006import java.io.IOException;
007import java.io.OutputStream;
008import java.io.PrintStream;
009import java.net.ServerSocket;
010import java.net.Socket;
011import java.util.LinkedList;
012import java.util.Properties;
013import java.util.Set;
014import jmri.InstanceInitializer;
015import jmri.InstanceManager;
016import jmri.implementation.AbstractInstanceInitializer;
017import jmri.jmrix.dccpp.DCCppConstants;
018import jmri.util.FileUtil;
019import jmri.util.zeroconf.ZeroConfService;
020import org.openide.util.lookup.ServiceProvider;
021import org.slf4j.Logger;
022import org.slf4j.LoggerFactory;
023
024/**
025 * Implementation of the DCCppOverTcp Server Protocol.
026 *
027 * @author Alex Shepherd Copyright (C) 2006
028 * @author Mark Underwood Copyright (C) 2015
029 */
030public class Server {
031
032    private final LinkedList<ClientRxHandler> clients = new LinkedList<>();
033    Thread socketListener;
034    ServerSocket serverSocket;
035    boolean settingsLoaded = false;
036    ServerListner stateListner;
037    boolean settingsChanged = false;
038    Runnable shutDownTask;
039    ZeroConfService service = null;
040    static final String AUTO_START_KEY = "AutoStart";
041    static final String PORT_NUMBER_KEY = "PortNumber";
042    static final String SETTINGS_FILE_NAME = "DCCppOverTcpSettings.ini";
043
044    private Server() {
045    }
046
047    public void setStateListner(ServerListner l) {
048        stateListner = l;
049    }
050
051    private void loadSettings() {
052        if (!settingsLoaded) {
053            settingsLoaded = true;
054            Properties settings = new Properties();
055
056            String settingsFileName = FileUtil.getUserFilesPath() + SETTINGS_FILE_NAME;
057
058            try {
059                log.debug("Server: opening settings file {}", settingsFileName);
060                java.io.InputStream settingsStream = new FileInputStream(settingsFileName);
061                try {
062                    settings.load(settingsStream);
063                } finally {
064                    settingsStream.close();
065                }
066
067                String val = settings.getProperty(AUTO_START_KEY, "0");
068                autoStart = (val.equals("1"));
069                val = settings.getProperty(PORT_NUMBER_KEY, Integer.toString(DCCppConstants.DCCPP_OVER_TCP_PORT));
070                portNumber = Integer.parseInt(val, 10);
071            } catch (FileNotFoundException ex) {
072                log.debug("Server: loadSettings file not found");
073            } catch (IOException ex) {
074                log.debug("Server: loadSettings exception: ", ex);
075            }
076            updateServerStateListener();
077        }
078    }
079
080    public void saveSettings() {
081        // we can't use the store capabilities of java.util.Properties, as
082        // they are not present in Java 1.1.8
083        String settingsFileName = FileUtil.getUserFilesPath() + SETTINGS_FILE_NAME;
084        log.debug("Server: saving settings file {}", settingsFileName);
085
086        try ( OutputStream outStream = new FileOutputStream(settingsFileName);
087            PrintStream settingsStream = new PrintStream(outStream); ) {
088
089            settingsStream.println("# DCCppOverTcp Configuration Settings");
090            settingsStream.println(AUTO_START_KEY + " = " + (autoStart ? "1" : "0"));
091            settingsStream.println(PORT_NUMBER_KEY + " = " + portNumber);
092
093            settingsStream.flush();
094            settingsStream.close();
095            settingsChanged = false;
096        } catch ( IOException ex) {
097            log.warn("Server: saveSettings exception: ", ex);
098        }
099        updateServerStateListener();
100    }
101    private boolean autoStart;
102
103    public boolean getAutoStart() {
104        loadSettings();
105        return autoStart;
106    }
107
108    public void setAutoStart(boolean start) {
109        loadSettings();
110        autoStart = start;
111        settingsChanged = true;
112        updateServerStateListener();
113    }
114    private int portNumber = DCCppConstants.DCCPP_OVER_TCP_PORT;
115
116    public int getPortNumber() {
117        loadSettings();
118        return portNumber;
119    }
120
121    public void setPortNumber(int port) {
122        loadSettings();
123        if ((port >= 1024) && (port <= 65535)) {
124            portNumber = port;
125            settingsChanged = true;
126            updateServerStateListener();
127        }
128    }
129
130    public boolean isEnabled() {
131        return (socketListener != null) && (socketListener.isAlive());
132    }
133
134    public boolean isSettingChanged() {
135        return settingsChanged;
136    }
137
138    public void enable() {
139        if (socketListener == null) {
140            socketListener = new Thread(new ClientListener());
141            socketListener.setDaemon(true);
142            socketListener.setName("DCCppOverTcpServer");
143            log.info("Starting new DCCppOverTcpServer listener on port {}", portNumber);
144            socketListener.start();
145            updateServerStateListener();
146            // advertise over Zeroconf/Bonjour
147            if (this.service == null) {
148                this.service = ZeroConfService.create("_dccppovertcpserver._tcp.local.", portNumber);
149            }
150            log.info("Starting ZeroConfService _dccppovertcpserver._tcp.local for DCCppOverTCP Server");
151            this.service.publish();
152            if (this.shutDownTask == null) {
153                this.shutDownTask = this::disable;
154            }
155            if (this.shutDownTask != null) {
156                InstanceManager.getDefault(jmri.ShutDownManager.class).register(this.shutDownTask);
157            }
158        }
159    }
160
161    public void disable() {
162        if (socketListener != null) {
163            socketListener.interrupt();
164            socketListener = null;
165            try {
166                if (serverSocket != null) {
167                    serverSocket.close();
168                }
169            } catch (IOException ex) {
170            }
171
172            updateServerStateListener();
173
174            // Now close all the client connections
175            Object[] clientsArray;
176
177            synchronized (clients) {
178                clientsArray = clients.toArray();
179            }
180            for (int i = 0; i < clientsArray.length; i++) {
181                ((ClientRxHandler) clientsArray[i]).close();
182            }
183        }
184        this.service.stop();
185        if (this.shutDownTask != null) {
186            InstanceManager.getDefault(jmri.ShutDownManager.class).deregister(this.shutDownTask);
187        }
188    }
189
190    public void updateServerStateListener() {
191        if (stateListner != null) {
192            stateListner.notifyServerStateChanged(this);
193        }
194    }
195
196    public void updateClinetStateListener() {
197        if (stateListner != null) {
198            stateListner.notifyClientStateChanged(this);
199        }
200    }
201
202    class ClientListener implements Runnable {
203
204        @Override
205        public void run() {
206            Socket newClientConnection;
207            String remoteAddress;
208            try {
209                serverSocket = new ServerSocket(getPortNumber());
210                serverSocket.setReuseAddress(true);
211                while (!socketListener.isInterrupted()) {
212                    newClientConnection = serverSocket.accept();
213                    remoteAddress = newClientConnection.getRemoteSocketAddress().toString();
214                    log.info("Server: Connection from: {}", remoteAddress);
215                    addClient(new ClientRxHandler(remoteAddress, newClientConnection));
216                }
217                serverSocket.close();
218            } catch (IOException ex) {
219                if (ex.toString().indexOf("socket closed") == -1) {
220                    log.error("Server: IO Exception: ", ex);
221                }
222            }
223            serverSocket = null;
224        }
225    }
226
227    protected void addClient(ClientRxHandler handler) {
228        synchronized (clients) {
229            clients.add(handler);
230        }
231        updateClinetStateListener();
232    }
233
234    protected void removeClient(ClientRxHandler handler) {
235        synchronized (clients) {
236            clients.remove(handler);
237        }
238        updateClinetStateListener();
239    }
240
241    public int getClientCount() {
242        synchronized (clients) {
243            return clients.size();
244        }
245    }
246
247    @ServiceProvider(service = InstanceInitializer.class)
248    public static class Initializer extends AbstractInstanceInitializer {
249
250        @Override
251        public <T> Object getDefault(Class<T> type) {
252            if (type.equals(Server.class)) {
253                Server instance = new Server();
254                if (instance.getAutoStart()) {
255                    instance.enable();
256                }
257                return instance;
258            }
259            return super.getDefault(type);
260        }
261
262        @Override
263        public Set<Class<?>> getInitalizes() {
264            Set<Class<?>> set = super.getInitalizes();
265            set.add(Server.class);
266            return set;
267        }
268    }
269
270    private final static Logger log = LoggerFactory.getLogger(Server.class);
271
272}