• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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                 ssh_port=22,
49                 extra_params=''):
50    """Creates a FuchsiaSyslogProcess that automatically attempts to reconnect.
51
52    Args:
53        serial: The unique identifier for the device.
54        base_path: The base directory used for syslog file output.
55        ip_address: The ip address of the device to get the syslog.
56        ssh_username: Username for the device for the Fuchsia Device.
57        ssh_config: Location of the ssh_config for connecting to the remote
58            device
59        ssh_port: The ssh port of the Fuchsia device.
60        extra_params: Any additional params to be added to the syslog cmdline.
61
62    Returns:
63        A FuchsiaSyslogProcess object.
64    """
65    logger = log_stream.create_logger('fuchsia_log_%s' % serial,
66                                      base_path=base_path,
67                                      log_styles=(LogStyles.LOG_DEBUG
68                                                  | LogStyles.MONOLITH_LOG))
69    syslog = FuchsiaSyslogProcess(ssh_username, ssh_config, ip_address,
70                                  extra_params, ssh_port)
71    timestamp_tracker = TimestampTracker()
72    syslog.set_on_output_callback(_log_line_func(logger, timestamp_tracker))
73    return syslog
74
75
76class FuchsiaSyslogError(Exception):
77    """Raised when invalid operations are run on a Fuchsia Syslog."""
78
79
80class FuchsiaSyslogProcess(object):
81    """A class representing a Fuchsia Syslog object that communicates over ssh.
82    """
83    def __init__(self,
84        ssh_username,
85        ssh_config,
86        ip_address,
87        extra_params,
88        ssh_port):
89        """
90        Args:
91            ssh_username: The username to connect to Fuchsia over ssh.
92            ssh_config: The ssh config that holds the information to connect to
93            a Fuchsia device over ssh.
94            ip_address: The ip address of the Fuchsia device.
95            ssh_port: The ssh port of the Fuchsia device.
96        """
97        self.ssh_config = ssh_config
98        self.ip_address = ip_address
99        self.extra_params = extra_params
100        self.ssh_username = ssh_username
101        self.ssh_port = ssh_port
102        self._output_file = None
103        self._ssh_client = None
104        self._listening_thread = None
105        self._redirection_thread = None
106        self._on_output_callback = lambda *args, **kw: None
107
108        self._started = False
109        self._stopped = False
110
111    def start(self):
112        """Starts reading the data from the syslog ssh connection."""
113        if self._started:
114            logging.info('Syslog has already started for FuchsiaDevice (%s).' %
115                         self.ip_address)
116            return None
117        self._started = True
118
119        self._listening_thread = Thread(target=self._exec_loop)
120        self._listening_thread.start()
121
122        time_up_at = time.time() + 10
123
124        while self._ssh_client is None:
125            if time.time() > time_up_at:
126                raise FuchsiaSyslogError('Unable to connect to syslog!')
127
128        self._stopped = False
129
130    def stop(self):
131        """Stops listening to the syslog ssh connection and coalesces the
132        threads.
133        """
134        if self._stopped:
135            logging.info('Syslog is already stopped for FuchsiaDevice (%s).' %
136                         self.ip_address)
137            return None
138        self._stopped = True
139
140        try:
141            self._ssh_client.close()
142        except Exception as e:
143            raise e
144        finally:
145            self._join_threads()
146            self._started = False
147            return None
148
149    def _join_threads(self):
150        """Waits for the threads associated with the process to terminate."""
151        if self._listening_thread is not None:
152            if self._redirection_thread is not None:
153                self._redirection_thread.join()
154                self._redirection_thread = None
155
156            self._listening_thread.join()
157            self._listening_thread = None
158
159    def _redirect_output(self):
160        """Redirects the output from the ssh connection into the
161        on_output_callback.
162        """
163        # In some cases, the parent thread (listening_thread) was being joined
164        # before the redirect_thread could finish initiating, meaning it would
165        # run forever attempting to redirect the output even if the listening
166        # thread was torn down. This allows the thread to close at the test
167        # end.
168        parent_listener = self._listening_thread
169        while True:
170            line = self._output_file.readline()
171
172            if not line:
173                return
174            if self._listening_thread != parent_listener:
175                break
176            else:
177                # Output the line without trailing \n and whitespace.
178                self._on_output_callback(line.rstrip())
179
180    def set_on_output_callback(self, on_output_callback, binary=False):
181        """Sets the on_output_callback function.
182
183        Args:
184            on_output_callback: The function to be called when output is sent to
185                the output. The output callback has the following signature:
186
187                >>> def on_output_callback(output_line):
188                >>>     return None
189
190            binary: If True, read the process output as raw binary.
191        Returns:
192            self
193        """
194        self._on_output_callback = on_output_callback
195        self._binary_output = binary
196        return self
197
198    def __start_process(self):
199        """A convenient wrapper function for starting the ssh connection and
200        starting the syslog."""
201
202        self._ssh_client = create_ssh_connection(self.ip_address,
203                                                 self.ssh_username,
204                                                 self.ssh_config,
205                                                 ssh_port=self.ssh_port)
206        transport = self._ssh_client.get_transport()
207        channel = transport.open_session()
208        channel.get_pty()
209        self._output_file = channel.makefile()
210        logging.debug('Starting FuchsiaDevice (%s) syslog over ssh.' %
211                      self.ssh_username)
212        channel.exec_command('log_listener %s' % self.extra_params)
213        return transport
214
215    def _exec_loop(self):
216        """Executes a ssh connection to the Fuchsia Device syslog in a loop.
217
218        When the ssh connection terminates without stop() being called,
219        the threads are coalesced and the syslog is restarted.
220        """
221        start_up = True
222        while True:
223            if self._stopped:
224                break
225            if start_up:
226                ssh_transport = self.__start_process()
227                self._redirection_thread = Thread(target=self._redirect_output)
228                self._redirection_thread.start()
229                start_up = False
230            else:
231                if not ssh_transport.is_alive():
232                    if self._redirection_thread is not None:
233                        self._redirection_thread.join()
234                        self._redirection_thread = None
235                    self.start_up = True
236