#!/usr/bin/env python3
"""Web interface to monitor AFHBA connections"""
import argparse
import os
from flask import Flask
import threading
import time
import json
import acq400_hapi
from acq400_hapi import afhba404
import logging
import psutil
import socket
[docs]class globals:
lock = threading.RLock()
gatherer = None
last_request = 0
last_ident = 0
connected_uuts = {}
uuts = {}
threads = {}
[docs]def get_parser():
parser = argparse.ArgumentParser(description='AFHBA stream monitor webapp')
parser.add_argument('--port', default=3000, help='Port to run webserver on')
parser.add_argument('--profile', default=None, help='1: enabled profiling')
return parser
[docs]def run_main(args):
logging.getLogger('werkzeug').disabled = True
print('Starting afhba mon')
print(f'http://{socket.gethostname()}:{args.port}/')
print(f'http://{socket.gethostbyname(socket.gethostname())}:{args.port}/')
run_webserver(args)
[docs]def get_devices_states():
while True:
#print("Getting states")
if time.time() - globals.last_ident > 60:
globals.last_ident = time.time()
globals.threads['ident'] = threading.Thread(target=get_connected_uuts)
globals.threads['ident'].start()
globals.lock.acquire()
for dev in globals.connected_uuts.items():
uut_name = dev[0]
globals.threads[uut_name] = threading.Thread(target=get_remote_state, args=(uut_name, ))
globals.threads[uut_name].start()
for lport in dev[1]['ports']:
dev[1]['ports'][lport]['job_state'] = afhba404.get_stream_state(lport)._asdict()
globals.lock.release()
if time.time() - globals.last_request > 60:
print('Monitor idle halting')
globals.gatherer = None
exit()
time.sleep(1)
[docs]def get_connected_uuts():
devs = afhba404.get_connections()
globals.lock.acquire()
for idx in devs:
dev = devs[idx]
lport = int(dev.dev)
uut_name = dev.uut
if uut_name not in globals.connected_uuts:
globals.connected_uuts[uut_name] = {}
globals.connected_uuts[uut_name]['uut_status'] = None
globals.connected_uuts[uut_name]['last_query'] = 0
globals.connected_uuts[uut_name]['ports'] = {}
if lport not in globals.connected_uuts[uut_name]['ports']:
globals.connected_uuts[uut_name]['ports'][lport] = {}
globals.connected_uuts[uut_name]['ports'][lport]['job_state'] = {}
globals.connected_uuts[uut_name]['ports'][lport]['connected'] = True
globals.connected_uuts[uut_name]['ports'][lport]['rport'] = dev.cx
globals.connected_uuts[uut_name]['ports'][lport]['buffer_len'] = afhba404.get_buffer_len(lport) / 1048576
if uut_name not in globals.uuts:
try:
globals.uuts[uut_name] = acq400_hapi.factory(dev.uut)
except Exception as e:
print(e)
globals.lock.release()
[docs]def get_remote_state(uut_name):
if time.time() - globals.connected_uuts[uut_name]['last_query'] < 5:
exit()
globals.connected_uuts[uut_name]['last_query'] = time.time()
uut_status = globals.uuts[uut_name].s0.CONTINUOUS_STATE
globals.lock.acquire()
globals.connected_uuts[uut_name]['uut_status'] = uut_status.split(' ')[1]
globals.lock.release()
exit()
[docs]def check_still_connected():
devs = afhba404.get_connections()
connections = {}
for idx in devs:
dev = devs[idx]
if dev.uut not in connections:
connections[dev.uut] = []
connections[dev.uut].append(int(dev.dev))
globals.lock.acquire()
for uut_name in globals.connected_uuts.copy():
if uut_name not in connections:
print(f'removing {uut_name} as not connected')
del globals.connected_uuts[uut_name]
globals.uuts[uut_name].close()
del globals.uuts[uut_name]
continue
for lport in globals.connected_uuts[uut_name]['ports'].copy():
if lport not in connections[uut_name]:
print(f'removing {uut_name}:{lport} as not connected')
del globals.connected_uuts[uut_name]['ports'][lport]
globals.lock.release()
[docs]def run_webserver(args):
app = Flask(__name__,)
@app.route("/")
def get_index():
globals.last_ident = 0
globals.last_request = time.time()
if not globals.gatherer:
globals.gatherer = threading.Thread(target=get_devices_states)
globals.gatherer.start()
t = threading.Thread(target=check_still_connected)
t.start()
#page = open("test.html", "r").read()
return page
@app.route("/state.json")
def get_state():
globals.last_request = time.time()
if not globals.gatherer:
print('Running')
globals.gatherer = threading.Thread(target=get_devices_states)
globals.gatherer.start()
globals.lock.acquire()
load1, load5, load15 = psutil.getloadavg()
num_cpu = os.cpu_count()
data = {
'cpu_usage1' : round((load1 / num_cpu) * 100, 1),
'cpu_usage5' : round((load5 / num_cpu) * 100, 1),
'cpu_usage15' : round((load15 / num_cpu) * 100, 1),
'num_cpu' : num_cpu,
'state' : globals.connected_uuts,
}
globals.lock.release()
return json.dumps(data)
app.run(host="0.0.0.0", port=args.port, debug=False)
page = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="google" value="notranslate">
<title>afhba monitor</title>
<style>
body {
background-color: #E8E9F3;
font-family: "Courier New", Courier, monospace;
}
#template, .job_template{
display: none;
}
.IDLE {
color: red;
}
.RUN {
color: greenyellow;
}
.ARM {
color: orange;
}
header{
padding: 0px 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
.button{
border: 1px solid black;
padding: 4px;
cursor: pointer;
}
#detailToggle{
display: none;
}
#subtitle{
margin-left: 5px;
font-size: 16px;
}
#container{
display: flex;
flex-flow: row wrap;
padding: 10px;
}
.summary .conn_box{
width: 100%;
}
.summary .device{
display: flex;
gap: 10px;
}
.summary .state{
display: none;
}
.summary .spi{
display: block;
}
.conn_box{
background-color: #8395a7;
font-size: 20px;
margin: 2px;
padding: 5px;
color: #130f40;
}
.device{
align-items: center;
}
.device a{
text-decoration: none;
color: #130f40;
}
.device a:hover{
color: #f9ca24;
}
.spi{
color: #130f40;
display: none;
line-height: 0px;
font-weight: bold;
font-size: 16px;
}
.speed{
display: inline;
border: 2px solid;
border-color: inherit;
padding: 2px 5px;
background-color: #C8D6E5;
color: #130f40;
}
sup{
line-height: 0px;
vertical-align: bottom;
}
#RUN .spi{
animation-name: pulse;
animation-duration: 0.5s;
animation-iteration-count: infinite;
animation-direction: alternate;
}
@keyframes pulse {
from {color: #130f40;border-color: #130f40}
to {color: #05c46b;border-color: #05c46b}
}
.state{
background-color: #c8d6e5;
box-sizing: border-box;
padding: 2px;
width: 100%;
font-size: 16px;
}
.MBPS{
font-weight: bold;
}
footer{
padding: 10px;
}
#cpuInfo {
background-color: #c8d6e5;
padding: 5px;
}
</style>
<script>
function main(){
url = "state.json"
if(document.visibilityState != "hidden"){
get_state(url, build_conn_boxes);
}
setTimeout(main, 1000);
}
function get_state(url, callback){
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'json';
xhr.onreadystatechange = function() {
if(xhr.readyState == 4 && xhr.status == 200) {
callback(xhr.response);
}
if(xhr.readyState == 4 && xhr.status == 0) {
callback(null);
}
}
xhr.send();
}
function build_conn_boxes(data){
container = document.getElementById('container');
template = document.getElementById('template');
if(data == null){
return;
}
container.innerHTML = "";
cpu_string = `Cpu: ${data['num_cpu']} Cores Usage: ${data['cpu_usage1']}\% 15min average: ${data['cpu_usage15']}\%`;
document.getElementById('cpuInfo').innerText = cpu_string;
for (const [key, value] of Object.entries(data['state'])) {
for (const [lport, lvalues] of Object.entries(value['ports'])) {
new_row = template.cloneNode(true);
new_row.id = value['uut_status'];
new_row.getElementsByClassName('uut_name')[0].innerText = key;
new_row.getElementsByClassName('uut_name')[0].href = `http://${key}`;
new_row.getElementsByClassName('uut_status')[0].innerText = value['uut_status'];
new_row.getElementsByClassName('uut_status')[0].className = value['uut_status'];
new_row.getElementsByClassName('rport')[0].innerText = lvalues['rport'];
new_row.getElementsByClassName('hostname')[0].innerText = location.hostname;
new_row.getElementsByClassName('lport')[0].innerText = lport;
state = "";
job_template = new_row.getElementsByClassName('job_template')[0]
for (var [job_key, job_value] of Object.entries(lvalues['job_state'])) {
if(job_key == 'MBPS'){
job_value = Math.round(job_value * 1.093);
new_row.getElementsByClassName('speed')[0].innerHTML = `${job_value}<sup>e6</sup> B/s`;
}
new_state = job_template.cloneNode(true);
new_state.className = job_key
new_state.firstChild.innerHTML = job_key
new_state.lastChild.innerHTML = job_value
new_row.getElementsByClassName('state')[0].appendChild(new_state);
}
container.appendChild(new_row);
}
}
}
function toggle_detail(event){
event.target.labels[0].textContent = event.srcElement.checked ? 'Details..' : 'Summary..';
document.getElementById('subtitle').textContent = event.srcElement.checked ? 'summary' : 'details';
document.getElementById('container').className = event.srcElement.checked ? 'summary' : 'details';
}
window.onload = main();
</script>
</head>
<body>
<div class="conn_box" id="template">
<div class="device">
<div>[<span class="uut_status">uut_status</span>]</div>
<div><a href="#" class="uut_name">uut_name</a>:<span class="rport">rport</span></div>
<span class="spi">――<span class="speed"></span>―⟶</span>
<div><span class="hostname">hostname</span>:<span class="lport">lport</span></div>
</div>
<table class="state">
<tr class="job_template"><td>key</td><td>value</td></tr>
</table>
</div>
<header>
<h1>Afhba monitor<span id="subtitle">summary</span></h1>
<input type="checkbox" checked id="detailToggle" onchange="toggle_detail(event)"></input>
<label for="detailToggle" class="button">Details...</label>
</header>
<div id="container" class="summary">Loading...</div>
<footer>
<span id="cpuInfo">No info</span>
</footer>
</body>
</html>
"""
if __name__ == '__main__':
args = get_parser().parse_args()
if args.profile is None:
run_main(args)
else:
print("Profiling ..")
import cProfile, pstats
profiler = cProfile.Profile()
profiler.enable()
try:
run_main(args)
except KeyboardInterrupt:
print("KeyboardInterrupt")
profiler.disable()
stats = pstats.Stats(profiler).sort_stats('tottime')
stats.print_stats()