metalos/lib/generator/src/generator.rs (91 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
use std::ffi::{CString, OsStr};
use std::fs::File;
use std::io::{Cursor, Seek, Write};
use std::os::unix::io::FromRawFd;
use std::process::ExitStatus;
use bufsize::SizeCounter;
use bytes::{Bytes, BytesMut};
use fbthrift::binary_protocol::{
deserialize, serialize, BinaryProtocolDeserializer, BinaryProtocolSerializer,
};
use fbthrift::{Deserialize, Serialize};
use thiserror::Error;
use sandbox::sandbox;
#[derive(Debug, Error)]
pub enum Error {
#[error("deserializing output failed: {0}")]
Deserialize(anyhow::Error),
#[error("preparing input fd failed: {0}")]
PrepareInput(std::io::Error),
#[error("sandboxing process failed: {0}")]
Sandbox(anyhow::Error),
#[error("spawning generator failed: {0}")]
Spawn(std::io::Error),
#[error("generator exited with {status}\nstdout: {stdout}\nstderr: {stderr}")]
Eval {
status: ExitStatus,
stderr: String,
stdout: String,
},
}
pub type Result<T> = std::result::Result<T, Error>;
/// Run a MetalOS generator in a sandboxed environment. This handles spawning
/// the process, handing it the input struct via binary thrift and deserializing
/// the output.
pub fn evaluate<B, I, O>(binary: B, input: &I) -> Result<O>
where
B: AsRef<OsStr>,
I: Serialize<BinaryProtocolSerializer<SizeCounter>>
+ Serialize<BinaryProtocolSerializer<BytesMut>>,
O: Deserialize<BinaryProtocolDeserializer<Cursor<Bytes>>>,
{
let input = serialize(input);
let mut stdin = unsafe {
File::from_raw_fd(
nix::sys::memfd::memfd_create(
&CString::new("input")
.expect("creating cstr can never fail with this static input"),
nix::sys::memfd::MemFdCreateFlag::empty(),
)
.map_err(|e| Error::PrepareInput(e.into()))?,
)
};
stdin.write_all(&input).map_err(Error::PrepareInput)?;
stdin.rewind().map_err(Error::PrepareInput)?;
let output = sandbox(binary, Default::default())
.map_err(Error::Sandbox)?
.stdin(stdin)
.output()
.map_err(Error::Spawn)?;
if !output.status.success() {
return Err(Error::Eval {
status: output.status,
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
});
}
deserialize(output.stdout).map_err(Error::Deserialize)
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn echo() -> Result<()> {
let input = test_if::Input {
hello: "world".into(),
};
let output: test_if::Output =
evaluate(std::env::var_os("ECHO_GENERATOR").unwrap(), &input)?;
assert_eq!(
output,
test_if::Output {
echo: "world".into(),
}
);
Ok(())
}
}