use std::time::Duration;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use tracing::{error, info, warn};
use crate::bridge::Translator;
use crate::transport::ShutdownSignal;
use crate::util::AbortOnDrop;
const LSP_INIT_TASK_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
const LSP_INIT_TASK_ABORT_GRACE: Duration = Duration::from_secs(1);
pub(super) async fn await_lsp_init_handle(handle: JoinHandle<()>, timeout: Duration) {
await_lsp_init_handle_within(handle, timeout, LSP_INIT_TASK_ABORT_GRACE).await;
}
async fn await_lsp_init_handle_within(
mut handle: JoinHandle<()>,
timeout: Duration,
abort_grace: Duration,
) {
match tokio::time::timeout(timeout, &mut handle).await {
Ok(Ok(())) => {}
Ok(Err(err)) => error!("Background LSP initialization task failed: {err}"),
Err(_) => {
warn!("Timed out waiting for background LSP initialization task to stop");
handle.abort();
if tokio::time::timeout(abort_grace, handle).await.is_err() {
warn!("Background LSP initialization task did not stop after abort");
}
}
}
}
pub async fn shutdown(
cancel: &CancellationToken,
translator: &Translator,
lsp_init_handle: Option<JoinHandle<()>>,
) {
translator.begin_shutdown();
cancel.cancel();
let mut cleanup_signal = ShutdownSignal::new();
let force_exit_on_signal = tokio::spawn(async move {
cleanup_signal.recv().await;
error!("shutdown signal received during cleanup, forcing immediate exit");
std::process::exit(1);
});
let _abort_force_exit_on_signal = AbortOnDrop(&force_exit_on_signal);
info!("Shutting down LSP servers...");
translator.shutdown_servers().await;
if let Some(handle) = lsp_init_handle {
await_lsp_init_handle(handle, LSP_INIT_TASK_SHUTDOWN_TIMEOUT).await;
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::time::Duration;
use tokio_util::sync::CancellationToken;
use crate::bridge::Translator;
use crate::config::ServerId;
#[tokio::test]
async fn test_shutdown_drains_registered_lsp_server() {
let translator = Translator::new();
translator.register_server(
ServerId::from_static("fake-server"),
crate::lsp::fake_lsp_server(),
);
assert_eq!(translator.registered_server_count(), 1);
let cancel = CancellationToken::new();
let result = tokio::time::timeout(
std::time::Duration::from_secs(20),
super::shutdown(&cancel, &translator, None),
)
.await;
assert!(
result.is_ok(),
"shutdown must not hang against a non-responsive mock LSP server"
);
assert_eq!(
translator.registered_server_count(),
0,
"shutdown must drain every registered LSP server"
);
assert!(
cancel.is_cancelled(),
"shutdown must signal background pump tasks to exit"
);
}
#[tokio::test]
async fn test_shutdown_awaits_background_init_task() {
use std::sync::atomic::{AtomicBool, Ordering};
let translator = Translator::new();
let cancel = CancellationToken::new();
let completed = Arc::new(AtomicBool::new(false));
let completed_clone = Arc::clone(&completed);
let handle = tokio::spawn(async move {
completed_clone.store(true, Ordering::SeqCst);
});
let result = tokio::time::timeout(
std::time::Duration::from_secs(5),
super::shutdown(&cancel, &translator, Some(handle)),
)
.await;
assert!(result.is_ok(), "shutdown must not hang on a live handle");
assert!(
completed.load(Ordering::SeqCst),
"shutdown must await the background init task before returning"
);
}
#[tokio::test]
async fn test_await_lsp_init_handle_aborts_on_timeout() {
use std::sync::atomic::{AtomicBool, Ordering};
struct DropFlag(Arc<AtomicBool>);
impl Drop for DropFlag {
fn drop(&mut self) {
self.0.store(true, Ordering::SeqCst);
}
}
let future_dropped = Arc::new(AtomicBool::new(false));
let guard = DropFlag(Arc::clone(&future_dropped));
let handle = tokio::spawn(async move {
let _guard = guard;
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
});
super::await_lsp_init_handle(handle, std::time::Duration::from_millis(20)).await;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
assert!(
future_dropped.load(Ordering::SeqCst),
"timed-out background init task's future must be dropped via abort(), \
not left running detached until its own sleep completes"
);
}
#[tokio::test]
async fn test_await_lsp_init_handle_logs_task_that_ignores_abort() {
use tracing_subscriber::layer::SubscriberExt as _;
use crate::test_lsp::CapturedLogs;
let (release, released) = std::sync::mpsc::channel::<()>();
let handle = tokio::task::spawn_blocking(move || {
let _ = released.recv();
});
let captured = CapturedLogs::default();
let subscriber = tracing_subscriber::registry().with(captured.clone());
let guard = tracing::subscriber::set_default(subscriber);
let grace = Duration::from_millis(20);
super::await_lsp_init_handle_within(handle, grace, grace).await;
drop(guard);
drop(release);
assert!(
captured
.messages()
.iter()
.any(|m| m.contains("did not stop after abort")),
"expected a warn! log for the task that outlived the abort grace"
);
}
#[tokio::test]
async fn test_await_lsp_init_handle_logs_panic() {
use tracing_subscriber::layer::SubscriberExt as _;
use crate::test_lsp::CapturedLogs;
let handle = tokio::spawn(async {
panic!("simulated background LSP init panic");
});
let captured = CapturedLogs::default();
let subscriber = tracing_subscriber::registry().with(captured.clone());
let guard = tracing::subscriber::set_default(subscriber);
super::await_lsp_init_handle(handle, std::time::Duration::from_secs(5)).await;
drop(guard);
let messages = captured.messages();
assert!(
messages
.iter()
.any(|m| m.contains("Background LSP initialization task failed")),
"expected an error! log for the panicking background init task, got: {messages:?}"
);
}
}