use std::io;
use std::process::ExitStatus;
use std::time::Duration;
use tokio::process::{ChildStdin, ChildStdout, Command};
#[cfg(unix)]
mod lifeline;
pub const LIFELINE_SWEEP_BUDGET: Duration = Duration::from_secs(8);
#[cfg(unix)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Binding {
Bound,
Unbound,
}
#[cfg(unix)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MarkFailure {
Unreachable,
NoConfirmation,
SnapshotTimedOut,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MarkOutcome {
Confirmed,
#[cfg(unix)]
Failed(MarkFailure),
}
#[cfg(unix)]
pub type ServerStderr = tokio::net::unix::pipe::Receiver;
#[cfg(windows)]
pub type ServerStderr = tokio::process::ChildStderr;
#[derive(Debug)]
pub struct ServerProcess {
#[cfg(unix)]
child: Option<tokio::process::Child>,
#[cfg(unix)]
stderr: Option<ServerStderr>,
#[cfg(unix)]
lifeline: Option<lifeline::Lifeline>,
#[cfg(windows)]
child: Box<dyn process_wrap::tokio::ChildWrapper>,
}
#[cfg(unix)]
impl ServerProcess {
pub(crate) fn spawn(command: Command) -> io::Result<Self> {
Self::spawn_with(command, &lifeline::Options::default())
}
fn spawn_with(command: Command, options: &lifeline::Options) -> io::Result<Self> {
let spawned = lifeline::spawn(command, options)?;
Ok(Self {
child: Some(spawned.child),
stderr: Some(spawned.stderr),
lifeline: spawned.lifeline,
})
}
pub(crate) fn binding(&mut self) -> Binding {
self.lifeline
.as_mut()
.map_or(Binding::Unbound, lifeline::Lifeline::binding)
}
pub(crate) fn take_stdin(&mut self) -> Option<ChildStdin> {
self.child.as_mut()?.stdin.take()
}
pub(crate) fn take_stdout(&mut self) -> Option<ChildStdout> {
self.child.as_mut()?.stdout.take()
}
pub(crate) const fn take_stderr(&mut self) -> Option<ServerStderr> {
self.stderr.take()
}
pub(crate) fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
let status = self.leader()?.try_wait()?;
if let Some(lifeline) = &mut self.lifeline {
lifeline.warn_if_watchdog_lost();
}
if status.is_some()
&& let Some(mut lifeline) = self.lifeline.take()
{
lifeline.forget_leader();
lifeline.release(None);
}
Ok(status)
}
pub(crate) async fn wait(&mut self) -> io::Result<ExitStatus> {
let status = self.leader()?.wait().await?;
if let Some(lifeline) = &mut self.lifeline {
lifeline.forget_leader();
}
Ok(status)
}
pub(crate) async fn mark_escapees(&mut self) -> MarkOutcome {
match &mut self.lifeline {
Some(lifeline) => lifeline.mark().await,
None => MarkOutcome::Confirmed,
}
}
pub(crate) async fn terminate_tree(&mut self, within: Duration) {
if let Some(lifeline) = self.lifeline.take() {
self.child = lifeline.terminate(self.child.take(), within).await;
} else if let Some(child) = self.child.as_mut() {
if let Err(e) = child.start_kill() {
tracing::debug!(error = %e, "leader kill signal failed during tree termination");
}
if tokio::time::timeout(within, child.wait()).await.is_err() {
tracing::warn!("LSP server leader did not exit after tree termination");
}
}
}
fn leader(&mut self) -> io::Result<&mut tokio::process::Child> {
self.child
.as_mut()
.ok_or_else(|| io::Error::other("server process already torn down"))
}
#[cfg(test)]
pub(crate) const fn from_unbound(child: tokio::process::Child) -> Self {
Self {
child: Some(child),
stderr: None,
lifeline: None,
}
}
#[cfg(test)]
pub(crate) async fn kill(&mut self) -> io::Result<()> {
self.leader()?.kill().await
}
}
#[cfg(unix)]
impl Drop for ServerProcess {
fn drop(&mut self) {
if let Some(lifeline) = self.lifeline.take() {
lifeline.release(self.child.take());
}
}
}
#[cfg(windows)]
impl ServerProcess {
pub(crate) fn spawn(command: Command) -> io::Result<Self> {
use process_wrap::tokio::{CommandWrap, JobObject, KillOnDrop};
let mut wrapped = CommandWrap::from(command);
wrapped.wrap(JobObject).wrap(KillOnDrop);
wrapped.spawn().map(|child| Self { child })
}
pub(crate) fn take_stdin(&mut self) -> Option<ChildStdin> {
self.child.stdin().take()
}
pub(crate) fn take_stdout(&mut self) -> Option<ChildStdout> {
self.child.stdout().take()
}
pub(crate) fn take_stderr(&mut self) -> Option<ServerStderr> {
self.child.stderr().take()
}
pub(crate) fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
self.child.try_wait()
}
pub(crate) async fn wait(&mut self) -> io::Result<ExitStatus> {
self.child.inner_mut().wait().await
}
pub(crate) async fn terminate_tree(&mut self, within: Duration) {
if let Err(e) = self.child.start_kill() {
tracing::debug!(error = %e, "job termination failed during tree termination");
}
if tokio::time::timeout(within, self.child.inner_mut().wait())
.await
.is_err()
{
tracing::warn!("LSP server leader did not exit after tree termination");
}
}
}
#[cfg(test)]
#[cfg(windows)]
mod tests {
use std::process::Stdio;
use std::time::Duration;
use tokio::time::{Instant, sleep};
use super::*;
async fn pid_listed(pid: u32) -> bool {
let output = Command::new("tasklist")
.args(["/FI", &format!("PID eq {pid}"), "/NH"])
.output()
.await
.unwrap();
String::from_utf8_lossy(&output.stdout).contains(&pid.to_string())
}
#[tokio::test]
async fn test_dropping_server_process_kills_grandchild() {
let dir = tempfile::tempdir().unwrap();
let pid_file = dir.path().join("grandchild.pid");
let script = format!(
"$p=Start-Process ping -ArgumentList '-n','600','127.0.0.1' -PassThru \
-WindowStyle Hidden; Set-Content -Path '{}' -Value $p.Id; Start-Sleep 600",
pid_file.display()
);
let mut command = Command::new("powershell");
command
.args(["-NoProfile", "-Command", &script])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.kill_on_drop(true);
let process = ServerProcess::spawn(command).unwrap();
let deadline = Instant::now() + Duration::from_secs(30);
let pid = loop {
if let Some(pid) = std::fs::read_to_string(&pid_file)
.ok()
.and_then(|s| s.trim().parse::<u32>().ok())
{
break pid;
}
assert!(
Instant::now() < deadline,
"grandchild pid file never appeared"
);
sleep(Duration::from_millis(100)).await;
};
assert!(pid_listed(pid).await);
drop(process);
let deadline = Instant::now() + Duration::from_secs(5);
while pid_listed(pid).await {
assert!(
Instant::now() < deadline,
"grandchild survived the job close"
);
sleep(Duration::from_millis(100)).await;
}
}
}