hl-engine 0.1.10

Safe Rust lifecycle API for the standalone HL Linux guest engine
use super::lowering::{Launch, ServiceLaunch};
use super::{
    ffi, wire, Arc, AsRawFd, CString, Child, Config, ConfigFile, Duration, Error, File, Guest,
    Instant, OpenOptions, OsStr, OsStrExt, Size, Stdio, EXECUTABLE,
};

pub(super) fn start(
    launch: Launch,
    io: crate::spec::ProcessIo,
    authorities: crate::extension::Authorities,
    resources: Vec<super::lowering::AllocatedResource>,
) -> Result<Child, Error> {
    start_legacy(
        launch.guest,
        &launch.config,
        launch.program,
        launch.arguments,
        (io.stdin, io.stdout, io.stderr),
        launch.terminal,
        launch.projections,
        launch.services.map(|services| (services, authorities)),
        resources,
    )
}

#[allow(clippy::too_many_arguments)]
pub(super) fn start_legacy<I, S>(
    guest: Guest,
    config: &Config,
    program: impl AsRef<OsStr>,
    arguments: I,
    streams: (Stdio, Stdio, Stdio),
    terminal: Option<Size>,
    projections: Vec<crate::projection::Projection>,
    services: Option<(ServiceLaunch, crate::extension::Authorities)>,
    resources: Vec<super::lowering::AllocatedResource>,
) -> 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 encoded = wire::encode(config, &argv, None)?;
    let domain = crate::Domain::from_identity(wire::domain(&encoded));
    let config = ConfigFile::create(&encoded)?;
    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 (process, stdin, stdout, stderr, terminal) = if let Some(size) = terminal {
        let result = if let Some((services, authority)) = services {
            start_services_terminal(
                executable,
                guest_number(guest),
                &config_path,
                size,
                services,
                &authority,
            )
        } else {
            ffi::start_terminal(executable, guest_number(guest), &config_path, size.native())
                .map_err(native_error)
        };
        let (process, file) = result?;
        (process, None, None, None, Some(crate::Terminal::new(file)))
    } else {
        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 = if let Some((services, authority)) = services {
            start_services(
                executable,
                guest_number(guest),
                &config_path,
                &native,
                services,
                &authority,
            )?
        } else {
            ffi::start(executable, guest_number(guest), &config_path, &native)
                .map_err(native_error)?
        };
        drop((input_child, output_child, error_child));
        (process, stdin, stdout, stderr, None)
    };
    Ok(Child {
        process: Some(process),
        _config: config,
        stdin,
        stdout,
        stderr,
        terminal,
        domain,
        completed: false,
        _projections: projections,
        _provider_resources: resources,
    })
}

fn start_services(
    executable: &std::ffi::CStr,
    guest: u32,
    config: &std::ffi::CStr,
    streams: &ffi::Streams,
    launch: ServiceLaunch,
    authority: &crate::extension::Authorities,
) -> Result<ffi::Handle, Error> {
    let (child, _server) = prepare_services(launch, authority)?;
    ffi::start_with_transport(executable, guest, config, streams, &child).map_err(native_error)
}

fn start_services_terminal(
    executable: &std::ffi::CStr,
    guest: u32,
    config: &std::ffi::CStr,
    size: Size,
    launch: ServiceLaunch,
    authority: &crate::extension::Authorities,
) -> Result<(ffi::Handle, File), Error> {
    let (child, _server) = prepare_services(launch, authority)?;
    ffi::start_terminal_with_transport(executable, guest, config, size.native(), &child)
        .map_err(native_error)
}

fn prepare_services(
    launch: ServiceLaunch,
    authority: &crate::extension::Authorities,
) -> Result<(std::os::unix::net::UnixStream, std::thread::JoinHandle<()>), Error> {
    let handles = authority
        .provider(&launch.provider)
        .and_then(|provider| provider.handles.as_ref())
        .cloned()
        .ok_or(Error::InvalidConfig("missing provider handle authority"))?;
    let maximum_request = launch
        .registrations
        .iter()
        .map(|service| service.max_request_bytes)
        .max()
        .unwrap_or(1);
    let maximum_handles = u32::try_from(launch.registrations.len().saturating_mul(64))
        .unwrap_or(u32::MAX)
        .max(1);
    let payload = crate::service::encode_namespace_install(&launch.projections, 64, 4096)
        .map_err(|_| Error::InvalidConfig("invalid service namespace transaction"))?;
    let (parent, child) = std::os::unix::net::UnixStream::pair()?;
    let channel = Arc::new(
        crate::transport::Channel::from_stream(
            parent,
            crate::transport::TransportLimits {
                payload_bytes: maximum_request
                    .max(u32::try_from(payload.len()).unwrap_or(u32::MAX)),
                providers: 1,
            },
        )
        .map_err(|error| Error::Distribution(format!("provider transport: {error:?}")))?,
    );
    let dispatcher = Arc::new(crate::service::ProviderDispatcher::new(
        handles,
        &launch.registrations,
        launch.credentials,
        maximum_handles,
        maximum_request,
    ));
    let server = crate::service::ServiceServer::new(
        channel.clone(),
        dispatcher,
        64,
        Duration::from_secs(30),
    );
    let server = std::thread::spawn(move || {
        let startup = Instant::now() + Duration::from_secs(5);
        if channel.accept_handshake(1, startup) == Ok(1)
            && channel.install_namespace(payload, startup).is_ok()
        {
            let _ = server.run(Instant::now() + Duration::from_secs(24 * 60 * 60));
        }
    });
    Ok((child, server))
}

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 }
}