use std::io;
use std::time::Duration;
use tokio::process::{Child, ChildStderr, ChildStdin, ChildStdout};
pub(super) struct ServerProcess {
child: Child,
}
impl ServerProcess {
pub(super) fn new(child: Child) -> Self {
Self { child }
}
pub(super) fn id(&self) -> Option<u32> {
self.child.id()
}
pub(super) fn take_io(&mut self) -> io::Result<(ChildStdout, ChildStdin, ChildStderr)> {
match (
self.child.stdout.take(),
self.child.stdin.take(),
self.child.stderr.take(),
) {
(Some(stdout), Some(stdin), Some(stderr)) => Ok((stdout, stdin, stderr)),
_ => Err(io::Error::other(
"language server is missing a configured stdio pipe",
)),
}
}
fn signal_group(&mut self) -> io::Result<()> {
let Some(pid) = self.child.id() else {
return Ok(());
};
#[cfg(unix)]
{
let pid = libc::pid_t::try_from(pid)
.map_err(|_| io::Error::other("child PID is outside the platform range"))?;
if unsafe { libc::kill(-pid, libc::SIGKILL) } == -1 {
let error = io::Error::last_os_error();
if error.raw_os_error() != Some(libc::ESRCH) {
return Err(error);
}
}
}
#[cfg(not(unix))]
{
let _ = pid;
self.child.start_kill()?;
}
Ok(())
}
pub(super) async fn finish(
&mut self,
drain: Option<tokio::task::JoinHandle<()>>,
grace: Duration,
) -> io::Result<()> {
if let Some(mut drain) = drain {
match tokio::time::timeout(grace, &mut drain).await {
Ok(Ok(())) | Err(_) => {}
Ok(Err(error)) => strop_trace::record_with(
strop_trace::EventKind::Error,
|| serde_json::json!({"source":"lsp_stderr_task","message":error.to_string()}),
),
}
}
self.signal_group()?;
tokio::time::timeout(Duration::from_secs(5), self.child.wait())
.await
.map_err(|error| io::Error::new(io::ErrorKind::TimedOut, error))??;
Ok(())
}
}
impl Drop for ServerProcess {
fn drop(&mut self) {
if let Err(error) = self.signal_group() {
strop_trace::record_with(
strop_trace::EventKind::Error,
|| serde_json::json!({"source":"lsp_process_cleanup","message":error.to_string()}),
);
}
}
}