156 lines
5.2 KiB
Python
156 lines
5.2 KiB
Python
import sys
|
|
import os
|
|
import socket
|
|
import threading
|
|
import atexit
|
|
import preserves.schema
|
|
import time
|
|
import re
|
|
|
|
from syndicate import patterns as P, relay, turn, dataspace, Symbol
|
|
from syndicate.actor import find_loop
|
|
from syndicate.during import During
|
|
|
|
try:
|
|
schemas = preserves.schema.load_schema_file('/usr/share/synit/schemas/schema-bundle.prb')
|
|
except:
|
|
schemas = preserves.schema.load_schema_file('/home/tonyg/src/syndicate-system/protocols/schema-bundle.bin')
|
|
network = schemas.network
|
|
|
|
server_socket_dir = '/run/wpa_supplicant'
|
|
|
|
class WPASupplicantClient:
|
|
# https://w1.fi/wpa_supplicant/devel/ctrl_iface_page.html
|
|
#
|
|
# According to the code (!) any response starting with '<' is an event, not a reply.
|
|
|
|
def __init__(self, interface, log):
|
|
self.interface = interface
|
|
self.log = log
|
|
self.server_socket_path = os.path.join(server_socket_dir, interface)
|
|
self.client_socket_path = f'/tmp/wpa_cli_py_{interface}_{os.getpid()}'
|
|
|
|
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
|
self.sock.bind(self.client_socket_path)
|
|
self.sock.connect(self.server_socket_path)
|
|
|
|
self.send('ATTACH')
|
|
answer = self.recv(timeout_sec=3.0)
|
|
if answer != 'OK':
|
|
raise AssertionError(f'wpa_supplicant replied {repr(answer)} to ATTACH')
|
|
|
|
self.cleanup = lambda: self.close()
|
|
atexit.register(self.cleanup)
|
|
|
|
def close(self):
|
|
if self.cleanup:
|
|
atexit.unregister(self.cleanup)
|
|
self.cleanup = None
|
|
self.send('DETACH')
|
|
self.sock.close()
|
|
os.unlink(self.client_socket_path)
|
|
|
|
def send(self, command):
|
|
self.sock.send(command.encode('utf-8'))
|
|
|
|
def recv(self, timeout_sec=None):
|
|
self.sock.settimeout(timeout_sec)
|
|
try:
|
|
reply = self.sock.recv(65536)
|
|
self.log.info(f'Raw input: {repr(reply)}')
|
|
return reply.strip().decode('utf-8')
|
|
except TimeoutError:
|
|
return None
|
|
finally:
|
|
self.sock.settimeout(None)
|
|
|
|
def gather_events(facet, callback, client, loop):
|
|
facet.log.debug('Background socket read thread started')
|
|
try:
|
|
while True:
|
|
facet.log.debug('waiting for event...')
|
|
e = client.recv()
|
|
# OMG python's incredibly broken mutable-variable-closures bit me AGAIN. I've had
|
|
# to add a layer of gratuitous rebinding of `e` plus the `lambda:` below to make it
|
|
# properly close over the current binding of `e`.
|
|
def handler_for_specific_e(e):
|
|
return lambda: callback(e)
|
|
turn.external(facet, handler_for_specific_e(e), loop=loop)
|
|
except Exception as e:
|
|
facet.log.debug(e)
|
|
finally:
|
|
facet.log.debug('Background socket read thread terminated')
|
|
|
|
@relay.service(name='wifi_daemon', debug=False)
|
|
@During().add_handler
|
|
def main(args):
|
|
machine_ds = args[Symbol('machine')].embeddedValue
|
|
ifname = args[Symbol('ifname')]
|
|
|
|
client = WPASupplicantClient(ifname, turn.log)
|
|
commands = []
|
|
active_network_id = None
|
|
|
|
def send_next_command():
|
|
while len(commands) > 0:
|
|
turn.log.info(f'Sending {commands[0][0]}')
|
|
client.send(commands[0][0])
|
|
if commands[0][1] is None:
|
|
commands.pop(0)
|
|
continue
|
|
else:
|
|
break
|
|
|
|
def send_command(c, handler = lambda reply: None):
|
|
turn.log.info(f'Scheduling {c}')
|
|
commands.append((c, handler))
|
|
if len(commands) == 1:
|
|
send_next_command()
|
|
|
|
def cleanout_networks(nets):
|
|
for line in nets.split('\n'):
|
|
if line.startswith('network id'):
|
|
continue
|
|
turn.log.info(f'Cleaning out old network: {repr(line)}')
|
|
send_command('REMOVE_NETWORK ' + line.split('\t')[0])
|
|
send_command('ADD_NETWORK', remember_network_id)
|
|
|
|
def remember_network_id(netid):
|
|
nonlocal active_network_id
|
|
active_network_id = netid
|
|
turn.log.info(f'Network ID: {active_network_id}')
|
|
def handle_scan_failure(reply):
|
|
if reply == 'FAIL':
|
|
send_command('TERMINATE')
|
|
send_command('SCAN', handle_scan_failure)
|
|
|
|
send_command('LEVEL 3')
|
|
send_command('LIST_NETWORKS', cleanout_networks)
|
|
|
|
def handle_event(e):
|
|
if e[0] == '<':
|
|
# It's an event
|
|
m = re.match(r'<(\d+)>(.*)', e)
|
|
if m:
|
|
(_level, message) = m.groups()
|
|
if message == 'CTRL-EVENT-SCAN-RESULTS':
|
|
send_command('SCAN_RESULTS')
|
|
turn.log.info(f'Level {_level}: {message}')
|
|
else:
|
|
turn.log.info(f'Unusual event: {e}')
|
|
else:
|
|
if len(commands) > 0:
|
|
turn.log.info(f'REPLY: {e}')
|
|
commands[0][1](e)
|
|
commands.pop(0)
|
|
send_next_command()
|
|
else:
|
|
turn.log.warning(f'Unexpected reply: {e}')
|
|
|
|
facet = turn.active_facet()
|
|
loop = find_loop()
|
|
turn.log.info('Starting background read thread')
|
|
threading.Thread(
|
|
name='background-wpa_supplicant-read-thread',
|
|
target=lambda: gather_events(facet, handle_event, client, loop)).start()
|