#! /usr/bin/env python3 # -*- coding: utf-8 -*- # # @RDE_HEADER@ # $Id$ # $HeadURL$ # $Author$ # Created by Christian Fröbel, NLE, 2019-01-29 # import socket import sys import textwrap import threading import weakref from argparse import ArgumentParser, RawDescriptionHelpFormatter from collections import namedtuple from contextlib import ExitStack, contextmanager from queue import Empty, Queue from socketserver import StreamRequestHandler, TCPServer, ThreadingMixIn from threading import Lock, Thread @contextmanager def autoShutdown(target): try: yield target finally: target.initShutdown() @contextmanager def autoJoin(thread): try: yield thread finally: thread.join() @contextmanager def startThreadContext(target, thread): with ExitStack() as stack: thread.start() stack.enter_context(autoJoin(thread)) stack.enter_context(autoShutdown(target)) yield # # Hier registrieren sich die Sender für die Verbindungen zu den AVS_Interface-Instanzen. # class _Registry(object): def __init__(self): self._senders = [] self._mutex = Lock() @property def senders(self): with self._mutex: return tuple(self._senders) def register(self, sender): with self._mutex: assert sender not in self._senders, 'sender is registered already' self._senders.append(sender) print('client count: {}'.format(len(self._senders))) def unregister(self, sender): with self._mutex: try: self._senders.remove(sender) except ValueError: assert False, 'sender is not registered' print('client count: {}'.format(len(self._senders))) class _Server(ThreadingMixIn, TCPServer): allow_reuse_address = True timeout = 1 class _Acceptor(object): def __init__(self, port, registry, handlerFactory): self._port = port self._registry = registry self._handlerFactory = handlerFactory self._doRun = True def run(self): server = _Server(('0.0.0.0', self._port), self._handlerFactory) while self._doRun: server.handle_request() # mit Timeout self._joinFinishedHandlers() self._joinAllHandlers() def initShutdown(self): self._doRun = False def _joinFinishedHandlers(self): for handler in self._registry.senders: if not handler.isAlive(): handler.join(0) # ohne Timeout def _joinAllHandlers(self): for handler in self._registry.senders: handler.initShutdown() for handler in self._registry.senders: handler.join() class _RequestHandlerFactory(object): def __init__(self, cmdQueue, registry, channelManager): self._cmdQueue = cmdQueue self._registry = registry self._channelManager = channelManager def __call__(self, *args): return _RequestHandler(self._cmdQueue, self._registry, self._channelManager, *args) # # Läuft in einem separaten Thread. Den Thread setzen wir aber nicht selbst auf. # Daher müssen wir den erst ermitteln. # class _RequestHandler(StreamRequestHandler): def __init__(self, cmdQueue, registry, channelManager, *args): # Reihenfolge wichtig; super() muss am Ende stehen self._cmdQueue = cmdQueue self._registry = registry self._channelManager = channelManager self._thread = None super().__init__(*args) def handle(self): print('new connection from: {}'.format(self.client_address)) self._thread = threading.current_thread() self._registry.register(self) self._channelManager.activateAllChannels(self) while True: line = self.rfile.readline().decode('utf-8') if len(line) == 0: # EOF break self._process(line) def send(self, line): terminatedLine = line + '\r\n' self.wfile.write(terminatedLine.encode('utf-8')) def isAlive(self): return self._thread.is_alive() def join(self, timeout=None): self._thread.join(timeout) self._registry.unregister(self) # Schließen des Sockets (der Leseseite), damit der Service aus dem blockierenden read() rauskommt. def initShutdown(self): _shutdownAndCloseSocket(self.request) def _process(self, line): cmd = self._parseLine(line) self.send('200 ok') self._cmdQueue.put(cmd) # Zeile parsen und AvsCommand liefern def _parseLine(self, line): parts = line.split() token = parts.pop(0).lower() args = parts if token == 'speechreq': assert len(args) == 1, 'invalid command format' elif token == 'speechreqwd': assert len(args) == 1, 'invalid command format' elif token == 'channelusage': assert len(args) == 3, 'invalid command format' elif token == 'configchanged': assert len(args) == 0, 'invalid command format' elif token == 'keepalive': assert len(args) == 0, 'invalid command format' else: assert False, 'unknown command: {}'.format(line) return AvsCommand(token, args) AvsCommand = namedtuple('AvsCommand', 'token, args') # # Liest die Control-Commands aus einem file-like Stream (z.B. stdin). # Sollte im Main-Thread laufen, weil hier auch die Behandlung von KeyboardInterrupt (Ctrl-C) gemacht wird. # class _ControlRequestHandler(object): def __init__(self, inputStream, cmdQueue): self._inputStream = inputStream self._cmdQueue = cmdQueue def handle(self): try: self._handle() except KeyboardInterrupt: pass def _handle(self): while True: line = self._inputStream.readline() if len(line) == 0: # EOF break self._process(line) def _process(self, line): cmd = self._parseLine(line.rstrip()) if cmd is not None: self._cmdQueue.put(cmd) def _parseLine(self, line): parts = line.split() if len(parts) == 0: return None token = parts.pop(0).lower() args = parts if token == 'speechreq': assert len(args) == 1, 'invalid command format' elif token == 'speechreqwd': assert len(args) == 1, 'invalid command format' else: print('unknown command: {}'.format(line)) return None return CtrlCommand(token, args) CtrlCommand = namedtuple('CtrlCommand', 'token, args') class _CommandExecutor(object): def __init__(self, cmdQueue, channelManager): self._cmdQueue = cmdQueue self._channelManager = channelManager self._doRun = True def run(self): while True: try: cmd = self._cmdQueue.get(timeout=1) except Empty: if self._doRun: continue else: break self._process(cmd) def initShutdown(self): self._doRun = False def _process(self, cmd): if cmd.token in ('speechreq', 'speechreqwd'): self._channelManager.handle(cmd) return # den Rest ignorieren class _ChannelState(object): def __init__(self, channel, registry): self._channel = channel self._registry = registry def handle(self, cmd): if isinstance(cmd, AvsCommand): return self._handleAvsCommand(cmd) else: return self._handleCtrlCommand(cmd) def _handleAvsCommand(self, cmd): assert False, 'must be implemented' def _handleCtrlCommand(self, cmd): assert False, 'must be implemented' def _send(self, line): for sender in self._registry.senders: sender.send(line) class _Idle(_ChannelState): def _handleAvsCommand(self, cmd): if cmd.token == 'speechreq': self._send('205 {} speechgrantop'.format(self._channel.ssrc)) return self._channel.avsTalking return self def _handleCtrlCommand(self, cmd): if cmd.token == 'speechreq': self._send('206 {} speechgrant'.format(self._channel.ssrc)) return self._channel.fuGeTalking return self def __str__(self): return '{}: Idle'.format(self._channel.ssrc) class _AvsTalking(_ChannelState): def _handleAvsCommand(self, cmd): if cmd.token == 'speechreqwd': self._send('207 {} speechreqwd'.format(self._channel.ssrc)) return self._channel.idle return self def _handleCtrlCommand(self, cmd): return self def __str__(self): return '{}: AvsTalking'.format(self._channel.ssrc) class _FuGeTalking(_ChannelState): def _handleAvsCommand(self, cmd): return self def _handleCtrlCommand(self, cmd): if cmd.token == 'speechreqwd': self._send('207 {} speechreqwd'.format(self._channel.ssrc)) return self._channel.idle return self def __str__(self): return '{}: FuGeTalking'.format(self._channel.ssrc) class _Channel(object): def __init__(self, ssrc, registry): self._ssrc = ssrc self._idle = _Idle(weakref.proxy(self), registry) self._avsTalking = _AvsTalking(weakref.proxy(self), registry) self._fuGeTalking = _FuGeTalking(weakref.proxy(self), registry) self._currentState = self._idle @property def ssrc(self): return self._ssrc @property def idle(self): return self._idle @property def avsTalking(self): return self._avsTalking @property def fuGeTalking(self): return self._fuGeTalking def handle(self, cmd): self._currentState = self._currentState.handle(cmd) print(self._currentState) class _ChannelManager(object): def __init__(self, ssrcStart, channelCount, registry): self._ssrcStart = ssrcStart self._channels = [] for i in range(channelCount): ssrc = i + self._ssrcStart self._channels.append(_Channel(ssrc, registry)) def handle(self, cmd): assert cmd.token in ('speechreq', 'speechreqwd'), 'cannot handle command: {} {}'.format(cmd.token, cmd.args) try: ssrc = int(cmd.args[0]) except (ValueError, IndexError): assert False, 'ssrc expected as first arg: {} {}'.format(cmd.token, cmd.args) index = ssrc - self._ssrcStart assert index >= 0 and index < len(self._channels), 'ssrc out of range: {}'.format(ssrc) channel = self._channels[index] channel.handle(cmd) def activateAllChannels(self, sender): for c in self._channels: sender.send('208 {} trcallactiv'.format(c.ssrc)) def _shutdownAndCloseSocket(sock): try: sock.shutdown(socket.SHUT_RD) # wichtig, close() alleine reicht nicht except socket.error: print('socket error') # kann passieren, wenn der Socket schon geschlossen wurde pass sock.close() def _parseCmdLine(): parser = ArgumentParser(description=textwrap.dedent(''' Simulator for the StReFu (Steuerrechner Funk). The program simulates the interface described in GUEZSB60_ICD-0059_Anb-TrSpFu_AVS. It provides a port where AVS_Interface instances can connect to. At the console (stdin) it waits for control messages. With the following messages a user can indicate that a radio unit was triggered or that the trigger was released: - speechreq - speechreqwd The implemented channel management is very simple. The first request for a channel -- either from AVS or from the user -- wins. The channel stays allocated until it is explicitly released. The data streams for each channel must be provided separately. '''), formatter_class=RawDescriptionHelpFormatter) parser.add_argument('--listen', '-l', type=int, metavar='N', default=8300, help='Listening port for connections from AVS_Interface; default: 8300') parser.add_argument('--ssrc-start', type=int, metavar='N', default=1001, help='SSRC of the first channel; default: 1001') parser.add_argument('--channel-count', '-c', type=int, metavar='N', default=56, help='Number of CNR channels; default: 56') return parser.parse_args() def _main(): options = _parseCmdLine() cmdQueue = Queue() registry = _Registry() channelManager = _ChannelManager(options.ssrc_start, options.channel_count, registry) handlerFactory = _RequestHandlerFactory(cmdQueue, registry, channelManager) acceptor = _Acceptor(options.listen, registry, handlerFactory) acceptorThread = Thread(target=acceptor.run) executor = _CommandExecutor(cmdQueue, channelManager) executorThread = Thread(target=executor.run) controlRequestHandler = _ControlRequestHandler(sys.stdin, cmdQueue) with ExitStack() as stack: stack.enter_context(startThreadContext(executor, executorThread)) stack.enter_context(startThreadContext(acceptor, acceptorThread)) controlRequestHandler.handle() # beendet sich bei EOF oder Ctrl-C if __name__ == '__main__': _main()