aion_server/worker/supervisor/
executable.rs1use std::path::{Path, PathBuf};
14
15use sha2::{Digest, Sha256};
16
17use crate::build_identity::BuildIdentity;
18
19use super::error::SupervisionError;
20use super::status::SpawnedBinary;
21
22#[derive(Clone, Debug, Eq, PartialEq)]
24pub enum ManagedExecutable {
25 CurrentServer,
29 Path(PathBuf),
32}
33
34impl ManagedExecutable {
35 pub fn resolve(&self) -> Result<PathBuf, SupervisionError> {
42 match self {
43 Self::CurrentServer => {
44 std::env::current_exe().map_err(|source| SupervisionError::ExecutableUnresolved {
45 detail: source.to_string(),
46 })
47 }
48 Self::Path(path) => Ok(path.clone()),
49 }
50 }
51
52 pub fn identify(&self, path: &Path) -> Result<SpawnedBinary, SupervisionError> {
63 let bytes =
64 std::fs::read(path).map_err(|source| SupervisionError::ExecutableUnreadable {
65 path: path.display().to_string(),
66 detail: source.to_string(),
67 })?;
68 let content_hash = lowercase_hex(&Sha256::digest(bytes));
69 let stamp = match self {
70 Self::CurrentServer => Some(BuildIdentity::current()),
71 Self::Path(_) => None,
72 };
73 Ok(SpawnedBinary {
74 path: path.display().to_string(),
75 content_hash,
76 version: stamp.as_ref().map(|identity| identity.version.to_owned()),
77 commit: stamp.as_ref().map(|identity| identity.commit.to_owned()),
78 dirty: stamp.as_ref().map(|identity| identity.dirty.to_owned()),
79 })
80 }
81}
82
83fn lowercase_hex(bytes: &[u8]) -> String {
84 const DIGITS: &[u8; 16] = b"0123456789abcdef";
85 let mut encoded = String::with_capacity(bytes.len().saturating_mul(2));
86 for byte in bytes {
87 encoded.push(char::from(DIGITS[usize::from(byte >> 4)]));
88 encoded.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
89 }
90 encoded
91}
92
93#[cfg(test)]
94mod tests {
95 use std::io::Write;
96
97 use super::ManagedExecutable;
98
99 type TestResult = Result<(), Box<dyn std::error::Error>>;
100
101 #[test]
106 fn identity_follows_the_bytes_on_disk_at_each_capture() -> TestResult {
107 let directory = tempfile::tempdir()?;
108 let path = directory.path().join("worker");
109 let mut file = std::fs::File::create(&path)?;
110 file.write_all(b"first build")?;
111 drop(file);
112
113 let executable = ManagedExecutable::Path(path.clone());
114 let first = executable.identify(&path)?;
115
116 std::fs::write(&path, b"second build")?;
117 let second = executable.identify(&path)?;
118
119 assert_ne!(first.content_hash, second.content_hash);
120 assert_eq!(first.path, path.display().to_string());
121 assert_eq!(first.version, None);
123 assert_eq!(first.commit, None);
124 assert_eq!(first.dirty, None);
125 Ok(())
126 }
127
128 #[test]
129 fn a_missing_executable_is_a_typed_unreadable_refusal() -> TestResult {
130 let directory = tempfile::tempdir()?;
131 let path = directory.path().join("absent");
132 let error = ManagedExecutable::Path(path.clone())
133 .identify(&path)
134 .err()
135 .ok_or("identifying an absent executable must fail")?;
136 assert!(
137 error.to_string().contains(&path.display().to_string()),
138 "the refusal must name the path it could not read: {error}"
139 );
140 Ok(())
141 }
142
143 #[test]
144 fn the_server_binary_carries_its_build_stamp() -> TestResult {
145 let executable = ManagedExecutable::CurrentServer;
146 let path = executable.resolve()?;
147 let identity = executable.identify(&path)?;
148 assert!(identity.version.is_some());
149 assert!(identity.commit.is_some());
150 assert!(identity.dirty.is_some());
151 assert!(!identity.content_hash.is_empty());
152 Ok(())
153 }
154}