You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
191 lines
7.1 KiB
191 lines
7.1 KiB
# Copyright (c) 2020 The Chromium OS Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
from collections import namedtuple
|
|
import os
|
|
import re
|
|
import time
|
|
|
|
class WpaMon(object):
|
|
"""wpa_supplicant event monitor."""
|
|
|
|
WPAS_CTRL_DIR = '/var/run/wpa_supplicant/'
|
|
LOCAL_CTRL = 'local_ctrl'
|
|
REQUEST_PIPE = 'request_pipe'
|
|
WPAS_EVENT_LOG = 'wpa_event.log'
|
|
|
|
CTRL_EVENT_DO_ROAM = 'CTRL-EVENT-DO-ROAM'
|
|
CTRL_EVENT_SKIP_ROAM = 'CTRL-EVENT-SKIP-ROAM'
|
|
CTRL_EVENT_DISCONNECTED = 'CTRL-EVENT-DISCONNECTED'
|
|
CTRL_EVENT_SCAN_RESULTS = 'CTRL-EVENT-SCAN-RESULTS'
|
|
CTRL_EVENT_BSS_ADDED = 'CTRL-EVENT-BSS-ADDED'
|
|
|
|
ROAM_MATCH = ' cur_bssid=([\da-fA-F:]+) cur_freq=(\d+) ' \
|
|
'cur_level=([\d-]+) cur_est=(\d+) ' \
|
|
'sel_bssid=([\da-fA-F:]+) sel_freq=(\d+) ' \
|
|
'sel_level=([\d-]+) sel_est=(\d+)'
|
|
DISCONNECT_MATCH = ' bssid=([\da-fA-F:]+) reason=(\d+)' \
|
|
'(?: locally_generated=(1))?'
|
|
SCAN_RESULTS_MATCH = '()'
|
|
BSS_ADDED_MATCH = ' ([\d]+) ([\da-fA-F:]+)'
|
|
|
|
Roam = namedtuple('Roam',
|
|
['cur_bssid', 'cur_freq', 'cur_level', 'cur_est',
|
|
'sel_bssid', 'sel_freq', 'sel_level', 'sel_est'])
|
|
Disconnect = namedtuple('Disconnect', ['bssid', 'reason',
|
|
'locally_generated'])
|
|
ScanResults = namedtuple('ScanResults', [])
|
|
Bss = namedtuple('Bss', ['id', 'bssid'])
|
|
|
|
MatchFields = namedtuple('MatchFields', ['match_str', 'obj'])
|
|
|
|
EVENT_MATCH_DICT = \
|
|
{CTRL_EVENT_DO_ROAM: MatchFields(ROAM_MATCH, Roam),
|
|
CTRL_EVENT_SKIP_ROAM: MatchFields(ROAM_MATCH, Roam),
|
|
CTRL_EVENT_DISCONNECTED: MatchFields(DISCONNECT_MATCH, Disconnect),
|
|
CTRL_EVENT_SCAN_RESULTS: MatchFields(SCAN_RESULTS_MATCH, ScanResults),
|
|
CTRL_EVENT_BSS_ADDED: MatchFields(BSS_ADDED_MATCH, Bss),
|
|
}
|
|
|
|
def __init__(self, host, wifi_if):
|
|
self._host = host
|
|
self._dest = os.path.join(self.WPAS_CTRL_DIR, wifi_if)
|
|
self._pgid = None
|
|
self._started = False
|
|
|
|
def __enter__(self):
|
|
"""Connect to wpa_supplicant control interface."""
|
|
tmp_dir = self._host.get_tmp_dir()
|
|
tmp_dir = self._host.get_tmp_dir(parent=tmp_dir)
|
|
# Relax permissions for self._tmp_dir so that socat (run as wpa user)
|
|
# can create files in this directory.
|
|
self._host.run('chmod 777 %s' % tmp_dir)
|
|
local = os.path.join(tmp_dir, self.LOCAL_CTRL)
|
|
self._pipe = os.path.join(tmp_dir, self.REQUEST_PIPE)
|
|
self._log_path = os.path.join(tmp_dir, self.WPAS_EVENT_LOG)
|
|
# Run socat as wpa user so that the socket we bind to can be written to
|
|
# by wpa_supplicant. We use a `tail -f` on a named pipe to send requests
|
|
# to wpa_supplicant because `tail -f` continues to read even after it
|
|
# encounters an EOF. Using `cat` or the PIPE address type would close
|
|
# the input stream after the first write, instructing socat to tear
|
|
# everything else down.
|
|
command = "nohup sudo -u wpa -g wpa socat SYSTEM:'mkfifo %s; " \
|
|
"tail -f %s'\!\!STDOUT UNIX-CONNECT:%s,type=2,bind=%s " \
|
|
"</dev/null >%s 2>&1 & echo $!" % \
|
|
(self._pipe, self._pipe, self._dest, local, self._log_path)
|
|
out_lines = self._host.run(command).stdout.splitlines()
|
|
pid = int(out_lines[0])
|
|
self._pgid = \
|
|
int(self._host.run('ps -p %d -o pgid=' % pid).stdout.strip())
|
|
self._capture_index = 0
|
|
self._start()
|
|
return self
|
|
|
|
def __exit__(self, exception, value, traceback):
|
|
"""Disconnect from wpa_supplicant control interface."""
|
|
self._stop()
|
|
# socat spawns a subprocess with the SYSTEM address type, so we must
|
|
# kill the process group in order to properly clean up.
|
|
self._host.run('kill -- -%d' % self._pgid)
|
|
self._pgid = None
|
|
self._capture_index = 0
|
|
|
|
def _start(self):
|
|
"""
|
|
Attach to the wpa_supplicant control interface to start subscribing to
|
|
events.
|
|
|
|
@return False if already attached, True otherwise.
|
|
"""
|
|
if self._started:
|
|
return False
|
|
self._request('ATTACH')
|
|
self._started = True
|
|
return True
|
|
|
|
def _stop(self):
|
|
"""
|
|
Detach from the wpa_supplicant control interface to no longer receive
|
|
events.
|
|
|
|
@return False if not currently attached, True otherwise.
|
|
"""
|
|
if not self._started:
|
|
return False
|
|
self._request('DETACH')
|
|
self._started = False
|
|
return True
|
|
|
|
def _request(self, cmd):
|
|
"""
|
|
Send a request to the control interface by writing to the named pipe.
|
|
|
|
We use the -n option because wpa_supplicant expects there to be no
|
|
newline character after the command.
|
|
|
|
@param cmd string: command to run
|
|
"""
|
|
self._host.run('echo -n "%s" > %s' % (cmd, self._pipe))
|
|
|
|
def get_log_entries(self):
|
|
"""
|
|
Get all event log entries and command replies.
|
|
|
|
@return string event log
|
|
"""
|
|
return self._host.run('cat %s' % self._log_path).stdout.rstrip()
|
|
|
|
def start_event_capture(self):
|
|
"""
|
|
Set _capture_index to mark the point in the logs at which an event
|
|
capture was started.
|
|
"""
|
|
self._capture_index = len(self.get_log_entries())
|
|
|
|
def wait_for_event(self, event, timeout=10, sleep_interval=1.0, attrs={}):
|
|
"""
|
|
Wait for a wpa_supplicant event. start_event_capture should be called
|
|
before this.
|
|
|
|
@param event string: the wpa_supplicant event to wait for.
|
|
@param timeout int: timeout in seconds.
|
|
@param sleep_interval float: sleep interval in seconds.
|
|
@return list of strings of all event occurrences.
|
|
"""
|
|
start_time = time.time()
|
|
while True:
|
|
objs = self.get_events(event, True, attrs)
|
|
if objs:
|
|
return objs
|
|
if time.time() + sleep_interval - start_time > timeout:
|
|
return []
|
|
time.sleep(sleep_interval)
|
|
return []
|
|
|
|
def get_events(self, event, captured_events=False, attrs={}):
|
|
"""
|
|
Get all wpa_supplicant events of type |event|.
|
|
|
|
@param event string: the wpa_supplicant event to get.
|
|
@param captured_events boolean: True to get events starting from the
|
|
last start_event_capture call, False to get all events.
|
|
@return list of namedtuples corresponding to the event.
|
|
"""
|
|
wpa_log = self.get_log_entries()
|
|
if captured_events:
|
|
wpa_log = wpa_log[self._capture_index:]
|
|
match_str = event + self.EVENT_MATCH_DICT[event].match_str
|
|
matches = re.findall(match_str, wpa_log)
|
|
objs = []
|
|
for match in matches:
|
|
obj = self.EVENT_MATCH_DICT[event].obj(*match)
|
|
does_match = True
|
|
for attr, val in attrs.items():
|
|
if getattr(obj, attr) != val:
|
|
does_match = False
|
|
break
|
|
if does_match:
|
|
objs.append(obj)
|
|
return objs
|