blob: d426da027703afaf8f0117f50b93b82b3e6703b5 [file] [log] [blame]
cliechtif1c882c2009-07-23 02:30:06 +00001#! /usr/bin/env python
Chris Liechtifbdd8a02015-08-09 02:37:45 +02002#
3# (C) 2001-2015 Chris Liechti <cliechti@gmx.net>
4#
5# SPDX-License-Identifier: BSD-3-Clause
cliechtif1c882c2009-07-23 02:30:06 +00006"""\
7Multi-port serial<->TCP/IP forwarder.
cliechti32c10332009-08-05 13:23:43 +00008- RFC 2217
cliechtif1c882c2009-07-23 02:30:06 +00009- check existence of serial port periodically
10- start/stop forwarders
11- each forwarder creates a server socket and opens the serial port
12- serial ports are opened only once. network connect/disconnect
13 does not influence serial port
14- only one client per connection
15"""
Chris Liechti4caf6a52015-08-04 01:07:45 +020016import os
cliechtif1c882c2009-07-23 02:30:06 +000017import select
Chris Liechti4caf6a52015-08-04 01:07:45 +020018import socket
19import sys
20import time
21import traceback
cliechti32c10332009-08-05 13:23:43 +000022
cliechtif1c882c2009-07-23 02:30:06 +000023import serial
cliechti32c10332009-08-05 13:23:43 +000024import serial.rfc2217
Chris Liechti6e683ed2015-08-04 16:59:04 +020025import serial.tools.list_ports
cliechti32c10332009-08-05 13:23:43 +000026
cliechtif1c882c2009-07-23 02:30:06 +000027import dbus
28
Chris Liechti6e683ed2015-08-04 16:59:04 +020029# Try to import the avahi service definitions properly. If the avahi module is
30# not available, fall back to a hard-coded solution that hopefully still works.
31try:
32 import avahi
33except ImportError:
34 class avahi:
35 DBUS_NAME = "org.freedesktop.Avahi"
36 DBUS_PATH_SERVER = "/"
37 DBUS_INTERFACE_SERVER = "org.freedesktop.Avahi.Server"
38 DBUS_INTERFACE_ENTRY_GROUP = DBUS_NAME + ".EntryGroup"
39 IF_UNSPEC = -1
Chris Liechtia4cde702015-09-08 01:11:52 +020040 PROTO_UNSPEC, PROTO_INET, PROTO_INET6 = -1, 0, 1
Chris Liechti6e683ed2015-08-04 16:59:04 +020041
42
cliechtif1c882c2009-07-23 02:30:06 +000043class ZeroconfService:
44 """\
45 A simple class to publish a network service with zeroconf using avahi.
46 """
47
48 def __init__(self, name, port, stype="_http._tcp",
49 domain="", host="", text=""):
50 self.name = name
51 self.stype = stype
52 self.domain = domain
53 self.host = host
54 self.port = port
55 self.text = text
56 self.group = None
57
58 def publish(self):
59 bus = dbus.SystemBus()
60 server = dbus.Interface(
61 bus.get_object(
62 avahi.DBUS_NAME,
63 avahi.DBUS_PATH_SERVER
64 ),
65 avahi.DBUS_INTERFACE_SERVER
66 )
67
68 g = dbus.Interface(
69 bus.get_object(
70 avahi.DBUS_NAME,
71 server.EntryGroupNew()
72 ),
73 avahi.DBUS_INTERFACE_ENTRY_GROUP
74 )
75
76 g.AddService(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, dbus.UInt32(0),
77 self.name, self.stype, self.domain, self.host,
78 dbus.UInt16(self.port), self.text)
79
80 g.Commit()
81 self.group = g
82
83 def unpublish(self):
84 if self.group is not None:
85 self.group.Reset()
86 self.group = None
87
88 def __str__(self):
89 return "%r @ %s:%s (%s)" % (self.name, self.host, self.port, self.stype)
90
91
cliechtif1c882c2009-07-23 02:30:06 +000092class Forwarder(ZeroconfService):
93 """\
94 Single port serial<->TCP/IP forarder that depends on an external select
cliechti32c10332009-08-05 13:23:43 +000095 loop.
96 - Buffers for serial -> network and network -> serial
97 - RFC 2217 state
98 - Zeroconf publish/unpublish on open/close.
cliechtif1c882c2009-07-23 02:30:06 +000099 """
100
Chris Liechti825637e2015-08-05 14:14:00 +0200101 def __init__(self, device, name, network_port, on_close=None, log=None):
cliechtif1c882c2009-07-23 02:30:06 +0000102 ZeroconfService.__init__(self, name, network_port, stype='_serial_port._tcp')
103 self.alive = False
104 self.network_port = network_port
105 self.on_close = on_close
Chris Liechti825637e2015-08-05 14:14:00 +0200106 self.log = log
cliechtif1c882c2009-07-23 02:30:06 +0000107 self.device = device
108 self.serial = serial.Serial()
109 self.serial.port = device
110 self.serial.baudrate = 115200
111 self.serial.timeout = 0
112 self.socket = None
113 self.server_socket = None
Chris Liechtia4cde702015-09-08 01:11:52 +0200114 self.rfc2217 = None # instantiate later, when connecting
cliechtif1c882c2009-07-23 02:30:06 +0000115
116 def __del__(self):
117 try:
Chris Liechtia4cde702015-09-08 01:11:52 +0200118 if self.alive:
119 self.close()
cliechtif1c882c2009-07-23 02:30:06 +0000120 except:
Chris Liechtia4cde702015-09-08 01:11:52 +0200121 pass # XXX errors on shutdown
cliechtif1c882c2009-07-23 02:30:06 +0000122
123 def open(self):
124 """open serial port, start network server and publish service"""
Chris Liechti01587b12015-08-05 02:39:32 +0200125 self.buffer_net2ser = bytearray()
126 self.buffer_ser2net = bytearray()
cliechtif1c882c2009-07-23 02:30:06 +0000127
128 # open serial port
129 try:
Chris Liechtia4cde702015-09-08 01:11:52 +0200130 self.serial.rts = False
cliechtif1c882c2009-07-23 02:30:06 +0000131 self.serial.open()
Chris Liechti4caf6a52015-08-04 01:07:45 +0200132 except Exception as msg:
cliechtif1c882c2009-07-23 02:30:06 +0000133 self.handle_serial_error(msg)
134
Chris Liechtia4cde702015-09-08 01:11:52 +0200135 self.serial_settings_backup = self.serial.get_settings()
cliechtid9a06ce2009-08-10 01:30:53 +0000136
cliechtif1c882c2009-07-23 02:30:06 +0000137 # start the socket server
Chris Liechti977916f2015-08-08 17:12:53 +0200138 # XXX add IPv6 support: use getaddrinfo for socket options, bind to multiple sockets?
139 # info_list = socket.getaddrinfo(None, port, 0, socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
cliechtif1c882c2009-07-23 02:30:06 +0000140 self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
141 self.server_socket.setsockopt(
142 socket.SOL_SOCKET,
143 socket.SO_REUSEADDR,
144 self.server_socket.getsockopt(
145 socket.SOL_SOCKET,
146 socket.SO_REUSEADDR
147 ) | 1
148 )
149 self.server_socket.setblocking(0)
150 try:
Chris Liechtia4cde702015-09-08 01:11:52 +0200151 self.server_socket.bind(('', self.network_port))
cliechtif1c882c2009-07-23 02:30:06 +0000152 self.server_socket.listen(1)
Chris Liechti4caf6a52015-08-04 01:07:45 +0200153 except socket.error as msg:
cliechtif1c882c2009-07-23 02:30:06 +0000154 self.handle_server_error()
155 #~ raise
Chris Liechti825637e2015-08-05 14:14:00 +0200156 if self.log is not None:
157 self.log.info("%s: Waiting for connection on %s..." % (self.device, self.network_port))
cliechtif1c882c2009-07-23 02:30:06 +0000158
159 # zeroconfig
160 self.publish()
161
162 # now we are ready
163 self.alive = True
164
165 def close(self):
166 """Close all resources and unpublish service"""
Chris Liechti825637e2015-08-05 14:14:00 +0200167 if self.log is not None:
168 self.log.info("%s: closing..." % (self.device, ))
cliechtif1c882c2009-07-23 02:30:06 +0000169 self.alive = False
170 self.unpublish()
Chris Liechtia4cde702015-09-08 01:11:52 +0200171 if self.server_socket:
172 self.server_socket.close()
cliechtif1c882c2009-07-23 02:30:06 +0000173 if self.socket:
174 self.handle_disconnect()
175 self.serial.close()
176 if self.on_close is not None:
177 # ensure it is only called once
178 callback = self.on_close
179 self.on_close = None
180 callback(self)
181
cliechti32c10332009-08-05 13:23:43 +0000182 def write(self, data):
183 """the write method is used by serial.rfc2217.PortManager. it has to
184 write to the network."""
185 self.buffer_ser2net += data
186
cliechtif1c882c2009-07-23 02:30:06 +0000187 def update_select_maps(self, read_map, write_map, error_map):
188 """Update dictionaries for select call. insert fd->callback mapping"""
189 if self.alive:
190 # always handle serial port reads
191 read_map[self.serial] = self.handle_serial_read
192 error_map[self.serial] = self.handle_serial_error
193 # handle serial port writes if buffer is not empty
194 if self.buffer_net2ser:
195 write_map[self.serial] = self.handle_serial_write
196 # handle network
197 if self.socket is not None:
198 # handle socket if connected
199 # only read from network if the internal buffer is not
200 # already filled. the TCP flow control will hold back data
201 if len(self.buffer_net2ser) < 2048:
202 read_map[self.socket] = self.handle_socket_read
203 # only check for write readiness when there is data
204 if self.buffer_ser2net:
205 write_map[self.socket] = self.handle_socket_write
206 error_map[self.socket] = self.handle_socket_error
207 else:
208 # no connection, ensure clear buffer
Chris Liechti01587b12015-08-05 02:39:32 +0200209 self.buffer_ser2net = bytearray()
cliechtif1c882c2009-07-23 02:30:06 +0000210 # check the server socket
211 read_map[self.server_socket] = self.handle_connect
212 error_map[self.server_socket] = self.handle_server_error
213
cliechtif1c882c2009-07-23 02:30:06 +0000214 def handle_serial_read(self):
215 """Reading from serial port"""
216 try:
217 data = os.read(self.serial.fileno(), 1024)
218 if data:
219 # store data in buffer if there is a client connected
220 if self.socket is not None:
cliechti32c10332009-08-05 13:23:43 +0000221 # escape outgoing data when needed (Telnet IAC (0xff) character)
222 if self.rfc2217:
223 data = serial.to_bytes(self.rfc2217.escape(data))
cliechtif1c882c2009-07-23 02:30:06 +0000224 self.buffer_ser2net += data
225 else:
226 self.handle_serial_error()
Chris Liechti4caf6a52015-08-04 01:07:45 +0200227 except Exception as msg:
cliechtif1c882c2009-07-23 02:30:06 +0000228 self.handle_serial_error(msg)
229
230 def handle_serial_write(self):
231 """Writing to serial port"""
232 try:
233 # write a chunk
Chris Liechti01587b12015-08-05 02:39:32 +0200234 n = os.write(self.serial.fileno(), bytes(self.buffer_net2ser))
cliechtif1c882c2009-07-23 02:30:06 +0000235 # and see how large that chunk was, remove that from buffer
236 self.buffer_net2ser = self.buffer_net2ser[n:]
Chris Liechti4caf6a52015-08-04 01:07:45 +0200237 except Exception as msg:
cliechtif1c882c2009-07-23 02:30:06 +0000238 self.handle_serial_error(msg)
239
240 def handle_serial_error(self, error=None):
241 """Serial port error"""
242 # terminate connection
243 self.close()
244
245 def handle_socket_read(self):
246 """Read from socket"""
247 try:
248 # read a chunk from the serial port
249 data = self.socket.recv(1024)
250 if data:
cliechti32c10332009-08-05 13:23:43 +0000251 # Process RFC 2217 stuff when enabled
252 if self.rfc2217:
253 data = serial.to_bytes(self.rfc2217.filter(data))
cliechtif1c882c2009-07-23 02:30:06 +0000254 # add data to buffer
255 self.buffer_net2ser += data
256 else:
257 # empty read indicates disconnection
258 self.handle_disconnect()
259 except socket.error:
260 self.handle_socket_error()
261
262 def handle_socket_write(self):
263 """Write to socket"""
264 try:
265 # write a chunk
Chris Liechti01587b12015-08-05 02:39:32 +0200266 count = self.socket.send(bytes(self.buffer_ser2net))
cliechtif1c882c2009-07-23 02:30:06 +0000267 # and remove the sent data from the buffer
268 self.buffer_ser2net = self.buffer_ser2net[count:]
269 except socket.error:
270 self.handle_socket_error()
271
272 def handle_socket_error(self):
273 """Socket connection fails"""
274 self.handle_disconnect()
275
276 def handle_connect(self):
277 """Server socket gets a connection"""
278 # accept a connection in any case, close connection
279 # below if already busy
280 connection, addr = self.server_socket.accept()
281 if self.socket is None:
282 self.socket = connection
Chris Liechti6e683ed2015-08-04 16:59:04 +0200283 # More quickly detect bad clients who quit without closing the
284 # connection: After 1 second of idle, start sending TCP keep-alive
285 # packets every 1 second. If 3 consecutive keep-alive packets
286 # fail, assume the client is gone and close the connection.
287 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
288 self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 1)
289 self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 1)
290 self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)
cliechtif1c882c2009-07-23 02:30:06 +0000291 self.socket.setblocking(0)
cliechtia35cad42009-08-10 20:57:48 +0000292 self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
Chris Liechti825637e2015-08-05 14:14:00 +0200293 if self.log is not None:
Chris Liechti977916f2015-08-08 17:12:53 +0200294 self.log.warning('%s: Connected by %s:%s' % (self.device, addr[0], addr[1]))
Chris Liechtia4cde702015-09-08 01:11:52 +0200295 self.serial.rts = True
296 self.serial.dtr = True
Chris Liechti825637e2015-08-05 14:14:00 +0200297 if self.log is not None:
298 self.rfc2217 = serial.rfc2217.PortManager(self.serial, self, logger=log.getChild(self.device))
299 else:
300 self.rfc2217 = serial.rfc2217.PortManager(self.serial, self)
cliechtif1c882c2009-07-23 02:30:06 +0000301 else:
302 # reject connection if there is already one
303 connection.close()
Chris Liechti825637e2015-08-05 14:14:00 +0200304 if self.log is not None:
Chris Liechti977916f2015-08-08 17:12:53 +0200305 self.log.warning('%s: Rejecting connect from %s:%s' % (self.device, addr[0], addr[1]))
cliechtif1c882c2009-07-23 02:30:06 +0000306
307 def handle_server_error(self):
cliechti7aed8332009-08-05 14:19:31 +0000308 """Socket server fails"""
cliechtif1c882c2009-07-23 02:30:06 +0000309 self.close()
310
311 def handle_disconnect(self):
312 """Socket gets disconnected"""
cliechtid9a06ce2009-08-10 01:30:53 +0000313 # signal disconnected terminal with control lines
cliechtie67c1f42009-09-10 15:07:44 +0000314 try:
Chris Liechtia4cde702015-09-08 01:11:52 +0200315 self.serial.rts = False
316 self.serial.dtr = False
cliechtie67c1f42009-09-10 15:07:44 +0000317 finally:
318 # restore original port configuration in case it was changed
Chris Liechtia4cde702015-09-08 01:11:52 +0200319 self.serial.apply_settings(self.serial_settings_backup)
cliechtie67c1f42009-09-10 15:07:44 +0000320 # stop RFC 2217 state machine
321 self.rfc2217 = None
322 # clear send buffer
Chris Liechti01587b12015-08-05 02:39:32 +0200323 self.buffer_ser2net = bytearray()
cliechtie67c1f42009-09-10 15:07:44 +0000324 # close network connection
325 if self.socket is not None:
326 self.socket.close()
327 self.socket = None
Chris Liechti825637e2015-08-05 14:14:00 +0200328 if self.log is not None:
Chris Liechti977916f2015-08-08 17:12:53 +0200329 self.log.warning('%s: Disconnected' % self.device)
cliechtif1c882c2009-07-23 02:30:06 +0000330
331
332def test():
333 service = ZeroconfService(name="TestService", port=3000)
334 service.publish()
335 raw_input("Press any key to unpublish the service ")
336 service.unpublish()
337
338
339if __name__ == '__main__':
Chris Liechti01587b12015-08-05 02:39:32 +0200340 import logging
Chris Liechtia4cde702015-09-08 01:11:52 +0200341 import argparse
cliechtif1c882c2009-07-23 02:30:06 +0000342
Chris Liechti825637e2015-08-05 14:14:00 +0200343 VERBOSTIY = [
344 logging.ERROR, # 0
345 logging.WARNING, # 1 (default)
346 logging.INFO, # 2
347 logging.DEBUG, # 3
348 ]
349
Chris Liechtia4cde702015-09-08 01:11:52 +0200350 parser = argparse.ArgumentParser(usage="""\
351%(prog)s [options]
cliechtif1c882c2009-07-23 02:30:06 +0000352
353Announce the existence of devices using zeroconf and provide
cliechti32c10332009-08-05 13:23:43 +0000354a TCP/IP <-> serial port gateway (implements RFC 2217).
cliechtif1c882c2009-07-23 02:30:06 +0000355
cliechtif1c882c2009-07-23 02:30:06 +0000356If running as daemon, write to syslog. Otherwise write to stdout.
Chris Liechtia4cde702015-09-08 01:11:52 +0200357""",
358 epilog="""\
359NOTE: no security measures are implemented. Anyone can remotely connect
360to this service over the network.
361
362Only one connection at once, per port, is supported. When the connection is
363terminated, it waits for the next connect.
cliechtif1c882c2009-07-23 02:30:06 +0000364""")
365
Chris Liechtia4cde702015-09-08 01:11:52 +0200366 group = parser.add_argument_group("serial port settings")
cliechtif1c882c2009-07-23 02:30:06 +0000367
Chris Liechtia4cde702015-09-08 01:11:52 +0200368 group.add_argument(
369 "--ports-regex",
370 help="specify a regex to search against the serial devices and their descriptions (default: %(default)s)",
Chris Liechti9d61c302015-08-05 14:35:16 +0200371 default='/dev/ttyUSB[0-9]+',
372 metavar="REGEX")
Chris Liechti825637e2015-08-05 14:14:00 +0200373
Chris Liechtia4cde702015-09-08 01:11:52 +0200374 group = parser.add_argument_group("network settings")
cliechtif1c882c2009-07-23 02:30:06 +0000375
Chris Liechtia4cde702015-09-08 01:11:52 +0200376 group.add_argument(
377 "--tcp-port",
Chris Liechti7225dd72015-08-04 17:08:05 +0200378 dest="base_port",
Chris Liechtia4cde702015-09-08 01:11:52 +0200379 help="specify lowest TCP port number (default: %(default)s)",
Chris Liechti7225dd72015-08-04 17:08:05 +0200380 default=7000,
Chris Liechtia4cde702015-09-08 01:11:52 +0200381 type=int,
Chris Liechti7225dd72015-08-04 17:08:05 +0200382 metavar="PORT")
383
Chris Liechtia4cde702015-09-08 01:11:52 +0200384 group = parser.add_argument_group("daemon")
Chris Liechti9d61c302015-08-05 14:35:16 +0200385
Chris Liechtia4cde702015-09-08 01:11:52 +0200386 group.add_argument(
387 "-d", "--daemon",
Chris Liechti9d61c302015-08-05 14:35:16 +0200388 dest="daemonize",
389 action="store_true",
390 help="start as daemon",
391 default=False)
392
Chris Liechtia4cde702015-09-08 01:11:52 +0200393 group.add_argument(
394 "--pidfile",
Chris Liechti9d61c302015-08-05 14:35:16 +0200395 help="specify a name for the PID file",
396 default=None,
397 metavar="FILE")
398
Chris Liechtia4cde702015-09-08 01:11:52 +0200399 group = parser.add_argument_group("diagnostics")
Chris Liechti9d61c302015-08-05 14:35:16 +0200400
Chris Liechtia4cde702015-09-08 01:11:52 +0200401 group.add_argument(
402 "-o", "--logfile",
Chris Liechti9d61c302015-08-05 14:35:16 +0200403 help="write messages file instead of stdout",
404 default=None,
405 metavar="FILE")
406
Chris Liechtia4cde702015-09-08 01:11:52 +0200407 group.add_argument(
408 "-q", "--quiet",
Chris Liechti9d61c302015-08-05 14:35:16 +0200409 dest="verbosity",
410 action="store_const",
411 const=0,
412 help="suppress most diagnostic messages",
Chris Liechti9d61c302015-08-05 14:35:16 +0200413 default=1)
414
Chris Liechtia4cde702015-09-08 01:11:52 +0200415 group.add_argument(
416 "-v", "--verbose",
417 dest="verbosity",
418 action="count",
419 help="increase diagnostic messages")
Chris Liechti6e683ed2015-08-04 16:59:04 +0200420
Chris Liechtia4cde702015-09-08 01:11:52 +0200421
422 args = parser.parse_args()
cliechtif1c882c2009-07-23 02:30:06 +0000423
Chris Liechti825637e2015-08-05 14:14:00 +0200424 # set up logging
Chris Liechtia4cde702015-09-08 01:11:52 +0200425 logging.basicConfig(level=VERBOSTIY[min(args.verbosity, len(VERBOSTIY) - 1)])
Chris Liechti825637e2015-08-05 14:14:00 +0200426 log = logging.getLogger('port_publisher')
Chris Liechti01587b12015-08-05 02:39:32 +0200427
cliechtif1c882c2009-07-23 02:30:06 +0000428 # redirect output if specified
Chris Liechtia4cde702015-09-08 01:11:52 +0200429 if args.logfile is not None:
cliechtif1c882c2009-07-23 02:30:06 +0000430 class WriteFlushed:
431 def __init__(self, fileobj):
432 self.fileobj = fileobj
433 def write(self, s):
434 self.fileobj.write(s)
435 self.fileobj.flush()
436 def close(self):
437 self.fileobj.close()
Chris Liechtia4cde702015-09-08 01:11:52 +0200438 sys.stdout = sys.stderr = WriteFlushed(open(args.logfile, 'a'))
cliechtif1c882c2009-07-23 02:30:06 +0000439 # atexit.register(lambda: sys.stdout.close())
440
Chris Liechtia4cde702015-09-08 01:11:52 +0200441 if args.daemonize:
cliechtif1c882c2009-07-23 02:30:06 +0000442 # if running as daemon is requested, do the fork magic
Chris Liechtia4cde702015-09-08 01:11:52 +0200443 # args.quiet = True
cliechtif1c882c2009-07-23 02:30:06 +0000444 # do the UNIX double-fork magic, see Stevens' "Advanced
445 # Programming in the UNIX Environment" for details (ISBN 0201563177)
446 try:
447 pid = os.fork()
448 if pid > 0:
449 # exit first parent
450 sys.exit(0)
Chris Liechti4caf6a52015-08-04 01:07:45 +0200451 except OSError as e:
Chris Liechti825637e2015-08-05 14:14:00 +0200452 log.critical("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
cliechtif1c882c2009-07-23 02:30:06 +0000453 sys.exit(1)
454
455 # decouple from parent environment
456 os.chdir("/") # don't prevent unmounting....
457 os.setsid()
458 os.umask(0)
459
460 # do second fork
461 try:
462 pid = os.fork()
463 if pid > 0:
Chris Liechti9d61c302015-08-05 14:35:16 +0200464 # exit from second parent, save eventual PID before
Chris Liechtia4cde702015-09-08 01:11:52 +0200465 if args.pidfile is not None:
466 open(args.pidfile, 'w').write("%d" % pid)
cliechtif1c882c2009-07-23 02:30:06 +0000467 sys.exit(0)
Chris Liechti4caf6a52015-08-04 01:07:45 +0200468 except OSError as e:
Chris Liechti825637e2015-08-05 14:14:00 +0200469 log.critical("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
cliechtif1c882c2009-07-23 02:30:06 +0000470 sys.exit(1)
471
Chris Liechtia4cde702015-09-08 01:11:52 +0200472 if args.logfile is None:
cliechtif1c882c2009-07-23 02:30:06 +0000473 import syslog
474 syslog.openlog("serial port publisher")
475 # redirect output to syslog
476 class WriteToSysLog:
477 def __init__(self):
478 self.buffer = ''
479 def write(self, s):
480 self.buffer += s
481 if '\n' in self.buffer:
482 output, self.buffer = self.buffer.split('\n', 1)
483 syslog.syslog(output)
484 def flush(self):
485 syslog.syslog(self.buffer)
486 self.buffer = ''
487 def close(self):
488 self.flush()
489 sys.stdout = sys.stderr = WriteToSysLog()
490
491 # ensure the that the daemon runs a normal user, if run as root
492 #if os.getuid() == 0:
493 # name, passwd, uid, gid, desc, home, shell = pwd.getpwnam('someuser')
494 # os.setgid(gid) # set group first
495 # os.setuid(uid) # set user
496
497 # keep the published stuff in a dictionary
498 published = {}
cliechtif1c882c2009-07-23 02:30:06 +0000499 # get a nice hostname
500 hostname = socket.gethostname()
501
502 def unpublish(forwarder):
503 """when forwarders die, we need to unregister them"""
504 try:
505 del published[forwarder.device]
506 except KeyError:
507 pass
508 else:
Chris Liechti825637e2015-08-05 14:14:00 +0200509 log.info("unpublish: %s" % (forwarder))
cliechtif1c882c2009-07-23 02:30:06 +0000510
511 alive = True
512 next_check = 0
513 # main loop
514 while alive:
515 try:
516 # if it is time, check for serial port devices
517 now = time.time()
518 if now > next_check:
519 next_check = now + 5
Chris Liechtia4cde702015-09-08 01:11:52 +0200520 connected = [d for d, p, i in serial.tools.list_ports.grep(args.ports_regex)]
Chris Liechti6e683ed2015-08-04 16:59:04 +0200521 # Handle devices that are published, but no longer connected
522 for device in set(published).difference(connected):
Chris Liechti825637e2015-08-05 14:14:00 +0200523 log.info("unpublish: %s" % (published[device]))
Chris Liechti6e683ed2015-08-04 16:59:04 +0200524 unpublish(published[device])
525 # Handle devices that are connected but not yet published
526 for device in set(connected).difference(published):
527 # Find the first available port, starting from 7000
Chris Liechtia4cde702015-09-08 01:11:52 +0200528 port = args.base_port
Chris Liechti6e683ed2015-08-04 16:59:04 +0200529 ports_in_use = [f.network_port for f in published.values()]
530 while port in ports_in_use:
531 port += 1
532 published[device] = Forwarder(
Chris Liechti9d61c302015-08-05 14:35:16 +0200533 device,
534 "%s on %s" % (device, hostname),
535 port,
536 on_close=unpublish,
537 log=log
Chris Liechti6e683ed2015-08-04 16:59:04 +0200538 )
Chris Liechti825637e2015-08-05 14:14:00 +0200539 log.warning("publish: %s" % (published[device]))
Chris Liechti6e683ed2015-08-04 16:59:04 +0200540 published[device].open()
cliechtif1c882c2009-07-23 02:30:06 +0000541
542 # select_start = time.time()
543 read_map = {}
544 write_map = {}
545 error_map = {}
546 for publisher in published.values():
547 publisher.update_select_maps(read_map, write_map, error_map)
Chris Liechti8f9b4442015-08-05 14:57:30 +0200548 readers, writers, errors = select.select(
549 read_map.keys(),
550 write_map.keys(),
551 error_map.keys(),
552 5
553 )
cliechtif1c882c2009-07-23 02:30:06 +0000554 # select_end = time.time()
555 # print "select used %.3f s" % (select_end - select_start)
556 for reader in readers:
557 read_map[reader]()
558 for writer in writers:
559 write_map[writer]()
560 for error in errors:
561 error_map[error]()
562 # print "operation used %.3f s" % (time.time() - select_end)
563 except KeyboardInterrupt:
564 alive = False
Chris Liechti7225dd72015-08-04 17:08:05 +0200565 sys.stdout.write('\n')
cliechtif1c882c2009-07-23 02:30:06 +0000566 except SystemExit:
567 raise
568 except:
cliechtie67c1f42009-09-10 15:07:44 +0000569 #~ raise
cliechtif1c882c2009-07-23 02:30:06 +0000570 traceback.print_exc()