interactive client: avoid messing up the input line

- trigger a redraw of the input line when asynchronous log
  messages arrive
+ do not print traceback on 'remote' errors
+ persistent readline history

Change-Id: If85fd064c1c09c44e0cb0ebccbfc1b6411ad5aac
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/30793
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
zolliker 2023-03-27 11:21:15 +02:00
parent 2dad4b4ee1
commit 4f69899fbe
2 changed files with 70 additions and 22 deletions

View File

@ -25,14 +25,14 @@
from __future__ import print_function from __future__ import print_function
import sys import sys
import code
import argparse import argparse
from os import path from os import path
# Add import path for inplace usage # Add import path for inplace usage
sys.path.insert(0, path.abspath(path.join(path.dirname(__file__), '..'))) sys.path.insert(0, path.abspath(path.join(path.dirname(__file__), '..')))
from frappy.client.interactive import Client, watch from frappy.client.interactive import Client, watch, Console
def parseArgv(argv): def parseArgv(argv):
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
@ -47,6 +47,7 @@ def parseArgv(argv):
nargs='*', type=str, default=[]) nargs='*', type=str, default=[])
return parser.parse_args(argv) return parser.parse_args(argv)
_USAGE = """ _USAGE = """
Usage: Usage:
%s %s
@ -101,4 +102,4 @@ if success:
print('skipping interactive mode') print('skipping interactive mode')
exit() exit()
print(_USAGE % _usage_args) print(_USAGE % _usage_args)
code.interact(banner='', local=sys.modules['__main__'].__dict__) Console(sys.modules['__main__'].__dict__)

View File

@ -42,18 +42,28 @@ watch(io, T=True) # watch io and all parameters of T
import sys import sys
import time import time
import re import re
import code
import signal
import os
from os.path import expanduser
from queue import Queue from queue import Queue
from frappy.client import SecopClient from frappy.client import SecopClient
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:
import readline
except ImportError:
readline = None
main = sys.modules['__main__'] main = sys.modules['__main__']
LOG_LEVELS = {'debug', 'comlog', 'info', 'warning', 'error', 'off'} LOG_LEVELS = {'debug', 'comlog', 'info', 'warning', 'error', 'off'}
CLR = '\r\x1b[K' # code to move to the left and clear current line
class Logger: class Logger:
show_time = False show_time = False
sigwinch = False
def __init__(self, loglevel='info'): def __init__(self, loglevel='info'):
func = self.noop func = self.noop
@ -66,22 +76,23 @@ class Logger:
def emit(self, fmt, *args, **kwds): def emit(self, fmt, *args, **kwds):
if self.show_time: if self.show_time:
now = time.time() now = time.time()
minute = now // 60 tm = time.localtime(now)
if minute != self._minute: if tm.tm_min != self._minute:
self._minute = minute self._minute = tm.tm_min
print(time.strftime('--- %H:%M:%S ---', time.localtime(now))) print(CLR + time.strftime('--- %H:%M:%S ---', tm))
print('%6.3f' % (now % 60.0), str(fmt) % args) sec = ('%6.3f' % (now % 60.0)).replace(' ', '0')
print(CLR + sec, str(fmt) % args)
else: else:
print(str(fmt) % args) print(CLR + (str(fmt) % args))
if self.sigwinch:
# SIGWINCH: 'window size has changed' -> triggers a refresh of the input line
os.kill(os.getpid(), signal.SIGWINCH)
@staticmethod @staticmethod
def noop(fmt, *args, **kwds): def noop(fmt, *args, **kwds):
pass pass
infologger = Logger('info')
class PrettyFloat(float): class PrettyFloat(float):
"""float with a nicer repr: """float with a nicer repr:
@ -118,16 +129,12 @@ class Module:
def _one_line(self, pname, minwid=0): def _one_line(self, pname, minwid=0):
"""return <module>.<param> = <value> truncated to one line""" """return <module>.<param> = <value> truncated to one line"""
param = getattr(type(self), pname) param = getattr(type(self), pname)
try: result = param.formatted(self)
value = getattr(self, pname)
r = param.format(value)
except Exception as e:
r = repr(e)
pname = pname.ljust(minwid) pname = pname.ljust(minwid)
vallen = 113 - len(self._name) - len(pname) vallen = 113 - len(self._name) - len(pname)
if len(r) > vallen: if len(result) > vallen:
r = r[:vallen - 4] + ' ...' result = result[:vallen - 4] + ' ...'
return '%s.%s = %s' % (self._name, pname, r) return '%s.%s = %s' % (self._name, pname, result)
def _isBusy(self): def _isBusy(self):
return self.status[0] // 100 == StatusType.BUSY // 100 return self.status[0] // 100 == StatusType.BUSY // 100
@ -185,6 +192,7 @@ class Module:
def _start_watching(self): def _start_watching(self):
for pname in self._watched_params: for pname in self._watched_params:
self._watch_parameter(self, pname, forced=True)
self._secnode.register_callback((self._name, pname), updateEvent=self._watch_parameter) self._secnode.register_callback((self._name, pname), updateEvent=self._watch_parameter)
self._secnode.request('logging', self._name, self._log_level) self._secnode.request('logging', self._name, self._log_level)
self._secnode.register_callback(None, nodeStateChange=self._set_log_level) self._secnode.register_callback(None, nodeStateChange=self._set_log_level)
@ -202,7 +210,7 @@ class Module:
def read(self, pname='value'): def read(self, pname='value'):
value, _, error = self._secnode.readParameter(self._name, pname) value, _, error = self._secnode.readParameter(self._name, pname)
if error: if error:
raise error Console.raise_without_traceback(error)
return value return value
def __call__(self, target=None): def __call__(self, target=None):
@ -252,16 +260,24 @@ class Param:
return self return self
value, _, error = obj._secnode.cache[obj._name, self.name] value, _, error = obj._secnode.cache[obj._name, self.name]
if error: if error:
Console.raise_without_traceback(error)
raise error raise error
return value return value
def formatted(self, obj):
value, _, error = obj._secnode.cache[obj._name, self.name]
if error:
return repr(error)
return self.format(value)
def __set__(self, obj, value): def __set__(self, obj, value):
if self.name == 'target': if self.name == 'target':
obj._running = Queue() obj._running = Queue()
try: try:
obj._secnode.setParameter(obj._name, self.name, value) obj._secnode.setParameter(obj._name, self.name, value)
except SECoPError as e: except SECoPError as e:
obj._secnode.log.error(repr(e)) Console.raise_without_traceback(e)
# obj._secnode.log.error(repr(e))
def format(self, value): def format(self, value):
return self.datatype.format_value(value) return self.datatype.format_value(value)
@ -390,3 +406,34 @@ class Client(SecopClient):
def __repr__(self): def __repr__(self):
return 'Client(%r)' % self.uri return 'Client(%r)' % self.uri
class Console(code.InteractiveConsole):
def __init__(self, local):
super().__init__(local)
history = None
if readline:
try:
history = expanduser('~/.frappy-cli-history')
readline.read_history_file(history)
except FileNotFoundError:
pass
try:
self.interact('', '')
finally:
if history:
readline.write_history_file(history)
def raw_input(self, prompt=""):
Logger.sigwinch = bool(readline) # activate refresh signal
line = input(prompt)
Logger.sigwinch = False
return line
@classmethod
def raise_without_traceback(cls, exc):
def omit_traceback_once(cls):
del Console.showtraceback
cls.showtraceback = omit_traceback_once
print('ERROR:', repr(exc))
raise exc