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