Add isolated Athena Deck prototype with demo control and live hardware telemetry
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Athena Deck prototype: loopback API, owned demo process, read-only telemetry."""
|
||||
import argparse
|
||||
import json
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import urllib.request
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
|
||||
class DemoService:
|
||||
def __init__(self):
|
||||
self.lock = threading.RLock()
|
||||
self.process = None
|
||||
self.port = None
|
||||
|
||||
def status(self):
|
||||
with self.lock:
|
||||
running = self.process is not None and self.process.poll() is None
|
||||
reachable = False
|
||||
if running:
|
||||
try:
|
||||
with urllib.request.urlopen(f'http://127.0.0.1:{self.port}/health', timeout=.5) as response:
|
||||
reachable = json.load(response).get('service') == 'athena-deck-demo'
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return dict(state='running' if running else 'stopped', reachable=reachable, pid=self.process.pid if running else None, port=self.port if running else None, location='Mac · lokaler Demo-Prozess')
|
||||
|
||||
def start(self):
|
||||
with self.lock:
|
||||
if self.process is None or self.process.poll() is not None:
|
||||
with socket.socket() as sock:
|
||||
sock.bind(('127.0.0.1', 0))
|
||||
sock.listen(8)
|
||||
self.port = sock.getsockname()[1]
|
||||
self.process = subprocess.Popen([sys.executable, str(ROOT/'demo.py'), str(sock.fileno())], pass_fds=(sock.fileno(),), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
for _ in range(30):
|
||||
if self.status()['reachable']:
|
||||
break
|
||||
time.sleep(.05)
|
||||
return self.status()
|
||||
|
||||
def stop(self):
|
||||
with self.lock:
|
||||
if self.process is not None and self.process.poll() is None:
|
||||
self.process.terminate()
|
||||
try:
|
||||
self.process.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
self.process.wait()
|
||||
return self.status()
|
||||
|
||||
class HardwareProvider:
|
||||
def __init__(self):
|
||||
self.lock = threading.Lock()
|
||||
self.cached = None
|
||||
self.checked = 0
|
||||
|
||||
def snapshot(self):
|
||||
with self.lock:
|
||||
if time.monotonic() - self.checked < 5 and self.cached is not None:
|
||||
return self.cached
|
||||
try:
|
||||
result = subprocess.run(['ssh', '-i', str(Path.home()/'.ssh/athena_key'), '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=4', '-o', 'StrictHostKeyChecking=yes', 'root@192.168.1.212', 'python3 -'], input=(ROOT/'collect_hardware.py').read_text(), capture_output=True, text=True, timeout=10, check=True)
|
||||
self.cached = json.loads(result.stdout)
|
||||
self.cached['available'] = True
|
||||
except (OSError, subprocess.SubprocessError, ValueError):
|
||||
self.cached = dict(available=False, sampled_at=None, cpu={}, ram={}, gpus=[], errors=['Athena per SSH nicht erreichbar oder Messung fehlgeschlagen.'])
|
||||
self.checked = time.monotonic()
|
||||
return self.cached
|
||||
|
||||
class Server(ThreadingHTTPServer):
|
||||
daemon_threads = True
|
||||
def __init__(self, port):
|
||||
super().__init__(('127.0.0.1', port), Handler)
|
||||
self.demo = DemoService()
|
||||
self.hardware = HardwareProvider()
|
||||
self.started = time.time()
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
def respond(self, data, status=200, mime='application/json'):
|
||||
body = json.dumps(data).encode() if mime == 'application/json' else data
|
||||
self.send_response(status)
|
||||
self.send_header('Content-Type', mime)
|
||||
self.send_header('Content-Length', str(len(body)))
|
||||
self.send_header('Cache-Control', 'no-store')
|
||||
self.send_header('X-Content-Type-Options', 'nosniff')
|
||||
self.send_header('Content-Security-Policy', "default-src 'self'; script-src 'self'; style-src 'self'; frame-ancestors 'none'")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def valid_host(self):
|
||||
return self.headers.get('Host') in (f'127.0.0.1:{self.server.server_port}', f'localhost:{self.server.server_port}')
|
||||
|
||||
def do_GET(self):
|
||||
if not self.valid_host():
|
||||
return self.respond({'error': 'Host rejected'}, 403)
|
||||
routes = {'/': ('index.html', 'text/html; charset=utf-8'), '/app.js': ('app.js', 'text/javascript'), '/style.css': ('style.css', 'text/css')}
|
||||
if self.path in routes:
|
||||
name, mime = routes[self.path]
|
||||
return self.respond((ROOT/name).read_bytes(), mime=mime)
|
||||
if self.path == '/api/v1/status':
|
||||
return self.respond(dict(name='Athena Deck', version='0.1.0', state='ready', uptime_seconds=round(time.time()-self.server.started), mode='isolated', demo=self.server.demo.status()))
|
||||
if self.path == '/api/v1/hardware':
|
||||
return self.respond(self.server.hardware.snapshot())
|
||||
if self.path == '/api/v1/demo':
|
||||
return self.respond(self.server.demo.status())
|
||||
return self.respond({'error': 'Not found'}, 404)
|
||||
|
||||
def do_POST(self):
|
||||
origin = self.headers.get('Origin')
|
||||
if not self.valid_host() or self.headers.get('X-Athena-Deck') != '1' or (origin and origin != 'http://' + self.headers.get('Host', '')):
|
||||
return self.respond({'error': 'Same-origin control required'}, 403)
|
||||
if self.path not in ('/api/v1/demo/start', '/api/v1/demo/stop'):
|
||||
return self.respond({'error': 'Not found'}, 404)
|
||||
action = self.server.demo.start if self.path.endswith('/start') else self.server.demo.stop
|
||||
result = action()
|
||||
self.respond(result, 503 if self.path.endswith('/start') and not result['reachable'] else 200)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--port', type=int, default=8108)
|
||||
args = parser.parse_args()
|
||||
server = Server(args.port) # Binding fails rather than taking over an occupied port.
|
||||
def shutdown(*_):
|
||||
threading.Thread(target=server.shutdown, daemon=True).start()
|
||||
signal.signal(signal.SIGTERM, shutdown)
|
||||
signal.signal(signal.SIGINT, shutdown)
|
||||
print(f'Athena Deck: http://127.0.0.1:{server.server_port}', flush=True)
|
||||
try:
|
||||
server.serve_forever()
|
||||
finally:
|
||||
server.demo.stop()
|
||||
server.server_close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user