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