2026-05-10 12:23:05 +02:00
|
|
|
"""
|
|
|
|
|
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
|
2026-05-10 13:40:48 +02:00
|
|
|
import subprocess
|
|
|
|
|
import sys
|
2026-05-10 13:02:52 +02:00
|
|
|
import threading
|
2026-05-10 12:23:05 +02:00
|
|
|
|
|
|
|
|
from aiohttp import web
|
|
|
|
|
|
|
|
|
|
from .python_lsp.catalog import PypiCatalog
|
2026-05-10 13:40:48 +02:00
|
|
|
from .bicep_lsp.modules import BicepModuleCatalog
|
2026-05-10 12:23:05 +02:00
|
|
|
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"))
|
|
|
|
|
|
|
|
|
|
|
2026-05-10 13:40:48 +02:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-05-10 12:23:05 +02:00
|
|
|
async def _health_app() -> web.Application:
|
|
|
|
|
app = web.Application()
|
|
|
|
|
|
|
|
|
|
async def health(_: web.Request) -> web.Response:
|
|
|
|
|
return web.json_response({
|
|
|
|
|
"status": "ok",
|
2026-05-10 13:40:48 +02:00
|
|
|
"pypi_packages": len(PypiCatalog._packages),
|
|
|
|
|
"bicep_modules": len(BicepModuleCatalog._modules),
|
2026-05-10 12:23:05 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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",
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-10 13:40:48 +02:00
|
|
|
# Pre-warm catalogs before accepting connections
|
2026-05-10 12:23:05 +02:00
|
|
|
logger.info("Pre-warming catalogs…")
|
2026-05-10 13:40:48 +02:00
|
|
|
BicepModuleCatalog.load()
|
|
|
|
|
await PypiCatalog.start_background_refresh()
|
2026-05-10 12:23:05 +02:00
|
|
|
|
2026-05-10 13:40:48 +02:00
|
|
|
# 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()
|
2026-05-10 13:02:52 +02:00
|
|
|
|
2026-05-10 13:40:48 +02:00
|
|
|
# Health HTTP server — keeps the process alive via the asyncio event loop
|
2026-05-10 12:23:05 +02:00
|
|
|
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)
|
|
|
|
|
|
2026-05-10 13:40:48 +02:00
|
|
|
# Wait indefinitely; signal handlers in main() will stop the loop
|
|
|
|
|
await asyncio.Event().wait()
|
2026-05-10 12:23:05 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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())
|