xml-pipeline/xml_pipeline/console/lsp/manager.py
dullfig e653d63bc1 Rename agentserver to xml_pipeline, add console example
OSS restructuring for open-core model:
- Rename package from agentserver/ to xml_pipeline/
- Update all imports (44 Python files, 31 docs/configs)
- Update pyproject.toml for OSS distribution (v0.3.0)
- Move prompt_toolkit from core to optional [console] extra
- Remove auth/server/lsp from core optional deps (-> Nextra)

New console example in examples/console/:
- Self-contained demo with handlers and config
- Uses prompt_toolkit (optional, falls back to input())
- No password auth, no TUI, no LSP — just the basics
- Shows how to use xml-pipeline as a library

Import changes:
- from agentserver.* -> from xml_pipeline.*
- CLI entry points updated: xml_pipeline.cli:main

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 21:41:19 -08:00

211 lines
7 KiB
Python

"""
LSP Server lifecycle manager.
Manages language server instances that can be shared across
multiple editor sessions. Supports multiple language servers:
- yaml-language-server (for config files)
- asls (for AssemblyScript listener source)
"""
from __future__ import annotations
import asyncio
import logging
from enum import Enum
from typing import Optional, Union
from .client import YAMLLSPClient, is_lsp_available
from .asls_client import ASLSClient, ASLSConfig, is_asls_available
logger = logging.getLogger(__name__)
class LSPServerType(Enum):
"""Supported language server types."""
YAML = "yaml"
ASSEMBLYSCRIPT = "assemblyscript"
# Type alias for any LSP client
LSPClient = Union[YAMLLSPClient, ASLSClient]
class LSPServerManager:
"""
Manages the lifecycle of LSP servers.
Provides singleton client instances that start on first use
and stop when explicitly requested or when the process exits.
Supports multiple language servers running concurrently.
"""
def __init__(self):
self._clients: dict[LSPServerType, LSPClient] = {}
self._ref_counts: dict[LSPServerType, int] = {}
self._lock = asyncio.Lock()
def is_running(self, server_type: LSPServerType = LSPServerType.YAML) -> bool:
"""Check if a specific LSP server is running."""
client = self._clients.get(server_type)
return client is not None and client._initialized
async def get_client(
self,
server_type: LSPServerType = LSPServerType.YAML,
asls_config: Optional[ASLSConfig] = None,
) -> Optional[LSPClient]:
"""
Get an LSP client, starting the server if needed.
Args:
server_type: Which language server to get
asls_config: Configuration for ASLS (only used if server_type is ASSEMBLYSCRIPT)
Returns None if the requested LSP is not available.
"""
async with self._lock:
# Check if already running
if server_type in self._clients:
client = self._clients[server_type]
if client._initialized:
self._ref_counts[server_type] = self._ref_counts.get(server_type, 0) + 1
return client
# Start the appropriate server
if server_type == LSPServerType.YAML:
return await self._start_yaml_server()
elif server_type == LSPServerType.ASSEMBLYSCRIPT:
return await self._start_asls_server(asls_config)
else:
logger.error(f"Unknown LSP server type: {server_type}")
return None
async def _start_yaml_server(self) -> Optional[YAMLLSPClient]:
"""Start the YAML language server."""
available, reason = is_lsp_available()
if not available:
logger.info(f"YAML LSP not available: {reason}")
return None
client = YAMLLSPClient()
success = await client.start()
if success:
self._clients[LSPServerType.YAML] = client
self._ref_counts[LSPServerType.YAML] = 1
logger.info("yaml-language-server started")
return client
else:
return None
async def _start_asls_server(
self, config: Optional[ASLSConfig] = None
) -> Optional[ASLSClient]:
"""Start the AssemblyScript language server."""
available, reason = is_asls_available()
if not available:
logger.info(f"ASLS not available: {reason}")
return None
client = ASLSClient(config=config)
success = await client.start()
if success:
self._clients[LSPServerType.ASSEMBLYSCRIPT] = client
self._ref_counts[LSPServerType.ASSEMBLYSCRIPT] = 1
logger.info("AssemblyScript language server started")
return client
else:
return None
async def release_client(
self, server_type: LSPServerType = LSPServerType.YAML
) -> None:
"""
Release a reference to a client.
Stops the server when the last reference is released.
"""
async with self._lock:
if server_type not in self._ref_counts:
return
self._ref_counts[server_type] -= 1
if self._ref_counts[server_type] <= 0:
client = self._clients.pop(server_type, None)
self._ref_counts.pop(server_type, None)
if client is not None:
await client.stop()
logger.info(f"{server_type.value} language server stopped")
async def stop(self, server_type: Optional[LSPServerType] = None) -> None:
"""
Force stop LSP server(s).
Args:
server_type: Specific server to stop, or None to stop all
"""
async with self._lock:
if server_type is not None:
# Stop specific server
client = self._clients.pop(server_type, None)
self._ref_counts.pop(server_type, None)
if client is not None:
await client.stop()
logger.info(f"{server_type.value} language server stopped (forced)")
else:
# Stop all servers
for st, client in list(self._clients.items()):
await client.stop()
logger.info(f"{st.value} language server stopped (forced)")
self._clients.clear()
self._ref_counts.clear()
async def stop_all(self) -> None:
"""Force stop all LSP servers."""
await self.stop(None)
# Convenience methods for YAML (backwards compatible)
async def get_yaml_client(self) -> Optional[YAMLLSPClient]:
"""Get YAML LSP client (convenience method)."""
client = await self.get_client(LSPServerType.YAML)
return client if isinstance(client, YAMLLSPClient) else None
async def get_asls_client(
self, config: Optional[ASLSConfig] = None
) -> Optional[ASLSClient]:
"""Get AssemblyScript LSP client (convenience method)."""
client = await self.get_client(LSPServerType.ASSEMBLYSCRIPT, asls_config=config)
return client if isinstance(client, ASLSClient) else None
# Context manager for YAML (backwards compatible)
async def __aenter__(self) -> Optional[YAMLLSPClient]:
"""Context manager entry - get YAML client."""
return await self.get_yaml_client()
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
"""Context manager exit - release YAML client."""
await self.release_client(LSPServerType.YAML)
# Global singleton
_manager: Optional[LSPServerManager] = None
def get_lsp_manager() -> LSPServerManager:
"""Get the global LSP server manager."""
global _manager
if _manager is None:
_manager = LSPServerManager()
return _manager
async def ensure_lsp_stopped() -> None:
"""Ensure all LSP servers are stopped. Call on application shutdown."""
if _manager is not None:
await _manager.stop_all()