"""VeilRender application entry point.""" from __future__ import annotations import asyncio import logging import signal import sys from veilrender._vendor.httpserver import App from veilrender.browser import browser_manager from veilrender.cdp_proxy import handle_cdp_upgrade, is_websocket_upgrade from veilrender.config import settings from veilrender.routes import dashboard, health, render, screenshot logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s", ) logger = logging.getLogger(__name__) async def _pipe(source: asyncio.StreamReader, dest: asyncio.StreamReader) -> None: """Relay remaining data from *source* into *dest* until EOF.""" try: while True: chunk = await source.read(8192) if not chunk: break dest.feed_data(chunk) except Exception: pass finally: dest.feed_eof() def create_app() -> App: """Create and configure the VeilRender application.""" app = App(max_body_size=10 * 1024 * 1024) # 10 MB # Register routes health.register(app) dashboard.register(app) render.register(app) screenshot.register(app) return app def main() -> None: """Run the VeilRender server with HTTP + CDP WebSocket multiplexing.""" app = create_app() async def run_server() -> None: await browser_manager.start() shutdown_event = asyncio.Event() async def handle_connection( reader: asyncio.StreamReader, writer: asyncio.StreamWriter ) -> None: """Multiplex: WebSocket upgrade to /cdp → CDP proxy, else → HTTP.""" try: # Peek at the first request to decide routing raw = b"" while b"\r\n\r\n" not in raw: chunk = await asyncio.wait_for(reader.read(8192), timeout=30) if not chunk: writer.close() return raw += chunk # Parse the request line and headers header_block, _, body_start = raw.partition(b"\r\n\r\n") lines = header_block.decode("latin-1").split("\r\n") request_line = lines[0] parts = request_line.split(" ", 2) if len(parts) < 2: writer.close() return method = parts[0].upper() raw_path = parts[1] # Parse path and query string if "?" in raw_path: path, query_string = raw_path.split("?", 1) else: path, query_string = raw_path, "" # Parse headers headers: dict[str, str] = {} for line in lines[1:]: if ":" in line: key, _, value = line.partition(":") headers[key.strip().lower()] = value.strip() # Check if this is a WebSocket upgrade to /cdp if is_websocket_upgrade(method, headers, path): await handle_cdp_upgrade( reader, writer, headers, path, query_string, browser_manager.get_cdp_url, ) return # Otherwise, delegate to httpserver. # Re-feed the buffered bytes and pipe any remaining body # data from the original reader so POST bodies are not # truncated (see #5). combined_reader = asyncio.StreamReader() combined_reader.feed_data(raw) pipe_task = asyncio.create_task(_pipe(reader, combined_reader)) try: await app._handle_connection(combined_reader, writer) finally: pipe_task.cancel() except Exception: logger.debug("Connection handler error", exc_info=True) try: writer.close() except Exception: pass server = await asyncio.start_server( handle_connection, settings.host, settings.port ) addr = ( server.sockets[0].getsockname() if server.sockets else (settings.host, settings.port) ) logger.info( "VeilRender serving on %s:%d (auth=%s, cdp=ws://%s:%d/cdp)", addr[0], addr[1], "enabled" if settings.api_token else "disabled", addr[0], addr[1], ) loop = asyncio.get_running_loop() if sys.platform != "win32": for sig in (signal.SIGINT, signal.SIGTERM): loop.add_signal_handler(sig, shutdown_event.set) try: async with server: await shutdown_event.wait() finally: await browser_manager.stop() try: asyncio.run(run_server()) except KeyboardInterrupt: pass