From e1a30f2297ec939ad8225c041eeed7a305699cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karel=20Ko=C4=8D=C3=AD?= <karel.koci@nic.cz> Date: Thu, 16 Jul 2020 11:00:12 +0200 Subject: [PATCH] Replace logging trough pexpect with realtime logging Problem with logging trough pexpect is that we log output only when we expect it. That is when we access it trough pexpect. This means that time and synchronization when logs arrive from multiple sources can't be archived and only one source logs at the time (the one that is used currently by pexpect). This is not ideal as we are interested in situation such as restarting network interfaces that produce some logs not only on router but also in container at the same time. Synchronization on time bases in such cases is important and we can't wait for code to call pexpect to print it to logs later on. This instead utilizes sockets and by reading source immediately and just passing it to socket. --- nsfarm/cli.py | 94 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/nsfarm/cli.py b/nsfarm/cli.py index e21b13c..b9f2c47 100644 --- a/nsfarm/cli.py +++ b/nsfarm/cli.py @@ -8,9 +8,14 @@ support for shell and u-boot. They differ in a way how they handle prompt and m # There are new line character matches in regular expressions. Correct one is \r\n but some serial controlles for some # reason also use \n\r so we match both alternatives. # +import os +import io import abc import logging import base64 +import fcntl +import socket +import threading import typing _FLUSH_BUFFLEN = 2048 @@ -52,7 +57,8 @@ class Cli(abc.ABC): All keyword arguments are passed to pexpect's expect call. """ - @abc.abstractproperty + @property + @abc.abstractmethod def output(self): """All output before latest prompt. @@ -190,6 +196,90 @@ class Uboot(Cli): return self._output +class FDLogging: + """Live logging with data passtrough. + + This is stream logging that logs communication comming from and to file descript trough socket. This intended use is + for direct output to be visible live in logs. + + This has one primary limitation and that is output only in lines. Log is created only when new line character is + located not before that. + """ + _EXPECTED_EOL = b'\n\r' + + def __init__(self, fd, logger, in_level=logging.INFO, out_level=logging.DEBUG): + self.logger = logger + self.in_level = in_level + self.out_level = out_level + self.fileno = fd.fileno() + self.our_sock, self.user_sock = socket.socketpair() + + # Input + self.inputthread = threading.Thread(target=self._input, daemon=True) + self.inputthread.start() + # Output + self.outputthread = threading.Thread(target=self._output, daemon=True) + self.outputthread.start() + + def socket(self): + """Returns socket for user to use to communicate trough this logged passtrough. + """ + return self.user_sock + + def close(self): + """Close socket and stop logging. + """ + if self.our_sock is not None: + # Terminates and cleans _input + _orig_filestatus = fcntl.fcntl(self.fileno, fcntl.F_GETFL) + fcntl.fcntl(self.fileno, fcntl.F_SETFL, _orig_filestatus | os.O_NONBLOCK) + self.inputthread.join() + fcntl.fcntl(self.fileno, fcntl.F_SETFL, _orig_filestatus) + # Terminates and cleans _output + self.our_sock.close() + self.outputthread.join() + self.our_sock = None + + def __del__(self): + self.close() + + def _log_line(self, line, level): + self.logger.log(level, repr(line.rstrip(self._EXPECTED_EOL).expandtabs())[2:-1]) + + def _log(self, prev_data, new_data, level): + data = prev_data + new_data + lines = data.splitlines(keepends=True) + if not lines: + return + # The last line does not have to be terminated (no new line character) so just preserve it + reminder = lines.pop() if lines[-1] or lines[-1][-1] not in self._EXPECTED_EOL else b'' + for line in lines: + self._log_line(line, level) + return reminder + + def _input(self): + data = b'' + while True: + try: + new_data = os.read(self.fileno, io.DEFAULT_BUFFER_SIZE) + except io.BlockingIOError: + self._log_line(data, self.in_level) + return + self.our_sock.sendall(new_data) + data = self._log(data, new_data, self.in_level) + + def _output(self): + data = b'' + while True: + try: + new_data = self.our_sock.recv(io.DEFAULT_BUFFER_SIZE) + except io.BlockingIOError: + self._log_line(data, self.out_level) + return + os.write(self.fileno, new_data) + data = self._log(data, new_data, self.out_level) + + class PexpectLogging: """Logging for pexpect. @@ -198,7 +288,7 @@ class PexpectLogging: _EXPECTED_EOL = b'\n\r' def __init__(self, logger): - self._level = logging.INFO + self._level = logging.DEBUG self.logger = logger self.linebuf = b'' -- GitLab