use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, PoisonError};
use isoloom_manifest::{HostOs, Protocol};
use crate::Result;
use crate::clock::{Clock, SystemClock};
use crate::events::Event;
use crate::hooks::Hooks;
use crate::manifest::load_manifest;
use crate::name::RunName;
use crate::net::{HostPorts, LoopbackPorts};
use crate::process::{ProcessRunner, SystemProcesses};
use crate::run::Running;
use crate::state::Op;
use crate::store::{RunLocks, RunRecord, Store};
#[derive(Clone)]
#[non_exhaustive]
pub struct RunnerConfig {
pub home: PathBuf,
pub processes: Arc<dyn ProcessRunner>,
pub ports: Arc<dyn HostPorts>,
pub clock: Arc<dyn Clock>,
pub host: HostOs,
pub path: Option<String>,
}
impl RunnerConfig {
pub fn new(home: impl Into<PathBuf>) -> RunnerConfig {
RunnerConfig {
home: home.into(),
processes: Arc::new(SystemProcesses::default()),
ports: Arc::new(LoopbackPorts),
clock: Arc::new(SystemClock),
host: this_host(),
path: std::env::var("PATH").ok(),
}
}
}
impl RunnerConfig {
#[must_use]
pub fn with_processes(mut self, processes: Arc<dyn ProcessRunner>) -> RunnerConfig {
self.processes = processes;
self
}
#[must_use]
pub fn with_ports(mut self, ports: Arc<dyn HostPorts>) -> RunnerConfig {
self.ports = ports;
self
}
#[must_use]
pub fn with_clock(mut self, clock: Arc<dyn Clock>) -> RunnerConfig {
self.clock = clock;
self
}
}
impl std::fmt::Debug for RunnerConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RunnerConfig")
.field("home", &self.home)
.field("host", &self.host)
.finish_non_exhaustive()
}
}
#[must_use]
pub fn this_host() -> HostOs {
if cfg!(windows) {
HostOs::Windows
} else if cfg!(target_os = "macos") {
HostOs::Macos
} else {
HostOs::Linux
}
}
pub struct Runner {
pub(crate) config: RunnerConfig,
pub(crate) store: Store,
pub(crate) hooks: Vec<Arc<dyn Hooks>>,
pub(crate) locks: RunLocks,
flight: Mutex<BTreeMap<RunName, Op>>,
reserved: Mutex<BTreeMap<(u16, Protocol), RunName>>,
}
impl Runner {
pub fn new(config: RunnerConfig) -> Result<Runner> {
let store = Store::open(&config.home)?;
Ok(Runner {
config,
store,
hooks: Vec::new(),
locks: RunLocks::default(),
flight: Mutex::default(),
reserved: Mutex::default(),
})
}
pub fn add_hooks(&mut self, hooks: Arc<dyn Hooks>) {
self.hooks.push(hooks);
}
pub fn open(&self, manifest: &Path, name: Option<RunName>) -> Result<Running<'_>> {
let loaded = load_manifest(manifest)?;
let name = match name {
Some(name) => name,
None => RunName::new(loaded.manifest.environment.name.as_str())?,
};
Running::new(self, loaded, name)
}
pub fn runs(&self) -> Result<Vec<RunRecord>> {
self.store.records()
}
pub fn events(&self, name: &RunName) -> Result<Vec<Event>> {
self.store.events(name)
}
pub async fn ssh_public_key(&self) -> Result<String> {
crate::ssh::ensure_key(self.config.processes.as_ref(), &self.config.home).await
}
pub fn in_flight(&self) -> Vec<(RunName, Op)> {
self.flight
.lock()
.unwrap_or_else(PoisonError::into_inner)
.iter()
.map(|(n, o)| (n.clone(), *o))
.collect()
}
pub(crate) fn enter(&self, name: &RunName, op: Op) -> Flight<'_> {
self.flight.lock().unwrap_or_else(PoisonError::into_inner).insert(name.clone(), op);
Flight {
runner: self,
name: name.clone(),
}
}
pub(crate) fn op_of(&self, name: &RunName) -> Option<Op> {
self.flight.lock().unwrap_or_else(PoisonError::into_inner).get(name).copied()
}
pub(crate) fn reserve(&self, name: &RunName, port: u16, protocol: Protocol) -> bool {
let mut reserved = self.reserved.lock().unwrap_or_else(PoisonError::into_inner);
let holder = reserved.entry((port, protocol)).or_insert_with(|| name.clone());
holder == name
}
pub(crate) fn reserved_elsewhere(&self, name: &RunName, port: u16, protocol: Protocol) -> bool {
self.reserved
.lock()
.unwrap_or_else(PoisonError::into_inner)
.get(&(port, protocol))
.is_some_and(|h| h != name)
}
pub(crate) fn release(&self, name: &RunName) {
self.reserved.lock().unwrap_or_else(PoisonError::into_inner).retain(|_, holder| holder != name);
}
}
impl std::fmt::Debug for Runner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Runner")
.field("config", &self.config)
.field("hooks", &self.hooks.len())
.field("in_flight", &self.in_flight())
.finish_non_exhaustive()
}
}
pub(crate) struct Flight<'r> {
runner: &'r Runner,
name: RunName,
}
impl Drop for Flight<'_> {
fn drop(&mut self) {
self.runner.flight.lock().unwrap_or_else(PoisonError::into_inner).remove(&self.name);
self.runner.release(&self.name);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn runner(home: &Path) -> Runner {
Runner::new(RunnerConfig::new(home)).unwrap()
}
#[test]
fn two_runners_share_nothing() {
let (a, b) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
let (a, b) = (runner(a.path()), runner(b.path()));
let name = RunName::new("lab").unwrap();
let flight = a.enter(&name, Op::Up);
assert!(a.reserve(&name, 8080, Protocol::Tcp));
assert_eq!(a.in_flight(), [(name.clone(), Op::Up)]);
assert_eq!(b.in_flight(), []);
assert!(b.reserve(&RunName::new("other").unwrap(), 8080, Protocol::Tcp), "b's ports aren't a's");
drop(flight);
assert_eq!(a.in_flight(), []);
}
#[test]
fn a_port_reserved_by_one_run_is_refused_to_another_until_released() {
let home = tempfile::tempdir().unwrap();
let r = runner(home.path());
let (one, two) = (RunName::new("one").unwrap(), RunName::new("two").unwrap());
assert!(r.reserve(&one, 41000, Protocol::Tcp));
assert!(r.reserve(&one, 41000, Protocol::Tcp), "its own reservation again");
assert!(!r.reserve(&two, 41000, Protocol::Tcp));
assert!(r.reserved_elsewhere(&two, 41000, Protocol::Tcp) && !r.reserved_elsewhere(&one, 41000, Protocol::Tcp));
assert!(r.reserve(&two, 41000, Protocol::Udp), "another protocol");
r.release(&one);
assert!(r.reserve(&two, 41000, Protocol::Tcp));
}
}