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