use anyhow::{Context, Result};
#[cfg(unix)]
use nix::sys::signal::{self, Signal};
#[cfg(unix)]
use nix::unistd::Pid;
use std::process::Stdio;
use std::time::{Duration, Instant};
use tokio::io::AsyncWriteExt;
use tokio::process::{Child, Command};
use tokio::time::sleep;
const PLS_SHUTDOWN_GRACE_MS: u64 = 250;
const PLS_SHUTDOWN_POLL_MS: u64 = 25;
const PLS_DAP_FLAG: &str = "-d:LanguageServer::DAP";
pub struct BridgeAdapter {
child_process: Option<Child>,
}
impl BridgeAdapter {
pub fn new() -> Self {
Self { child_process: None }
}
pub async fn spawn_pls_dap(&mut self) -> Result<()> {
if self.child_process.is_some() {
let _ = self.shutdown().await;
}
let perl_path =
crate::platform::resolve_perl_path().context("Failed to find perl binary on PATH")?;
let child = Command::new(perl_path)
.arg(PLS_DAP_FLAG)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.context("Failed to spawn Perl::LanguageServer DAP process")?;
self.child_process = Some(child);
Ok(())
}
pub async fn proxy_messages(&mut self) -> Result<()> {
let Some(child) = self.child_process.as_mut() else {
anyhow::bail!("Child process not spawned. Call spawn_pls_dap() first.");
};
let mut child_stdin = child.stdin.take().context("Failed to capture child stdin")?;
let mut child_stdout = child.stdout.take().context("Failed to capture child stdout")?;
let mut parent_stdin = tokio::io::stdin();
let mut parent_stdout = tokio::io::stdout();
let client_to_server = async move {
tokio::io::copy(&mut parent_stdin, &mut child_stdin)
.await
.context("Error copying from client to server")?;
let _ = child_stdin.shutdown().await;
Ok::<(), anyhow::Error>(())
};
let server_to_client = async move {
tokio::io::copy(&mut child_stdout, &mut parent_stdout)
.await
.context("Error copying from server to client")?;
parent_stdout.flush().await.context("Error flushing to client")?;
Ok::<(), anyhow::Error>(())
};
let (res1, res2) = tokio::join!(client_to_server, server_to_client);
res1?;
res2?;
Ok(())
}
pub async fn shutdown(&mut self) -> Result<()> {
if let Some(mut child) = self.child_process.take() {
if !Self::wait_for_child_exit(&mut child, Duration::from_millis(0)).await {
#[cfg(unix)]
{
if let Some(pid) = child.id() {
if let Ok(()) = signal::kill(Pid::from_raw(pid as i32), Signal::SIGTERM) {
if Self::wait_for_child_exit(
&mut child,
Duration::from_millis(PLS_SHUTDOWN_GRACE_MS),
)
.await
{
return Ok(());
}
}
}
}
let _ = child.kill().await;
if !Self::wait_for_child_exit(
&mut child,
Duration::from_millis(PLS_SHUTDOWN_GRACE_MS),
)
.await
{
let _ = child.wait().await?;
}
}
}
Ok(())
}
async fn wait_for_child_exit(child: &mut Child, timeout: Duration) -> bool {
if let Ok(Some(_)) = child.try_wait() {
return true;
}
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
match child.try_wait() {
Ok(Some(_)) => return true,
Ok(None) => sleep(Duration::from_millis(PLS_SHUTDOWN_POLL_MS)).await,
Err(e) => {
tracing::error!(error = %e, "Failed to poll Perl::LanguageServer process");
return false;
}
}
}
false
}
}
impl Default for BridgeAdapter {
fn default() -> Self {
Self::new()
}
}
impl Drop for BridgeAdapter {
fn drop(&mut self) {
if let Some(mut child) = self.child_process.take() {
let _ = child.start_kill();
}
}
}