use anyhow::{Context as _, Error};
use fidl::endpoints::ServerEnd;
use fidl_fuchsia_hardware_pty::{DeviceMarker, DeviceProxy, WindowSize};
use fuchsia_component::client::connect_to_protocol;
use fuchsia_trace as ftrace;
use std::ffi::CStr;
use std::fs::File;
use std::os::fd::OwnedFd;
use zx::{self as zx, HandleBased as _, ProcessInfo, ProcessInfoFlags};
#[derive(Clone)]
pub struct ServerPty {
proxy: DeviceProxy,
}
pub struct ShellProcess {
pub pty: ServerPty,
process: zx::Process,
}
impl ServerPty {
pub fn new() -> Result<Self, Error> {
ftrace::duration!(c"pty", c"Pty:new");
let proxy =
connect_to_protocol::<DeviceMarker>().context("could not connect to pty service")?;
Ok(Self { proxy })
}
pub async fn spawn(
self,
command: Option<&CStr>,
environ: Option<&[&CStr]>,
) -> Result<ShellProcess, Error> {
let command = command.unwrap_or(&c"/boot/bin/sh");
self.spawn_with_argv(command, &[command], environ).await
}
pub async fn spawn_with_argv(
self,
command: &CStr,
argv: &[&CStr],
environ: Option<&[&CStr]>,
) -> Result<ShellProcess, Error> {
ftrace::duration!(c"pty", c"Pty:spawn");
let client_pty = self.open_client_pty().await.context("unable to create client_pty")?;
let process = match fdio::spawn_etc(
&zx::Job::from_handle(zx::Handle::invalid()),
fdio::SpawnOptions::CLONE_ALL - fdio::SpawnOptions::CLONE_STDIO,
command,
argv,
environ,
&mut [fdio::SpawnAction::transfer_fd(client_pty, fdio::SpawnAction::USE_FOR_STDIO)],
) {
Ok(process) => process,
Err((status, reason)) => {
return Err(status).context(format!("failed to spawn shell: {}", reason));
}
};
Ok(ShellProcess { pty: self, process })
}
pub fn try_clone_fd(&self) -> Result<File, Error> {
use std::os::fd::AsRawFd as _;
let Self { proxy } = self;
let (client_end, server_end) = fidl::endpoints::create_endpoints();
let () = proxy.clone2(server_end)?;
let file: File = fdio::create_fd(client_end.into())
.context("failed to create FD from server PTY")?
.into();
let fd = file.as_raw_fd();
let previous = {
let res = unsafe { libc::fcntl(fd, libc::F_GETFL) };
if res == -1 {
Err(std::io::Error::last_os_error()).context("failed to get file status flags")
} else {
Ok(res)
}
}?;
let new = previous | libc::O_NONBLOCK;
if new != previous {
let res = unsafe { libc::fcntl(fd, libc::F_SETFL, new) };
let () = if res == -1 {
Err(std::io::Error::last_os_error()).context("failed to set file status flags")
} else {
Ok(())
}?;
}
Ok(file)
}
pub async fn resize(&self, window_size: WindowSize) -> Result<(), Error> {
ftrace::duration!(c"pty", c"Pty:resize");
let Self { proxy } = self;
let () = proxy
.set_window_size(&window_size)
.await
.map(zx::Status::ok)
.context("unable to call resize window")?
.context("failed to resize window")?;
Ok(())
}
async fn open_client_pty(&self) -> Result<OwnedFd, Error> {
ftrace::duration!(c"pty", c"Pty:open_client_pty");
let (client_end, server_end) = fidl::endpoints::create_endpoints();
let () = self.open_client(server_end).await.context("failed to open client")?;
let fd =
fdio::create_fd(client_end.into()).context("failed to create FD from client PTY")?;
Ok(fd)
}
pub async fn open_client(&self, server_end: ServerEnd<DeviceMarker>) -> Result<(), Error> {
let Self { proxy } = self;
ftrace::duration!(c"pty", c"Pty:open_client");
let () = proxy
.open_client(0, server_end)
.await
.map(zx::Status::ok)
.context("failed to interact with PTY device")?
.context("failed to attach PTY to channel")?;
Ok(())
}
}
impl ShellProcess {
pub fn process_info(&self) -> Result<ProcessInfo, Error> {
let Self { pty: _, process } = self;
process.info().context("failed to get process info")
}
pub fn is_running(&self) -> bool {
self.process_info()
.map(|info| {
let flags = ProcessInfoFlags::from_bits(info.flags).unwrap();
flags.contains(zx::ProcessInfoFlags::STARTED)
&& !flags.contains(ProcessInfoFlags::EXITED)
})
.unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use cstr::cstr;
use fuchsia_async as fasync;
use futures::io::AsyncWriteExt as _;
use std::os::unix::io::AsRawFd as _;
use zx::AsHandleRef as _;
#[fasync::run_singlethreaded(test)]
async fn can_create_pty() -> Result<(), Error> {
let _ = ServerPty::new()?;
Ok(())
}
#[fasync::run_singlethreaded(test)]
async fn can_open_client_pty() -> Result<(), Error> {
let server_pty = ServerPty::new()?;
let client_pty = server_pty.open_client_pty().await?;
assert!(client_pty.as_raw_fd() > 0);
Ok(())
}
#[fasync::run_singlethreaded(test)]
async fn can_spawn_shell_process() -> Result<(), Error> {
let server_pty = ServerPty::new()?;
let cmd = cstr!("/pkg/bin/sh");
let process = server_pty.spawn_with_argv(&cmd, &[cmd], None).await?;
let mut started = false;
if let Ok(info) = process.process_info() {
started = ProcessInfoFlags::from_bits(info.flags)
.unwrap()
.contains(zx::ProcessInfoFlags::STARTED);
}
assert_eq!(started, true);
Ok(())
}
#[fasync::run_singlethreaded(test)]
async fn shell_process_is_spawned() -> Result<(), Error> {
let process = spawn_pty().await?;
let info = process.process_info().unwrap();
assert!(ProcessInfoFlags::from_bits(info.flags)
.unwrap()
.contains(zx::ProcessInfoFlags::STARTED));
Ok(())
}
#[fasync::run_singlethreaded(test)]
async fn spawned_shell_process_is_running() -> Result<(), Error> {
let process = spawn_pty().await?;
assert!(process.is_running());
Ok(())
}
#[fasync::run_singlethreaded(test)]
async fn exited_shell_process_is_not_running() -> Result<(), Error> {
let window_size = WindowSize { width: 300 as u32, height: 300 as u32 };
let pty = ServerPty::new().unwrap();
let process = pty
.spawn_with_argv(&cstr!("/pkg/bin/exit_with_code_util"), &[cstr!("42")], None)
.await?;
let () = process.pty.resize(window_size).await?;
process
.process
.wait_handle(
zx::Signals::PROCESS_TERMINATED,
zx::MonotonicInstant::after(zx::MonotonicDuration::from_seconds(60)),
)
.expect("shell process did not exit in time");
assert!(!process.is_running());
Ok(())
}
#[fasync::run_singlethreaded(test)]
async fn can_write_to_shell() -> Result<(), Error> {
let process = spawn_pty().await?;
let mut evented_fd = unsafe { fasync::net::EventedFd::new(process.pty.try_clone_fd()?)? };
evented_fd.write_all("a".as_bytes()).await?;
Ok(())
}
#[ignore] #[fasync::run_singlethreaded(test)]
async fn shell_process_is_not_running_after_writing_exit() -> Result<(), Error> {
let process = spawn_pty().await?;
let mut evented_fd = unsafe { fasync::net::EventedFd::new(process.pty.try_clone_fd()?)? };
evented_fd.write_all("exit\n".as_bytes()).await?;
process
.process
.wait_handle(
zx::Signals::PROCESS_TERMINATED,
zx::MonotonicInstant::after(zx::MonotonicDuration::from_seconds(60)),
)
.expect("shell process did not exit in time");
assert!(!process.is_running());
Ok(())
}
#[fasync::run_singlethreaded(test)]
async fn can_resize_window() -> Result<(), Error> {
let process = spawn_pty().await?;
let () = process.pty.resize(WindowSize { width: 400, height: 400 }).await?;
Ok(())
}
async fn spawn_pty() -> Result<ShellProcess, Error> {
let window_size = WindowSize { width: 300 as u32, height: 300 as u32 };
let pty = ServerPty::new()?;
let process = pty.spawn(Some(&c"/pkg/bin/sh"), None).await.context("failed to spawn")?;
let () = process.pty.resize(window_size).await?;
Ok(process)
}
}