#[cfg(feature = "unixexec")]
use std::os::fd::BorrowedFd;
use std::{
io,
os::fd::OwnedFd,
process::{Command, Stdio},
sync::Arc,
};
#[cfg(feature = "unixexec")]
use super::io::Reader;
use super::{
Runtime,
io::{PipeOps, RegisteredIo, registered},
};
#[cfg(feature = "unixexec")]
use crate::{
Message,
connection::socket::{BoxedSplit, ReadHalf, Split, WriteHalf},
fdo::ConnectionCredentials,
};
#[cfg(feature = "unixexec")]
pub(crate) fn spawn(runtime: &Runtime, command: Command) -> io::Result<Child> {
let (stdin, child_stdin) = pipe_to(runtime)?;
let (stdout, child_stdout) = pipe_from(runtime)?;
let exit = runtime.spawn_process(command, child_stdin, child_stdout, Stdio::inherit())?;
runtime
.spawn("helper process", async move {
let _ = exit.await;
})
.detach();
Ok(Child { stdin, stdout })
}
#[cfg(any(feature = "ibus", target_os = "macos"))]
pub(crate) async fn stdout(runtime: &Runtime, command: Command) -> io::Result<Vec<u8>> {
let (pipe, child_stdout) = pipe_from(runtime)?;
let exit = runtime.spawn_process(command, Stdio::null(), child_stdout, Stdio::null())?;
let mut collected = Vec::new();
let mut buffer = [0; 4096];
loop {
let (read, _) = pipe.recv(&mut buffer).await?;
if read == 0 {
break;
}
collected.extend_from_slice(&buffer[..read]);
}
drop(pipe);
let status = exit.await?;
if !status.success() {
return Err(io::Error::other(format!(
"the helper process ended with {status}"
)));
}
Ok(collected)
}
#[cfg(feature = "unixexec")]
#[derive(Debug)]
pub(crate) struct Child {
stdin: Arc<RegisteredIo<PipeOps>>,
stdout: Arc<RegisteredIo<PipeOps>>,
}
#[cfg(feature = "unixexec")]
impl Child {
pub(crate) fn into_split(self) -> BoxedSplit {
let Self { stdin, stdout } = self;
Split::new(
Box::new(Reader::new(stdout)) as Box<dyn ReadHalf>,
Box::new(Stdin { pipe: Some(stdin) }) as Box<dyn WriteHalf>,
)
}
}
#[cfg(feature = "unixexec")]
#[derive(Debug)]
struct Stdin {
pipe: Option<Arc<RegisteredIo<PipeOps>>>,
}
#[cfg(feature = "unixexec")]
#[async_trait::async_trait]
impl WriteHalf for Stdin {
async fn send_messages(&mut self, msgs: &[Message]) -> crate::Result<()> {
WriteHalf::send_messages(self.pipe()?, msgs).await
}
async fn sendmsg(&mut self, buffer: &[u8], fds: &[BorrowedFd<'_>]) -> io::Result<usize> {
WriteHalf::sendmsg(self.pipe()?, buffer, fds).await
}
#[cfg(any(target_os = "freebsd", target_os = "dragonfly"))]
async fn send_zero_byte(&mut self) -> io::Result<Option<usize>> {
WriteHalf::send_zero_byte(self.pipe()?).await
}
async fn close(&mut self) -> io::Result<()> {
self.pipe.take();
Ok(())
}
fn can_pass_unix_fd(&self) -> bool {
self.pipe.as_ref().is_some_and(WriteHalf::can_pass_unix_fd)
}
async fn peer_credentials(&mut self) -> io::Result<ConnectionCredentials> {
WriteHalf::peer_credentials(self.pipe()?).await
}
}
#[cfg(feature = "unixexec")]
impl Stdin {
fn pipe(&mut self) -> io::Result<&mut Arc<RegisteredIo<PipeOps>>> {
self.pipe.as_mut().ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotConnected,
"the standard input of the helper process is closed",
)
})
}
}
#[cfg(feature = "unixexec")]
fn pipe_to(runtime: &Runtime) -> io::Result<(Arc<RegisteredIo<PipeOps>>, Stdio)> {
let (reader, writer) = io::pipe()?;
let ours = registered(runtime, OwnedFd::from(writer), PipeOps).map(Arc::new)?;
Ok((ours, Stdio::from(reader)))
}
fn pipe_from(runtime: &Runtime) -> io::Result<(Arc<RegisteredIo<PipeOps>>, Stdio)> {
let (reader, writer) = io::pipe()?;
let ours = registered(runtime, OwnedFd::from(reader), PipeOps).map(Arc::new)?;
Ok((ours, Stdio::from(writer)))
}
#[cfg(all(test, any(feature = "ibus", target_os = "macos")))]
mod tests {
use ntest::timeout;
use super::*;
use crate::runtime::test_runtime::under_every_runtime;
#[test]
#[timeout(15000)]
fn a_helper_process_is_reaped() {
under_every_runtime(|runtime| async move {
let printed = stdout(&runtime, Command::new("true")).await.unwrap();
assert!(printed.is_empty(), "got {printed:?}");
});
}
#[test]
#[timeout(15000)]
fn a_helper_process_that_runs_on_after_its_output_ends_is_waited_for() {
under_every_runtime(|runtime| async move {
let mut command = Command::new("sh");
command.args(["-c", "exec >&-; sleep 0.2; exit 3"]);
let failed = stdout(&runtime, command).await.unwrap_err();
assert!(
failed.to_string().contains("exit status: 3"),
"got `{failed}`",
);
});
}
#[test]
#[timeout(15000)]
fn stdout_is_collected() {
under_every_runtime(|runtime| async move {
let mut command = Command::new("echo");
command.arg("hello");
let printed = stdout(&runtime, command).await.unwrap();
assert_eq!(printed, b"hello\n");
});
}
#[test]
#[timeout(15000)]
fn a_helper_process_asked_for_its_output_reads_no_input() {
under_every_runtime(|runtime| async move {
let mut command = Command::new("sh");
command.args(["-c", "cat; echo done"]);
let printed = stdout(&runtime, command).await.unwrap();
assert_eq!(printed, b"done\n");
});
}
#[test]
#[timeout(15000)]
fn a_failing_helper_process_is_an_error() {
under_every_runtime(|runtime| async move {
let mut command = Command::new("sh");
command.args(["-c", "exit 3"]);
let failed = stdout(&runtime, command).await.unwrap_err();
assert!(
failed.to_string().contains("exit status: 3"),
"got `{failed}`",
);
});
}
#[test]
#[timeout(15000)]
fn a_program_that_cannot_be_started_is_an_error() {
under_every_runtime(|runtime| async move {
let failed = stdout(&runtime, Command::new("/nonexistent/zbus-helper"))
.await
.unwrap_err();
assert_eq!(failed.kind(), io::ErrorKind::NotFound);
});
}
}
#[cfg(all(test, feature = "unixexec"))]
mod split_tests {
use ntest::timeout;
use super::*;
#[cfg(target_os = "linux")]
use crate::runtime::process_table::{is_in_the_process_table, wait_until_collected};
use crate::runtime::test_runtime::under_every_runtime;
#[cfg(target_os = "linux")]
#[test]
#[timeout(15000)]
fn a_helper_process_that_exits_is_collected_while_the_connection_holds_both_halves() {
under_every_runtime(|runtime| async move {
let (pid, mut stdout, stdin) = start(&runtime, "echo $$").await;
wait_until_collected(pid);
assert_eq!(read_to_the_end(&mut stdout).await, b"");
drop(stdin);
});
}
#[cfg(target_os = "linux")]
#[test]
#[timeout(15000)]
fn a_helper_process_is_collected_as_it_exits_with_its_output_unread() {
under_every_runtime(|runtime| async move {
let (pid, mut stdout, mut stdin) = start(&runtime, "echo $$; exec cat").await;
WriteHalf::sendmsg(&mut stdin, b"hello", &[]).await.unwrap();
WriteHalf::close(&mut stdin).await.unwrap();
wait_until_collected(pid);
assert_eq!(read_to_the_end(&mut stdout).await, b"hello");
});
}
#[cfg(target_os = "linux")]
#[test]
#[timeout(15000)]
fn a_helper_process_still_running_is_collected_once_it_exits() {
under_every_runtime(|runtime| async move {
let (pid, stdout, mut stdin) = start(&runtime, "echo $$; exec cat").await;
drop(stdout);
assert!(is_in_the_process_table(pid));
WriteHalf::close(&mut stdin).await.unwrap();
wait_until_collected(pid);
});
}
#[cfg(target_os = "linux")]
#[test]
#[timeout(15000)]
fn a_helper_process_leaves_the_process_table() {
under_every_runtime(|runtime| async move {
let (pid, mut stdout, mut stdin) = start(&runtime, "echo $$; exec cat").await;
WriteHalf::close(&mut stdin).await.unwrap();
read_to_the_end(&mut stdout).await;
drop(stdout);
drop(stdin);
wait_until_collected(pid);
});
}
#[test]
#[timeout(15000)]
fn a_write_to_a_closed_half_fails() {
under_every_runtime(|runtime| async move {
let (_stdout, mut stdin) = spawn(&runtime, Command::new("cat"))
.unwrap()
.into_split()
.take();
WriteHalf::close(&mut stdin).await.unwrap();
let refused = WriteHalf::sendmsg(&mut stdin, b"hello", &[])
.await
.unwrap_err();
assert_eq!(refused.kind(), io::ErrorKind::NotConnected);
});
}
#[cfg(target_os = "linux")]
async fn start(
runtime: &Runtime,
script: &str,
) -> (u32, Box<dyn ReadHalf>, Box<dyn WriteHalf>) {
let mut command = Command::new("sh");
command.args(["-c", script]);
let (mut stdout, stdin) = spawn(runtime, command).unwrap().into_split().take();
let mut announced = Vec::new();
while !announced.ends_with(b"\n") {
let mut byte = [0];
let (read, _) = ReadHalf::recvmsg(&mut stdout, &mut byte).await.unwrap();
assert_ne!(read, 0, "the helper ended before it said what it was");
announced.push(byte[0]);
}
let pid = String::from_utf8_lossy(&announced).trim().parse().unwrap();
(pid, stdout, stdin)
}
#[cfg(target_os = "linux")]
async fn read_to_the_end(stdout: &mut Box<dyn ReadHalf>) -> Vec<u8> {
let mut read_so_far = Vec::new();
let mut buffer = [0; 64];
loop {
let (read, _) = ReadHalf::recvmsg(stdout, &mut buffer).await.unwrap();
if read == 0 {
return read_so_far;
}
read_so_far.extend_from_slice(&buffer[..read]);
}
}
}