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