use std::ffi::{CString, OsStr};
use std::io::{Read, Write, stdin};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::thread::sleep;
use std::time::{Duration, Instant};
use rkyv::api::high::to_bytes_in;
use rkyv::rancor::{self, Fallible};
use rkyv::ser::writer::IoWriter;
use rkyv::ser::{Allocator, Writer};
use rkyv::vec::{ArchivedVec, VecResolver};
use rkyv::{Archive, Deserialize, Place, Serialize, from_bytes};
use crate::{Daemonize, Error, Group, Mask, Outcome, User};
pub const STDOUT_DATA: &str = "stdout data";
pub const STDERR_DATA: &str = "stderr data";
pub const ADDITIONAL_FILE_DATA: &str = "additional file data";
pub const USER_NAME: &str = "daemonize-test";
pub const GROUP_NAME: &str = "daemonize-test";
const TESTER_PATH: &str = "target/debug/examples/tester";
const MAX_WAIT_DURATION: Duration = Duration::from_secs(5);
#[repr(transparent)]
pub struct ArchivablePathBuf(PathBuf);
impl ArchivablePathBuf {
pub fn into_inner(self) -> PathBuf {
self.0
}
}
impl<P: Into<PathBuf>> From<P> for ArchivablePathBuf {
fn from(value: P) -> Self {
Self(value.into())
}
}
impl Archive for ArchivablePathBuf {
type Archived = ArchivedVec<u8>;
type Resolver = VecResolver;
fn resolve(&self, resolver: Self::Resolver, out: Place<Self::Archived>) {
ArchivedVec::resolve_from_len(self.0.as_os_str().len(), resolver, out)
}
}
impl<S: Allocator + Fallible + Writer + ?Sized> Serialize<S> for ArchivablePathBuf {
fn serialize(&self, serializer: &mut S) -> Result<Self::Resolver, S::Error> {
ArchivedVec::serialize_from_slice(self.0.as_os_str().as_encoded_bytes(), serializer)
}
}
impl<D: Fallible + ?Sized> Deserialize<ArchivablePathBuf, D> for ArchivedVec<u8> {
fn deserialize(&self, _: &mut D) -> Result<ArchivablePathBuf, D::Error> {
unsafe { Ok(PathBuf::from(OsStr::from_encoded_bytes_unchecked(self.as_slice())).into()) }
}
}
#[derive(Archive, Serialize, Deserialize, Default)]
pub struct TesterConfig {
pid_file: Option<ArchivablePathBuf>,
chown_pid_file_user: Option<User>,
chown_pid_file_group: Option<Group>,
working_directory: Option<ArchivablePathBuf>,
user: Option<User>,
group: Option<Group>,
umask: Option<Mask>,
chroot: Option<ArchivablePathBuf>,
stdout: Option<ArchivablePathBuf>,
stderr: Option<ArchivablePathBuf>,
additional_files: Vec<ArchivablePathBuf>,
additional_files_privileged: Vec<ArchivablePathBuf>,
sleep_duration: Option<Duration>,
}
impl TesterConfig {
pub fn new() -> Self {
Default::default()
}
pub fn pid_file<F: Into<ArchivablePathBuf>>(&mut self, pid_file: F) -> &mut Self {
self.pid_file = Some(pid_file.into());
self
}
pub fn chown_pid_file_user<U: Into<User>>(&mut self, user: U) -> &mut Self {
self.chown_pid_file_user = Some(user.into());
self
}
pub fn chown_pid_file_group<G: Into<Group>>(&mut self, group: G) -> &mut Self {
self.chown_pid_file_group = Some(group.into());
self
}
pub fn working_directory<F: Into<ArchivablePathBuf>>(&mut self, path: F) -> &mut Self {
self.working_directory = Some(path.into());
self
}
pub fn user<U: Into<User>>(&mut self, user: U) -> &mut Self {
self.user = Some(user.into());
self
}
pub fn group<G: Into<Group>>(&mut self, group: G) -> &mut Self {
self.group = Some(group.into());
self
}
pub fn umask<M: Into<Mask>>(&mut self, umask: M) -> &mut Self {
self.umask = Some(umask.into());
self
}
pub fn chroot<F: Into<ArchivablePathBuf>>(&mut self, path: F) -> &mut Self {
self.chroot = Some(path.into());
self
}
pub fn stdout<F: Into<ArchivablePathBuf>>(&mut self, path: F) -> &mut Self {
self.stdout = Some(path.into());
self
}
pub fn stderr<F: Into<ArchivablePathBuf>>(&mut self, path: F) -> &mut Self {
self.stderr = Some(path.into());
self
}
pub fn additional_file<F: Into<ArchivablePathBuf>>(&mut self, path: F) -> &mut Self {
self.additional_files.push(path.into());
self
}
pub fn additional_file_privileged<F: Into<ArchivablePathBuf>>(&mut self, path: F) -> &mut Self {
self.additional_files_privileged.push(path.into());
self
}
pub fn sleep(&mut self, duration: Duration) -> &mut Self {
self.sleep_duration = Some(duration);
self
}
pub fn run(&self) -> Result<EnvData, Error> {
let mut child = Command::new(TESTER_PATH)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("unable to spawn child");
let mut stdout = child.stdout.take().unwrap();
let stdout_thread = std::thread::spawn(move || {
let mut stdout_bytes = Vec::new();
stdout
.read_to_end(&mut stdout_bytes)
.expect("failed to read from stdout");
stdout_bytes
});
let mut stderr = child.stderr.take().unwrap();
let stderr_thread = std::thread::spawn(move || {
let mut stderr_bytes = Vec::new();
stderr
.read_to_end(&mut stderr_bytes)
.expect("failed to read from stderr");
stderr_bytes
});
let stdin = child.stdin.take().unwrap();
let writer = IoWriter::new(stdin);
to_bytes_in::<_, rancor::Error>(self, writer).expect("failed to encode config");
let st = Instant::now();
let exit_status = loop {
let now = Instant::now();
if now - st > MAX_WAIT_DURATION {
panic!("wait for result timeout")
}
match child.try_wait().expect("unable to wait for result") {
Some(result) => break result,
None => sleep(Duration::from_millis(1)),
}
};
if !exit_status.success() {
let stderr = stderr_thread.join().unwrap();
panic!(
"invalid tester exit status ({}), stderr: {}",
exit_status.code().expect("unable to get status code"),
String::from_utf8_lossy(&stderr)
);
}
from_bytes::<_, rancor::Error>(&stdout_thread.join().unwrap())
.expect("unable to read tester stdout")
}
}
#[derive(Archive, Serialize, Deserialize)]
pub struct EnvData {
pub cwd: ArchivablePathBuf,
pub pid: u32,
pub euid: u32,
pub egid: u32,
}
impl EnvData {
fn new() -> EnvData {
Self {
cwd: std::env::current_dir()
.expect("unable to get current dir")
.into(),
pid: std::process::id(),
euid: unsafe { libc::geteuid() as u32 },
egid: unsafe { libc::getegid() as u32 },
}
}
}
pub fn execute_tester() {
let mut daemonize = Daemonize::new();
let mut buf = Vec::new();
stdin().read_to_end(&mut buf).unwrap();
let config: TesterConfig =
from_bytes::<_, rancor::Error>(&buf).expect("failed to decode config");
if let Some(f) = config.pid_file {
daemonize = daemonize.pid_file(f.into_inner());
}
if let Some(u) = config.chown_pid_file_user {
daemonize = daemonize.chown_pid_file_user(u);
}
if let Some(g) = config.chown_pid_file_group {
daemonize = daemonize.chown_pid_file_group(g);
}
if let Some(wd) = config.working_directory {
daemonize = daemonize.working_directory(wd.into_inner());
}
if let Some(u) = config.user {
daemonize = daemonize.user(u);
}
if let Some(g) = config.group {
daemonize = daemonize.group(g);
}
if let Some(m) = config.umask {
daemonize = daemonize.umask(m);
}
if let Some(chroot) = config.chroot {
daemonize = daemonize.chroot(chroot.into_inner());
}
let mut redirected_stdout = false;
if let Some(stdout) = config.stdout {
let file = std::fs::File::create(stdout.into_inner()).expect("unable to open stdout file");
daemonize = daemonize.stdout(file);
redirected_stdout = true;
}
let mut redirected_stderr = false;
if let Some(stderr) = config.stderr {
let file = std::fs::File::create(stderr.into_inner()).expect("unable to open stder file");
daemonize = daemonize.stderr(file);
redirected_stderr = true;
}
daemonize = daemonize.privileged_action(move || {
for file_path in config.additional_files_privileged {
if let Ok(mut file) = std::fs::File::create(file_path.into_inner()) {
file.write_all(ADDITIONAL_FILE_DATA.as_bytes()).ok();
}
}
});
let (mut read_pipe, write_pipe) = os_pipe::pipe().expect("unable to open pipe");
match unsafe { daemonize.execute() } {
Outcome::Parent(_) => {
drop(write_pipe);
let mut data = Vec::new();
read_pipe
.read_to_end(&mut data)
.expect("unable to read pipe");
std::io::stdout()
.write_all(&data)
.expect("unable to write data")
}
Outcome::Child(result) => {
drop(read_pipe);
let result = result.map(|_| EnvData::new());
if redirected_stdout {
print!("{}", STDOUT_DATA);
}
if redirected_stderr {
eprint!("{}", STDERR_DATA);
}
for file_path in config.additional_files {
if let Ok(mut file) = std::fs::File::create(file_path.into_inner()) {
let _ = file.write_all(ADDITIONAL_FILE_DATA.as_bytes());
}
}
let writer = IoWriter::new(write_pipe);
to_bytes_in::<_, rancor::Error>(&result, writer).expect("failed to write result");
if let Some(duration) = config.sleep_duration {
std::thread::sleep(duration)
}
}
}
}
pub fn get_test_uid() -> u32 {
let name = CString::new(USER_NAME).unwrap();
unsafe {
let ptr = libc::getpwnam(name.as_ptr() as *const libc::c_char);
if ptr.is_null() {
panic!("getpwnam failed")
} else {
let s = &*ptr;
s.pw_uid
}
}
}
pub fn get_test_gid() -> u32 {
let name = CString::new(GROUP_NAME).unwrap();
unsafe {
let ptr = libc::getgrnam(name.as_ptr() as *const libc::c_char);
if ptr.is_null() {
panic!("getgrnam failed")
} else {
let s = &*ptr;
s.gr_gid
}
}
}