Files
seweb/webserver.py
Markus Zolliker 2c59e37074 major rework
using influxdb structure independed of nicos cache
2025-03-18 14:14:45 +01:00

291 lines
8.8 KiB
Python
Executable File

from gevent import monkey
monkey.patch_all()
import sys
import time
import signal
import socket
import traceback
import logging
import json
import gevent
import gevent.pywsgi
import gevent.queue
import flask
import circularlog
def guess_mimetype(filename):
if filename.endswith('.js'):
mimetype = 'text/javascript'
elif filename.endswith('.css'):
mimetype = 'text/css'
elif filename.endswith('.ico'):
mimetype = 'image/x-icon'
elif filename.endswith(".png"):
mimetype = "image/png"
else:
mimetype = 'text/html'
return mimetype
class MyEncoder(json.JSONEncoder):
def default(self, obj):
try:
return super().default(obj)
except TypeError:
return int(obj) # try to convert SECoP Enum
# SSE 'protocol' is described here: https://bit.ly/UPFyxY
def to_json_sse(msg):
txt = json.dumps(msg, separators=(',', ': '), cls=MyEncoder)
logging.debug('data: %s', txt)
return 'data: %s\n\n' % txt
instrument = None
app = flask.Flask(__name__)
update_rider = circularlog.Rider("upd")
pollinterval = 0.2
@app.route('/update')
def get_update(path=None):
# Client Adress: socket.getfqdn(flask.request.remote_addr)
client = instrument.new_client()
client.remote_info = circularlog.strtm() + " " + socket.getfqdn(flask.request.remote_addr.split(':')[-1])
@flask.stream_with_context
def generator():
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)
yield to_json_sse(msg)
try:
lastmsg = time.time()
while True:
if client.info() == "":
print(time.time()-lastmsg)
messages = client.poll()
for msg in messages:
update_rider.put('-', repr(msg))
yield to_json_sse(msg)
if messages:
lastmsg = time.time()
else:
if time.time() > lastmsg + 30:
if not client.info():
raise GeneratorExit("no activity")
logging.info('HEARTBEAT %s (%s)', client.id, "; ".join(client.info()))
yield to_json_sse(dict(type='heartbeat'))
lastmsg = time.time()
else:
gevent.sleep(pollinterval)
except GeneratorExit as e:
logging.info("except clause %r", repr(e))
logging.info('CLOSED %s', client.id)
print('CLOSE client')
instrument.remove(client)
pass
except Exception as e:
logging.info('error')
logging.error('%s', traceback.format_exc())
instrument.remove(client)
# msg = dict(type='error',error=traceback.format_exc())
# yield to_json_sse(msg)
resp = flask.Response(generator(), mimetype='text/event-stream')
resp.headers['Access-Control-Allow-Origin'] = '*'
return resp
@app.route('/circular')
def dump_circular():
circularlog.log()
return "log"
@app.route('/clients')
def show_clients():
result = ""
for id in instrument.clients:
c = instrument.clients[id]
result += c.remote_info + " " + "; ".join(c.info()) + "<br>"
return result
@app.route('/export')
def export():
args = flask.request.args
kwargs = dict((k, args.get(k)) for k in args)
path = flask.request.path
logging.info('GET %s %s', path, repr(kwargs))
try:
id = kwargs.pop('id')
print('export')
client = instrument.clients[id]
bytes = client.w_export(**kwargs)
return flask.send_file(
bytes,
as_attachment=True,
download_name='export.tsv',
mimetype='text/tab-separated-values'
)
except Exception as e:
logging.error('%s', traceback.format_exc())
circularlog.log()
msg = dict(type='error', request=path[1:], error=repr(e))
logging.error('MSG: %r', msg)
resp = flask.Response(json.dumps(msg), mimetype='application/json')
resp.headers['Access-Control-Allow-Origin'] = '*'
return resp
@app.route('/getblock')
@app.route('/updateblock')
@app.route('/sendcommand')
@app.route('/console')
@app.route('/graph')
@app.route('/updategraph')
@app.route('/gettime')
@app.route('/getvars', methods=["GET", "POST"])
def reply():
args = flask.request.values
kwargs = dict((k, args.get(k)) for k in args)
path = flask.request.path
logging.info('GET %s %r', path, kwargs)
try:
id = kwargs.pop('id')
client = instrument.clients[id]
msg = getattr(client, "w_" + path[1:])(**kwargs)
except Exception as e:
logging.error('%s', traceback.format_exc())
circularlog.log()
msg = dict(type='error', request=path[1:], error=repr(e))
jsonmsg = json.dumps(msg)
if len(jsonmsg) < 120:
logging.info('REPLY %s %s', path, jsonmsg)
else:
logging.info('REPLY %s %s...', path, jsonmsg[:80])
logging.debug('REPLY %s %r', path, jsonmsg)
resp = flask.Response(jsonmsg, mimetype='application/json')
resp.headers['Access-Control-Allow-Origin'] = '*'
return resp
@app.route('/test/<file>')
def subdir_test_file(file):
gevent.sleep(2)
resp = flask.send_file("client/test/"+file, mimetype=guess_mimetype(file))
return resp
@app.route('/components/curves_settings_popup/color_selector/<file>')
@app.route('/components/curves_settings_popup/<file>')
@app.route('/components/action_entry/<file>')
@app.route('/components/export_popup/<file>')
@app.route('/components/dates_popup/<file>')
@app.route('/components/menu_popup/<file>')
@app.route('/components/help_popup/<file>')
@app.route('/components/help_entry/<file>')
@app.route('/components/control/<file>')
@app.route('/components/divider/<file>')
@app.route('/components/states_indicator/dates/<file>')
@app.route('/res/<file>')
@app.route('/jsFiles/<file>')
@app.route('/cssFiles/<file>')
@app.route('/externalFiles/<file>')
def subdir_file(file):
subdir = "/".join(flask.request.path.split("/")[1:-1])
resp = flask.send_file("client/" + subdir+"/"+file, mimetype=guess_mimetype(file))
# resp.headers['Content-Security-Policy'] = "sandbox; script-src 'unsafe-inline';"
return resp
@app.route('/externalFiles/maps/<file>.map')
def replace_by_empty(file):
return ""
@app.route('/')
def main():
return general_file('SEAWebClient.html')
@app.route('/select')
def default():
out = ['''<html><body><table>
<style>
th {
text-align: left;
}
</style>
<tr><th>Instrument</th><th colspan=99>Devices</th></tr>''']
result = {}
for stream, tags in instrument.get_stream_tags().items():
ins = tags.get('instrument', '0')
result.setdefault(ins, []).append((stream, tags.get('device')))
bare_streams = result.pop('0', [])
for ins, streams in result.items():
out.append(f'<tr><td><a href="/?ins={ins}">{ins}</a></td>')
out.extend(f'<td>{d or s}</td>' for s, d in streams)
out.append('</tr>')
for stream, device in bare_streams:
out.append(f'<tr><td><a href="/?srv={stream}">{stream}</a></td><td>{device}</td><tr>')
out.extend(['</table></body?</html>', ''])
return '\n'.join(out)
@app.route('/<file>')
def general_file(file):
subdir = "client/"
try:
resp = flask.send_file(subdir+file, mimetype=guess_mimetype(file))
except FileNotFoundError:
logging.warning('file %s not found', file)
return 'file not found'
# resp.headers['Content-Security-Policy'] = "sandbox; script-src 'unsafe-inline';"
return resp
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()