001package jmri.jmrit.withrottle;
002
003import java.io.IOException;
004import java.net.ServerSocket;
005import java.util.ArrayList;
006import jmri.InstanceManager;
007import jmri.UserPreferencesManager;
008import jmri.util.zeroconf.ZeroConfService;
009import jmri.util.zeroconf.ZeroConfServiceEvent;
010import jmri.util.zeroconf.ZeroConfServiceListener;
011import org.slf4j.Logger;
012import org.slf4j.LoggerFactory;
013
014/**
015 * Copied from UserInterface, but with the UI stuff removed. Sets up to
016 * advertise service, and creates a thread for it to run in.
017 * <p>
018 * listen() has to run in a separate thread.
019 *
020 * @author Brett Hoffman Copyright (C) 2009, 2010
021 * @author Paul Bender Copyright (C) 2018
022 */
023public class FacelessServer implements DeviceListener, DeviceManager, ZeroConfServiceListener {
024
025    private final static Logger log = LoggerFactory.getLogger(FacelessServer.class);
026
027    UserPreferencesManager userPreferences = InstanceManager.getNullableDefault(UserPreferencesManager.class);
028
029// Server iVars
030    int port;
031    ZeroConfService service;
032    boolean isListen = true;
033    ServerSocket socket = null;
034    final private ArrayList<DeviceServer> deviceList = new ArrayList<>();
035    final private ArrayList<DeviceListener> deviceListenerList = new ArrayList<>();
036    private int threadNumber = 1;
037
038    FacelessServer() {
039        createServerThread();
040        setShutDownTask();
041    } // End of constructor
042
043    @Override
044    public void listen() {
045        int socketPort = InstanceManager.getDefault(WiThrottlePreferences.class).getPort();
046
047        try { //Create socket on available port
048            socket = new ServerSocket(socketPort);
049        } catch (IOException e1) {
050            log.error("New ServerSocket({}) Failed during listen()", socketPort);
051            return;
052        }
053
054        port = socket.getLocalPort();
055        log.debug("WiThrottle listening on TCP port: {}", port);
056
057        service = ZeroConfService.create("_withrottle._tcp.local.", port);
058        service.addEventListener(this);
059        service.publish();
060
061        addDeviceListener(this);
062
063        while (isListen) { //Create DeviceServer threads
064            DeviceServer device;
065            try {
066                log.info("Creating new WiThrottle DeviceServer(socket) on port {}, waiting for incoming connection...", port);
067                device = new DeviceServer(socket.accept(), this);  //blocks here until a connection is made
068
069                String threadName = "DeviceServer-" + threadNumber++;  // NOI18N
070                Thread t = jmri.util.ThreadingUtil.newThread(device, threadName);
071                for (DeviceListener dl : deviceListenerList) {
072                    device.addDeviceListener(dl);
073                }
074                log.debug("Starting thread '{}'", threadName);  // NOI18N
075                t.start();
076            } catch (IOException e3) {
077                if (isListen) {
078                    log.error("Listen Failed on port {}", port);
079                }
080                return;
081            }
082
083        }
084
085    }
086
087    // package protected getters
088    ZeroConfService getZeroConfService() {
089        return service;
090    }
091
092    int getPort() {
093        return port;
094    }
095
096    /**
097     * Add a device listener that will be added for each new device connection
098     *
099     * @param dl the device listener to add
100     */
101    @Override
102    public void addDeviceListener(DeviceListener dl) {
103        if (!deviceListenerList.contains(dl)) {
104            deviceListenerList.add(dl);
105        }
106    }
107
108    /**
109     * Remove a device listener from the list that will be added for each new
110     * device connection
111     *
112     * @param dl the device listener to remove
113     */
114    @Override
115    public void removeDeviceListener(DeviceListener dl) {
116        if (deviceListenerList.contains(dl)) {
117            deviceListenerList.remove(dl);
118        }
119    }
120
121    @Override
122    public void notifyDeviceConnected(DeviceServer device) {
123
124        deviceList.add(device);
125    }
126
127    @Override
128    public void notifyDeviceDisconnected(DeviceServer device) {
129        if (deviceList.size() < 1) {
130            return;
131        }
132        if (!deviceList.remove(device)) {
133            return;
134        }
135
136        device.removeDeviceListener(this);
137    }
138
139//    public void notifyDeviceAddressChanged(DeviceServer device){
140//    }
141    /**
142     * Received an UDID, filter out any duplicate.
143     *
144     * @param device the device to filter for duplicates
145     */
146    @Override
147    public void notifyDeviceInfoChanged(DeviceServer device) {
148
149        //  Filter duplicate connections
150        if ((device.getUDID() != null)) {
151            for (DeviceServer listDevice : deviceList) {
152                if (device != listDevice && device.getUDID().equals(listDevice.getUDID())) {
153                    //  If in here, array contains duplicate of a device
154                    log.debug("Has duplicate of device '{}', clearing old one.", listDevice.getUDID());
155                    listDevice.closeThrottles();
156                    break;
157                }
158            }
159        }
160    }
161
162    public ArrayList<DeviceServer> getDeviceList() {
163        return deviceList;
164    }
165
166    @Override
167    public void notifyDeviceAddressChanged(DeviceServer device) {
168        // TODO Auto-generated method stub
169
170    }
171
172    private String rosterGroup = null;
173
174    @Override
175    public void setSelectedRosterGroup(String group) {
176        rosterGroup = group;
177    }
178
179    @Override
180    public String getSelectedRosterGroup() {
181        return rosterGroup;
182    }
183
184    @Override
185    public void serviceQueued(ZeroConfServiceEvent se) {
186    }
187
188    @Override
189    public void servicePublished(ZeroConfServiceEvent se) {
190        try {
191            log.info("Published ZeroConf service for '{}' on {}:{}", se.getService().getKey(), se.getAddress().getHostAddress(), port); // NOI18N
192        } catch (NullPointerException ex) {
193            log.error("NPE in FacelessServer.servicePublished(): {}", ex.getLocalizedMessage());
194        }
195    }
196
197    // package protected method to disable the server.
198    void disableServer() {
199        isListen = false;
200        stopDevices();
201        try {
202            socket.close();
203            log.debug("closed socket in ServerThread");
204            service.stop();
205        } catch (NullPointerException ex) {
206            log.debug("NPE while attempting to close socket, ignored");
207        } catch (IOException ex) {
208            log.error("socket in ServerThread won't close");
209        }
210    }
211
212    // Clear out the deviceList array and close each device thread
213    private void stopDevices() {
214        DeviceServer device;
215        int cnt = 0;
216        if (deviceList.size() > 0) {
217            do {
218                device = deviceList.get(0);
219                if (device != null) {
220                    device.closeThrottles(); //Tell device to stop its throttles,
221                    device.closeSocket();   //close its sockets
222                    //close() will throw read error and it will be caught
223                    //and drop the thread.
224                    cnt++;
225                    if (cnt > 200) {
226                        break;
227                    }
228                }
229            } while (!deviceList.isEmpty());
230        }
231        deviceList.clear();
232    }
233
234    private void setShutDownTask() {
235        jmri.InstanceManager.getDefault(jmri.ShutDownManager.class).register(this::disableServer);
236    }
237
238    @Override
239    public void serviceUnpublished(ZeroConfServiceEvent se) {
240    }
241
242}