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
|
cat > "$SUDOERS_FILE" <<EOF
|
||||||
# Allow $REAL_USER to run commands as $LS_USER (for antigravity proxy)
|
# Allow $REAL_USER to run commands as $LS_USER (for antigravity proxy)
|
||||||
$REAL_USER ALL=($LS_USER) NOPASSWD: ALL
|
$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
|
EOF
|
||||||
chmod 440 "$SUDOERS_FILE"
|
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) ────────────────────────────
|
# ── 4. iptables REDIRECT (scoped to UID) ────────────────────────────
|
||||||
# Remove existing rule first (idempotent)
|
# Remove existing rule first (idempotent)
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ pub struct StandaloneLS {
|
|||||||
ls_pid: Option<u32>,
|
ls_pid: Option<u32>,
|
||||||
/// Whether the LS was spawned via sudo (needs sudo kill).
|
/// Whether the LS was spawned via sudo (needs sudo kill).
|
||||||
use_sudo: bool,
|
use_sudo: bool,
|
||||||
|
/// Whether kill() has already been called.
|
||||||
|
killed: bool,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub csrf: String,
|
pub csrf: String,
|
||||||
}
|
}
|
||||||
@@ -61,6 +63,8 @@ impl StandaloneLS {
|
|||||||
main_config: &MainLSConfig,
|
main_config: &MainLSConfig,
|
||||||
mitm_config: Option<&StandaloneMitmConfig>,
|
mitm_config: Option<&StandaloneMitmConfig>,
|
||||||
) -> Result<Self, String> {
|
) -> Result<Self, String> {
|
||||||
|
// Kill any orphaned LS processes from previous runs
|
||||||
|
cleanup_orphaned_ls();
|
||||||
let port = find_free_port()?;
|
let port = find_free_port()?;
|
||||||
let ts = std::time::SystemTime::now()
|
let ts = std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
@@ -219,6 +223,7 @@ impl StandaloneLS {
|
|||||||
child,
|
child,
|
||||||
ls_pid,
|
ls_pid,
|
||||||
use_sudo,
|
use_sudo,
|
||||||
|
killed: false,
|
||||||
port,
|
port,
|
||||||
csrf: main_config.csrf.clone(),
|
csrf: main_config.csrf.clone(),
|
||||||
})
|
})
|
||||||
@@ -259,21 +264,41 @@ impl StandaloneLS {
|
|||||||
|
|
||||||
/// Kill the standalone LS process.
|
/// Kill the standalone LS process.
|
||||||
pub fn kill(&mut self) {
|
pub fn kill(&mut self) {
|
||||||
|
if self.killed {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.killed = true;
|
||||||
info!("Killing standalone LS");
|
info!("Killing standalone LS");
|
||||||
|
|
||||||
if self.use_sudo {
|
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 {
|
if let Some(pid) = self.ls_pid {
|
||||||
info!(pid, "Killing LS process via sudo");
|
info!(pid, "Killing LS process via sudo -u {}", LS_USER);
|
||||||
let _ = std::process::Command::new("sudo")
|
// Run kill AS the antigravity-ls user (same UID can signal)
|
||||||
.args(["-n", "kill", "-TERM", &pid.to_string()])
|
let ok = std::process::Command::new("sudo")
|
||||||
.status();
|
.args(["-n", "-u", LS_USER, "kill", "-TERM", &pid.to_string()])
|
||||||
// Give it a moment to exit gracefully
|
.stdout(Stdio::null())
|
||||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
.stderr(Stdio::null())
|
||||||
// Force kill if still alive
|
.status()
|
||||||
let _ = std::process::Command::new("sudo")
|
.map(|s| s.success())
|
||||||
.args(["-n", "kill", "-KILL", &pid.to_string()])
|
.unwrap_or(false);
|
||||||
.status();
|
|
||||||
|
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 {
|
} else {
|
||||||
let _ = self.child.kill();
|
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}"))
|
.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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user