1use std::ffi::OsStr;
7use std::future::Future;
8use std::io;
9use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd, RawFd};
10use std::pin::Pin;
11use std::process::ExitStatus as StdExitStatus;
12use std::sync::Arc;
13use std::sync::atomic::{AtomicBool, Ordering};
14
15use rustix::process::{Pid, Signal, WaitStatus, kill_process};
16use tokio::process::Child as TokioChild;
17use tokio::sync::Mutex;
18
19use crate::config::{PtyConfig, PtySignal};
20use crate::error::{PtyError, Result};
21use crate::traits::{ExitStatus, PtyChild};
22
23pub struct UnixPtyChild {
28 child: Arc<Mutex<Option<TokioChild>>>,
30 pid: u32,
32 running: Arc<AtomicBool>,
34 exit_status: Arc<Mutex<Option<ExitStatus>>>,
36}
37
38impl std::fmt::Debug for UnixPtyChild {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 f.debug_struct("UnixPtyChild")
41 .field("pid", &self.pid)
42 .field("running", &self.running.load(Ordering::SeqCst))
43 .finish()
44 }
45}
46
47impl UnixPtyChild {
48 #[must_use]
50 pub fn new(child: TokioChild) -> Self {
51 let pid = child.id().expect("child should have pid");
52 Self {
53 child: Arc::new(Mutex::new(Some(child))),
54 pid,
55 running: Arc::new(AtomicBool::new(true)),
56 exit_status: Arc::new(Mutex::new(None)),
57 }
58 }
59
60 #[must_use]
62 pub fn from_pid(pid: u32) -> Self {
63 Self {
64 child: Arc::new(Mutex::new(None)),
65 pid,
66 running: Arc::new(AtomicBool::new(true)),
67 exit_status: Arc::new(Mutex::new(None)),
68 }
69 }
70
71 #[must_use]
73 pub const fn pid(&self) -> u32 {
74 self.pid
75 }
76
77 #[must_use]
79 pub fn is_running(&self) -> bool {
80 self.running.load(Ordering::SeqCst)
81 }
82
83 pub async fn wait(&mut self) -> Result<ExitStatus> {
85 {
87 let status = self.exit_status.lock().await;
88 if let Some(s) = *status {
89 return Ok(s);
90 }
91 }
92
93 let mut child_guard = self.child.lock().await;
95 if let Some(ref mut child) = *child_guard {
96 let status = child.wait().await.map_err(PtyError::Wait)?;
97 let exit_status = convert_exit_status(status);
98
99 self.running.store(false, Ordering::SeqCst);
100 *self.exit_status.lock().await = Some(exit_status);
101
102 return Ok(exit_status);
103 }
104
105 drop(child_guard);
107 self.wait_pid().await
108 }
109
110 async fn wait_pid(&self) -> Result<ExitStatus> {
112 use rustix::process::{WaitOptions, waitpid};
113
114 let pid = Pid::from_raw(self.pid as i32).ok_or_else(|| {
115 PtyError::Wait(io::Error::new(io::ErrorKind::InvalidInput, "invalid pid"))
116 })?;
117
118 let result = tokio::task::spawn_blocking(move || waitpid(Some(pid), WaitOptions::empty()))
120 .await
121 .map_err(|e| PtyError::Wait(io::Error::other(e)))?;
122
123 match result {
124 Ok(Some((_pid, wait_status))) => {
125 let exit_status = convert_wait_status(wait_status);
126
127 self.running.store(false, Ordering::SeqCst);
128 *self.exit_status.lock().await = Some(exit_status);
129
130 Ok(exit_status)
131 }
132 Ok(None) => {
133 Err(PtyError::Wait(io::Error::new(
135 io::ErrorKind::WouldBlock,
136 "process still running",
137 )))
138 }
139 Err(e) => Err(PtyError::Wait(io::Error::from_raw_os_error(
140 e.raw_os_error(),
141 ))),
142 }
143 }
144
145 pub fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
147 use rustix::process::{WaitOptions, waitpid};
148
149 if let Ok(guard) = self.exit_status.try_lock()
151 && let Some(s) = *guard
152 {
153 return Ok(Some(s));
154 }
155
156 match self.child.try_lock() {
162 Ok(mut child_guard) => {
163 if let Some(ref mut child) = *child_guard {
164 return match child.try_wait().map_err(PtyError::Wait)? {
165 Some(status) => {
166 let exit_status = convert_exit_status(status);
167 self.running.store(false, Ordering::SeqCst);
168 if let Ok(mut guard) = self.exit_status.try_lock() {
169 *guard = Some(exit_status);
170 }
171 Ok(Some(exit_status))
172 }
173 None => Ok(None),
174 };
175 }
176 }
179 Err(_) => {
180 return Ok(None);
184 }
185 }
186
187 let pid = Pid::from_raw(self.pid as i32).ok_or_else(|| {
188 PtyError::Wait(io::Error::new(io::ErrorKind::InvalidInput, "invalid pid"))
189 })?;
190
191 match waitpid(Some(pid), WaitOptions::NOHANG) {
192 Ok(Some((_pid, wait_status))) => {
193 let exit_status = convert_wait_status(wait_status);
194
195 self.running.store(false, Ordering::SeqCst);
196 if let Ok(mut guard) = self.exit_status.try_lock() {
197 *guard = Some(exit_status);
198 }
199
200 Ok(Some(exit_status))
201 }
202 Ok(None) => Ok(None), Err(e) => Err(PtyError::Wait(io::Error::from_raw_os_error(
204 e.raw_os_error(),
205 ))),
206 }
207 }
208
209 pub fn signal(&self, signal: PtySignal) -> Result<()> {
211 if !self.is_running() {
212 return Err(PtyError::ProcessExited(0));
213 }
214
215 let sig_num = signal.as_unix_signal().ok_or_else(|| {
216 PtyError::Signal(io::Error::new(
217 io::ErrorKind::Unsupported,
218 "unsupported signal",
219 ))
220 })?;
221
222 let pid = Pid::from_raw(self.pid as i32).ok_or_else(|| {
223 PtyError::Signal(io::Error::new(io::ErrorKind::InvalidInput, "invalid pid"))
224 })?;
225
226 let signal = Signal::from_named_raw(sig_num).ok_or_else(|| {
227 PtyError::Signal(io::Error::new(
228 io::ErrorKind::InvalidInput,
229 "invalid signal",
230 ))
231 })?;
232
233 kill_process(pid, signal)
234 .map_err(|e| PtyError::Signal(io::Error::from_raw_os_error(e.raw_os_error())))
235 }
236
237 pub fn kill(&mut self) -> Result<()> {
239 self.signal(PtySignal::Kill)
240 }
241}
242
243impl PtyChild for UnixPtyChild {
244 fn pid(&self) -> u32 {
245 Self::pid(self)
246 }
247
248 fn is_running(&self) -> bool {
249 Self::is_running(self)
250 }
251
252 fn wait(&mut self) -> Pin<Box<dyn Future<Output = Result<ExitStatus>> + Send + '_>> {
253 Box::pin(Self::wait(self))
254 }
255
256 fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
257 Self::try_wait(self)
258 }
259
260 fn signal(&self, signal: PtySignal) -> Result<()> {
261 Self::signal(self, signal)
262 }
263
264 fn kill(&mut self) -> Result<()> {
265 Self::kill(self)
266 }
267}
268
269fn convert_wait_status(status: WaitStatus) -> ExitStatus {
271 if status.exited() {
272 let code = status.exit_status().unwrap_or(0);
274 ExitStatus::Exited(code)
275 } else if status.signaled() {
276 let signal = status.terminating_signal().unwrap_or(0);
278 ExitStatus::Signaled(signal)
279 } else {
280 ExitStatus::Exited(-1)
282 }
283}
284
285fn convert_exit_status(status: StdExitStatus) -> ExitStatus {
287 #[cfg(unix)]
288 {
289 use std::os::unix::process::ExitStatusExt;
290 if let Some(code) = status.code() {
291 ExitStatus::Exited(code)
292 } else if let Some(signal) = status.signal() {
293 ExitStatus::Signaled(signal)
294 } else {
295 ExitStatus::Exited(-1)
296 }
297 }
298
299 #[cfg(not(unix))]
300 {
301 ExitStatus::Exited(status.code().unwrap_or(-1))
302 }
303}
304
305#[allow(unsafe_code)]
310pub async fn spawn_child<S, I>(
311 slave_fd: OwnedFd,
312 program: S,
313 args: I,
314 config: &PtyConfig,
315) -> Result<UnixPtyChild>
316where
317 S: AsRef<OsStr>,
318 I: IntoIterator,
319 I::Item: AsRef<OsStr>,
320{
321 use std::process::Stdio;
322
323 use tokio::process::Command;
324
325 let slave_raw = slave_fd.as_raw_fd();
327
328 let env = config.effective_env();
330
331 let mut cmd = Command::new(program.as_ref());
333 cmd.args(args);
334 cmd.env_clear();
335 cmd.envs(env);
336
337 if let Some(ref dir) = config.working_directory {
338 cmd.current_dir(dir);
339 }
340
341 let stdin_fd = dup_slave(slave_raw)?;
345 let stdout_fd = match dup_slave(slave_raw) {
346 Ok(fd) => fd,
347 Err(e) => {
348 unsafe { libc::close(stdin_fd) };
350 return Err(e);
351 }
352 };
353 let stderr_fd = match dup_slave(slave_raw) {
354 Ok(fd) => fd,
355 Err(e) => {
356 unsafe {
358 libc::close(stdin_fd);
359 libc::close(stdout_fd);
360 }
361 return Err(e);
362 }
363 };
364
365 unsafe {
368 cmd.stdin(Stdio::from_raw_fd(stdin_fd));
369 cmd.stdout(Stdio::from_raw_fd(stdout_fd));
370 cmd.stderr(Stdio::from_raw_fd(stderr_fd));
371 }
372
373 if config.new_session && !config.controlling_terminal {
384 cmd.process_group(0);
385 }
386
387 #[cfg(unix)]
389 if config.controlling_terminal {
390 unsafe {
392 cmd.pre_exec(move || {
393 if libc::setsid() == -1 {
395 return Err(io::Error::last_os_error());
396 }
397
398 if libc::ioctl(slave_raw, libc::c_ulong::from(libc::TIOCSCTTY), 0) == -1 {
401 return Err(io::Error::last_os_error());
402 }
403
404 Ok(())
405 });
406 }
407 }
408
409 let child = cmd.spawn().map_err(PtyError::Spawn)?;
410
411 Ok(UnixPtyChild::new(child))
412}
413
414#[allow(unsafe_code)]
417fn dup_slave(slave_raw: RawFd) -> Result<RawFd> {
418 let fd = unsafe { libc::dup(slave_raw) };
420 if fd == -1 {
421 return Err(PtyError::Spawn(io::Error::last_os_error()));
422 }
423 Ok(fd)
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 #[tokio::test]
431 async fn child_from_pid() {
432 let child = UnixPtyChild::from_pid(1234);
433 assert_eq!(child.pid(), 1234);
434 assert!(child.is_running());
435 }
436}