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:
parent
5784aa0f5d
commit
748ea1400a
@ -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()
|
||||||
|
@ -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__)
|
||||||
|
@ -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
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user