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 = '%s > "%s" 2>&1' % (base_command, self._log_file)
90        self._runner.run_async(job_str)
91
92        try:
93            self._wait_for_process(timeout=timeout)
94            self._wait_for_interface(timeout=timeout)
95        except:
96            self.stop()
97            raise
98
99    def stop(self):
100        """Kills the daemon if it is running."""
101        self._shell.kill(self._identifier)
102
103    def is_alive(self):
104        """
105        Returns:
106            True if the daemon is running.
107        """
108        return self._shell.is_alive(self._identifier)
109
110    def pull_logs(self):
111        """Pulls the log files from where hostapd is running.
112
113        Returns:
114            A string of the hostapd logs.
115        """
116        # TODO: Auto pulling of logs when stop is called.
117        return self._shell.read_file(self._log_file)
118
119    def _wait_for_process(self, timeout=60):
120        """Waits for the process to come up.
121
122        Waits until the hostapd process is found running, or there is
123        a timeout. If the program never comes up then the log file
124        will be scanned for errors.
125
126        Raises: See _scan_for_errors
127        """
128        start_time = time.time()
129        while time.time() - start_time < timeout and not self.is_alive():
130            self._scan_for_errors(False)
131            time.sleep(0.1)
132
133    def _wait_for_interface(self, timeout=60):
134        """Waits for hostapd to report that the interface is up.
135
136        Waits until hostapd says the interface has been brought up or an
137        error occurs.
138
139        Raises: see _scan_for_errors
140        """
141        start_time = time.time()
142        while time.time() - start_time < timeout:
143            success = self._shell.search_file('Setup of interface done',
144                                              self._log_file)
145            if success:
146                return
147
148            self._scan_for_errors(True)
149
150    def _scan_for_errors(self, should_be_up):
151        """Scans the hostapd log for any errors.
152
153        Args:
154            should_be_up: If true then hostapd program is expected to be alive.
155                          If it is found not alive while this is true an error
156                          is thrown.
157
158        Raises:
159            Error: Raised when a hostapd error is found.
160        """
161        # Store this so that all other errors have priority.
162        is_dead = not self.is_alive()
163
164        bad_config = self._shell.search_file('Interface initialization failed',
165                                             self._log_file)
166        if bad_config:
167            raise Error('Interface failed to start', self)
168
169        bad_config = self._shell.search_file(
170            "Interface %s wasn't started" % self._interface, self._log_file)
171        if bad_config:
172            raise Error('Interface failed to start', self)
173
174        if should_be_up and is_dead:
175            raise Error('Hostapd failed to start', self)
176
177    def _write_configs(self, additional_parameters=None):
178        """Writes the configs to the hostapd config file."""
179        self._shell.delete_file(self._config_file)
180
181        interface_configs = collections.OrderedDict()
182        interface_configs['interface'] = self._interface
183        interface_configs['ctrl_interface'] = self._ctrl_file
184        pairs = ('%s=%s' % (k, v) for k, v in interface_configs.items())
185
186        packaged_configs = self.config.package_configs()
187        if additional_parameters:
188            packaged_configs.append(additional_parameters)
189        for packaged_config in packaged_configs:
190            config_pairs = ('%s=%s' % (k, v)
191                            for k, v in packaged_config.items()
192                            if v is not None)
193            pairs = itertools.chain(pairs, config_pairs)
194
195        hostapd_conf = '\n'.join(pairs)
196
197        logging.info('Writing %s' % self._config_file)
198        logging.debug('******************Start*******************')
199        logging.debug('\n%s' % hostapd_conf)
200        logging.debug('*******************End********************')
201
202        self._shell.write_file(self._config_file, hostapd_conf)
203