#!/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()