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