""" iLSP main entrypoint. Starts three services concurrently: - pylsp server on TCP :2087 (Python LSP + i80 completions) - Bicep proxy on TCP :2088 (Bicep LS wrapper + LRU modules) - Health HTTP on TCP :2089 (for Consul/Nomad health checks) """ import asyncio import logging import os import signal import subprocess import sys import threading from aiohttp import web from .python_lsp.catalog import PypiCatalog from .bicep_lsp.modules import BicepModuleCatalog from .bicep_lsp.proxy import serve_bicep logger = logging.getLogger(__name__) PYTHON_LSP_PORT = int(os.getenv("PYTHON_LSP_PORT", "2087")) BICEP_LSP_PORT = int(os.getenv("BICEP_LSP_PORT", "2088")) HEALTH_PORT = int(os.getenv("HEALTH_PORT", "2089")) def _serve_python_lsp(port: int) -> None: """Start pylsp in TCP server mode; restart on unexpected exit (blocking).""" while True: proc = subprocess.Popen( [sys.executable, "-m", "pylsp", "--tcp", "--host", "0.0.0.0", "--port", str(port)], ) logger.info("Python LSP (pylsp) listening on TCP :%d PID=%d", port, proc.pid) proc.wait() logger.warning("pylsp exited (code %s) — restarting", proc.returncode) async def _health_app() -> web.Application: app = web.Application() async def health(_: web.Request) -> web.Response: return web.json_response({ "status": "ok", "pypi_packages": len(PypiCatalog._packages), "bicep_modules": len(BicepModuleCatalog._modules), }) app.router.add_get("/health", health) return app async def main_async() -> None: logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", ) # Pre-warm catalogs before accepting connections logger.info("Pre-warming catalogs…") BicepModuleCatalog.load() await PypiCatalog.start_background_refresh() # Both LSP servers run in daemon threads (blocking socket/process loops) threading.Thread(target=_serve_python_lsp, args=(PYTHON_LSP_PORT,), daemon=True).start() threading.Thread(target=serve_bicep, args=(BICEP_LSP_PORT,), daemon=True).start() # Health HTTP server — keeps the process alive via the asyncio event loop health_app = await _health_app() runner = web.AppRunner(health_app) await runner.setup() site = web.TCPSite(runner, "0.0.0.0", HEALTH_PORT) await site.start() logger.info("Health endpoint on http://0.0.0.0:%d/health", HEALTH_PORT) # Wait indefinitely; signal handlers in main() will stop the loop await asyncio.Event().wait() def main() -> None: loop = asyncio.new_event_loop() for sig in (signal.SIGTERM, signal.SIGINT): loop.add_signal_handler(sig, loop.stop) loop.run_until_complete(main_async())