pty_info.rs

  1use alacritty_terminal::tty::Pty;
  2use gpui::{Context, Task};
  3use parking_lot::{MappedRwLockReadGuard, Mutex, RwLock, RwLockReadGuard};
  4#[cfg(target_os = "windows")]
  5use std::num::NonZeroU32;
  6#[cfg(unix)]
  7use std::os::fd::AsRawFd;
  8use std::{path::PathBuf, sync::Arc};
  9
 10#[cfg(target_os = "windows")]
 11use windows::Win32::{Foundation::HANDLE, System::Threading::GetProcessId};
 12
 13use sysinfo::{Pid, Process, ProcessRefreshKind, RefreshKind, System, UpdateKind};
 14
 15use crate::{Event, Terminal};
 16
 17#[derive(Clone, Copy)]
 18pub struct ProcessIdGetter {
 19    handle: i32,
 20    fallback_pid: u32,
 21}
 22
 23impl ProcessIdGetter {
 24    pub fn fallback_pid(&self) -> Pid {
 25        Pid::from_u32(self.fallback_pid)
 26    }
 27}
 28
 29#[cfg(unix)]
 30impl ProcessIdGetter {
 31    fn new(pty: &Pty) -> ProcessIdGetter {
 32        ProcessIdGetter {
 33            handle: pty.file().as_raw_fd(),
 34            fallback_pid: pty.child().id(),
 35        }
 36    }
 37
 38    fn pid(&self) -> Option<Pid> {
 39        // Negative pid means error.
 40        // Zero pid means no foreground process group is set on the PTY yet.
 41        // Avoid killing the current process by returning a zero pid.
 42        let pid = unsafe { libc::tcgetpgrp(self.handle) };
 43        if pid > 0 {
 44            return Some(Pid::from_u32(pid as u32));
 45        }
 46
 47        if self.fallback_pid > 0 {
 48            return Some(Pid::from_u32(self.fallback_pid));
 49        }
 50
 51        None
 52    }
 53}
 54
 55#[cfg(windows)]
 56impl ProcessIdGetter {
 57    fn new(pty: &Pty) -> ProcessIdGetter {
 58        let child = pty.child_watcher();
 59        let handle = child.raw_handle();
 60        let fallback_pid = child.pid().unwrap_or_else(|| unsafe {
 61            NonZeroU32::new_unchecked(GetProcessId(HANDLE(handle as _)))
 62        });
 63
 64        ProcessIdGetter {
 65            handle: handle as i32,
 66            fallback_pid: u32::from(fallback_pid),
 67        }
 68    }
 69
 70    fn pid(&self) -> Option<Pid> {
 71        let pid = unsafe { GetProcessId(HANDLE(self.handle as _)) };
 72        // the GetProcessId may fail and returns zero, which will lead to a stack overflow issue
 73        if pid == 0 {
 74            // in the builder process, there is a small chance, almost negligible,
 75            // that this value could be zero, which means child_watcher returns None,
 76            // GetProcessId returns 0.
 77            if self.fallback_pid == 0 {
 78                return None;
 79            }
 80            return Some(Pid::from_u32(self.fallback_pid));
 81        }
 82        Some(Pid::from_u32(pid))
 83    }
 84}
 85
 86#[derive(Clone, Debug)]
 87pub struct ProcessInfo {
 88    pub name: String,
 89    pub cwd: PathBuf,
 90    pub argv: Vec<String>,
 91}
 92
 93/// Fetches Zed-relevant Pseudo-Terminal (PTY) process information
 94pub struct PtyProcessInfo {
 95    system: RwLock<System>,
 96    refresh_kind: ProcessRefreshKind,
 97    pid_getter: ProcessIdGetter,
 98    pub current: RwLock<Option<ProcessInfo>>,
 99    task: Mutex<Option<Task<()>>>,
100}
101
102impl PtyProcessInfo {
103    pub fn new(pty: &Pty) -> PtyProcessInfo {
104        let process_refresh_kind = ProcessRefreshKind::nothing()
105            .with_cmd(UpdateKind::Always)
106            .with_cwd(UpdateKind::Always)
107            .with_exe(UpdateKind::Always);
108        let refresh_kind = RefreshKind::nothing().with_processes(process_refresh_kind);
109        let system = System::new_with_specifics(refresh_kind);
110
111        PtyProcessInfo {
112            system: RwLock::new(system),
113            refresh_kind: process_refresh_kind,
114            pid_getter: ProcessIdGetter::new(pty),
115            current: RwLock::new(None),
116            task: Mutex::new(None),
117        }
118    }
119
120    pub fn pid_getter(&self) -> &ProcessIdGetter {
121        &self.pid_getter
122    }
123
124    fn refresh(&self) -> Option<MappedRwLockReadGuard<'_, Process>> {
125        let pid = self.pid_getter.pid()?;
126        if self.system.write().refresh_processes_specifics(
127            sysinfo::ProcessesToUpdate::Some(&[pid]),
128            true,
129            self.refresh_kind,
130        ) == 1
131        {
132            RwLockReadGuard::try_map(self.system.read(), |system| system.process(pid)).ok()
133        } else {
134            None
135        }
136    }
137
138    fn get_child(&self) -> Option<MappedRwLockReadGuard<'_, Process>> {
139        let pid = self.pid_getter.fallback_pid();
140        RwLockReadGuard::try_map(self.system.read(), |system| system.process(pid)).ok()
141    }
142
143    #[cfg(unix)]
144    pub(crate) fn kill_current_process(&self) -> bool {
145        let Some(pid) = self.pid_getter.pid() else {
146            return false;
147        };
148        unsafe { libc::killpg(pid.as_u32() as i32, libc::SIGKILL) == 0 }
149    }
150
151    #[cfg(not(unix))]
152    pub(crate) fn kill_current_process(&self) -> bool {
153        self.refresh().is_some_and(|process| process.kill())
154    }
155
156    pub(crate) fn kill_child_process(&self) -> bool {
157        self.get_child().is_some_and(|process| process.kill())
158    }
159
160    fn load(&self) -> Option<ProcessInfo> {
161        let process = self.refresh()?;
162        let cwd = process.cwd().map_or(PathBuf::new(), |p| p.to_owned());
163
164        let info = ProcessInfo {
165            name: process.name().to_str()?.to_owned(),
166            cwd,
167            argv: process
168                .cmd()
169                .iter()
170                .filter_map(|s| s.to_str().map(ToOwned::to_owned))
171                .collect(),
172        };
173        *self.current.write() = Some(info.clone());
174        Some(info)
175    }
176
177    /// Updates the cached process info, emitting a [`Event::TitleChanged`] event if the Zed-relevant info has changed
178    pub fn emit_title_changed_if_changed(self: &Arc<Self>, cx: &mut Context<'_, Terminal>) {
179        if self.task.lock().is_some() {
180            return;
181        }
182        let this = self.clone();
183        let has_changed = cx.background_executor().spawn(async move {
184            let current = this.load();
185            let has_changed = match (this.current.read().as_ref(), current.as_ref()) {
186                (None, None) => false,
187                (Some(prev), Some(now)) => prev.cwd != now.cwd || prev.name != now.name,
188                _ => true,
189            };
190            if has_changed {
191                *this.current.write() = current;
192            }
193            has_changed
194        });
195        let this = Arc::downgrade(self);
196        *self.task.lock() = Some(cx.spawn(async move |term, cx| {
197            if has_changed.await {
198                term.update(cx, |_, cx| cx.emit(Event::TitleChanged)).ok();
199            }
200            if let Some(this) = this.upgrade() {
201                this.task.lock().take();
202            }
203        }));
204    }
205
206    pub fn pid(&self) -> Option<Pid> {
207        self.pid_getter.pid()
208    }
209}