2019-07-10 19:10:14 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
libexec_dir = __LIBEXECDIR__
|
2019-07-10 19:44:20 +00:00
|
|
|
|
2019-07-12 22:02:13 +00:00
|
|
|
import select
|
|
|
|
import socket
|
2019-07-10 19:44:20 +00:00
|
|
|
import subprocess
|
|
|
|
import sys
|
2019-07-12 22:02:13 +00:00
|
|
|
import threading
|
2019-07-10 19:44:20 +00:00
|
|
|
import time
|
|
|
|
|
2019-07-12 22:02:13 +00:00
|
|
|
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()
|
|
|
|
|
2019-07-10 20:37:39 +00:00
|
|
|
def writeall(f, b):
|
|
|
|
written = 0
|
|
|
|
while written < len(b):
|
|
|
|
written += f.write(b[written:])
|
|
|
|
|
2019-07-13 16:34:54 +00:00
|
|
|
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)
|
|
|
|
|
2019-07-10 20:37:39 +00:00
|
|
|
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
|
|
|
|
|
2019-07-13 16:34:54 +00:00
|
|
|
def format_mac(mac):
|
|
|
|
return ':'.join(mac[i:i+1].hex() for i in range(len(mac)))
|
|
|
|
|
2019-07-12 22:02:13 +00:00
|
|
|
class PollBasedThread(threading.Thread):
|
|
|
|
def run(self):
|
2019-07-13 16:34:54 +00:00
|
|
|
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'")
|
2019-07-10 19:44:20 +00:00
|
|
|
|
2019-07-13 16:34:54 +00:00
|
|
|
if not restart:
|
|
|
|
break
|
2019-07-10 19:44:20 +00:00
|
|
|
|
2019-07-12 22:02:13 +00:00
|
|
|
self.finalize()
|
2019-07-10 19:44:20 +00:00
|
|
|
|
2019-07-12 22:02:13 +00:00
|
|
|
def initialize(self):
|
|
|
|
...
|
|
|
|
|
|
|
|
def poll_loop(self, fd, event):
|
|
|
|
...
|
|
|
|
|
|
|
|
def finalize(self):
|
|
|
|
...
|
2019-07-10 21:00:44 +00:00
|
|
|
|
2019-07-12 22:02:13 +00:00
|
|
|
class Backend(PollBasedThread):
|
2019-07-13 16:34:54 +00:00
|
|
|
def __init__(self, interface, nick, writes_channel, control_channel):
|
2019-07-12 22:05:33 +00:00
|
|
|
self.interface = interface
|
2019-07-13 16:34:54 +00:00
|
|
|
self.nick = nick
|
2019-07-12 22:02:13 +00:00
|
|
|
self.writes_channel = writes_channel
|
|
|
|
self.control_channel = control_channel
|
|
|
|
super().__init__()
|
2019-07-10 20:37:39 +00:00
|
|
|
|
2019-07-12 22:02:13 +00:00
|
|
|
def initialize(self):
|
2019-07-13 16:34:54 +00:00
|
|
|
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))
|
2019-07-10 20:37:39 +00:00
|
|
|
|
2019-07-12 22:02:13 +00:00
|
|
|
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'
|
2019-07-10 20:37:39 +00:00
|
|
|
|
2019-07-10 21:00:44 +00:00
|
|
|
else:
|
2019-07-12 22:02:13 +00:00
|
|
|
raise Exception('Unreachable')
|
|
|
|
|
2019-07-13 16:34:54 +00:00
|
|
|
elif fd == self.proc.stdout.fileno() and event & select.POLLIN:
|
|
|
|
data = self.proc.stdout.read(1024)
|
|
|
|
sys.stdout.buffer.write(data)
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
|
|
|
elif fd == self.proc.stdout.fileno() and event & select.POLLHUP:
|
|
|
|
print('Backend exited')
|
|
|
|
return 'quit'
|
|
|
|
|
2019-07-12 22:02:13 +00:00
|
|
|
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__()
|
2019-07-10 20:37:39 +00:00
|
|
|
|
2019-07-12 22:02:13 +00:00
|
|
|
def run(self):
|
|
|
|
print('s - request status, i - request msgid, m - send message, ^D - quit')
|
2019-07-10 19:44:20 +00:00
|
|
|
|
2019-07-12 22:02:13 +00:00
|
|
|
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()
|
|
|
|
|
2019-07-12 22:05:33 +00:00
|
|
|
_, interface, nick = sys.argv
|
|
|
|
|
2019-07-13 16:34:54 +00:00
|
|
|
Backend(interface, nick, writes_channel, control_channel).start()
|
2019-07-12 22:02:13 +00:00
|
|
|
Input(writes_channel, control_channel).start()
|