#!/usr/bin/env python3 libexec_dir = __LIBEXECDIR__ import select import socket import subprocess import sys import threading import time class Channel: """An asynchronic communication channel that can be used to send python object and can be poll()ed.""" def __init__(self): # We use a socket to enable polling and blocking reads self.write_socket, self.read_socket = socket.socketpair() self.poll = select.poll() self.poll.register(self.read_socket, select.POLLIN) # Store messages in a list self.messages = [] self.messages_lock = threading.Lock() def send(self, message): # Add message to the list of messages and write to the write socket to signal there's data to read with self.messages_lock: self.write_socket.sendall(b'!') self.messages.append(message) def recv(self, blocking = True): # Timeout of -1 will make poll wait until data is available # Timeout of 0 will make poll exit immediately if there's no data if blocking: timeout = -1 else: timeout = 0 # See if there is data to read / wait until there is results = self.poll.poll(timeout) # None of the sockets were ready. This can only happen if we weren't blocking # Return None to signal lack of data if len(results) == 0: assert not blocking return None # Remove first message from the list (FIFO principle), and read one byte from the socket # This keeps the number of available messages and the number of bytes readable in the socket in sync with self.messages_lock: message = self.messages.pop(0) self.read_socket.recv(1) return message def fileno(self): # Allows for a Channel object to be passed directly to poll() return self.read_socket.fileno() def close(self): # Close the file descriptors, so that we aren't leaking them self.write_socket.close() self.read_socket.close() # Support with-statements def __enter__(self): return self def __exit__(self, exception_type, exception_value, traceback): self.close() def writeall(f, b): written = 0 while written < len(b): written += f.write(b[written:]) def readall(f, length): read = bytearray() while len(read) < length: data = f.read(length - len(read)) if data is None: raise ConnectionError('Could not satisfy read of %i bytes' % length) read.extend(data) return bytes(read) def parse_mac(text): parts = text.split(':') if len(parts) != 6: raise ValueError('Invalid MAC format: %s' % text) try: parsed = bytes(int(field, 16) for field in parts) except ValueError: raise ValueError('Invalid MAC format %s' % text) return parsed def format_mac(mac): return ':'.join(mac[i:i+1].hex() for i in range(len(mac))) def format_status(status): if status == 0: return 'available' elif status == 1: return 'unavailable' elif status == 2: return 'offline' else: raise ValueError('Unknown status %i' % status) class PollBasedThread(threading.Thread): def run(self): while True: restart = False self.initialize() poll = select.poll() for f in self.pollin: poll.register(f, select.POLLIN) running = True while running and not restart: for fd, event in poll.poll(): command = self.poll_loop(fd, event) if command == None: pass elif command == 'quit': running = False elif command == 'restart': restart = True else: raise ValueError("poll_loop() needs to return either None, 'quit', or 'restart'") if not restart: break self.finalize() def initialize(self): ... def poll_loop(self, fd, event): ... def finalize(self): ... class Backend(PollBasedThread): def __init__(self, interface, nick, writes_channel, control_channel): self.interface = interface self.nick = nick self.writes_channel = writes_channel self.control_channel = control_channel super().__init__() def initialize(self): self.proc = subprocess.Popen(['sudo', libexec_dir + '/ethermess-backend', self.interface], stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = sys.stderr, bufsize = 0) self.pollin = [self.writes_channel, self.control_channel, self.proc.stdout] # Tell the backend the status and nick status = 0 nick = self.nick.encode('utf-8') writeall(self.proc.stdin, bytes([status, len(nick)]) + nick) # Read our MAC self.mac = readall(self.proc.stdout, 6) print('Own mac: %s' % format_mac(self.mac)) def poll_loop(self, fd, event): if fd == self.writes_channel.fileno() and event & select.POLLIN: data = self.writes_channel.recv() writeall(self.proc.stdin, data) elif fd == self.control_channel.fileno() and event & select.POLLIN: command = self.control_channel.recv() if command == 'quit': return 'quit' else: raise Exception('Unreachable') elif fd == self.proc.stdout.fileno() and event & select.POLLIN: event_type = readall(self.proc.stdout, 1) if event_type == b's': # Status source_mac = readall(self.proc.stdout, 6) status, = readall(self.proc.stdout, 1) nick_length, = readall(self.proc.stdout, 1) nick = readall(self.proc.stdout, nick_length,) print('%s (%s) ~%s' % (format_mac(source_mac), format_status(status), nick.decode('utf-8'))) else: # Not sth we handle yet data = self.proc.stdout.read(1023) if data is None: data = b'[!] ' + event_type else: data = b'[!] ' + event_type + data sys.stdout.buffer.write(data) sys.stdout.flush() elif fd == self.proc.stdout.fileno() and event & select.POLLHUP: print('Backend exited') return 'quit' else: raise Exception('Unreachable') def finalize(self): self.proc.wait() class Input(threading.Thread): def __init__(self, writes_channel, control_channel): self.writes_channel = writes_channel self.control_channel = control_channel super().__init__() def run(self): print('s - request status, i - request msgid, m - send message, ^D - quit') while True: try: try: command = input('') if command == 's': mac = parse_mac(input('mac> ')) self.writes_channel.send(b's' + mac) elif command == 'i': mac = parse_mac(input('mac> ')) self.writes_channel.send(b'i' + mac) elif command == 'm': mac = parse_mac(input('mac> ')) message = input('message> ').encode('utf-8') self.writes_channel.send(b'm' + mac + bytes([len(message) >> 8, len(message) & 0xff]) + message) else: print('s - request status, i - request msgid, m - send message, ^D - quit') except EOFError: self.writes_channel.send(b'q') self.control_channel.send('quit') break except Exception as err: print(err) writes_channel = Channel() control_channel = Channel() _, interface, nick = sys.argv Backend(interface, nick, writes_channel, control_channel).start() Input(writes_channel, control_channel).start()