hl-engine 0.1.0

Safe Rust lifecycle API for the standalone HL Linux guest engine
use crate::{
    ffi, runtime::ConfigFile, wire, Child, Command as GuestCommand, Config, Error, Guest, Stdio,
};
use std::{
    ffi::{CString, OsStr},
    fs::{File, OpenOptions},
    os::fd::AsRawFd,
    os::unix::ffi::OsStrExt,
    sync::OnceLock,
};

static EXECUTABLE: OnceLock<Result<CString, String>> = OnceLock::new();

/// Entry point for constructing guest commands.
#[derive(Clone, Copy, Debug, Default)]
pub struct Engine;
impl Engine {
    #[must_use]
    pub const fn new() -> Self {
        Self
    }
    #[must_use]
    pub fn command(&self, guest: Guest, program: impl Into<std::ffi::OsString>) -> GuestCommand {
        GuestCommand::new(guest, program.into())
    }
    pub(crate) fn start<I, S>(
        guest: Guest,
        config: &Config,
        program: impl AsRef<OsStr>,
        arguments: I,
        streams: (Stdio, Stdio, Stdio),
    ) -> Result<Child, Error>
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        let mut argv = vec![program.as_ref().to_owned()];
        argv.extend(arguments.into_iter().map(|value| value.as_ref().to_owned()));
        if argv[0].as_bytes().is_empty() {
            return Err(Error::InvalidConfig("program must not be empty"));
        }
        let config = ConfigFile::create(&wire::encode(config, &argv, None)?)?;
        let executable = EXECUTABLE
            .get_or_init(|| {
                let path = std::env::current_exe().map_err(|error| error.to_string())?;
                CString::new(path.as_os_str().as_bytes())
                    .map_err(|_| "current executable contains NUL".into())
            })
            .as_ref()
            .map_err(|message| Error::Distribution(message.clone()))?;
        let config_path = CString::new(config.path().as_os_str().as_bytes())
            .map_err(|_| Error::InvalidConfig("config path contains NUL"))?;
        let (input, stdin, input_child) = prepare(streams.0, true)?;
        let (output, stdout, output_child) = prepare(streams.1, false)?;
        let (error, stderr, error_child) = prepare(streams.2, false)?;
        let native = ffi::Streams {
            input,
            output,
            error,
        };
        let process = ffi::start(executable, guest_number(guest), &config_path, &native)
            .map_err(native_error)?;
        drop((input_child, output_child, error_child));
        Ok(Child {
            process: Some(process),
            _config: config,
            stdin,
            stdout,
            stderr,
        })
    }
}

fn prepare(value: Stdio, input: bool) -> Result<(i32, Option<File>, Option<File>), Error> {
    match value {
        Stdio::Inherit => Ok((-1, None, None)),
        Stdio::Null => {
            let file = OpenOptions::new()
                .read(input)
                .write(!input)
                .open("/dev/null")?;
            Ok((file.as_raw_fd(), None, Some(file)))
        }
        Stdio::Piped => {
            let (read, write) = ffi::pipe_pair()?;
            let (parent, child) = if input { (write, read) } else { (read, write) };
            Ok((child.as_raw_fd(), Some(parent), Some(child)))
        }
    }
}
const fn guest_number(guest: Guest) -> u32 {
    match guest {
        Guest::Aarch64 => 1,
        Guest::X86_64 => 2,
    }
}
fn native_error(status: i32) -> Error {
    Error::Engine { status, detail: 0 }
}