fix: LS cleanup uses sudo -u for same-UID kill, prevent double kill
This commit is contained in:
@@ -60,9 +60,11 @@ install() {
|
||||
cat > "$SUDOERS_FILE" <<EOF
|
||||
# Allow $REAL_USER to run commands as $LS_USER (for antigravity proxy)
|
||||
$REAL_USER ALL=($LS_USER) NOPASSWD: ALL
|
||||
# Allow $REAL_USER to kill $LS_USER's processes (for clean shutdown)
|
||||
$REAL_USER ALL=(root) NOPASSWD: /usr/bin/kill -TERM *, /usr/bin/kill -KILL *, /usr/bin/pkill -TERM -u $LS_USER *, /usr/bin/pkill -KILL -u $LS_USER *
|
||||
EOF
|
||||
chmod 440 "$SUDOERS_FILE"
|
||||
echo " + sudoers: $REAL_USER can run as $LS_USER"
|
||||
echo " + sudoers: $REAL_USER can run as $LS_USER + kill $LS_USER processes"
|
||||
|
||||
# ── 4. iptables REDIRECT (scoped to UID) ────────────────────────────
|
||||
# Remove existing rule first (idempotent)
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
Reference in New Issue
Block a user