Files
oak-gpui/crates/terminal/src/pty_info.rs
T
Lukas Wirth f73d6fe4ce terminal: Kill the terminal child process, not the terminal process on exit (#41631)
When rerunning a task, our process id fetching seems to sometimes return
the previous terminal's process id when respawning the task, causing us
to kill the new terminal once the previous one drops as we spawn a new
one, then drop the old one. This results in rerun sometimes spawning a
blank task as the terminal immediately exits. The fix here is simple, we
actually want to kill the process running inside the terminal process,
not the terminal process itself when we exit in the terminal.

No relnotes as this was introduced yesterday in
https://github.com/zed-industries/zed/pull/41562

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-31 15:01:50 +00:00

171 lines
4.8 KiB
Rust

use alacritty_terminal::tty::Pty;
#[cfg(target_os = "windows")]
use std::num::NonZeroU32;
#[cfg(unix)]
use std::os::fd::AsRawFd;
use std::path::PathBuf;
#[cfg(target_os = "windows")]
use windows::Win32::{Foundation::HANDLE, System::Threading::GetProcessId};
use sysinfo::{Pid, Process, ProcessRefreshKind, RefreshKind, System, UpdateKind};
pub struct ProcessIdGetter {
handle: i32,
fallback_pid: u32,
}
impl ProcessIdGetter {
pub fn fallback_pid(&self) -> Pid {
Pid::from_u32(self.fallback_pid)
}
}
#[cfg(unix)]
impl ProcessIdGetter {
fn new(pty: &Pty) -> ProcessIdGetter {
ProcessIdGetter {
handle: pty.file().as_raw_fd(),
fallback_pid: pty.child().id(),
}
}
fn pid(&self) -> Option<Pid> {
let pid = unsafe { libc::tcgetpgrp(self.handle) };
if pid < 0 {
return Some(Pid::from_u32(self.fallback_pid));
}
Some(Pid::from_u32(pid as u32))
}
}
#[cfg(windows)]
impl ProcessIdGetter {
fn new(pty: &Pty) -> ProcessIdGetter {
let child = pty.child_watcher();
let handle = child.raw_handle();
let fallback_pid = child.pid().unwrap_or_else(|| unsafe {
NonZeroU32::new_unchecked(GetProcessId(HANDLE(handle as _)))
});
ProcessIdGetter {
handle: handle as i32,
fallback_pid: u32::from(fallback_pid),
}
}
fn pid(&self) -> Option<Pid> {
let pid = unsafe { GetProcessId(HANDLE(self.handle as _)) };
// the GetProcessId may fail and returns zero, which will lead to a stack overflow issue
if pid == 0 {
// in the builder process, there is a small chance, almost negligible,
// that this value could be zero, which means child_watcher returns None,
// GetProcessId returns 0.
if self.fallback_pid == 0 {
return None;
}
return Some(Pid::from_u32(self.fallback_pid));
}
Some(Pid::from_u32(pid))
}
}
#[derive(Clone, Debug)]
pub struct ProcessInfo {
pub name: String,
pub cwd: PathBuf,
pub argv: Vec<String>,
}
/// Fetches Zed-relevant Pseudo-Terminal (PTY) process information
pub struct PtyProcessInfo {
system: System,
refresh_kind: ProcessRefreshKind,
pid_getter: ProcessIdGetter,
pub current: Option<ProcessInfo>,
}
impl PtyProcessInfo {
pub fn new(pty: &Pty) -> PtyProcessInfo {
let process_refresh_kind = ProcessRefreshKind::nothing()
.with_cmd(UpdateKind::Always)
.with_cwd(UpdateKind::Always)
.with_exe(UpdateKind::Always);
let refresh_kind = RefreshKind::nothing().with_processes(process_refresh_kind);
let system = System::new_with_specifics(refresh_kind);
PtyProcessInfo {
system,
refresh_kind: process_refresh_kind,
pid_getter: ProcessIdGetter::new(pty),
current: None,
}
}
pub fn pid_getter(&self) -> &ProcessIdGetter {
&self.pid_getter
}
fn refresh(&mut self) -> Option<&Process> {
let pid = self.pid_getter.pid()?;
if self.system.refresh_processes_specifics(
sysinfo::ProcessesToUpdate::Some(&[pid]),
true,
self.refresh_kind,
) == 1
{
self.system.process(pid)
} else {
None
}
}
fn get_child(&self) -> Option<&Process> {
let pid = self.pid_getter.fallback_pid();
self.system.process(pid)
}
pub(crate) fn kill_current_process(&mut self) -> bool {
self.refresh().is_some_and(|process| process.kill())
}
pub(crate) fn kill_child_process(&mut self) -> bool {
self.get_child().is_some_and(|process| process.kill())
}
fn load(&mut self) -> Option<ProcessInfo> {
let process = self.refresh()?;
let cwd = process.cwd().map_or(PathBuf::new(), |p| p.to_owned());
let info = ProcessInfo {
name: process.name().to_str()?.to_owned(),
cwd,
argv: process
.cmd()
.iter()
.filter_map(|s| s.to_str().map(ToOwned::to_owned))
.collect(),
};
self.current = Some(info.clone());
Some(info)
}
/// Updates the cached process info, returns whether the Zed-relevant info has changed
pub fn has_changed(&mut self) -> bool {
let current = self.load();
let has_changed = match (self.current.as_ref(), current.as_ref()) {
(None, None) => false,
(Some(prev), Some(now)) => prev.cwd != now.cwd || prev.name != now.name,
_ => true,
};
if has_changed {
self.current = current;
}
has_changed
}
pub fn pid(&self) -> Option<Pid> {
self.pid_getter.pid()
}
}