diff --git a/scripts/mitm-redirect.sh b/scripts/mitm-redirect.sh index cb46912..2ed877c 100755 --- a/scripts/mitm-redirect.sh +++ b/scripts/mitm-redirect.sh @@ -60,9 +60,11 @@ install() { cat > "$SUDOERS_FILE" <, /// 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 { + // 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 { .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 = 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::*;