use axum::{
extract::{
ws::{Message, WebSocket},
State, WebSocketUpgrade,
},
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use futures_util::{SinkExt, StreamExt};
use std::sync::Arc;
use tokio::{
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
process::{Child, Command},
sync::{mpsc, OwnedSemaphorePermit, Semaphore},
};
use tracing::{debug, error, info, warn};
use super::AppState;
const LSP_MESSAGE_BUFFER_SIZE: usize = 32;
pub const DEFAULT_MAX_SESSIONS: u16 = 8;
pub struct SessionLimit {
permits: Arc<Semaphore>,
max: u16,
}
impl SessionLimit {
pub fn new(max: u16) -> Self {
Self {
permits: Arc::new(Semaphore::new(usize::from(max))),
max,
}
}
fn try_claim(&self) -> Option<OwnedSemaphorePermit> {
Arc::clone(&self.permits).try_acquire_owned().ok()
}
fn max(&self) -> u16 {
self.max
}
}
pub async fn lsp_websocket(ws: WebSocketUpgrade, State(state): State<Arc<AppState>>) -> Response {
let Some(permit) = state.lsp_sessions.try_claim() else {
let max = state.lsp_sessions.max();
warn!("refused an LSP WebSocket: {max} concurrent sessions allowed");
let error = if max == 0 {
"The language server bridge is disabled (--max-lsp-sessions 0).".to_string()
} else {
format!(
"All {max} language server sessions are in use. Close an editor tab, \
or restart the server with a larger --max-lsp-sessions."
)
};
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(serde_json::json!({ "error": error })),
)
.into_response();
};
ws.on_upgrade(move |socket| handle_lsp_connection(socket, state, permit))
}
async fn handle_lsp_connection(
socket: WebSocket,
state: Arc<AppState>,
_permit: OwnedSemaphorePermit,
) {
info!("LSP WebSocket connection established");
let lsp_process = match spawn_lsp_process(&state.base_path).await {
Ok(process) => process,
Err(e) => {
error!("Failed to spawn LSP process: {}", e);
return;
}
};
if let Err(e) = run_bridge(socket, lsp_process).await {
error!("LSP bridge error: {}", e);
}
info!("LSP WebSocket connection closed");
}
async fn spawn_lsp_process(base_path: &camino::Utf8Path) -> Result<Child, std::io::Error> {
let exe_path = std::env::current_exe()?;
debug!("Spawning LSP process: {} lsp", exe_path.display());
Command::new(exe_path)
.arg("lsp")
.current_dir(base_path)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit()) .kill_on_drop(true)
.spawn()
}
async fn run_bridge(
socket: WebSocket,
mut lsp_process: Child,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let stdin = lsp_process.stdin.take().ok_or("Failed to get stdin")?;
let stdout = lsp_process.stdout.take().ok_or("Failed to get stdout")?;
let (mut ws_sender, mut ws_receiver) = socket.split();
let mut stdin_writer = stdin;
let mut stdout_reader = BufReader::new(stdout);
let (tx, mut rx) = mpsc::channel::<String>(LSP_MESSAGE_BUFFER_SIZE);
let mut stdout_handle = tokio::spawn(async move {
loop {
let mut content_length: usize = 0;
loop {
let mut line = String::new();
match stdout_reader.read_line(&mut line).await {
Ok(0) => {
debug!("LSP stdout closed");
return;
}
Ok(_) => {
let line = line.trim();
if line.is_empty() {
break;
}
if let Some(len_str) = line.strip_prefix("Content-Length: ") {
if let Ok(len) = len_str.parse() {
content_length = len;
}
}
}
Err(e) => {
error!("Error reading LSP stdout: {}", e);
return;
}
}
}
if content_length == 0 {
warn!("No Content-Length header found");
continue;
}
let mut content = vec![0u8; content_length];
if let Err(e) =
tokio::io::AsyncReadExt::read_exact(&mut stdout_reader, &mut content).await
{
error!("Error reading LSP content: {}", e);
return;
}
let json = match String::from_utf8(content) {
Ok(s) => s,
Err(e) => {
error!("Invalid UTF-8 from LSP: {}", e);
continue;
}
};
debug!("LSP -> WS: {}", json);
if tx.send(json).await.is_err() {
debug!("WebSocket channel closed");
return;
}
}
});
let mut stdin_handle = tokio::spawn(async move {
while let Some(msg) = ws_receiver.next().await {
match msg {
Ok(Message::Text(text)) => {
debug!("WS -> LSP: {}", text);
let message = format!("Content-Length: {}\r\n\r\n{}", text.len(), text);
if let Err(e) = stdin_writer.write_all(message.as_bytes()).await {
error!("Error writing to LSP stdin: {}", e);
return;
}
if let Err(e) = stdin_writer.flush().await {
error!("Error flushing LSP stdin: {}", e);
return;
}
}
Ok(Message::Close(_)) => {
debug!("WebSocket closed by client");
return;
}
Ok(_) => {
}
Err(e) => {
error!("WebSocket error: {}", e);
return;
}
}
}
});
let mut ws_send_handle = tokio::spawn(async move {
while let Some(msg) = rx.recv().await {
if let Err(e) = ws_sender.send(Message::Text(msg.into())).await {
error!("Error sending to WebSocket: {}", e);
return;
}
}
});
tokio::select! {
result = &mut stdout_handle => {
debug!("LSP stdout task completed: {:?}", result);
}
result = &mut stdin_handle => {
debug!("WebSocket stdin task completed: {:?}", result);
}
result = &mut ws_send_handle => {
debug!("WebSocket send task completed: {:?}", result);
}
}
stdout_handle.abort();
stdin_handle.abort();
ws_send_handle.abort();
let _ = lsp_process.kill().await;
Ok(())
}