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>
211 lines
7 KiB
Python
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()
|