# Lint as: python2, python3 # Copyright (c) 2014 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. import collections import logging import os import re import six IW_REMOTE_EVENT_LOG_FILE_NAME = 'iw_event.log' LogEntry = collections.namedtuple('LogEntry', ['timestamp', 'interface', 'message']) class IwEventLogger(object): """Context enclosing the use of iw event logger.""" def __init__(self, host, command_iw, local_file): self._host = host self._command_iw = command_iw self._iw_event_log_path = os.path.join(self._host.get_tmp_dir(), IW_REMOTE_EVENT_LOG_FILE_NAME) self._local_file = local_file self._pid = None self._start_time = 0 def __enter__(self): return self def __exit__(self, exception, value, traceback): self.stop() def _check_message_for_disconnect(self, message): """Check log message for disconnect event. This function checks log messages for signs the connection was terminated. @param: message String message to check for disconnect event. @returns True if the log message is a disconnect event, false otherwise. """ return (message.startswith('disconnected') or message.startswith('Deauthenticated') or message == 'Previous authentication no longer valid') @property def local_file(self): """@return string local host path for log file.""" return self._local_file def start(self): """Start event logger. This function will start iw event process in remote host, and redirect output to a temporary file in remote host. """ command = 'nohup %s event -t %s 2>&1 & echo $!' % ( self._command_iw, self._iw_event_log_path) command += ';date +%s' out_lines = self._host.run(command).stdout.splitlines() self._pid = int(out_lines[0]) self._start_time = float(out_lines[1]) def stop(self): """Stop event logger. This function will kill iw event process, and copy the log file from remote to local. """ if self._pid is None: return # Kill iw event process self._host.run('kill %d' % self._pid, ignore_status=True) self._pid = None # Copy iw event log file from remote host self._host.get_file(self._iw_event_log_path, self._local_file) logging.info('iw event log saved to %s', self._local_file) def get_log_entries(self): """Parse local log file and yield LogEntry named tuples. This function will parse the iw event log and return individual LogEntry tuples for each parsed line. Here are example of lines to be parsed: 1393961008.058711: wlan0 (phy #0): scan started 1393961019.758599: wlan0 (phy #0): connected to 04:f0:21:03:7d:bd @yields LogEntry tuples for each log entry. """ iw_log = self._host.run('cat %s' % self._iw_event_log_path).stdout iw_log_file = six.StringIO(iw_log) for line in iw_log_file.readlines(): parse_line = re.match('\s*(\d+).(\d+): (\w[^:]*): (\w.*)', line) if parse_line: time_integer = parse_line.group(1) time_decimal = parse_line.group(2) timestamp = float('%s.%s' % (time_integer, time_decimal)) yield LogEntry(timestamp=timestamp, interface=parse_line.group(3), message=parse_line.group(4)) def get_reassociation_time(self): """Return reassociation time. This function will search the iw event log to determine the time it takes from start of reassociation request to being connected. Start of reassociation request could be either an attempt to scan or to disconnect. Assume the one that appeared in the log first is the start of the reassociation request. @returns float number of seconds it take from start of reassociation request to being connected. Return None if unable to determine the time based on the log. """ start_time = None end_time = None # Figure out the time when reassociation process started and the time # when client is connected. for entry in self.get_log_entries(): if (entry.message.startswith('scan started') and start_time is None): start_time = entry.timestamp # Newer wpa_supplicant would attempt to disconnect then reconnect # without scanning. So if no scan event is detected before the # disconnect attempt, we'll assume the disconnect attempt is the # beginning of the reassociate attempt. if (self._check_message_for_disconnect(entry.message) and start_time is None): start_time = entry.timestamp if entry.message.startswith('connected'): if start_time is None: return None end_time = entry.timestamp break; else: return None return end_time - start_time def get_disconnect_count(self): """Return number of times the system disconnected during the log. This function will search the iw event log to determine how many times the "disconnect" and "Deauthenticated" messages appear. @returns int number of times the system disconnected in the logs. """ count = 0 for entry in self.get_log_entries(): if self._check_message_for_disconnect(entry.message): count += 1 return count def get_time_to_disconnected(self): """Return disconnect time. This function will search the iw event log to determine the number of seconds between the time iw event logger is started to the time the first "disconnected" or "Deauthenticated" event is received. @return float number of seconds between the time iw event logger is started to the time "disconnected" or "Deauthenticated" event is received. Return None if no "disconnected" or "Deauthenticated" event is detected in the iw event log. """ for entry in self.get_log_entries(): if self._check_message_for_disconnect(entry.message): return entry.timestamp - self._start_time return None