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