1 """ TCP server that communicates with terminals """
3 from getopt import getopt
4 from logging import getLogger, StreamHandler, DEBUG, INFO
5 from logging.handlers import SysLogHandler
6 from socket import socket, AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR
11 from .config import readconfig
12 from .gps303proto import parse_message, HIBERNATION, LOGIN, set_config
14 CONF = "/etc/gps303.conf"
16 log = getLogger("gps303/collector")
20 """Zmq message to broadcast what was received from the terminal"""
21 def __init__(self, imei, msg):
22 self.as_bytes = imei.encode() + msg.to_packet()
26 """Zmq message received from a third party to send to the terminal"""
27 def __init__(self, msg):
28 self.imei = msg[:16].decode()
29 self.payload = msg[16:]
33 """Connected socket to the terminal plus buffer and metadata"""
34 def __init__(self, sock, addr):
41 log.debug("Closing fd %d (IMEI %s)", self.sock.fileno(), self.imei)
47 """ Read from the socket and parse complete messages """
49 segment = self.sock.recv(4096)
51 log.warning("Reading from fd %d (IMEI %s): %s",
52 self.sock.fileno(), self.imei, e)
54 if not segment: # Terminal has closed connection
55 log.info("EOF reading from fd %d (IMEI %s)",
56 self.sock.fileno(), self.imei)
59 self.buffer += segment
62 framestart = self.buffer.find(b"xx")
63 if framestart == -1: # No frames, return whatever we have
65 if framestart > 0: # Should not happen, report
66 log.warning("Undecodable data \"%s\" from fd %d (IMEI %s)",
67 self.buffer[:framestart].hex(), self.sock.fileno(), self.imei)
68 self.buffer = self.buffer[framestart:]
69 # At this point, buffer starts with a packet
70 frameend = self.buffer.find(b"\r\n", 4)
71 if frameend == -1: # Incomplete frame, return what we have
73 msg = parse_message(self.buffer[2:frameend])
74 self.buffer = self.buffer[frameend+2:]
75 if isinstance(msg, LOGIN):
77 log.info("LOGIN from fd %d: IMEI %s",
78 self.sock.fileno(), self.imei)
82 def send(self, buffer):
84 self.sock.send(b"xx" + buffer + b"\r\n")
86 log.error("Sending to fd %d (IMEI %s): %s",
87 self.sock.fileno, self.imei, e)
94 def add(self, clntsock, clntaddr):
95 fd = clntsock.fileno()
96 self.by_fd[fd] = Client(clntsock, clntaddr)
100 clnt = self.by_fd[fd]
101 log.info("Stop serving fd %d (IMEI %s)", clnt.sock.fileno(), clnt.imei)
104 del self.by_imei[clnt.imei]
108 clnt = self.by_fd[fd]
112 if isinstance(msg, LOGIN):
113 self.by_imei[clnt.imei] = clnt
114 result.append((clnt.imei, msg))
117 def response(self, resp):
118 if resp.imei in self.by_imei:
119 self.by_imei[resp.imei].send(resp.payload)
122 def runserver(opts, conf):
124 zpub = zctx.socket(zmq.PUB)
125 zpub.bind(conf.get("collector", "publishurl"))
126 zsub = zctx.socket(zmq.SUB)
127 zsub.connect(conf.get("collector", "listenurl"))
128 tcpl = socket(AF_INET, SOCK_STREAM)
129 tcpl.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
130 tcpl.bind(("", conf.getint("collector", "port")))
132 tcpfd = tcpl.fileno()
133 poller = zmq.Poller()
134 poller.register(zsub, flags=zmq.POLLIN)
135 poller.register(tcpfd, flags=zmq.POLLIN)
142 events = poller.poll(10)
143 for sk, fl in events:
147 msg = zsub.recv(zmq.NOBLOCK)
148 tosend.append(Resp(msg))
152 clntsock, clntaddr = tcpl.accept()
153 topoll.append((clntsock, clntaddr))
155 for imei, msg in clients.recv(sk):
156 zpub.send(Bcast(imei, msg).as_bytes)
157 if msg is None or isinstance(msg, HIBERNATION):
158 log.debug("HIBERNATION from fd %d", sk)
160 # poll queue consumed, make changes now
162 poller.unregister(fd)
165 clients.response(zmsg)
166 for clntsock, clntaddr in topoll:
167 fd = clients.add(clntsock, clntaddr)
169 except KeyboardInterrupt:
173 if __name__.endswith("__main__"):
174 opts, _ = getopt(sys.argv[1:], "c:d")
176 conf = readconfig(opts["-c"] if "-c" in opts else CONF)
177 if sys.stdout.isatty():
178 log.addHandler(StreamHandler(sys.stderr))
180 log.addHandler(SysLogHandler(address="/dev/log"))
181 log.setLevel(DEBUG if "-d" in opts else INFO)
182 log.info("starting with options: %s", opts)
183 runserver(opts, conf)