fix: LS cleanup uses sudo -u for same-UID kill, prevent double kill

This commit is contained in:
Nikketryhard
2026-02-15 17:08:43 -06:00
parent b1bd57ab5e
commit cc5f48967a
2 changed files with 134 additions and 12 deletions

View File

@@ -33,6 +33,8 @@ pub struct StandaloneLS {
ls_pid: Option<u32>,
/// Whether the LS was spawned via sudo (needs sudo kill).
use_sudo: bool,
/// Whether kill() has already been called.
killed: bool,
pub port: u16,
pub csrf: String,
}
@@ -61,6 +63,8 @@ impl StandaloneLS {
main_config: &MainLSConfig,
mitm_config: Option<&StandaloneMitmConfig>,
) -> Result<Self, String> {
// Kill any orphaned LS processes from previous runs
cleanup_orphaned_ls();
let port = find_free_port()?;
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
@@ -219,6 +223,7 @@ impl StandaloneLS {
child,
ls_pid,
use_sudo,
killed: false,
port,
csrf: main_config.csrf.clone(),
})
@@ -259,21 +264,41 @@ impl StandaloneLS {
/// Kill the standalone LS process.
pub fn kill(&mut self) {
if self.killed {
return;
}
self.killed = true;
info!("Killing standalone LS");
if self.use_sudo {
// The child is sudo which already exited. Kill the actual LS via sudo.
// The child is sudo which already exited. Kill the actual LS.
if let Some(pid) = self.ls_pid {
info!(pid, "Killing LS process via sudo");
let _ = std::process::Command::new("sudo")
.args(["-n", "kill", "-TERM", &pid.to_string()])
.status();
// Give it a moment to exit gracefully
std::thread::sleep(std::time::Duration::from_millis(500));
// Force kill if still alive
let _ = std::process::Command::new("sudo")
.args(["-n", "kill", "-KILL", &pid.to_string()])
.status();
info!(pid, "Killing LS process via sudo -u {}", LS_USER);
// Run kill AS the antigravity-ls user (same UID can signal)
let ok = std::process::Command::new("sudo")
.args(["-n", "-u", LS_USER, "kill", "-TERM", &pid.to_string()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if ok {
std::thread::sleep(std::time::Duration::from_millis(500));
// Force kill if still alive
let _ = std::process::Command::new("sudo")
.args(["-n", "-u", LS_USER, "kill", "-KILL", &pid.to_string()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
} else {
// Fallback: try with root sudo, then cleanup
info!("sudo -u kill failed, trying fallback cleanup");
cleanup_orphaned_ls();
}
} else {
// No PID recorded, try blanket cleanup
cleanup_orphaned_ls();
}
} else {
let _ = self.child.kill();
@@ -421,6 +446,101 @@ fn find_ls_pid_for_user(user: &str) -> Result<u32, String> {
.ok_or_else(|| format!("No LS process found for user {user}"))
}
/// Kill any orphaned standalone LS processes from previous runs.
///
/// This handles the case where the proxy crashed or was killed without
/// properly cleaning up the sudo-spawned LS process.
///
/// Key insight: the sudoers rule allows running commands AS antigravity-ls
/// (`ALL=(antigravity-ls) NOPASSWD: ALL`). A process can send signals to
/// other processes with the same UID, so we run `kill` as antigravity-ls
/// rather than as root.
fn cleanup_orphaned_ls() {
if !has_ls_user() {
return;
}
// Find all LS processes owned by antigravity-ls user
let output = Command::new("pgrep")
.args(["-u", LS_USER, "-f", "language_server_linux"])
.output();
let pids: Vec<u32> = match output {
Ok(out) => {
String::from_utf8_lossy(&out.stdout)
.lines()
.filter_map(|l| l.trim().parse().ok())
.collect()
}
Err(_) => return,
};
if pids.is_empty() {
return;
}
info!(count = pids.len(), ?pids, "Cleaning up orphaned standalone LS processes");
// Kill each PID by running `kill` AS the antigravity-ls user.
// This works because same-UID processes can signal each other,
// and the sudoers rule allows ALL commands as antigravity-ls.
for pid in &pids {
let ok = Command::new("sudo")
.args(["-n", "-u", LS_USER, "kill", "-TERM", &pid.to_string()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if ok {
info!(pid, "Killed orphaned LS process");
} else {
// Fallback: try as root (needs separate sudoers entry)
let _ = Command::new("sudo")
.args(["-n", "kill", "-TERM", &pid.to_string()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
}
// Wait for graceful exit
std::thread::sleep(std::time::Duration::from_millis(500));
// Force-kill any survivors
let still_alive = Command::new("pgrep")
.args(["-u", LS_USER, "-f", "language_server_linux"])
.output()
.map(|o| !o.stdout.is_empty())
.unwrap_or(false);
if still_alive {
info!("Orphaned LS still alive, force killing");
for pid in &pids {
let _ = Command::new("sudo")
.args(["-n", "-u", LS_USER, "kill", "-KILL", &pid.to_string()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
std::thread::sleep(std::time::Duration::from_millis(300));
// Final check
let still_alive = Command::new("pgrep")
.args(["-u", LS_USER, "-f", "language_server_linux"])
.output()
.map(|o| !o.stdout.is_empty())
.unwrap_or(false);
if still_alive {
eprintln!("\n \x1b[1;31m⚠ Cannot kill orphaned LS process\x1b[0m");
eprintln!(" Run: \x1b[1msudo pkill -u {LS_USER} -f language_server_linux\x1b[0m\n");
}
} else {
info!("Orphaned LS processes cleaned up");
}
}
#[cfg(test)]
mod tests {
use super::*;