1use std::collections::BTreeMap;
6use std::path::{Path, PathBuf};
7use std::sync::{Arc, Mutex, PoisonError};
8
9use isoloom_manifest::{HostOs, Protocol};
10
11use crate::Result;
12use crate::clock::{Clock, SystemClock};
13use crate::events::Event;
14use crate::hooks::Hooks;
15use crate::manifest::load_manifest;
16use crate::name::RunName;
17use crate::net::{HostPorts, LoopbackPorts};
18use crate::process::{ProcessRunner, SystemProcesses};
19use crate::run::Running;
20use crate::state::Op;
21use crate::store::{RunLocks, RunRecord, Store};
22
23#[derive(Clone)]
25#[non_exhaustive]
26pub struct RunnerConfig {
27 pub home: PathBuf,
30 pub processes: Arc<dyn ProcessRunner>,
32 pub ports: Arc<dyn HostPorts>,
34 pub clock: Arc<dyn Clock>,
36 pub host: HostOs,
38 pub path: Option<String>,
41}
42
43impl RunnerConfig {
44 pub fn new(home: impl Into<PathBuf>) -> RunnerConfig {
46 RunnerConfig {
47 home: home.into(),
48 processes: Arc::new(SystemProcesses::default()),
49 ports: Arc::new(LoopbackPorts),
50 clock: Arc::new(SystemClock),
51 host: this_host(),
52 path: std::env::var("PATH").ok(),
53 }
54 }
55}
56
57impl RunnerConfig {
58 #[must_use]
60 pub fn with_processes(mut self, processes: Arc<dyn ProcessRunner>) -> RunnerConfig {
61 self.processes = processes;
62 self
63 }
64
65 #[must_use]
67 pub fn with_ports(mut self, ports: Arc<dyn HostPorts>) -> RunnerConfig {
68 self.ports = ports;
69 self
70 }
71
72 #[must_use]
74 pub fn with_clock(mut self, clock: Arc<dyn Clock>) -> RunnerConfig {
75 self.clock = clock;
76 self
77 }
78}
79
80impl std::fmt::Debug for RunnerConfig {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 f.debug_struct("RunnerConfig")
83 .field("home", &self.home)
84 .field("host", &self.host)
85 .finish_non_exhaustive()
86 }
87}
88
89#[must_use]
91pub fn this_host() -> HostOs {
92 if cfg!(windows) {
93 HostOs::Windows
94 } else if cfg!(target_os = "macos") {
95 HostOs::Macos
96 } else {
97 HostOs::Linux
98 }
99}
100
101pub struct Runner {
103 pub(crate) config: RunnerConfig,
104 pub(crate) store: Store,
105 pub(crate) hooks: Vec<Arc<dyn Hooks>>,
106 pub(crate) locks: RunLocks,
107 flight: Mutex<BTreeMap<RunName, Op>>,
108 reserved: Mutex<BTreeMap<(u16, Protocol), RunName>>,
109}
110
111impl Runner {
112 pub fn new(config: RunnerConfig) -> Result<Runner> {
118 let store = Store::open(&config.home)?;
119 Ok(Runner {
120 config,
121 store,
122 hooks: Vec::new(),
123 locks: RunLocks::default(),
124 flight: Mutex::default(),
125 reserved: Mutex::default(),
126 })
127 }
128
129 pub fn add_hooks(&mut self, hooks: Arc<dyn Hooks>) {
131 self.hooks.push(hooks);
132 }
133
134 pub fn open(&self, manifest: &Path, name: Option<RunName>) -> Result<Running<'_>> {
142 let loaded = load_manifest(manifest)?;
143 let name = match name {
144 Some(name) => name,
145 None => RunName::new(loaded.manifest.environment.name.as_str())?,
146 };
147 Running::new(self, loaded, name)
148 }
149
150 pub fn runs(&self) -> Result<Vec<RunRecord>> {
156 self.store.records()
157 }
158
159 pub fn events(&self, name: &RunName) -> Result<Vec<Event>> {
165 self.store.events(name)
166 }
167
168 pub async fn ssh_public_key(&self) -> Result<String> {
176 crate::ssh::ensure_key(self.config.processes.as_ref(), &self.config.home).await
177 }
178
179 pub fn in_flight(&self) -> Vec<(RunName, Op)> {
181 self.flight
182 .lock()
183 .unwrap_or_else(PoisonError::into_inner)
184 .iter()
185 .map(|(n, o)| (n.clone(), *o))
186 .collect()
187 }
188
189 pub(crate) fn enter(&self, name: &RunName, op: Op) -> Flight<'_> {
191 self.flight.lock().unwrap_or_else(PoisonError::into_inner).insert(name.clone(), op);
192 Flight {
193 runner: self,
194 name: name.clone(),
195 }
196 }
197
198 pub(crate) fn op_of(&self, name: &RunName) -> Option<Op> {
200 self.flight.lock().unwrap_or_else(PoisonError::into_inner).get(name).copied()
201 }
202
203 pub(crate) fn reserve(&self, name: &RunName, port: u16, protocol: Protocol) -> bool {
205 let mut reserved = self.reserved.lock().unwrap_or_else(PoisonError::into_inner);
206 let holder = reserved.entry((port, protocol)).or_insert_with(|| name.clone());
207 holder == name
208 }
209
210 pub(crate) fn reserved_elsewhere(&self, name: &RunName, port: u16, protocol: Protocol) -> bool {
212 self.reserved
213 .lock()
214 .unwrap_or_else(PoisonError::into_inner)
215 .get(&(port, protocol))
216 .is_some_and(|h| h != name)
217 }
218
219 pub(crate) fn release(&self, name: &RunName) {
221 self.reserved.lock().unwrap_or_else(PoisonError::into_inner).retain(|_, holder| holder != name);
222 }
223}
224
225impl std::fmt::Debug for Runner {
226 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227 f.debug_struct("Runner")
228 .field("config", &self.config)
229 .field("hooks", &self.hooks.len())
230 .field("in_flight", &self.in_flight())
231 .finish_non_exhaustive()
232 }
233}
234
235pub(crate) struct Flight<'r> {
237 runner: &'r Runner,
238 name: RunName,
239}
240
241impl Drop for Flight<'_> {
242 fn drop(&mut self) {
243 self.runner.flight.lock().unwrap_or_else(PoisonError::into_inner).remove(&self.name);
244 self.runner.release(&self.name);
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 fn runner(home: &Path) -> Runner {
253 Runner::new(RunnerConfig::new(home)).unwrap()
254 }
255
256 #[test]
257 fn two_runners_share_nothing() {
258 let (a, b) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
261 let (a, b) = (runner(a.path()), runner(b.path()));
262 let name = RunName::new("lab").unwrap();
263 let flight = a.enter(&name, Op::Up);
264 assert!(a.reserve(&name, 8080, Protocol::Tcp));
265 assert_eq!(a.in_flight(), [(name.clone(), Op::Up)]);
266 assert_eq!(b.in_flight(), []);
267 assert!(b.reserve(&RunName::new("other").unwrap(), 8080, Protocol::Tcp), "b's ports aren't a's");
268 drop(flight);
269 assert_eq!(a.in_flight(), []);
270 }
271
272 #[test]
273 fn a_port_reserved_by_one_run_is_refused_to_another_until_released() {
274 let home = tempfile::tempdir().unwrap();
275 let r = runner(home.path());
276 let (one, two) = (RunName::new("one").unwrap(), RunName::new("two").unwrap());
277 assert!(r.reserve(&one, 41000, Protocol::Tcp));
278 assert!(r.reserve(&one, 41000, Protocol::Tcp), "its own reservation again");
279 assert!(!r.reserve(&two, 41000, Protocol::Tcp));
280 assert!(r.reserved_elsewhere(&two, 41000, Protocol::Tcp) && !r.reserved_elsewhere(&one, 41000, Protocol::Tcp));
281 assert!(r.reserve(&two, 41000, Protocol::Udp), "another protocol");
282 r.release(&one);
283 assert!(r.reserve(&two, 41000, Protocol::Tcp));
284 }
285}