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}