1#!/usr/bin/env python3
2#
3#   Copyright 2019 - The Android Open Source Project
4#
5#   Licensed under the Apache License, Version 2.0 (the "License");
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of the License at
8#
9#       http://www.apache.org/licenses/LICENSE-2.0
10#
11#   Unless required by applicable law or agreed to in writing, software
12#   distributed under the License is distributed on an "AS IS" BASIS,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17import logging
18import time
19
20from threading import Thread
21
22from acts.libs.logging import log_stream
23from acts.libs.logging.log_stream import LogStyles
24from acts.controllers.android_lib.logcat import TimestampTracker
25from acts.controllers.fuchsia_lib.utils_lib import create_ssh_connection
26
27# paramiko-ng has a log line, line number in 1982 in paramiko/transport.py that
28# presents a ERROR log message that is innocuous but could confuse the user.
29# Therefore by setting the log level to CRITICAL the message is not displayed
30# and everything is recovered as expected.
31logging.getLogger("paramiko").setLevel(logging.CRITICAL)
32
33
34def _log_line_func(log, timestamp_tracker):
35    """Returns a lambda that logs a message to the given logger."""
36    def log_line(message):
37        timestamp_tracker.read_output(message)
38        log.info(message)
39
40    return log_line
41
42
43def start_syslog(serial,
44                 base_path,
45                 ip_address,
46                 ssh_username,
47                 ssh_config,
48                 extra_params=''):
49    """Creates a FuchsiaSyslogProcess that automatically attempts to reconnect.
50
51    Args:
52        serial: The unique identifier for the device.
53        base_path: The base directory used for syslog file output.
54        ip_address: The ip address of the device to get the syslog.
55        ssh_username: Username for the device for the Fuchsia Device.
56        ssh_config: Location of the ssh_config for connecting to the remote
57            device
58        extra_params: Any additional params to be added to the syslog cmdline.
59
60    Returns:
61        A FuchsiaSyslogProcess object.
62    """
63    logger = log_stream.create_logger('fuchsia_log_%s' % serial,
64                                      base_path=base_path,
65                                      log_styles=(LogStyles.LOG_DEBUG
66                                                  | LogStyles.MONOLITH_LOG))
67    syslog = FuchsiaSyslogProcess(ssh_username, ssh_config, ip_address,
68                                  extra_params)
69    timestamp_tracker = TimestampTracker()
70    syslog.set_on_output_callback(_log_line_func(logger, timestamp_tracker))
71    return syslog
72
73
74class FuchsiaSyslogError(Exception):
75    """Raised when invalid operations are run on a Fuchsia Syslog."""
76
77
78class FuchsiaSyslogProcess(object):
79    """A class representing a Fuchsia Syslog object that communicates over ssh.
80    """
81    def __init__(self, ssh_username, ssh_config, ip_address, extra_params):
82        """
83        Args:
84            ssh_username: The username to connect to Fuchsia over ssh.
85            ssh_config: The ssh config that holds the information to connect to
86            a Fuchsia device over ssh.
87            ip_address: The ip address of the Fuchsia device.
88        """
89        self.ssh_config = ssh_config
90        self.ip_address = ip_address
91        self.extra_params = extra_params
92        self.ssh_username = ssh_username
93        self._output_file = None
94        self._ssh_client = None
95        self._listening_thread = None
96        self._redirection_thread = None
97        self._on_output_callback = lambda *args, **kw: None
98
99        self._started = False
100        self._stopped = False
101
102    def start(self):
103        """Starts reading the data from the syslog ssh connection."""
104        if self._started:
105            logging.info('Syslog has already started for FuchsiaDevice (%s).' %
106                         self.ip_address)
107            return None
108        self._started = True
109
110        self._listening_thread = Thread(target=self._exec_loop)
111        self._listening_thread.start()
112
113        time_up_at = time.time() + 10
114
115        while self._ssh_client is None:
116            if time.time() > time_up_at:
117                raise FuchsiaSyslogError('Unable to connect to syslog!')
118
119        self._stopped = False
120
121    def stop(self):
122        """Stops listening to the syslog ssh connection and coalesces the
123        threads.
124        """
125        if self._stopped:
126            logging.info('Syslog is already stopped for FuchsiaDevice (%s).' %
127                         self.ip_address)
128            return None
129        self._stopped = True
130
131        try:
132            self._ssh_client.close()
133        except Exception as e:
134            raise e
135        finally:
136            self._join_threads()
137            self._started = False
138            return None
139
140    def _join_threads(self):
141        """Waits for the threads associated with the process to terminate."""
142        if self._listening_thread is not None:
143            if self._redirection_thread is not None:
144                self._redirection_thread.join()
145                self._redirection_thread = None
146
147            self._listening_thread.join()
148            self._listening_thread = None
149
150    def _redirect_output(self):
151        """Redirects the output from the ssh connection into the
152        on_output_callback.
153        """
154        # In some cases, the parent thread (listening_thread) was being joined
155        # before the redirect_thread could finish initiating, meaning it would
156        # run forever attempting to redirect the output even if the listening
157        # thread was torn down. This allows the thread to close at the test
158        # end.
159        parent_listener = self._listening_thread
160        while True:
161            line = self._output_file.readline()
162
163            if not line:
164                return
165            if self._listening_thread != parent_listener:
166                break
167            else:
168                # Output the line without trailing \n and whitespace.
169                self._on_output_callback(line.rstrip())
170
171    def set_on_output_callback(self, on_output_callback, binary=False):
172        """Sets the on_output_callback function.
173
174        Args:
175            on_output_callback: The function to be called when output is sent to
176                the output. The output callback has the following signature:
177
178                >>> def on_output_callback(output_line):
179                >>>     return None
180
181            binary: If True, read the process output as raw binary.
182        Returns:
183            self
184        """
185        self._on_output_callback = on_output_callback
186        self._binary_output = binary
187        return self
188
189    def __start_process(self):
190        """A convenient wrapper function for starting the ssh connection and
191        starting the syslog."""
192
193        self._ssh_client = create_ssh_connection(self.ip_address,
194                                                 self.ssh_username,
195                                                 self.ssh_config)
196        transport = self._ssh_client.get_transport()
197        channel = transport.open_session()
198        channel.get_pty()
199        self._output_file = channel.makefile()
200        logging.debug('Starting FuchsiaDevice (%s) syslog over ssh.' %
201                      self.ssh_username)
202        channel.exec_command('log_listener %s' % self.extra_params)
203        return transport
204
205    def _exec_loop(self):
206        """Executes a ssh connection to the Fuchsia Device syslog in a loop.
207
208        When the ssh connection terminates without stop() being called,
209        the threads are coalesced and the syslog is restarted.
210        """
211        start_up = True
212        while True:
213            if self._stopped:
214                break
215            if start_up:
216                ssh_transport = self.__start_process()
217                self._redirection_thread = Thread(target=self._redirect_output)
218                self._redirection_thread.start()
219                start_up = False
220            else:
221                if not ssh_transport.is_alive():
222                    if self._redirection_thread is not None:
223                        self._redirection_thread.join()
224                        self._redirection_thread = None
225                    self.start_up = True
226