1use std::ffi::OsStr;
7use std::future::Future;
8use std::io;
9use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd};
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 let pid = Pid::from_raw(self.pid as i32).ok_or_else(|| {
157 PtyError::Wait(io::Error::new(io::ErrorKind::InvalidInput, "invalid pid"))
158 })?;
159
160 match waitpid(Some(pid), WaitOptions::NOHANG) {
161 Ok(Some((_pid, wait_status))) => {
162 let exit_status = convert_wait_status(wait_status);
163
164 self.running.store(false, Ordering::SeqCst);
165 if let Ok(mut guard) = self.exit_status.try_lock() {
166 *guard = Some(exit_status);
167 }
168
169 Ok(Some(exit_status))
170 }
171 Ok(None) => Ok(None), Err(e) => Err(PtyError::Wait(io::Error::from_raw_os_error(
173 e.raw_os_error(),
174 ))),
175 }
176 }
177
178 pub fn signal(&self, signal: PtySignal) -> Result<()> {
180 if !self.is_running() {
181 return Err(PtyError::ProcessExited(0));
182 }
183
184 let sig_num = signal.as_unix_signal().ok_or_else(|| {
185 PtyError::Signal(io::Error::new(
186 io::ErrorKind::Unsupported,
187 "unsupported signal",
188 ))
189 })?;
190
191 let pid = Pid::from_raw(self.pid as i32).ok_or_else(|| {
192 PtyError::Signal(io::Error::new(io::ErrorKind::InvalidInput, "invalid pid"))
193 })?;
194
195 let signal = Signal::from_named_raw(sig_num).ok_or_else(|| {
196 PtyError::Signal(io::Error::new(
197 io::ErrorKind::InvalidInput,
198 "invalid signal",
199 ))
200 })?;
201
202 kill_process(pid, signal)
203 .map_err(|e| PtyError::Signal(io::Error::from_raw_os_error(e.raw_os_error())))
204 }
205
206 pub fn kill(&mut self) -> Result<()> {
208 self.signal(PtySignal::Kill)
209 }
210}
211
212impl PtyChild for UnixPtyChild {
213 fn pid(&self) -> u32 {
214 Self::pid(self)
215 }
216
217 fn is_running(&self) -> bool {
218 Self::is_running(self)
219 }
220
221 fn wait(&mut self) -> Pin<Box<dyn Future<Output = Result<ExitStatus>> + Send + '_>> {
222 Box::pin(Self::wait(self))
223 }
224
225 fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
226 Self::try_wait(self)
227 }
228
229 fn signal(&self, signal: PtySignal) -> Result<()> {
230 Self::signal(self, signal)
231 }
232
233 fn kill(&mut self) -> Result<()> {
234 Self::kill(self)
235 }
236}
237
238fn convert_wait_status(status: WaitStatus) -> ExitStatus {
240 if status.exited() {
241 let code = status.exit_status().unwrap_or(0);
243 ExitStatus::Exited(code)
244 } else if status.signaled() {
245 let signal = status.terminating_signal().unwrap_or(0);
247 ExitStatus::Signaled(signal)
248 } else {
249 ExitStatus::Exited(-1)
251 }
252}
253
254fn convert_exit_status(status: StdExitStatus) -> ExitStatus {
256 #[cfg(unix)]
257 {
258 use std::os::unix::process::ExitStatusExt;
259 if let Some(code) = status.code() {
260 ExitStatus::Exited(code)
261 } else if let Some(signal) = status.signal() {
262 ExitStatus::Signaled(signal)
263 } else {
264 ExitStatus::Exited(-1)
265 }
266 }
267
268 #[cfg(not(unix))]
269 {
270 ExitStatus::Exited(status.code().unwrap_or(-1))
271 }
272}
273
274#[allow(unsafe_code)]
279pub async fn spawn_child<S, I>(
280 slave_fd: OwnedFd,
281 program: S,
282 args: I,
283 config: &PtyConfig,
284) -> Result<UnixPtyChild>
285where
286 S: AsRef<OsStr>,
287 I: IntoIterator,
288 I::Item: AsRef<OsStr>,
289{
290 use std::process::Stdio;
291
292 use tokio::process::Command;
293
294 let slave_raw = slave_fd.as_raw_fd();
296
297 let env = config.effective_env();
299
300 let mut cmd = Command::new(program.as_ref());
302 cmd.args(args);
303 cmd.env_clear();
304 cmd.envs(env);
305
306 if let Some(ref dir) = config.working_directory {
307 cmd.current_dir(dir);
308 }
309
310 unsafe {
313 cmd.stdin(Stdio::from_raw_fd(libc::dup(slave_raw)));
314 cmd.stdout(Stdio::from_raw_fd(libc::dup(slave_raw)));
315 cmd.stderr(Stdio::from_raw_fd(libc::dup(slave_raw)));
316 }
317
318 if config.new_session {
320 cmd.process_group(0);
321 }
322
323 #[cfg(unix)]
325 if config.controlling_terminal {
326 unsafe {
328 cmd.pre_exec(move || {
329 if libc::setsid() == -1 {
331 return Err(io::Error::last_os_error());
332 }
333
334 if libc::ioctl(slave_raw, libc::c_ulong::from(libc::TIOCSCTTY), 0) == -1 {
337 return Err(io::Error::last_os_error());
338 }
339
340 Ok(())
341 });
342 }
343 }
344
345 let child = cmd.spawn().map_err(PtyError::Spawn)?;
346
347 Ok(UnixPtyChild::new(child))
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[tokio::test]
355 async fn child_from_pid() {
356 let child = UnixPtyChild::from_pid(1234);
357 assert_eq!(child.pid(), 1234);
358 assert!(child.is_running());
359 }
360}