rework of the server side

main change: the same server may be used for several instruments

- client classes are 'Interactors' dealing with the parameters etc.
- methods from history are added to the clients
+ improvements on the js client side
This commit is contained in:
l_samenv
2025-03-19 08:14:06 +01:00
parent b8ac8f8bb5
commit 958e472f3b
8 changed files with 445 additions and 292 deletions

View File

@ -43,7 +43,91 @@ def to_json_sse(msg):
return 'data: %s\n\n' % txt
instrument = None
class Server:
"""singleton: created once in this module"""
interactor_classes = None
client_cls = None
history_cls = None
history = None
single_instrument = None
db = None
def __init__(self):
self.instruments = {}
self.clients = {}
def remove(self, client):
try:
del self.clients[client.id]
except KeyError:
logging.warning('client already removed %s', client.id)
def lookup_streams(self, instrument, stream=None, device=None):
if self.single_instrument:
instrument = self.single_instrument
if stream:
if isinstance(stream, str):
streams = stream.split(',') if stream else []
else:
streams = stream
else:
streams = []
device_names = devices = device.split(',') if device else []
tags = {}
if instrument:
# tags['instrument'] = instrument
stream_dict = self.db.get_streams(instrument, stream=list(streams), device=devices)
streams.extend((s for s in stream_dict if s not in streams))
if not devices:
device_names = list(filter(None, (t.get('device') for t in stream_dict.values())))
if streams:
tags['stream'] = streams[0] if len(streams) == 1 else streams
if devices:
tags['device'] = devices[0] if len(devices) == 1 else devices
return streams, tags, ','.join(device_names)
def register_client(self, instrument=None, stream=None, device=None):
streams, tags, device_name = self.lookup_streams(instrument, stream, device)
client = self.client_cls(self, streams, instrument or '', device_name)
history = self.history_cls(self, instrument, device_name, tags)
# history.db.debug = True
# all relevant methods of the history instance are saved in client.handlers
# so there is no reference needed to history anymore
client.handlers.update(history.handlers)
self.clients[client.id] = client
return client
def run(self, port, db, history_cls, client_cls, single_instrument=None, **interactor_classes):
self.single_instrument = single_instrument
self.db = db
self.history_cls = history_cls
self.client_cls = client_cls
self.interactor_classes = interactor_classes
app.debug = True
logging.basicConfig(filename='webserver.log', filemode='w', level=logging.INFO,
format='%(asctime)s %(levelname)s %(message)s')
# srv = gevent.wsgi.WSGIServer(('', port), app, keyfile='key.key', certfile='key.crt')
srv = gevent.pywsgi.WSGIServer(('', port), app, log=logging.getLogger('server'))
def handle_term(sig, frame):
srv.stop()
srv.close()
signal.signal(signal.SIGTERM, handle_term)
# def handle_pdb(sig, frame):
# import pdb
# print('PDB')
# pdb.Pdb().set_trace(frame)
# signal.signal(signal.SIGUSR1, handle_pdb)
srv.serve_forever()
server = Server()
app = flask.Flask(__name__)
update_rider = circularlog.Rider("upd")
@ -51,9 +135,11 @@ pollinterval = 0.2
@app.route('/update')
def get_update(path=None):
def get_update(_=None):
# Client Adress: socket.getfqdn(flask.request.remote_addr)
client = instrument.new_client()
kwargs = {k: flask.request.values.get(k) for k in ('instrument', 'stream', 'device')}
client = server.register_client(**kwargs)
client.remote_info = circularlog.strtm() + " " + socket.getfqdn(flask.request.remote_addr.split(':')[-1])
@flask.stream_with_context
@ -61,7 +147,8 @@ def get_update(path=None):
logging.info('UPDATE %s %s', client.id, socket.getfqdn(flask.request.remote_addr.split(':')[-1]))
# msg = dict(type='id', id=client.id, title=instrument.title);
# yield to_json_sse(msg)
msg = dict(type='id', id=client.id, instrument=instrument.title, device=instrument.device)
msg = dict(type='id', id=client.id, instrument=kwargs.get('instrument', '<unknown>'),
device=client.device_name)
yield to_json_sse(msg)
try:
lastmsg = time.time()
@ -87,11 +174,11 @@ def get_update(path=None):
logging.info("except clause %r", repr(e))
logging.info('CLOSED %s', client.id)
print('CLOSE client')
instrument.remove(client)
server.remove(client)
except Exception as e:
logging.info('error')
logging.error('%s', traceback.format_exc())
instrument.remove(client)
server.remove(client)
# msg = dict(type='error',error=traceback.format_exc())
# yield to_json_sse(msg)
@ -109,8 +196,8 @@ def dump_circular():
@app.route('/clients')
def show_clients():
result = ""
for id in instrument.clients:
c = instrument.clients[id]
for id in server.clients:
c = server.clients[id]
result += c.remote_info + " " + "; ".join(c.info()) + "<br>"
return result
@ -123,9 +210,8 @@ def export():
logging.info('GET %s %s', path, repr(kwargs))
try:
id = kwargs.pop('id')
print('export')
client = instrument.clients[id]
bytes = client.w_export(**kwargs)
client = server.clients[id]
bytes = client.handlers['export'](**kwargs)
return flask.send_file(
bytes,
as_attachment=True,
@ -158,8 +244,8 @@ def reply():
logging.info('GET %s %r', path, kwargs)
try:
id = kwargs.pop('id')
client = instrument.clients[id]
msg = getattr(client, "w_" + path[1:])(**kwargs)
client = server.clients[id]
msg = client.handlers[path[1:]](**kwargs)
except Exception as e:
logging.error('%s', traceback.format_exc())
circularlog.log()
@ -211,6 +297,9 @@ def replace_by_empty(file):
@app.route('/')
def default():
if not any(flask.request.values.get(k) for k in ('instrument', 'server', 'device')):
if not server.single_instrument:
return select_experiment()
return general_file('SEAWebClient.html')
@ -224,7 +313,7 @@ th {
</style>
<tr><th>instrument</th><th colspan=99>devices</th></tr>''']
result = {}
for stream, tags in instrument.get_streams().items():
for stream, tags in server.db.get_streams().items():
ins = tags.get('instrument', '0')
result.setdefault(ins, []).append((stream, tags.get('device')))
bare_streams = result.pop('0', [])
@ -240,7 +329,9 @@ th {
@app.route('/select_experiment')
def select_experiment():
out = ['''<html><body>
out = ['''<html><head>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<style>
th {
text-align: left;
@ -249,7 +340,8 @@ th {
a {
text-decoration: none;
}
</style><table>
</style></head>
<body><table>
''']
showtitle = 0
ONEMONTH = 30 * 24 * 3600
@ -271,7 +363,7 @@ a {
starttime, endtime = now - ONEMONTH, now
chunk_list = []
for key, chunk_dict in instrument.get_experiments(starttime, endtime).items():
for key, chunk_dict in server.db.get_experiments(starttime, endtime).items():
for (streams, devices), chunks in chunk_dict.items():
chunk_list.extend((r[1], r[0], key, devices) for r in chunks)
chunk_list.sort(reverse=True)
@ -322,37 +414,3 @@ def general_file(file):
def hostport_split(hostport):
h = hostport.split(':')
return (h[0], int(h[1]))
# def handle_pdb(sig, frame):
# import pdb
# print('PDB')
# pdb.Pdb().set_trace(frame)
def main(cls, **config):
global instrument
if not config:
for arg in sys.argv[1:]:
key, _, value = arg.partition('=')
config[key] = value
port = int(config['port'])
inst_name = config['instrument']
instrument = cls(inst_name, config)
# signal.signal(signal.SIGUSR1, handle_pdb)
def handle_term(sig, _):
server.stop()
server.close()
signal.signal(signal.SIGTERM, handle_term)
app.debug = True
logging.basicConfig(filename='log/%s.log' % inst_name, filemode='w', level=logging.INFO,
format='%(asctime)s %(levelname)s %(message)s')
#server = gevent.wsgi.WSGIServer(('', port), app, keyfile='key.key', certfile='key.crt')
server = gevent.pywsgi.WSGIServer(('', port), app, log=logging.getLogger('server'))
server.serve_forever()