001package jmri.jmrix.loconet.loconetovertcp; 002 003import java.beans.PropertyChangeEvent; 004import java.io.IOException; 005import java.net.ServerSocket; 006import java.net.Socket; 007import java.util.ArrayList; 008import java.util.LinkedList; 009import java.util.List; 010import jmri.InstanceManager; 011import jmri.ShutDownManager; 012import jmri.jmrix.loconet.LnTrafficController; 013import jmri.jmrix.loconet.LocoNetSystemConnectionMemo; 014import jmri.util.zeroconf.ZeroConfService; 015import org.slf4j.Logger; 016import org.slf4j.LoggerFactory; 017 018import javax.annotation.Nonnull; 019 020/** 021 * Implementation of the LocoNetOverTcp LbServer Server Protocol. 022 * 023 * @author Alex Shepherd Copyright (C) 2006 024 */ 025public class LnTcpServer { 026 027 private final List<ClientRxHandler> clients = new LinkedList<>(); 028 private Thread socketListener; 029 private ServerSocket serverSocket; 030 private final List<LnTcpServerListener> stateListeners = new ArrayList<>(); 031 private boolean settingsChanged = false; 032 private final Runnable shutDownTask = this::disable; 033 private ZeroConfService service = null; 034 035 private int portNumber; 036 private final LnTrafficController tc; 037 038 private LnTcpServer(@Nonnull LocoNetSystemConnectionMemo memo) { 039 tc = memo.getLnTrafficController(); // store tc in order to know where to send messages 040 LnTcpPreferences pm = LnTcpPreferences.getDefault(); 041 portNumber = pm.getPort(); 042 pm.addPropertyChangeListener((PropertyChangeEvent evt) -> { 043 // ignore uninteresting property changes 044 if (LnTcpPreferences.PORT.equals(evt.getPropertyName())) {// only change the port if stopped 045 if (!isEnabled()) { 046 portNumber = pm.getPort(); 047 } 048 } 049 }); 050 } 051 052 /** 053 * Get the default server instance, creating it if necessary. 054 * 055 * @return the default LnTcpServer instance 056 */ 057 public static synchronized LnTcpServer getDefault() { 058 return InstanceManager.getOptionalDefault(LnTcpServer.class).orElseGet(() -> { 059 LnTcpServer server = new LnTcpServer(jmri.InstanceManager.getDefault(LocoNetSystemConnectionMemo.class)); 060 return InstanceManager.setDefault(LnTcpServer.class, server); 061 }); 062 } 063 064 public boolean isEnabled() { 065 return (socketListener != null) && (socketListener.isAlive()); 066 } 067 068 public boolean isSettingChanged() { 069 return settingsChanged; 070 } 071 072 public void enable() { 073 if (socketListener == null) { 074 socketListener = new Thread(new ClientListener()); 075 socketListener.setDaemon(true); 076 socketListener.setName("LocoNetOverTcpServer"); 077 log.info("Starting new LocoNetOverTcpServer listener on port {}", portNumber); 078 socketListener.start(); 079 updateServerStateListeners(); 080 // advertise over Zeroconf/Bonjour 081 if (this.service == null) { 082 this.service = ZeroConfService.create("_loconetovertcpserver._tcp.local.", portNumber); 083 } 084 log.info("Starting ZeroConfService _loconetovertcpserver._tcp.local for LocoNetOverTCP Server"); 085 this.service.publish(); 086 InstanceManager.getDefault(jmri.ShutDownManager.class).register(this.shutDownTask); 087 } 088 } 089 090 public void disable() { 091 if (socketListener != null) { 092 socketListener.interrupt(); 093 socketListener = null; 094 try { 095 if (serverSocket != null) { 096 serverSocket.close(); 097 } 098 } catch (IOException ignore) { 099 } 100 101 updateServerStateListeners(); 102 103 // Now close all the client connections 104 Object[] clientsArray; 105 106 synchronized (clients) { 107 clientsArray = clients.toArray(); 108 } 109 for (Object o : clientsArray) { 110 ((ClientRxHandler) o).close(); 111 } 112 } 113 if (this.service != null) { 114 this.service.stop(); 115 } 116 InstanceManager.getDefault(ShutDownManager.class).deregister(this.shutDownTask); 117 } 118 119 private void updateServerStateListeners() { 120 synchronized (this) { 121 this.stateListeners.stream().filter((l) -> (l != null)).forEachOrdered((l) -> { 122 l.notifyServerStateChanged(this); 123 }); 124 } 125 } 126 127 private void updateClientStateListeners() { 128 synchronized (this) { 129 this.stateListeners.stream().filter((l) -> (l != null)).forEachOrdered((l) -> { 130 l.notifyClientStateChanged(this); 131 }); 132 } 133 } 134 135 public void addStateListener(LnTcpServerListener l) { 136 this.stateListeners.add(l); 137 } 138 139 public boolean removeStateListener(LnTcpServerListener l) { 140 return this.stateListeners.remove(l); 141 } 142 143 /** 144 * Get the port this server is using. 145 * 146 * @return the port 147 */ 148 public int getPort() { 149 return this.portNumber; 150 } 151 152 class ClientListener implements Runnable { 153 154 @Override 155 public void run() { 156 Socket newClientConnection; 157 String remoteAddress; 158 try { 159 serverSocket = new ServerSocket(portNumber); 160 serverSocket.setReuseAddress(true); 161 while (!socketListener.isInterrupted()) { 162 newClientConnection = serverSocket.accept(); 163 remoteAddress = newClientConnection.getRemoteSocketAddress().toString(); 164 log.info("Server: Connection from: {}", remoteAddress); 165 addClient(new ClientRxHandler(remoteAddress, newClientConnection, tc)); 166 } 167 serverSocket.close(); 168 } catch (IOException ex) { 169 if (!ex.toString().toLowerCase().contains("socket closed")) { 170 log.error("Server: IO Exception: ", ex); 171 } 172 } 173 serverSocket = null; 174 } 175 } 176 177 protected void addClient(ClientRxHandler handler) { 178 synchronized (clients) { 179 clients.add(handler); 180 } 181 updateClientStateListeners(); 182 } 183 184 protected void removeClient(ClientRxHandler handler) { 185 synchronized (clients) { 186 clients.remove(handler); 187 } 188 updateClientStateListeners(); 189 } 190 191 public int getClientCount() { 192 synchronized (clients) { 193 return clients.size(); 194 } 195 } 196 197 private final static Logger log = LoggerFactory.getLogger(LnTcpServer.class); 198 199}