1 """ TCP server that communicates with terminals """
3 from configparser import ConfigParser
4 from logging import getLogger
14 from struct import pack
16 from typing import Dict, List, Optional, Tuple
20 from .gps303proto import (
28 from .zmsg import Bcast, Resp
30 log = getLogger("gps303/collector")
36 """Connected socket to the terminal plus buffer and metadata"""
38 def __init__(self, sock: socket, addr: Tuple[str, int]) -> None:
41 self.stream = GPS303Conn()
42 self.imei: Optional[str] = None
44 def close(self) -> None:
45 log.debug("Closing fd %d (IMEI %s)", self.sock.fileno(), self.imei)
47 rest = self.stream.close()
49 log.warning("%d bytes in buffer on close: %s", len(rest), rest)
51 def recv(self) -> Optional[List[Tuple[float, Tuple[str, int], bytes]]]:
52 """Read from the socket and parse complete messages"""
54 segment = self.sock.recv(MAXBUFFER)
57 "Reading from fd %d (IMEI %s): %s",
63 if not segment: # Terminal has closed connection
65 "EOF reading from fd %d (IMEI %s)",
72 for elem in self.stream.recv(segment):
73 if isinstance(elem, bytes):
74 msgs.append((when, self.addr, elem))
77 "%s from fd %d (IMEI %s)",
84 def send(self, buffer: bytes) -> None:
86 self.sock.send(self.stream.enframe(buffer))
89 "Sending to fd %d (IMEI %s): %s",
95 def set_imei(self, imei: str) -> None:
100 def __init__(self) -> None:
101 self.by_fd: Dict[int, Client] = {}
102 self.by_imei: Dict[str, Client] = {}
104 def add(self, clntsock: socket, clntaddr: Tuple[str, int]) -> int:
105 fd = clntsock.fileno()
106 log.info("Start serving fd %d from %s", fd, clntaddr)
107 self.by_fd[fd] = Client(clntsock, clntaddr)
110 def stop(self, fd: int) -> None:
111 clnt = self.by_fd[fd]
112 log.info("Stop serving fd %d (IMEI %s)", clnt.sock.fileno(), clnt.imei)
115 del self.by_imei[clnt.imei]
120 ) -> Optional[List[Tuple[Optional[str], float, Tuple[str, int], bytes]]]:
121 clnt = self.by_fd[fd]
126 for when, peeraddr, packet in msgs:
127 if proto_of_message(packet) == LOGIN.PROTO:
128 msg = parse_message(packet)
129 if isinstance(msg, LOGIN): # Can be unparseable
130 if clnt.imei is None:
133 "LOGIN from fd %d (IMEI %s)",
137 oldclnt = self.by_imei.get(clnt.imei)
138 if oldclnt is not None:
140 "Orphaning fd %d with the same IMEI",
141 oldclnt.sock.fileno(),
144 self.by_imei[clnt.imei] = clnt
147 "Login message from %s: %s, but client imei unfilled",
151 result.append((clnt.imei, when, peeraddr, packet))
153 "Received from %s (IMEI %s): %s",
160 def response(self, resp: Resp) -> None:
161 if resp.imei in self.by_imei:
162 self.by_imei[resp.imei].send(resp.packet)
164 log.info("Not connected (IMEI %s)", resp.imei)
167 def runserver(conf: ConfigParser, handle_hibernate: bool = True) -> None:
168 # Is this https://github.com/zeromq/pyzmq/issues/1627 still not fixed?!
169 zctx = zmq.Context() # type: ignore
170 zpub = zctx.socket(zmq.PUB) # type: ignore
171 zpull = zctx.socket(zmq.PULL) # type: ignore
172 oldmask = umask(0o117)
173 zpub.bind(conf.get("collector", "publishurl"))
174 zpull.bind(conf.get("collector", "listenurl"))
176 tcpl = socket(AF_INET6, SOCK_STREAM)
177 tcpl.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
178 tcpl.bind(("", conf.getint("collector", "port")))
180 tcpfd = tcpl.fileno()
181 poller = zmq.Poller() # type: ignore
182 poller.register(zpull, flags=zmq.POLLIN)
183 poller.register(tcpfd, flags=zmq.POLLIN)
190 events = poller.poll(1000)
191 for sk, fl in events:
195 msg = zpull.recv(zmq.NOBLOCK)
201 clntsock, clntaddr = tcpl.accept()
202 clntsock.setsockopt(SOL_SOCKET, SO_KEEPALIVE, 1)
203 topoll.append((clntsock, clntaddr))
204 elif fl & zmq.POLLIN:
205 received = clients.recv(sk)
207 log.debug("Terminal gone from fd %d", sk)
210 for imei, when, peeraddr, packet in received:
211 proto = proto_of_message(packet)
221 if proto == HIBERNATION.PROTO and handle_hibernate:
223 "HIBERNATION from fd %d (IMEI %s)",
228 respmsg = inline_response(packet)
229 if respmsg is not None:
231 Resp(imei=imei, when=when, packet=respmsg)
234 log.debug("Stray event: %s on socket %s", fl, sk)
235 # poll queue consumed, make changes now
240 proto=proto_of_message(zmsg.packet),
246 log.debug("Sending to the client: %s", zmsg)
247 clients.response(zmsg)
249 poller.unregister(fd) # type: ignore
251 for clntsock, clntaddr in topoll:
252 fd = clients.add(clntsock, clntaddr)
253 poller.register(fd, flags=zmq.POLLIN)
254 except KeyboardInterrupt:
257 zctx.destroy() # type: ignore
261 if __name__.endswith("__main__"):
262 runserver(common.init(log))