frappy.client.interactive: improve logging and error handling

- redesign logging, using python logging
- stop watching when an erro happens during update

Change-Id: Ibe96569ecc45df429b571232374c451de3f82f1f
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/33431
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
zolliker 2024-04-05 11:17:32 +02:00
parent 2561e82086
commit c7d23e40e9

View File

@ -28,8 +28,9 @@ import signal
import os import os
import traceback import traceback
import threading import threading
import logging
from os.path import expanduser from os.path import expanduser
from frappy.client import SecopClient from frappy.client import SecopClient, UnregisterCallback
from frappy.errors import SECoPError from frappy.errors import SECoPError
from frappy.datatypes import get_datatype, StatusType from frappy.datatypes import get_datatype, StatusType
try: try:
@ -55,40 +56,45 @@ watch(io, T=True) # watch io and all parameters of T
{tail}""" {tail}"""
LOG_LEVELS = {'debug', 'comlog', 'info', 'warning', 'error', 'off'} LOG_LEVELS = {
'debug': logging.DEBUG,
'comlog': logging.DEBUG+1,
'info': logging.INFO,
'warning': logging.WARN,
'error': logging.ERROR,
'off': logging.ERROR+1}
CLR = '\r\x1b[K' # code to move to the left and clear current line CLR = '\r\x1b[K' # code to move to the left and clear current line
class Logger: class Handler(logging.StreamHandler):
show_time = False def emit(self, record):
sigwinch = False super().emit(record)
if clientenv.sigwinch:
def __init__(self, loglevel='info'):
func = self.noop
for lev in 'debug', 'info', 'warning', 'error', 'exception':
if lev == loglevel:
func = self.emit
setattr(self, lev, func)
self._minute = 0
def emit(self, fmt, *args, **kwds):
if self.show_time:
now = time.time()
tm = time.localtime(now)
if tm.tm_min != self._minute:
self._minute = tm.tm_min
print(CLR + time.strftime('--- %H:%M:%S ---', tm))
sec = f'{now % 60.0:6.3f}'.replace(' ', '0')
print(CLR + sec, str(fmt) % args)
else:
print(CLR + (str(fmt) % args))
if self.sigwinch:
# SIGWINCH: 'window size has changed' -> triggers a refresh of the input line # SIGWINCH: 'window size has changed' -> triggers a refresh of the input line
os.kill(os.getpid(), signal.SIGWINCH) os.kill(os.getpid(), signal.SIGWINCH)
@staticmethod
def noop(fmt, *args, **kwds): class Logger(logging.Logger):
pass show_time = False
_minute = None
def __init__(self, name, loglevel='info'):
super().__init__(name, LOG_LEVELS.get(loglevel, logging.INFO))
handler = Handler()
handler.formatter = logging.Formatter('%(asctime)s%(message)s')
handler.formatter.formatTime = self.format_time
self.addHandler(handler)
def format_time(self, record, datefmt=None):
if self.show_time:
now = record.created
tm = time.localtime(now)
sec = f'{now % 60.0:6.3f}'.replace(' ', '0')
if tm.tm_min == self._minute:
return f'{CLR}{sec} '
self._minute = tm.tm_min
return f"{CLR}{time.strftime('--- %H:%M:%S ---', tm)}\n{sec} "
return ''
class PrettyFloat(float): class PrettyFloat(float):
@ -238,6 +244,9 @@ class Module:
return self.value return self.value
def __repr__(self): def __repr__(self):
return f'<module {self._name}>'
def showAll(self):
wid = max((len(k) for k in self._parameters), default=0) wid = max((len(k) for k in self._parameters), default=0)
return '%s\n%s%s' % ( return '%s\n%s%s' % (
self._title, self._title,
@ -271,10 +280,7 @@ class Param:
return value return value
def formatted(self, obj): def formatted(self, obj):
value, _, error = obj._secnode.cache[obj._name, self.name] return obj._secnode.cache[obj._name, self.name].formatted()
if error:
return repr(error)
return self.format(value)
def __set__(self, obj, value): def __set__(self, obj, value):
try: try:
@ -286,9 +292,6 @@ class Param:
clientenv.raise_with_short_traceback(e) clientenv.raise_with_short_traceback(e)
obj._secnode.log.error(repr(e)) obj._secnode.log.error(repr(e))
def format(self, value):
return self.datatype.format_value(value)
class Command: class Command:
def __init__(self, name, modname, secnode): def __init__(self, name, modname, secnode):
@ -337,9 +340,28 @@ def watch(*args, **kwds):
mobj._set_watching(arg) mobj._set_watching(arg)
print('---') print('---')
try: try:
nodes = set()
for mobj in modules: for mobj in modules:
nodes.add(mobj._secnode)
mobj._start_watching() mobj._start_watching()
time.sleep(3600)
close_event = threading.Event()
def close_node(online, state):
if online and state != 'shutdown':
return
close_event.set()
return UnregisterCallback
def handle_error(*_):
close_event.set()
return UnregisterCallback
for node in nodes:
node.register_callback(None, nodeStateChange=close_node, handleError=handle_error)
close_event.wait()
except KeyboardInterrupt as e: except KeyboardInterrupt as e:
clientenv.raise_with_short_traceback(e) clientenv.raise_with_short_traceback(e)
finally: finally:
@ -359,7 +381,7 @@ class Client(SecopClient):
clientenv.init(sys.modules['__main__'].__dict__) clientenv.init(sys.modules['__main__'].__dict__)
# remove previous client: # remove previous client:
prev = self.secnodes.pop(uri, None) prev = self.secnodes.pop(uri, None)
log = Logger(loglevel) log = Logger(name, loglevel)
removed_modules = [] removed_modules = []
if prev: if prev:
log.info('remove previous client to %s', uri) log.info('remove previous client to %s', uri)
@ -433,8 +455,10 @@ def run(filepath):
class ClientEnvironment: class ClientEnvironment:
namespace = None namespace = None
last_frames = 0 last_frames = 0
sigwinch = False
def init(self, namespace=None): def init(self, namespace=None):
self.nodes = []
self.namespace = namespace or {} self.namespace = namespace or {}
self.namespace.update(run=run, watch=watch, Client=Client) self.namespace.update(run=run, watch=watch, Client=Client)
@ -444,7 +468,7 @@ class ClientEnvironment:
raise exc raise exc
def short_traceback(self): def short_traceback(self):
"""cleanup tracback from irrelevant lines""" """cleanup traceback from irrelevant lines"""
lines = traceback.format_exception(*sys.exc_info()) lines = traceback.format_exception(*sys.exc_info())
# line 0: Traceback header # line 0: Traceback header
# skip line 1+2 (contains unspecific console line and exec code) # skip line 1+2 (contains unspecific console line and exec code)
@ -482,11 +506,15 @@ class Console(code.InteractiveConsole):
readline.write_history_file(history) readline.write_history_file(history)
def raw_input(self, prompt=""): def raw_input(self, prompt=""):
Logger.sigwinch = bool(readline) # activate refresh signal clientenv.sigwinch = bool(readline) # activate refresh signal
line = input(prompt) line = input(prompt)
Logger.sigwinch = False clientenv.sigwinch = False
if line.startswith('/'): if line.startswith('/'):
line = f"run('{line[1:].strip()}')" line = f"run('{line[1:].strip()}')"
module = clientenv.namespace.get(line.strip())
if isinstance(module, Module):
print(module.showAll())
line = ''
return line return line
def showtraceback(self): def showtraceback(self):
@ -499,7 +527,8 @@ def init(*nodes):
for idx, node in enumerate(nodes): for idx, node in enumerate(nodes):
client_name = '_c%d' % idx client_name = '_c%d' % idx
try: try:
clientenv.namespace[client_name] = Client(node, name=client_name) node = clientenv.namespace[client_name] = Client(node, name=client_name)
clientenv.nodes.append(node)
success = True success = True
except Exception as e: except Exception as e:
print(repr(e)) print(repr(e))