improve interactive client

- remove irrelevant traceback on remote errors
- add run() function to execute scripts
- when started with bin/frappy-cli, use separate namespace

Change-Id: Ic808a76fa76ecd8d814d52b15a6d7d2203c6a2f3
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/30957
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-04-13 16:02:43 +02:00
parent 5784aa0f5d
commit 748ea1400a
3 changed files with 110 additions and 65 deletions

View File

@ -23,8 +23,6 @@
# #
# ***************************************************************************** # *****************************************************************************
from __future__ import print_function
import sys import sys
import argparse import argparse
from os import path from os import path
@ -32,7 +30,7 @@ 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, Console from frappy.client.interactive import Client, Console, clientenv, run
def parseArgv(argv): def parseArgv(argv):
@ -49,58 +47,56 @@ def parseArgv(argv):
return parser.parse_args(argv) return parser.parse_args(argv)
_USAGE = """ USAGE = """
Usage: Usage:
%s {client_assign}
# for all SECoP modules objects are created in the main namespace # for all SECoP modules objects are created in the main namespace
<module> # list all parameters <module> # list all parameters
<module>.<param> = <value> # change parameter <module>.<param> = <value> # change parameter
<module>(<target>) # set target and wait until not busy <module>(<target>) # set target and wait until not busy
# 'status' and 'value' changes are shown every 1 sec # 'status' and 'value' changes are shown every 1 sec
%s.mininterval = 0.2 # change minimal update interval to 0.2 sec (default is 1 second) {client_name}.mininterval = 0.2 # change minimal update interval to 0.2 s (default is 1 s)
watch(T) # watch changes of T.status and T.value (stop with ctrl-C) watch(T) # watch changes of T.status and T.value (stop with ctrl-C)
watch(T='status target') # watch status and target parameters watch(T='status target') # watch status and target parameters
watch(io, T=True) # watch io and all parameters of T watch(io, T=True) # watch io and all parameters of T
""" {tail}"""
_CLIENT_USAGE = """
c = Client('localhost:5000')
"""
Client.show_usage = False
args = parseArgv(sys.argv[1:]) args = parseArgv(sys.argv[1:])
if not args.node: if not args.node:
_usage_args = ("\ncli = Client('localhost:5000')\n", 'cli') usage_args = {
'client_assign': "\ncli = Client('localhost:5000')\n",
'client_name': 'cli'}
success = True success = True
else: else:
_usage_args = ('', '_c0') usage_args = {
'client_assign': '',
'client_name': '_c0'}
success = False success = False
for _idx, _node in enumerate(args.node): clientenv.init()
_client_name = '_c%d' % _idx
for idx, node in enumerate(args.node):
client_name = '_c%d' % idx
try: try:
setattr(sys.modules['__main__'], _client_name, Client(_node, name=_client_name)) clientenv.namespace[client_name] = Client(node, name=client_name)
success = True success = True
except Exception as e: except Exception as e:
print(repr(e)) print(repr(e))
run_error = ''
file_success = False file_success = False
try: try:
for file in args.include: for file in args.include:
with open(file, 'r') as f: run(file)
exec(f.read())
file_success = True file_success = True
except Exception as e: except Exception as e:
print('Error while executing %s: %s' % (file, e)) run_error = f'\n{clientenv.short_traceback()}'
if success: if success:
if args.include and file_success and args.only_execute: if args.include and file_success and args.only_execute:
print('skipping interactive mode') print('skipping interactive mode')
exit() exit()
print(_USAGE % _usage_args) print(USAGE.format(tail=run_error, **usage_args))
Console(sys.modules['__main__'].__dict__) Console()

View File

@ -36,4 +36,4 @@ if len(sys.argv) > 1:
else: else:
print(USAGE) print(USAGE)
Console(sys.modules['__main__'].__dict__, 'play') Console('play', sys.modules['__main__'].__dict__)

View File

@ -32,11 +32,14 @@ client = Client('localhost:5000') # start client.
<module>.<param> = <value> # change parameter <module>.<param> = <value> # change parameter
<module>(<target>) # set target and wait until not busy <module>(<target>) # set target and wait until not busy
# 'status' and 'value' changes are shown every 1 sec # 'status' and 'value' changes are shown every 1 sec
client.mininterval = 0.2 # change minimal update interval to 0.2 sec (default is 1 second) client.mininterval = 0.2 # change minimal update interval to 0.2 s (default is 1 s)
watch(T) # watch changes of T.status and T.value (stop with ctrl-C) watch(T) # watch changes of T.status and T.value (stop with ctrl-C)
watch(T='status target') # watch status and target parameters watch(T='status target') # watch status and target parameters
watch(io, T=True) # watch io and all parameters of T watch(io, T=True) # watch io and all parameters of T
run('filename') # execute a script
/filename # execute a script
""" """
import sys import sys
@ -45,6 +48,7 @@ import re
import code import code
import signal import signal
import os import os
import traceback
from os.path import expanduser from os.path import expanduser
from queue import Queue from queue import Queue
from frappy.client import SecopClient from frappy.client import SecopClient
@ -55,8 +59,6 @@ try:
except ImportError: except ImportError:
readline = None readline = None
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 CLR = '\r\x1b[K' # code to move to the left and clear current line
@ -210,7 +212,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:
Console.raise_without_traceback(error) clientenv.raise_with_short_traceback(error)
return value return value
def __call__(self, target=None): def __call__(self, target=None):
@ -231,7 +233,7 @@ class Module:
return self.value return self.value
def __repr__(self): def __repr__(self):
wid = max(len(k) for k in self._parameters) 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,
'\n'.join(self._one_line(k, wid) for k in self._parameters), '\n'.join(self._one_line(k, wid) for k in self._parameters),
@ -260,8 +262,7 @@ 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) clientenv.raise_with_short_traceback(error)
raise error
return value return value
def formatted(self, obj): def formatted(self, obj):
@ -275,9 +276,10 @@ class Param:
obj._running = Queue() obj._running = Queue()
try: try:
obj._secnode.setParameter(obj._name, self.name, value) obj._secnode.setParameter(obj._name, self.name, value)
return
except SECoPError as e: except SECoPError as e:
Console.raise_without_traceback(e) clientenv.raise_with_short_traceback(e)
# obj._secnode.log.error(repr(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)
@ -294,6 +296,8 @@ class Command:
if args: if args:
raise TypeError('mixed arguments forbidden') raise TypeError('mixed arguments forbidden')
result, _ = self.exec(self.modname, self.name, kwds) result, _ = self.exec(self.modname, self.name, kwds)
elif len(args) == 1:
result, _ = self.exec(self.modname, self.name, *args)
else: else:
result, _ = self.exec(self.modname, self.name, args or None) result, _ = self.exec(self.modname, self.name, args or None)
return result return result
@ -306,7 +310,7 @@ class Command:
def show_parameter(modname, pname, *args, forced=False, mininterval=0): def show_parameter(modname, pname, *args, forced=False, mininterval=0):
"""show parameter update""" """show parameter update"""
mobj = getattr(main, modname) mobj = clientenv.namespace[modname]
mobj._watch_parameter(modname, pname, *args) mobj._watch_parameter(modname, pname, *args)
@ -320,7 +324,7 @@ def watch(*args, **kwds):
else: else:
print(f'do not know {mobj!r}') print(f'do not know {mobj!r}')
for key, arg in kwds.items(): for key, arg in kwds.items():
mobj = getattr(main, key, None) mobj = clientenv.namespace.get(key)
if mobj is None: if mobj is None:
print(f'do not know {key!r}') print(f'do not know {key!r}')
else: else:
@ -345,6 +349,9 @@ class Client(SecopClient):
mininterval = 1 mininterval = 1
def __init__(self, uri, loglevel='info', name=''): def __init__(self, uri, loglevel='info', name=''):
if clientenv.namespace is None:
# called from a simple python interpeter
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(loglevel)
@ -352,10 +359,10 @@ class Client(SecopClient):
if prev: if prev:
log.info('remove previous client to %s', uri) log.info('remove previous client to %s', uri)
for modname in prev.modules: for modname in prev.modules:
prevnode = getattr(getattr(main, modname, None), '_secnode', None) prevnode = getattr(clientenv.namespace.get(modname), '_secnode', None)
if prevnode == prev: if prevnode == prev:
removed_modules.append(modname) removed_modules.append(modname)
delattr(main, modname) clientenv.namespace.pop(modname)
prev.disconnect() prev.disconnect()
self.secnodes[uri] = self self.secnodes[uri] = self
if name: if name:
@ -365,7 +372,7 @@ class Client(SecopClient):
created_modules = [] created_modules = []
skipped_modules = [] skipped_modules = []
for modname, moddesc in self.modules.items(): for modname, moddesc in self.modules.items():
prev = getattr(main, modname, None) prev = clientenv.namespace.get(modname)
if prev is None: if prev is None:
created_modules.append(modname) created_modules.append(modname)
else: else:
@ -383,7 +390,7 @@ class Client(SecopClient):
if 'status' in mobj._parameters: if 'status' in mobj._parameters:
self.register_callback((modname, 'status'), updateEvent=mobj._status_value_update) self.register_callback((modname, 'status'), updateEvent=mobj._status_value_update)
self.register_callback((modname, 'value'), updateEvent=mobj._status_value_update) self.register_callback((modname, 'value'), updateEvent=mobj._status_value_update)
setattr(main, modname, mobj) clientenv.namespace[modname] = mobj
if removed_modules: if removed_modules:
self.log.info('removed modules: %s', ' '.join(removed_modules)) self.log.info('removed modules: %s', ' '.join(removed_modules))
if skipped_modules: if skipped_modules:
@ -397,7 +404,7 @@ class Client(SecopClient):
"""handle logging messages""" """handle logging messages"""
if action == 'log': if action == 'log':
modname, loglevel = ident.split(':') modname, loglevel = ident.split(':')
modobj = getattr(main, modname, None) modobj = clientenv.namespace.get(modname)
if modobj: if modobj:
modobj.handle_log_message_(loglevel, data) modobj.handle_log_message_(loglevel, data)
return return
@ -408,9 +415,54 @@ class Client(SecopClient):
return f'Client({self.uri!r})' return f'Client({self.uri!r})'
def run(filepath):
clientenv.namespace.update({
"__file__": filepath,
"__name__": "__main__",
})
with open(filepath, 'rb') as file:
# pylint: disable=exec-used
exec(compile(file.read(), filepath, 'exec'), clientenv.namespace, None)
class ClientEnvironment:
namespace = None
last_frames = 0
def init(self, namespace=None):
self.namespace = namespace or {}
self.namespace.update(run=run, watch=watch, Client=Client)
def raise_with_short_traceback(self, exc):
# count number of lines of internal irrelevant stack (for remote errors)
self.last_frames = len(traceback.format_exception(*sys.exc_info()))
raise exc
def short_traceback(self):
"""cleanup tracback from irrelevant lines"""
lines = traceback.format_exception(*sys.exc_info())
# line 0: Traceback header
# skip line 1+2 (contains unspecific console line and exec code)
lines[1:3] = []
if ' exec(' in lines[1]:
# replace additional irrelevant exec line if needed with run command
lines[1:2] = []
# skip lines of client code not relevant for remote errors
lines[-self.last_frames-1:-1] = []
self.last_frames = 0
if len(lines) <= 2: # traceback contains only console line
lines = lines[-1:]
return ''.join(lines)
clientenv = ClientEnvironment()
class Console(code.InteractiveConsole): class Console(code.InteractiveConsole):
def __init__(self, local, name='cli'): def __init__(self, name='cli', namespace=None):
super().__init__(local) if namespace:
clientenv.namespace = namespace
super().__init__(clientenv.namespace)
history = None history = None
if readline: if readline:
try: try:
@ -428,12 +480,9 @@ class Console(code.InteractiveConsole):
Logger.sigwinch = bool(readline) # activate refresh signal Logger.sigwinch = bool(readline) # activate refresh signal
line = input(prompt) line = input(prompt)
Logger.sigwinch = False Logger.sigwinch = False
if line.startswith('/'):
line = f"run('{line[1:].strip()}')"
return line return line
@classmethod def showtraceback(self):
def raise_without_traceback(cls, exc): self.write(clientenv.short_traceback())
def omit_traceback_once(cls):
del Console.showtraceback
cls.showtraceback = omit_traceback_once
print('ERROR:', repr(exc))
raise exc