use std::ffi::OsStr;
use std::future::Future;
use std::io;
use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd};
use std::pin::Pin;
use std::process::ExitStatus as StdExitStatus;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use rustix::process::{Pid, Signal, WaitStatus, kill_process};
use tokio::process::Child as TokioChild;
use tokio::sync::Mutex;
use crate::config::{PtyConfig, PtySignal};
use crate::error::{PtyError, Result};
use crate::traits::{ExitStatus, PtyChild};
pub struct UnixPtyChild {
child: Arc<Mutex<Option<TokioChild>>>,
pid: u32,
running: Arc<AtomicBool>,
exit_status: Arc<Mutex<Option<ExitStatus>>>,
}
impl std::fmt::Debug for UnixPtyChild {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UnixPtyChild")
.field("pid", &self.pid)
.field("running", &self.running.load(Ordering::SeqCst))
.finish()
}
}
impl UnixPtyChild {
#[must_use]
pub fn new(child: TokioChild) -> Self {
let pid = child.id().expect("child should have pid");
Self {
child: Arc::new(Mutex::new(Some(child))),
pid,
running: Arc::new(AtomicBool::new(true)),
exit_status: Arc::new(Mutex::new(None)),
}
}
#[must_use]
pub fn from_pid(pid: u32) -> Self {
Self {
child: Arc::new(Mutex::new(None)),
pid,
running: Arc::new(AtomicBool::new(true)),
exit_status: Arc::new(Mutex::new(None)),
}
}
#[must_use]
pub const fn pid(&self) -> u32 {
self.pid
}
#[must_use]
pub fn is_running(&self) -> bool {
self.running.load(Ordering::SeqCst)
}
pub async fn wait(&mut self) -> Result<ExitStatus> {
{
let status = self.exit_status.lock().await;
if let Some(s) = *status {
return Ok(s);
}
}
let mut child_guard = self.child.lock().await;
if let Some(ref mut child) = *child_guard {
let status = child.wait().await.map_err(PtyError::Wait)?;
let exit_status = convert_exit_status(status);
self.running.store(false, Ordering::SeqCst);
*self.exit_status.lock().await = Some(exit_status);
return Ok(exit_status);
}
drop(child_guard);
self.wait_pid().await
}
async fn wait_pid(&self) -> Result<ExitStatus> {
use rustix::process::{WaitOptions, waitpid};
let pid = Pid::from_raw(self.pid as i32).ok_or_else(|| {
PtyError::Wait(io::Error::new(io::ErrorKind::InvalidInput, "invalid pid"))
})?;
let result = tokio::task::spawn_blocking(move || waitpid(Some(pid), WaitOptions::empty()))
.await
.map_err(|e| PtyError::Wait(io::Error::other(e)))?;
match result {
Ok(Some((_pid, wait_status))) => {
let exit_status = convert_wait_status(wait_status);
self.running.store(false, Ordering::SeqCst);
*self.exit_status.lock().await = Some(exit_status);
Ok(exit_status)
}
Ok(None) => {
Err(PtyError::Wait(io::Error::new(
io::ErrorKind::WouldBlock,
"process still running",
)))
}
Err(e) => Err(PtyError::Wait(io::Error::from_raw_os_error(
e.raw_os_error(),
))),
}
}
pub fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
use rustix::process::{WaitOptions, waitpid};
if let Ok(guard) = self.exit_status.try_lock()
&& let Some(s) = *guard
{
return Ok(Some(s));
}
let pid = Pid::from_raw(self.pid as i32).ok_or_else(|| {
PtyError::Wait(io::Error::new(io::ErrorKind::InvalidInput, "invalid pid"))
})?;
match waitpid(Some(pid), WaitOptions::NOHANG) {
Ok(Some((_pid, wait_status))) => {
let exit_status = convert_wait_status(wait_status);
self.running.store(false, Ordering::SeqCst);
if let Ok(mut guard) = self.exit_status.try_lock() {
*guard = Some(exit_status);
}
Ok(Some(exit_status))
}
Ok(None) => Ok(None), Err(e) => Err(PtyError::Wait(io::Error::from_raw_os_error(
e.raw_os_error(),
))),
}
}
pub fn signal(&self, signal: PtySignal) -> Result<()> {
if !self.is_running() {
return Err(PtyError::ProcessExited(0));
}
let sig_num = signal.as_unix_signal().ok_or_else(|| {
PtyError::Signal(io::Error::new(
io::ErrorKind::Unsupported,
"unsupported signal",
))
})?;
let pid = Pid::from_raw(self.pid as i32).ok_or_else(|| {
PtyError::Signal(io::Error::new(io::ErrorKind::InvalidInput, "invalid pid"))
})?;
let signal = Signal::from_named_raw(sig_num).ok_or_else(|| {
PtyError::Signal(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid signal",
))
})?;
kill_process(pid, signal)
.map_err(|e| PtyError::Signal(io::Error::from_raw_os_error(e.raw_os_error())))
}
pub fn kill(&mut self) -> Result<()> {
self.signal(PtySignal::Kill)
}
}
impl PtyChild for UnixPtyChild {
fn pid(&self) -> u32 {
Self::pid(self)
}
fn is_running(&self) -> bool {
Self::is_running(self)
}
fn wait(&mut self) -> Pin<Box<dyn Future<Output = Result<ExitStatus>> + Send + '_>> {
Box::pin(Self::wait(self))
}
fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
Self::try_wait(self)
}
fn signal(&self, signal: PtySignal) -> Result<()> {
Self::signal(self, signal)
}
fn kill(&mut self) -> Result<()> {
Self::kill(self)
}
}
fn convert_wait_status(status: WaitStatus) -> ExitStatus {
if status.exited() {
let code = status.exit_status().unwrap_or(0);
ExitStatus::Exited(code)
} else if status.signaled() {
let signal = status.terminating_signal().unwrap_or(0);
ExitStatus::Signaled(signal)
} else {
ExitStatus::Exited(-1)
}
}
fn convert_exit_status(status: StdExitStatus) -> ExitStatus {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
if let Some(code) = status.code() {
ExitStatus::Exited(code)
} else if let Some(signal) = status.signal() {
ExitStatus::Signaled(signal)
} else {
ExitStatus::Exited(-1)
}
}
#[cfg(not(unix))]
{
ExitStatus::Exited(status.code().unwrap_or(-1))
}
}
#[allow(unsafe_code)]
pub async fn spawn_child<S, I>(
slave_fd: OwnedFd,
program: S,
args: I,
config: &PtyConfig,
) -> Result<UnixPtyChild>
where
S: AsRef<OsStr>,
I: IntoIterator,
I::Item: AsRef<OsStr>,
{
use std::process::Stdio;
use tokio::process::Command;
let slave_raw = slave_fd.as_raw_fd();
let env = config.effective_env();
let mut cmd = Command::new(program.as_ref());
cmd.args(args);
cmd.env_clear();
cmd.envs(env);
if let Some(ref dir) = config.working_directory {
cmd.current_dir(dir);
}
unsafe {
cmd.stdin(Stdio::from_raw_fd(libc::dup(slave_raw)));
cmd.stdout(Stdio::from_raw_fd(libc::dup(slave_raw)));
cmd.stderr(Stdio::from_raw_fd(libc::dup(slave_raw)));
}
if config.new_session {
cmd.process_group(0);
}
#[cfg(unix)]
if config.controlling_terminal {
unsafe {
cmd.pre_exec(move || {
if libc::setsid() == -1 {
return Err(io::Error::last_os_error());
}
if libc::ioctl(slave_raw, libc::c_ulong::from(libc::TIOCSCTTY), 0) == -1 {
return Err(io::Error::last_os_error());
}
Ok(())
});
}
}
let child = cmd.spawn().map_err(PtyError::Spawn)?;
Ok(UnixPtyChild::new(child))
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn child_from_pid() {
let child = UnixPtyChild::from_pid(1234);
assert_eq!(child.pid(), 1234);
assert!(child.is_running());
}
}