mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
refactor(core): share process-tree kill helpers across tool crates (#853)
Five crates (tools, build, vcs, github, docker) each carried their own copy of killing a child and what it started, and only the tools installer killed the whole tree on Windows, so cancelling git, gh, docker or Maven/Gradle there left descendants running. ide-core now provides own_group, kill_tree and terminate_tree (process-group signals on Unix, taskkill /T /F on Windows) and the copies are removed. Unix behaviour is unchanged; the Windows path was not run locally. Co-authored-by: bograh <69661398+bograh@users.noreply.github.com>
This commit is contained in:
Generated
+1
-3
@@ -4080,7 +4080,6 @@ dependencies = [
|
||||
"dirs 6.0.0",
|
||||
"dunce",
|
||||
"ide-core",
|
||||
"libc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"specta",
|
||||
@@ -4104,6 +4103,7 @@ version = "0.1.33"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"encoding_rs",
|
||||
"libc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"specta",
|
||||
@@ -4173,7 +4173,6 @@ name = "ide-docker"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"libc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"specta",
|
||||
@@ -4200,7 +4199,6 @@ version = "0.1.33"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"ide-vcs",
|
||||
"libc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"specta",
|
||||
|
||||
@@ -15,8 +15,5 @@ specta.workspace = true
|
||||
tracing.workspace = true
|
||||
dirs.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
//! line to a callback, and cancellation kills the whole process group (the
|
||||
//! `mvnw` / `gradlew` wrapper scripts start the JVM as a child).
|
||||
|
||||
use ide_core::process::kill_tree;
|
||||
use ide_core::{CancellationToken, IdeError, IdeResult};
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::{BufRead, BufReader, Read};
|
||||
use std::path::Path;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -22,18 +23,6 @@ pub(crate) struct Output {
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
fn kill_tree(child: &mut Child) {
|
||||
#[cfg(unix)]
|
||||
if let Ok(pid) = i32::try_from(child.id()) {
|
||||
// SAFETY: plain syscall; the group was created by `process_group(0)` at spawn.
|
||||
unsafe {
|
||||
libc::kill(-pid, libc::SIGKILL);
|
||||
}
|
||||
}
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
|
||||
/// `program` as spawnable: a bare name (`mvn`, `mvn.cmd`) resolves on the IDE's
|
||||
/// search path — the login shell's `PATH` plus version-manager shims — since the
|
||||
/// app's own `PATH` can be minimal and `Command` would not see `mvn` there.
|
||||
@@ -80,12 +69,8 @@ pub(crate) fn run_with_env(
|
||||
let mut command = Command::new(&program);
|
||||
command.args(args).current_dir(cwd).envs(env).envs(encoding.env.iter().map(|(k, v)| (k, v))).stdin(Stdio::null());
|
||||
command.stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
command.process_group(0);
|
||||
}
|
||||
ide_core::process::prepare(&mut command);
|
||||
// Its own group: `mvnw` / `gradlew` start the JVM as a child, and cancelling kills both.
|
||||
ide_core::process::prepare(ide_core::process::own_group(&mut command));
|
||||
if clear_env {
|
||||
command.env_clear().envs(env).envs(encoding.env);
|
||||
}
|
||||
|
||||
@@ -18,5 +18,8 @@ tokio-util.workspace = true
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.61", features = ["Win32_Globalization", "Win32_Security", "Win32_System_Registry", "Win32_Foundation"] }
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
//! Child-process helpers shared by every crate that spawns tools.
|
||||
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::process::Command;
|
||||
#[cfg(windows)]
|
||||
use std::process::Stdio;
|
||||
use std::process::{Child, Command};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Windows `CREATE_NO_WINDOW` creation flag, for call sites that combine it
|
||||
/// with other flags (and then call [`host_env`] themselves). Prefer [`prepare`] otherwise.
|
||||
@@ -25,6 +28,70 @@ pub fn prepare(cmd: &mut Command) -> &mut Command {
|
||||
host_env(cmd)
|
||||
}
|
||||
|
||||
/// Starts the child as the leader of its own process group on Unix, so [`kill_tree`] and
|
||||
/// [`terminate_tree`] reach what it starts in turn (`gh` → `git`, `npm` → `node`, `mvnw` →
|
||||
/// the JVM). A no-op elsewhere: on Windows the tree is found from the child's process id.
|
||||
pub fn own_group(cmd: &mut Command) -> &mut Command {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
cmd.process_group(0);
|
||||
}
|
||||
cmd
|
||||
}
|
||||
|
||||
/// Kills the child and everything it started, then reaps it.
|
||||
///
|
||||
/// - Unix: `SIGKILL` to its process group, so spawn it with [`own_group`].
|
||||
/// - Windows: `taskkill /T /F`, which walks the tree from the child's process id.
|
||||
pub fn kill_tree(child: &mut Child) {
|
||||
#[cfg(unix)]
|
||||
signal_group(child, libc::SIGKILL);
|
||||
#[cfg(windows)]
|
||||
taskkill_tree(child);
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
|
||||
/// [`kill_tree`] that first asks the group to exit: `SIGTERM`, then `grace`, then `SIGKILL`.
|
||||
/// Git removes its `index.lock` on `SIGTERM`, where `SIGKILL` leaves it behind and the next
|
||||
/// git command fails. Windows has no such signal to send from outside the child's console,
|
||||
/// so there it is [`kill_tree`].
|
||||
pub fn terminate_tree(child: &mut Child, grace: Duration) {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
signal_group(child, libc::SIGTERM);
|
||||
// Sleep rather than `try_wait`: the child stays unreaped, so its process id (and the
|
||||
// group id) cannot be reused before the `SIGKILL` below.
|
||||
std::thread::sleep(grace);
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
let _ = grace;
|
||||
kill_tree(child);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn signal_group(child: &Child, signal: libc::c_int) {
|
||||
if let Ok(pid) = libc::pid_t::try_from(child.id()) {
|
||||
// SAFETY: plain syscall. The child is not reaped yet, so `-pid` is still its group (or
|
||||
// no group at all when it was not spawned with `own_group`, and the call fails).
|
||||
unsafe {
|
||||
libc::kill(-pid, signal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn taskkill_tree(child: &Child) {
|
||||
let mut taskkill = Command::new("taskkill");
|
||||
prepare(&mut taskkill)
|
||||
.args(["/T", "/F", "/PID", &child.id().to_string()])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
let _ = taskkill.status();
|
||||
}
|
||||
|
||||
/// Gives `cmd` the host's environment back when the IDE runs from an AppImage. `AppRun` and
|
||||
/// its hooks point `LD_LIBRARY_PATH`, `PATH`, `XDG_DATA_DIRS`, `GIO_MODULE_DIR`, `GTK_PATH`,
|
||||
/// `GST_PLUGIN_*`, ... into the mounted image for the IDE itself; a host program that
|
||||
@@ -597,3 +664,71 @@ mod tests {
|
||||
assert_eq!(find_executable("nope", &search), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix))]
|
||||
mod kill_tests {
|
||||
use super::*;
|
||||
use std::path::Path;
|
||||
use std::time::Instant;
|
||||
|
||||
/// Runs `script` in `sh` as its own group's leader and waits for it to write `ready`.
|
||||
fn start(script: &str, ready: &Path) -> Child {
|
||||
let mut cmd = Command::new("sh");
|
||||
cmd.arg("-c").arg(script);
|
||||
let child = own_group(&mut cmd).spawn().unwrap();
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
while !ready.exists() {
|
||||
assert!(Instant::now() < deadline, "the script never got ready");
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
child
|
||||
}
|
||||
|
||||
fn assert_gone(pid: libc::pid_t) {
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
// SAFETY: signal 0 only checks that the process exists.
|
||||
while unsafe { libc::kill(pid, 0) } == 0 {
|
||||
assert!(Instant::now() < deadline, "process {pid} survived");
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kill_tree_kills_what_the_child_started() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let pid_file = dir.path().join("pid");
|
||||
let script = format!("sleep 30 & echo $! > '{0}.tmp' && mv '{0}.tmp' '{0}'; wait", pid_file.display());
|
||||
let mut child = start(&script, &pid_file);
|
||||
let grandchild: libc::pid_t = std::fs::read_to_string(&pid_file).unwrap().trim().parse().unwrap();
|
||||
kill_tree(&mut child);
|
||||
assert!(child.try_wait().unwrap().is_some(), "the child is reaped");
|
||||
assert_gone(grandchild);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminate_tree_lets_the_child_clean_up() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let (ready, cleaned) = (dir.path().join("ready"), dir.path().join("cleaned"));
|
||||
let script = format!(
|
||||
"trap 'touch \"{}\"; exit 0' TERM; touch \"{}\"; sleep 30 & wait",
|
||||
cleaned.display(),
|
||||
ready.display()
|
||||
);
|
||||
let mut child = start(&script, &ready);
|
||||
terminate_tree(&mut child, Duration::from_millis(500));
|
||||
assert!(cleaned.exists(), "the SIGTERM handler ran before the SIGKILL");
|
||||
assert!(child.try_wait().unwrap().is_some(), "the child is reaped");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminate_tree_still_kills_a_child_that_ignores_sigterm() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let ready = dir.path().join("ready");
|
||||
let script = format!("trap '' TERM; touch \"{}\"; while :; do sleep 1; done", ready.display());
|
||||
let mut child = start(&script, &ready);
|
||||
let started = Instant::now();
|
||||
terminate_tree(&mut child, Duration::from_millis(50));
|
||||
assert!(child.try_wait().unwrap().is_some(), "the child is reaped");
|
||||
assert!(started.elapsed() < Duration::from_secs(5));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,5 @@ specta.workspace = true
|
||||
tracing.workspace = true
|
||||
which = "8"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
//! Child processes of the docker CLI: bounded runs and cancellable line streams.
|
||||
|
||||
use ide_core::process::terminate_tree;
|
||||
use ide_core::{CancellationToken, IdeError, IdeResult};
|
||||
use std::io::Read;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::mpsc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// How long a cancelled or timed-out child's process group (CLI plugins such as compose)
|
||||
/// gets to exit on `SIGTERM`, and clean up, before `SIGKILL`.
|
||||
const KILL_GRACE: Duration = Duration::from_millis(20);
|
||||
|
||||
pub(crate) struct Output {
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
@@ -20,36 +25,13 @@ pub(crate) enum Stream {
|
||||
|
||||
fn prepare(cmd: &mut Command) {
|
||||
cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
cmd.process_group(0);
|
||||
}
|
||||
ide_core::process::prepare(cmd);
|
||||
ide_core::process::prepare(ide_core::process::own_group(cmd));
|
||||
}
|
||||
|
||||
fn spawn(cmd: &mut Command) -> IdeResult<Child> {
|
||||
cmd.spawn().map_err(|e| IdeError::unavailable(format!("could not run docker: {e}")))
|
||||
}
|
||||
|
||||
/// Kills the child and, on Unix, its whole process group (CLI plugins such as compose).
|
||||
pub(crate) fn kill(child: &mut Child) {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let pid = child.id() as libc::pid_t;
|
||||
// SAFETY: plain syscall; the child leads its own process group (`prepare`).
|
||||
unsafe {
|
||||
libc::kill(-pid, libc::SIGTERM);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
unsafe {
|
||||
libc::kill(-pid, libc::SIGKILL);
|
||||
}
|
||||
}
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
|
||||
fn read_all(mut pipe: impl Read + Send + 'static) -> std::thread::JoinHandle<String> {
|
||||
std::thread::spawn(move || {
|
||||
let mut buf = Vec::new();
|
||||
@@ -88,11 +70,11 @@ pub(crate) fn run_with_input(
|
||||
break status;
|
||||
}
|
||||
if cancel.is_some_and(CancellationToken::is_cancelled) {
|
||||
kill(&mut child);
|
||||
terminate_tree(&mut child, KILL_GRACE);
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
kill(&mut child);
|
||||
terminate_tree(&mut child, KILL_GRACE);
|
||||
return Err(IdeError::unavailable("docker did not respond in time"));
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
@@ -166,7 +148,7 @@ pub(crate) fn stream(
|
||||
let mut stderr_tail: Vec<String> = Vec::new();
|
||||
while open > 0 {
|
||||
if cancel.is_cancelled() {
|
||||
kill(&mut child);
|
||||
terminate_tree(&mut child, KILL_GRACE);
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
match rx.recv_timeout(Duration::from_millis(50)) {
|
||||
@@ -189,7 +171,7 @@ pub(crate) fn stream(
|
||||
break status;
|
||||
}
|
||||
if cancel.is_cancelled() {
|
||||
kill(&mut child);
|
||||
terminate_tree(&mut child, KILL_GRACE);
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
|
||||
@@ -13,6 +13,3 @@ serde_json.workspace = true
|
||||
specta.workspace = true
|
||||
tracing.workspace = true
|
||||
which = "8"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
//! Child processes of the gh CLI: bounded runs and cancellable line streams.
|
||||
|
||||
use ide_core::process::terminate_tree;
|
||||
use ide_core::{CancellationToken, IdeError, IdeResult};
|
||||
use std::io::Read;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::mpsc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// How long a cancelled or timed-out child's process group (gh spawns git and helpers)
|
||||
/// gets to exit on `SIGTERM` before `SIGKILL`: git removes its lock files on `SIGTERM`.
|
||||
const KILL_GRACE: Duration = Duration::from_millis(20);
|
||||
|
||||
pub(crate) struct Output {
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
@@ -20,36 +25,13 @@ pub(crate) enum Stream {
|
||||
|
||||
fn prepare(cmd: &mut Command) {
|
||||
cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
cmd.process_group(0);
|
||||
}
|
||||
ide_core::process::prepare(cmd);
|
||||
ide_core::process::prepare(ide_core::process::own_group(cmd));
|
||||
}
|
||||
|
||||
fn spawn(cmd: &mut Command) -> IdeResult<Child> {
|
||||
cmd.spawn().map_err(|e| IdeError::unavailable(format!("could not run gh: {e}")))
|
||||
}
|
||||
|
||||
/// Kills the child and, on Unix, its whole process group (gh spawns git and helpers).
|
||||
pub(crate) fn kill(child: &mut Child) {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let pid = child.id() as libc::pid_t;
|
||||
// SAFETY: plain syscall; the child leads its own process group (`prepare`).
|
||||
unsafe {
|
||||
libc::kill(-pid, libc::SIGTERM);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
unsafe {
|
||||
libc::kill(-pid, libc::SIGKILL);
|
||||
}
|
||||
}
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
|
||||
fn read_all(mut pipe: impl Read + Send + 'static) -> std::thread::JoinHandle<String> {
|
||||
std::thread::spawn(move || {
|
||||
let mut buf = Vec::new();
|
||||
@@ -92,11 +74,11 @@ pub(crate) fn run_with_input(
|
||||
break status;
|
||||
}
|
||||
if cancel.is_some_and(CancellationToken::is_cancelled) {
|
||||
kill(&mut child);
|
||||
terminate_tree(&mut child, KILL_GRACE);
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
kill(&mut child);
|
||||
terminate_tree(&mut child, KILL_GRACE);
|
||||
return Err(IdeError::unavailable("gh did not respond in time"));
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
@@ -170,7 +152,7 @@ pub(crate) fn stream(
|
||||
let mut stderr_tail: Vec<String> = Vec::new();
|
||||
while open > 0 {
|
||||
if cancel.is_cancelled() {
|
||||
kill(&mut child);
|
||||
terminate_tree(&mut child, KILL_GRACE);
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
match rx.recv_timeout(Duration::from_millis(50)) {
|
||||
@@ -193,7 +175,7 @@ pub(crate) fn stream(
|
||||
break status;
|
||||
}
|
||||
if cancel.is_cancelled() {
|
||||
kill(&mut child);
|
||||
terminate_tree(&mut child, KILL_GRACE);
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
|
||||
@@ -20,8 +20,8 @@ zip.workspace = true
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dev-dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
@@ -4,48 +4,16 @@
|
||||
use crate::InstallEvent;
|
||||
use ide_core::error::ErrorKind;
|
||||
use ide_core::output_encoding::decode_tool_output;
|
||||
use ide_core::process::{kill_tree, own_group};
|
||||
use ide_core::{CancellationToken, IdeError, IdeResult};
|
||||
use std::io::{BufRead, BufReader, Read};
|
||||
use std::path::Path;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::mpsc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub type Emit<'a> = &'a dyn Fn(InstallEvent);
|
||||
|
||||
/// Starts the child as the leader of its own process group, so [`kill`] reaches what it
|
||||
/// starts in turn (`npm` → `node`, `go install` → the compiler, `pip` → build backends).
|
||||
fn own_group(cmd: &mut Command) -> &mut Command {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
cmd.process_group(0);
|
||||
}
|
||||
cmd
|
||||
}
|
||||
|
||||
/// Kills the child and everything it started.
|
||||
fn kill(child: &mut Child) {
|
||||
#[cfg(unix)]
|
||||
if let Ok(pid) = i32::try_from(child.id()) {
|
||||
// SAFETY: plain syscall; the child leads its own process group (`own_group`) and is not reaped yet.
|
||||
unsafe {
|
||||
libc::kill(-pid, libc::SIGKILL);
|
||||
}
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let mut taskkill = Command::new("taskkill");
|
||||
ide_core::process::prepare(&mut taskkill)
|
||||
.args(["/T", "/F", "/PID", &child.id().to_string()])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
let _ = taskkill.status();
|
||||
}
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
|
||||
fn describe(cmd: &Command) -> String {
|
||||
let mut s = Path::new(cmd.get_program()).file_name().unwrap_or_default().to_string_lossy().into_owned();
|
||||
for a in cmd.get_args() {
|
||||
@@ -100,7 +68,7 @@ pub fn run_collect(mut cmd: Command, cancel: &CancellationToken, emit: Emit) ->
|
||||
let mut output = String::new();
|
||||
let status = loop {
|
||||
if cancel.is_cancelled() {
|
||||
kill(&mut child);
|
||||
kill_tree(&mut child);
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
match rx.recv_timeout(Duration::from_millis(100)) {
|
||||
@@ -144,7 +112,7 @@ pub fn run_quiet(mut cmd: Command, timeout: Duration) -> Option<(bool, String)>
|
||||
Ok(Some(s)) => break s,
|
||||
Ok(None) if Instant::now() < deadline => std::thread::sleep(Duration::from_millis(15)),
|
||||
_ => {
|
||||
kill(&mut child);
|
||||
kill_tree(&mut child);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@@ -256,7 +224,7 @@ impl Http {
|
||||
});
|
||||
let status = loop {
|
||||
if cancel.is_cancelled() {
|
||||
kill(&mut child);
|
||||
kill_tree(&mut child);
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
match child.try_wait()? {
|
||||
@@ -308,7 +276,7 @@ impl Http {
|
||||
let mut last_bytes = 0;
|
||||
let status = loop {
|
||||
if cancel.is_cancelled() {
|
||||
kill(&mut child);
|
||||
kill_tree(&mut child);
|
||||
let _ = std::fs::remove_file(dest);
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
@@ -413,7 +381,7 @@ pub(crate) fn run_input(
|
||||
}
|
||||
};
|
||||
if result.is_err() {
|
||||
kill(&mut child);
|
||||
kill_tree(&mut child);
|
||||
}
|
||||
result?;
|
||||
let output = output.unwrap()?;
|
||||
|
||||
+11
-33
@@ -1,13 +1,18 @@
|
||||
//! Thin wrapper around the `git` executable used for every mutating operation.
|
||||
|
||||
use ide_core::process::{own_group, terminate_tree};
|
||||
use ide_core::{CancellationToken, IdeError, IdeResult};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::mpsc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// How long a cancelled git's process group (remote helpers, `ssh`, credential helpers) gets
|
||||
/// to exit on `SIGTERM` before `SIGKILL`: git removes its `index.lock` on `SIGTERM`.
|
||||
const KILL_GRACE: Duration = Duration::from_millis(20);
|
||||
|
||||
/// Explicit git executable from Settings › Version Control › Git (`None` = auto-detect).
|
||||
static CONFIGURED: std::sync::RwLock<Option<PathBuf>> = std::sync::RwLock::new(None);
|
||||
|
||||
@@ -361,11 +366,7 @@ pub(crate) fn run_cancellable_raw(root: &Path, args: &[&str], cancel: &Cancellat
|
||||
}
|
||||
let mut cmd = command(root)?;
|
||||
cmd.args(args).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
cmd.process_group(0);
|
||||
}
|
||||
own_group(&mut cmd);
|
||||
let mut session = CommandSession::start(&mut cmd);
|
||||
let mut child = cmd.spawn().map_err(spawn_error)?;
|
||||
let capture = |mut pipe: Box<dyn Read + Send>| {
|
||||
@@ -378,8 +379,7 @@ pub(crate) fn run_cancellable_raw(root: &Path, args: &[&str], cancel: &Cancellat
|
||||
let stderr = capture(Box::new(child.stderr.take().expect("piped stderr")));
|
||||
let status = loop {
|
||||
if cancel.is_cancelled() {
|
||||
kill(&mut child);
|
||||
let _ = child.wait();
|
||||
terminate_tree(&mut child, KILL_GRACE);
|
||||
let _ = stdout.join();
|
||||
let _ = stderr.join();
|
||||
return Err(IdeError::cancelled());
|
||||
@@ -504,11 +504,7 @@ pub(crate) fn run_streaming_env(
|
||||
cmd.env(k, v);
|
||||
}
|
||||
cmd.args(args).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
cmd.process_group(0);
|
||||
}
|
||||
own_group(&mut cmd);
|
||||
tracing::debug!(?args, "git (streaming)");
|
||||
let mut session = CommandSession::start(&mut cmd);
|
||||
let mut child = cmd.spawn().map_err(spawn_error)?;
|
||||
@@ -529,8 +525,7 @@ pub(crate) fn run_streaming_env(
|
||||
let mut open = readers.len();
|
||||
while open > 0 {
|
||||
if cancel.is_cancelled() {
|
||||
kill(&mut child);
|
||||
let _ = child.wait();
|
||||
terminate_tree(&mut child, KILL_GRACE);
|
||||
for r in readers {
|
||||
let _ = r.join();
|
||||
}
|
||||
@@ -559,8 +554,7 @@ pub(crate) fn run_streaming_env(
|
||||
break status;
|
||||
}
|
||||
if cancel.is_cancelled() {
|
||||
kill(&mut child);
|
||||
let _ = child.wait();
|
||||
terminate_tree(&mut child, KILL_GRACE);
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
@@ -644,22 +638,6 @@ fn spawn_reader(
|
||||
})
|
||||
}
|
||||
|
||||
fn kill(child: &mut Child) {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let pid = child.id() as libc::pid_t;
|
||||
// SAFETY: plain syscall; the child was spawned as leader of its own process group.
|
||||
unsafe {
|
||||
libc::kill(-pid, libc::SIGTERM);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
unsafe {
|
||||
libc::kill(-pid, libc::SIGKILL);
|
||||
}
|
||||
}
|
||||
let _ = child.kill();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod command_session_tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user