feat: bake bicep catalog into image; fix dict-based modules parsing
- Remove all DevOpsMCP/aiohttp runtime deps from BicepModuleCatalog
- BicepModuleCatalog.load() reads bicep_modules_catalog.json from disk at startup (sync)
- Fix _load_catalog: catalog uses dict {path: {versions, schema}} not a list
- server.py: call BicepModuleCatalog.load() synchronously, not via asyncio.gather
- Dockerfile: COPY bicep_modules_catalog.json into both builder + runtime stages
- Health endpoint now reports bicep_modules: 27
Verified locally: make run-quick → health returns pypi_packages:40 bicep_modules:27
This commit is contained in:
@@ -11,11 +11,14 @@ 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__)
|
||||
@@ -25,58 +28,47 @@ 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:
|
||||
pypi_count = len(PypiCatalog._packages)
|
||||
from .bicep_lsp.modules import BicepModuleCatalog
|
||||
bicep_count = len(BicepModuleCatalog._modules)
|
||||
return web.json_response({
|
||||
"status": "ok",
|
||||
"pypi_packages": pypi_count,
|
||||
"bicep_modules": bicep_count,
|
||||
"pypi_packages": len(PypiCatalog._packages),
|
||||
"bicep_modules": len(BicepModuleCatalog._modules),
|
||||
})
|
||||
|
||||
app.router.add_get("/health", health)
|
||||
return app
|
||||
|
||||
|
||||
async def _serve_python_lsp(port: int) -> None:
|
||||
"""Start pylsp in TCP server mode."""
|
||||
import subprocess, sys
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
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)
|
||||
await proc.wait()
|
||||
logger.warning("pylsp exited — restarting")
|
||||
|
||||
|
||||
async def main_async() -> None:
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
)
|
||||
logging.getLogger("ilsp.bicep_lsp.proxy").setLevel(logging.DEBUG)
|
||||
|
||||
# Pre-warm caches
|
||||
# Pre-warm catalogs before accepting connections
|
||||
logger.info("Pre-warming catalogs…")
|
||||
from .bicep_lsp.modules import BicepModuleCatalog
|
||||
await asyncio.gather(
|
||||
PypiCatalog.start_background_refresh(),
|
||||
BicepModuleCatalog.start_background_refresh(),
|
||||
)
|
||||
BicepModuleCatalog.load()
|
||||
await PypiCatalog.start_background_refresh()
|
||||
|
||||
# Start Bicep LSP proxy in a daemon thread (blocking socket server)
|
||||
threading.Thread(
|
||||
target=serve_bicep,
|
||||
args=(BICEP_LSP_PORT,),
|
||||
daemon=True,
|
||||
).start()
|
||||
# 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()
|
||||
|
||||
# Build health app
|
||||
# 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()
|
||||
@@ -84,8 +76,8 @@ async def main_async() -> None:
|
||||
await site.start()
|
||||
logger.info("Health endpoint on http://0.0.0.0:%d/health", HEALTH_PORT)
|
||||
|
||||
# Run remaining asyncio services
|
||||
await _serve_python_lsp(PYTHON_LSP_PORT)
|
||||
# Wait indefinitely; signal handlers in main() will stop the loop
|
||||
await asyncio.Event().wait()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
Reference in New Issue
Block a user