import json import threading import uuid from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler import logging import session_logger class HookServerInstance(ThreadingHTTPServer): """Custom HTTPServer that carries a reference to the main App instance.""" def __init__(self, server_address, RequestHandlerClass, app): super().__init__(server_address, RequestHandlerClass) self.app = app class HookHandler(BaseHTTPRequestHandler): """Handles incoming HTTP requests for the API hooks.""" def do_GET(self): app = self.server.app session_logger.log_api_hook("GET", self.path, "") if self.path == '/status': self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() self.wfile.write(json.dumps({'status': 'ok'}).encode('utf-8')) elif self.path == '/api/project': import project_manager self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() flat = project_manager.flat_config(app.project) self.wfile.write(json.dumps({'project': flat}).encode('utf-8')) elif self.path == '/api/session': self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() self.wfile.write( json.dumps({'session': {'entries': app.disc_entries}}). encode('utf-8')) elif self.path == '/api/performance': self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() metrics = {} if hasattr(app, 'perf_monitor'): metrics = app.perf_monitor.get_metrics() self.wfile.write(json.dumps({'performance': metrics}).encode('utf-8')) elif self.path == '/api/events': # Long-poll or return current event queue self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() events = [] if hasattr(app, '_api_event_queue'): with app._api_event_queue_lock: events = list(app._api_event_queue) app._api_event_queue.clear() self.wfile.write(json.dumps({'events': events}).encode('utf-8')) elif self.path == '/api/gui/value': # POST with {"field": "field_tag"} to get value content_length = int(self.headers.get('Content-Length', 0)) body = self.rfile.read(content_length) data = json.loads(body.decode('utf-8')) field_tag = data.get("field") print(f"[DEBUG] Hook Server: get_value for {field_tag}") event = threading.Event() result = {"value": None} def get_val(): try: if field_tag in app._settable_fields: attr = app._settable_fields[field_tag] val = getattr(app, attr, None) print(f"[DEBUG] Hook Server: attr={attr}, val={val}") result["value"] = val else: print(f"[DEBUG] Hook Server: {field_tag} NOT in settable_fields") finally: event.set() with app._pending_gui_tasks_lock: app._pending_gui_tasks.append({ "action": "custom_callback", "callback": get_val }) if event.wait(timeout=2): self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() self.wfile.write(json.dumps(result).encode('utf-8')) else: self.send_response(504) self.end_headers() elif self.path.startswith('/api/gui/value/'): # Generic endpoint to get the value of any settable field field_tag = self.path.split('/')[-1] event = threading.Event() result = {"value": None} def get_val(): try: if field_tag in app._settable_fields: attr = app._settable_fields[field_tag] result["value"] = getattr(app, attr, None) finally: event.set() with app._pending_gui_tasks_lock: app._pending_gui_tasks.append({ "action": "custom_callback", "callback": get_val }) if event.wait(timeout=2): self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() self.wfile.write(json.dumps(result).encode('utf-8')) else: self.send_response(504) self.end_headers() elif self.path == '/api/gui/diagnostics': # Safe way to query multiple states at once via the main thread queue event = threading.Event() result = {} def check_all(): try: # Generic state check based on App attributes (works for both DPG and ImGui versions) status = getattr(app, "ai_status", "idle") result["thinking"] = status in ["sending...", "running powershell..."] result["live"] = status in ["running powershell...", "fetching url...", "searching web...", "powershell done, awaiting AI..."] result["prior"] = getattr(app, "is_viewing_prior_session", False) finally: event.set() with app._pending_gui_tasks_lock: app._pending_gui_tasks.append({ "action": "custom_callback", "callback": check_all }) if event.wait(timeout=2): self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() self.wfile.write(json.dumps(result).encode('utf-8')) else: self.send_response(504) self.end_headers() self.wfile.write(json.dumps({'error': 'timeout'}).encode('utf-8')) else: self.send_response(404) self.end_headers() def do_POST(self): app = self.server.app content_length = int(self.headers.get('Content-Length', 0)) body = self.rfile.read(content_length) body_str = body.decode('utf-8') if body else "" session_logger.log_api_hook("POST", self.path, body_str) try: data = json.loads(body_str) if body_str else {} if self.path == '/api/project': app.project = data.get('project', app.project) self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() self.wfile.write( json.dumps({'status': 'updated'}).encode('utf-8')) elif self.path == '/api/session': app.disc_entries = data.get('session', {}).get( 'entries', app.disc_entries) self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() self.wfile.write( json.dumps({'status': 'updated'}).encode('utf-8')) elif self.path == '/api/gui': with app._pending_gui_tasks_lock: app._pending_gui_tasks.append(data) self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() self.wfile.write( json.dumps({'status': 'queued'}).encode('utf-8')) elif self.path == '/api/ask': request_id = str(uuid.uuid4()) event = threading.Event() if not hasattr(app, '_pending_asks'): app._pending_asks = {} if not hasattr(app, '_ask_responses'): app._ask_responses = {} app._pending_asks[request_id] = event # Emit event for test/client discovery with app._api_event_queue_lock: app._api_event_queue.append({ "type": "ask_received", "request_id": request_id, "data": data }) with app._pending_gui_tasks_lock: app._pending_gui_tasks.append({ "type": "ask", "request_id": request_id, "data": data }) if event.wait(timeout=60.0): response_data = app._ask_responses.get(request_id) # Clean up response after reading if request_id in app._ask_responses: del app._ask_responses[request_id] self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() self.wfile.write(json.dumps({'status': 'ok', 'response': response_data}).encode('utf-8')) else: if request_id in app._pending_asks: del app._pending_asks[request_id] self.send_response(504) self.end_headers() self.wfile.write(json.dumps({'error': 'timeout'}).encode('utf-8')) elif self.path == '/api/ask/respond': request_id = data.get('request_id') response_data = data.get('response') if request_id and hasattr(app, '_pending_asks') and request_id in app._pending_asks: app._ask_responses[request_id] = response_data event = app._pending_asks[request_id] event.set() # Clean up pending ask entry del app._pending_asks[request_id] self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() self.wfile.write(json.dumps({'status': 'ok'}).encode('utf-8')) else: self.send_response(404) self.end_headers() else: self.send_response(404) self.end_headers() except Exception as e: self.send_response(500) self.send_header('Content-Type', 'application/json') self.end_headers() self.wfile.write(json.dumps({'error': str(e)}).encode('utf-8')) def log_message(self, format, *args): logging.info("Hook API: " + format % args) class HookServer: def __init__(self, app, port=8999): self.app = app self.port = port self.server = None self.thread = None def start(self): if not getattr(self.app, 'test_hooks_enabled', False): return # Ensure the app has the task queue and lock initialized if not hasattr(self.app, '_pending_gui_tasks'): self.app._pending_gui_tasks = [] if not hasattr(self.app, '_pending_gui_tasks_lock'): self.app._pending_gui_tasks_lock = threading.Lock() # Initialize ask-related dictionaries if not hasattr(self.app, '_pending_asks'): self.app._pending_asks = {} if not hasattr(self.app, '_ask_responses'): self.app._ask_responses = {} # Event queue for test script subscriptions if not hasattr(self.app, '_api_event_queue'): self.app._api_event_queue = [] if not hasattr(self.app, '_api_event_queue_lock'): self.app._api_event_queue_lock = threading.Lock() self.server = HookServerInstance(('127.0.0.1', self.port), HookHandler, self.app) self.thread = threading.Thread(target=self.server.serve_forever, daemon=True) self.thread.start() logging.info(f"Hook server started on port {self.port}") def stop(self): if self.server: self.server.shutdown() self.server.server_close() if self.thread: self.thread.join() logging.info("Hook server stopped")