use super::*;
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::controller::LocalEngineReadiness;
use crate::server::{ViewerTarget, ViewerTargetCapacity};
pub(super) async fn options(State(state): State<ServerState>) -> Json<LaunchOptions> {
let snapshot = state.snapshot_rx.borrow().clone();
let kinds = snapshot
.targets
.iter()
.map(|target| target.kind.clone())
.collect::<BTreeSet<_>>();
let checks = state.engine_checks.clone();
let engines = tokio::task::spawn_blocking(move || checks.readiness(kinds))
.await
.unwrap_or_default();
Json(launch_options(&snapshot, &state.preferences_path, &engines))
}
pub(crate) type EngineProbe = Arc<dyn Fn(&str) -> Option<LocalEngineReadiness> + Send + Sync>;
pub(crate) struct LocalEngineChecks {
probe: EngineProbe,
cache: std::sync::Mutex<BTreeMap<String, (Instant, LocalEngineReadiness)>>,
}
const ENGINE_READY_TTL: Duration = Duration::from_secs(30 * 60);
const ENGINE_FAILURE_TTL: Duration = Duration::from_secs(60);
impl LocalEngineChecks {
pub(crate) fn new(probe: EngineProbe) -> Self {
Self {
probe,
cache: std::sync::Mutex::new(BTreeMap::new()),
}
}
pub(crate) fn on_this_host() -> Self {
Self::new(Arc::new(|kind| {
crate::controller::local_engine_readiness(kind, &crate::targets::ProcessExecutor)
}))
}
fn readiness(&self, kinds: BTreeSet<String>) -> BTreeMap<String, LocalEngineReadiness> {
let now = Instant::now();
let mut found = BTreeMap::new();
for kind in kinds {
let cached = self
.cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(&kind)
.copied();
let readiness = match cached {
Some((checked, readiness))
if now.duration_since(checked)
< match readiness {
LocalEngineReadiness::Ready => ENGINE_READY_TTL,
_ => ENGINE_FAILURE_TTL,
} =>
{
Some(readiness)
}
_ => {
let readiness = (self.probe)(&kind);
if let Some(readiness) = readiness {
self.cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(kind.clone(), (Instant::now(), readiness));
}
readiness
}
};
if let Some(readiness) = readiness {
found.insert(kind, readiness);
}
}
found
}
}
pub(super) fn launch_options(
snapshot: &ViewerSnapshot,
preferences_path: &Path,
engines: &BTreeMap<String, LocalEngineReadiness>,
) -> LaunchOptions {
LaunchOptions {
revision: snapshot.revision,
profiles: snapshot
.profiles
.iter()
.map(|profile| LaunchProfile {
id: profile.id.clone(),
harness: profile.harness_kind.clone(),
})
.collect(),
targets: snapshot
.targets
.iter()
.map(|target| launch_target(target, &snapshot.capacity, engines.get(&target.kind)))
.collect(),
bundles: snapshot
.bundles
.iter()
.map(|bundle| LaunchBundle {
id: bundle.id.clone(),
primary_repository: bundle.primary_repository.clone(),
repositories: bundle
.repositories
.iter()
.map(|repository| LaunchRepository {
id: repository.id.clone(),
github: repository.github.clone(),
destination: repository.destination.clone(),
})
.collect(),
})
.collect(),
hosts: snapshot
.capacity
.iter()
.map(|host| LaunchHost {
id: host.id.clone(),
label: host.label.clone(),
targets: host.target_ids.clone(),
stale: host.stale,
refreshing: host.refreshing,
has_error: host.has_error,
})
.collect(),
default: saved_default(preferences_path),
}
}
fn launch_target(
target: &ViewerTarget,
hosts: &[ViewerTargetCapacity],
engine: Option<&LocalEngineReadiness>,
) -> LaunchTarget {
let host = hosts
.iter()
.find(|host| host.target_ids.iter().any(|id| id == &target.id));
let engine_failure = match engine {
Some(LocalEngineReadiness::NotInstalled) => Some(crate::targets::engine_not_installed(
engine_name(&target.kind),
)),
Some(LocalEngineReadiness::NotReady) => Some(format!(
"{} did not answer its check on this host; start it and try again",
engine_name(&target.kind)
)),
Some(LocalEngineReadiness::Ready) | None => None,
};
let availability = match host {
_ if engine_failure.is_some() => LaunchAvailability::Unavailable,
None => LaunchAvailability::Unknown,
Some(host) if host.has_error => LaunchAvailability::Unavailable,
Some(host) if host.stale => LaunchAvailability::Stale,
Some(_) => LaunchAvailability::Ready,
};
let unavailable_reason = match (availability, host) {
_ if engine_failure.is_some() => engine_failure,
(LaunchAvailability::Unavailable, Some(host)) => Some(format!(
"the host \"{}\" did not answer its last check",
host.label
)),
_ => None,
};
LaunchTarget {
id: target.id.clone(),
kind: target.kind.clone(),
requires_project_directory: target.requires_project_directory,
availability,
unavailable_reason,
host: host.map(|host| host.label.clone()),
}
}
fn engine_name(kind: &str) -> &'static str {
match kind {
"local-podman" => "Podman",
"local-docker" => "Docker",
"apple-container" => "Apple container",
_ => "The container engine",
}
}
pub(super) fn saved_default(path: &Path) -> Option<LaunchDefault> {
match mj_core::go::GoPreferences::load(path) {
Ok(preferences) => preferences.default.map(|recipe| LaunchDefault {
profile_id: recipe.profile_id,
target_id: recipe.target_id,
}),
Err(error) => {
tracing::debug!(
%error,
path = %path.display(),
"could not read fast-start preferences for launch options"
);
None
}
}
}