blob: 67a850a38bd5d1ba241e5e841f9f438f0bca5cd5 [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright 2019 - The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import time
from threading import Thread
from acts.libs.logging import log_stream
from acts.libs.logging.log_stream import LogStyles
from acts.controllers.android_lib.logcat import TimestampTracker
from acts.controllers.fuchsia_lib.utils_lib import create_ssh_connection
# paramiko-ng has a log line, line number in 1982 in paramiko/transport.py that
# presents a ERROR log message that is innocuous but could confuse the user.
# Therefore by setting the log level to CRITICAL the message is not displayed
# and everything is recovered as expected.
logging.getLogger("paramiko").setLevel(logging.CRITICAL)
def _log_line_func(log, timestamp_tracker):
"""Returns a lambda that logs a message to the given logger."""
def log_line(message):
timestamp_tracker.read_output(message)
log.info(message)
return log_line
def create_syslog_process(serial,
base_path,
ip_address,
ssh_username,
ssh_config,
ssh_port=22,
extra_params=''):
"""Creates a FuchsiaSyslogProcess that automatically attempts to reconnect.
Args:
serial: The unique identifier for the device.
base_path: The base directory used for syslog file output.
ip_address: The ip address of the device to get the syslog.
ssh_username: Username for the device for the Fuchsia Device.
ssh_config: Location of the ssh_config for connecting to the remote
device
ssh_port: The ssh port of the Fuchsia device.
extra_params: Any additional params to be added to the syslog cmdline.
Returns:
A FuchsiaSyslogProcess object.
"""
logger = log_stream.create_logger('fuchsia_log_%s' % serial,
base_path=base_path,
log_styles=(LogStyles.LOG_DEBUG
| LogStyles.MONOLITH_LOG))
syslog = FuchsiaSyslogProcess(ssh_username, ssh_config, ip_address,
extra_params, ssh_port)
timestamp_tracker = TimestampTracker()
syslog.set_on_output_callback(_log_line_func(logger, timestamp_tracker))
return syslog
class FuchsiaSyslogError(Exception):
"""Raised when invalid operations are run on a Fuchsia Syslog."""
class FuchsiaSyslogProcess(object):
"""A class representing a Fuchsia Syslog object that communicates over ssh.
"""
def __init__(self, ssh_username, ssh_config, ip_address, extra_params,
ssh_port):
"""
Args:
ssh_username: The username to connect to Fuchsia over ssh.
ssh_config: The ssh config that holds the information to connect to
a Fuchsia device over ssh.
ip_address: The ip address of the Fuchsia device.
ssh_port: The ssh port of the Fuchsia device.
"""
self.ssh_config = ssh_config
self.ip_address = ip_address
self.extra_params = extra_params
self.ssh_username = ssh_username
self.ssh_port = ssh_port
self._output_file = None
self._ssh_client = None
self._listening_thread = None
self._redirection_thread = None
self._on_output_callback = lambda *args, **kw: None
self._started = False
self._stopped = False
def start(self):
"""Starts reading the data from the syslog ssh connection."""
if self._started:
logging.info('Syslog has already started for FuchsiaDevice (%s).' %
self.ip_address)
return None
self._started = True
self._listening_thread = Thread(target=self._exec_loop)
self._listening_thread.start()
time_up_at = time.time() + 10
while self._ssh_client is None:
if time.time() > time_up_at:
raise FuchsiaSyslogError('Unable to connect to syslog!')
self._stopped = False
def stop(self):
"""Stops listening to the syslog ssh connection and coalesces the
threads.
"""
if self._stopped:
logging.info('Syslog is already stopped for FuchsiaDevice (%s).' %
self.ip_address)
return None
self._stopped = True
try:
self._ssh_client.close()
except Exception as e:
raise e
finally:
self._join_threads()
self._started = False
return None
def _join_threads(self):
"""Waits for the threads associated with the process to terminate."""
if self._listening_thread is not None:
if self._redirection_thread is not None:
self._redirection_thread.join()
self._redirection_thread = None
self._listening_thread.join()
self._listening_thread = None
def _redirect_output(self):
"""Redirects the output from the ssh connection into the
on_output_callback.
"""
# In some cases, the parent thread (listening_thread) was being joined
# before the redirect_thread could finish initiating, meaning it would
# run forever attempting to redirect the output even if the listening
# thread was torn down. This allows the thread to close at the test
# end.
parent_listener = self._listening_thread
while True:
line = self._output_file.readline()
if not line:
return
if self._listening_thread != parent_listener:
break
else:
# Output the line without trailing \n and whitespace.
self._on_output_callback(line.rstrip())
def set_on_output_callback(self, on_output_callback, binary=False):
"""Sets the on_output_callback function.
Args:
on_output_callback: The function to be called when output is sent to
the output. The output callback has the following signature:
>>> def on_output_callback(output_line):
>>> return None
binary: If True, read the process output as raw binary.
Returns:
self
"""
self._on_output_callback = on_output_callback
self._binary_output = binary
return self
def __start_process(self):
"""A convenient wrapper function for starting the ssh connection and
starting the syslog."""
self._ssh_client = create_ssh_connection(self.ip_address,
self.ssh_username,
self.ssh_config,
ssh_port=self.ssh_port)
transport = self._ssh_client.get_transport()
channel = transport.open_session()
channel.get_pty()
self._output_file = channel.makefile()
logging.debug('Starting FuchsiaDevice (%s) syslog over ssh.' %
self.ssh_username)
channel.exec_command('log_listener %s' % self.extra_params)
return transport
def _exec_loop(self):
"""Executes a ssh connection to the Fuchsia Device syslog in a loop.
When the ssh connection terminates without stop() being called,
the threads are coalesced and the syslog is restarted.
"""
start_up = True
while True:
if self._stopped:
break
if start_up:
ssh_transport = self.__start_process()
self._redirection_thread = Thread(target=self._redirect_output)
self._redirection_thread.start()
start_up = False
else:
if not ssh_transport.is_alive():
if self._redirection_thread is not None:
self._redirection_thread.join()
self._redirection_thread = None
self.start_up = True