use std::path::{Path, PathBuf};
use sha2::{Digest, Sha256};
use crate::build_identity::BuildIdentity;
use super::error::SupervisionError;
use super::status::SpawnedBinary;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ManagedExecutable {
CurrentServer,
Path(PathBuf),
}
impl ManagedExecutable {
pub fn resolve(&self) -> Result<PathBuf, SupervisionError> {
match self {
Self::CurrentServer => {
std::env::current_exe().map_err(|source| SupervisionError::ExecutableUnresolved {
detail: source.to_string(),
})
}
Self::Path(path) => Ok(path.clone()),
}
}
pub fn identify(&self, path: &Path) -> Result<SpawnedBinary, SupervisionError> {
let bytes =
std::fs::read(path).map_err(|source| SupervisionError::ExecutableUnreadable {
path: path.display().to_string(),
detail: source.to_string(),
})?;
let content_hash = lowercase_hex(&Sha256::digest(bytes));
let stamp = match self {
Self::CurrentServer => Some(BuildIdentity::current()),
Self::Path(_) => None,
};
Ok(SpawnedBinary {
path: path.display().to_string(),
content_hash,
version: stamp.as_ref().map(|identity| identity.version.to_owned()),
commit: stamp.as_ref().map(|identity| identity.commit.to_owned()),
dirty: stamp.as_ref().map(|identity| identity.dirty.to_owned()),
})
}
}
fn lowercase_hex(bytes: &[u8]) -> String {
const DIGITS: &[u8; 16] = b"0123456789abcdef";
let mut encoded = String::with_capacity(bytes.len().saturating_mul(2));
for byte in bytes {
encoded.push(char::from(DIGITS[usize::from(byte >> 4)]));
encoded.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
}
encoded
}
#[cfg(test)]
mod tests {
use std::io::Write;
use super::ManagedExecutable;
type TestResult = Result<(), Box<dyn std::error::Error>>;
#[test]
fn identity_follows_the_bytes_on_disk_at_each_capture() -> TestResult {
let directory = tempfile::tempdir()?;
let path = directory.path().join("worker");
let mut file = std::fs::File::create(&path)?;
file.write_all(b"first build")?;
drop(file);
let executable = ManagedExecutable::Path(path.clone());
let first = executable.identify(&path)?;
std::fs::write(&path, b"second build")?;
let second = executable.identify(&path)?;
assert_ne!(first.content_hash, second.content_hash);
assert_eq!(first.path, path.display().to_string());
assert_eq!(first.version, None);
assert_eq!(first.commit, None);
assert_eq!(first.dirty, None);
Ok(())
}
#[test]
fn a_missing_executable_is_a_typed_unreadable_refusal() -> TestResult {
let directory = tempfile::tempdir()?;
let path = directory.path().join("absent");
let error = ManagedExecutable::Path(path.clone())
.identify(&path)
.err()
.ok_or("identifying an absent executable must fail")?;
assert!(
error.to_string().contains(&path.display().to_string()),
"the refusal must name the path it could not read: {error}"
);
Ok(())
}
#[test]
fn the_server_binary_carries_its_build_stamp() -> TestResult {
let executable = ManagedExecutable::CurrentServer;
let path = executable.resolve()?;
let identity = executable.identify(&path)?;
assert!(identity.version.is_some());
assert!(identity.commit.is_some());
assert!(identity.dirty.is_some());
assert!(!identity.content_hash.is_empty());
Ok(())
}
}