1# Copyright 2016 - The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15import collections 16import itertools 17import logging 18import os 19import time 20 21from acts.controllers.ap_lib import hostapd_config 22from acts.controllers.utils_lib.commands import shell 23 24 25class Error(Exception): 26 """An error caused by hostapd.""" 27 28 29class Hostapd(object): 30 """Manages the hostapd program. 31 32 Attributes: 33 config: The hostapd configuration that is being used. 34 """ 35 36 PROGRAM_FILE = '/usr/sbin/hostapd' 37 38 def __init__(self, runner, interface, working_dir='/tmp'): 39 """ 40 Args: 41 runner: Object that has run_async and run methods for executing 42 shell commands (e.g. connection.SshConnection) 43 interface: string, The name of the interface to use (eg. wlan0). 44 working_dir: The directory to work out of. 45 """ 46 self._runner = runner 47 self._interface = interface 48 self._working_dir = working_dir 49 self.config = None 50 self._shell = shell.ShellCommand(runner, working_dir) 51 self._log_file = 'hostapd-%s.log' % self._interface 52 self._ctrl_file = 'hostapd-%s.ctrl' % self._interface 53 self._config_file = 'hostapd-%s.conf' % self._interface 54 self._identifier = '%s.*%s' % (self.PROGRAM_FILE, self._config_file) 55 56 def start(self, config, timeout=60, additional_parameters=None): 57 """Starts hostapd 58 59 Starts the hostapd daemon and runs it in the background. 60 61 Args: 62 config: Configs to start the hostapd with. 63 timeout: Time to wait for DHCP server to come up. 64 additional_parameters: A dictionary of parameters that can sent 65 directly into the hostapd config file. This 66 can be used for debugging and or adding one 67 off parameters into the config. 68 69 Returns: 70 True if the daemon could be started. Note that the daemon can still 71 start and not work. Invalid configurations can take a long amount 72 of time to be produced, and because the daemon runs indefinitely 73 it's impossible to wait on. If you need to check if configs are ok 74 then periodic checks to is_running and logs should be used. 75 """ 76 if self.is_alive(): 77 self.stop() 78 79 self.config = config 80 81 self._shell.delete_file(self._ctrl_file) 82 self._shell.delete_file(self._log_file) 83 self._shell.delete_file(self._config_file) 84 self._write_configs(additional_parameters=additional_parameters) 85 86 hostapd_command = '%s -dd -t "%s"' % (self.PROGRAM_FILE, 87 self._config_file) 88 base_command = 'cd "%s"; %s' % (self._working_dir, hostapd_command) 89 job_str = 'rfkill unblock all; %s > "%s" 2>&1' %\ 90 (base_command, self._log_file) 91 self._runner.run_async(job_str) 92 93 try: 94 self._wait_for_process(timeout=timeout) 95 self._wait_for_interface(timeout=timeout) 96 except: 97 self.stop() 98 raise 99 100 def stop(self): 101 """Kills the daemon if it is running.""" 102 if self.is_alive(): 103 self._shell.kill(self._identifier) 104 105 def is_alive(self): 106 """ 107 Returns: 108 True if the daemon is running. 109 """ 110 return self._shell.is_alive(self._identifier) 111 112 def pull_logs(self): 113 """Pulls the log files from where hostapd is running. 114 115 Returns: 116 A string of the hostapd logs. 117 """ 118 # TODO: Auto pulling of logs when stop is called. 119 return self._shell.read_file(self._log_file) 120 121 def _wait_for_process(self, timeout=60): 122 """Waits for the process to come up. 123 124 Waits until the hostapd process is found running, or there is 125 a timeout. If the program never comes up then the log file 126 will be scanned for errors. 127 128 Raises: See _scan_for_errors 129 """ 130 start_time = time.time() 131 while time.time() - start_time < timeout and not self.is_alive(): 132 self._scan_for_errors(False) 133 time.sleep(0.1) 134 135 def _wait_for_interface(self, timeout=60): 136 """Waits for hostapd to report that the interface is up. 137 138 Waits until hostapd says the interface has been brought up or an 139 error occurs. 140 141 Raises: see _scan_for_errors 142 """ 143 start_time = time.time() 144 while time.time() - start_time < timeout: 145 success = self._shell.search_file('Setup of interface done', 146 self._log_file) 147 if success: 148 return 149 150 self._scan_for_errors(True) 151 152 def _scan_for_errors(self, should_be_up): 153 """Scans the hostapd log for any errors. 154 155 Args: 156 should_be_up: If true then hostapd program is expected to be alive. 157 If it is found not alive while this is true an error 158 is thrown. 159 160 Raises: 161 Error: Raised when a hostapd error is found. 162 """ 163 # Store this so that all other errors have priority. 164 is_dead = not self.is_alive() 165 166 bad_config = self._shell.search_file('Interface initialization failed', 167 self._log_file) 168 if bad_config: 169 raise Error('Interface failed to start', self) 170 171 bad_config = self._shell.search_file( 172 "Interface %s wasn't started" % self._interface, self._log_file) 173 if bad_config: 174 raise Error('Interface failed to start', self) 175 176 if should_be_up and is_dead: 177 raise Error('Hostapd failed to start', self) 178 179 def _write_configs(self, additional_parameters=None): 180 """Writes the configs to the hostapd config file.""" 181 self._shell.delete_file(self._config_file) 182 183 interface_configs = collections.OrderedDict() 184 interface_configs['interface'] = self._interface 185 interface_configs['ctrl_interface'] = self._ctrl_file 186 pairs = ('%s=%s' % (k, v) for k, v in interface_configs.items()) 187 188 packaged_configs = self.config.package_configs() 189 if additional_parameters: 190 packaged_configs.append(additional_parameters) 191 for packaged_config in packaged_configs: 192 config_pairs = ('%s=%s' % (k, v) 193 for k, v in packaged_config.items() 194 if v is not None) 195 pairs = itertools.chain(pairs, config_pairs) 196 197 hostapd_conf = '\n'.join(pairs) 198 199 logging.info('Writing %s' % self._config_file) 200 logging.debug('******************Start*******************') 201 logging.debug('\n%s' % hostapd_conf) 202 logging.debug('*******************End********************') 203 204 self._shell.write_file(self._config_file, hostapd_conf) 205