use std::future::Future;
use std::pin::Pin;
use tokio::io::{AsyncRead, AsyncWrite};
use crate::config::{PtyConfig, PtySignal, WindowSize};
use crate::error::Result;
pub trait PtyMaster: AsyncRead + AsyncWrite + Send + Sync + Unpin {
fn resize(&self, size: WindowSize) -> Result<()>;
fn window_size(&self) -> Result<WindowSize>;
fn close(&mut self) -> Result<()>;
fn is_open(&self) -> bool;
#[cfg(unix)]
fn as_raw_fd(&self) -> std::os::unix::io::RawFd;
#[cfg(windows)]
fn as_raw_handle(&self) -> std::os::windows::io::RawHandle;
}
pub trait PtyChild: Send + Sync {
fn pid(&self) -> u32;
fn is_running(&self) -> bool;
fn wait(&mut self) -> Pin<Box<dyn Future<Output = Result<ExitStatus>> + Send + '_>>;
fn try_wait(&mut self) -> Result<Option<ExitStatus>>;
fn signal(&self, signal: PtySignal) -> Result<()>;
fn kill(&mut self) -> Result<()>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExitStatus {
Exited(i32),
#[cfg(unix)]
Signaled(i32),
#[cfg(windows)]
Terminated(u32),
}
impl ExitStatus {
#[must_use]
pub const fn success(&self) -> bool {
matches!(self, Self::Exited(0))
}
#[must_use]
pub const fn code(&self) -> Option<i32> {
match self {
Self::Exited(code) => Some(*code),
#[cfg(unix)]
Self::Signaled(_) => None,
#[cfg(windows)]
Self::Terminated(code) => Some(*code as i32),
}
}
#[cfg(unix)]
#[must_use]
pub const fn signal(&self) -> Option<i32> {
match self {
Self::Signaled(sig) => Some(*sig),
Self::Exited(_) => None,
}
}
}
impl std::fmt::Display for ExitStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Exited(code) => write!(f, "exited with code {code}"),
#[cfg(unix)]
Self::Signaled(sig) => write!(f, "terminated by signal {sig}"),
#[cfg(windows)]
Self::Terminated(code) => write!(f, "terminated with code {code}"),
}
}
}
pub trait PtySystem: Send + Sync {
type Master: PtyMaster;
type Child: PtyChild;
fn spawn<S, I>(
program: S,
args: I,
config: &PtyConfig,
) -> impl Future<Output = Result<(Self::Master, Self::Child)>> + Send
where
S: AsRef<std::ffi::OsStr> + Send,
I: IntoIterator + Send,
I::Item: AsRef<std::ffi::OsStr>;
#[must_use]
fn spawn_shell(
config: &PtyConfig,
) -> impl Future<Output = Result<(Self::Master, Self::Child)>> + Send {
async move {
#[cfg(unix)]
let shell =
std::env::var_os("SHELL").unwrap_or_else(|| std::ffi::OsString::from("/bin/sh"));
#[cfg(windows)]
let shell = std::ffi::OsString::from("cmd.exe");
Self::spawn(&shell, std::iter::empty::<&str>(), config).await
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exit_status_success() {
let status = ExitStatus::Exited(0);
assert!(status.success());
assert_eq!(status.code(), Some(0));
}
#[test]
fn exit_status_failure() {
let status = ExitStatus::Exited(1);
assert!(!status.success());
assert_eq!(status.code(), Some(1));
}
#[cfg(unix)]
#[test]
fn exit_status_signaled() {
let status = ExitStatus::Signaled(9);
assert!(!status.success());
assert_eq!(status.code(), None);
assert_eq!(status.signal(), Some(9));
}
}