use std::time::{Duration, Instant};
use super::settings::Settings;
use super::store::{Connection, Store};
use crate::docker::{Engine, Starter, builtin_engine, starter};
use crate::vm;
use crate::worker::{Request, Worker};
const BUILTIN: &str = "Ferrodock's engine";
const PATIENCE: Duration = Duration::from_secs(180);
#[derive(Debug, Clone, PartialEq)]
pub enum Offer {
Nothing,
Start(String),
SetUp,
Downloading(Option<f32>),
Starting(String),
Failed { name: String, error: String },
}
enum Plan {
App(Starter),
BuiltIn,
}
pub struct EngineStarter {
pub builtin: vm::Engine,
repaint: Option<eframe::egui::Context>,
started: Option<(Instant, String)>,
error: Option<String>,
tried_at_launch: bool,
provider_checked: bool,
}
fn engine(store: &Store) -> Option<(&str, &str)> {
let name = store.engine.as_deref()?;
store
.engines
.iter()
.find(|e| e.name == name)
.map(|e| (e.name.as_str(), e.host.as_str()))
}
fn host(store: &Store) -> Option<&str> {
engine(store).map(|(_, host)| host)
}
impl EngineStarter {
pub fn new(ctx: &eframe::egui::Context) -> Self {
Self {
builtin: vm::Engine::new(),
repaint: Some(ctx.clone()),
started: None,
error: None,
tried_at_launch: false,
provider_checked: false,
}
}
fn starter(store: &Store, settings: &Settings) -> Option<Starter> {
let (name, host) = engine(store)?;
if name == vm::NAME {
return None;
}
starter(host, name, settings.engine_app.as_deref())
}
fn plan(store: &Store, settings: &Settings) -> Option<Plan> {
let builtin_picked = engine(store).is_some_and(|(name, _)| name == vm::NAME);
if !builtin_picked && let Some(starter) = Self::starter(store, settings) {
return Some(Plan::App(starter));
}
vm::SUPPORTED.then_some(Plan::BuiltIn)
}
pub fn start(&mut self, store: &Store, settings: &Settings, worker: &Worker) -> Option<Engine> {
match Self::plan(store, settings)? {
Plan::App(starter) => {
self.started = Some((Instant::now(), starter.name()));
self.error = None;
worker.send(Request::StartEngine(starter));
None
}
Plan::BuiltIn => {
let ctx = self.repaint.clone();
self.builtin.start(vm::Resources::default(), move || {
if let Some(ctx) = &ctx {
ctx.request_repaint();
}
});
let in_use = engine(store).is_some_and(|(name, _)| name == vm::NAME);
if in_use { None } else { builtin_engine() }
}
}
}
pub fn finished(&mut self, result: Result<(), String>) {
if let Err(e) = result {
self.started = None;
self.error = Some(e);
}
}
pub fn update(
&mut self,
store: &Store,
settings: &Settings,
worker: &Worker,
) -> Option<Engine> {
let mut switch = None;
match &store.connection {
Connection::Connected { .. } => {
self.started = None;
self.error = None;
self.tried_at_launch = true;
if !self.provider_checked {
self.provider_checked = true;
if let Some(host) = host(store) {
worker.send(Request::FindProvider(host.to_owned()));
}
}
}
Connection::Disconnected { reason } => {
if !self.tried_at_launch {
self.tried_at_launch = true;
if settings.start_engine && super::shell::is_not_running(reason) {
switch = self.start(store, settings, worker);
}
}
if let Some((since, name)) = &self.started
&& since.elapsed() > PATIENCE
{
self.error = Some(format!("{name} didn't answer after 3 minutes"));
self.started = None;
}
self.builtin.check();
}
Connection::Connecting => {}
}
switch
}
pub fn engine_switched(&mut self) {
self.provider_checked = false;
self.started = None;
self.error = None;
}
pub fn offer(&self, store: &Store, settings: &Settings) -> Offer {
if let Some(Plan::BuiltIn) = Self::plan(store, settings) {
return match self.builtin.state() {
vm::State::NotInstalled => Offer::SetUp,
vm::State::Downloading { done, total } => {
Offer::Downloading(total.filter(|t| *t > 0).map(|t| done as f32 / t as f32))
}
vm::State::Starting | vm::State::Running => Offer::Starting(BUILTIN.into()),
vm::State::Stopped => Offer::Start(BUILTIN.into()),
vm::State::Failed(error) => Offer::Failed {
name: BUILTIN.into(),
error,
},
};
}
let name = || {
Self::starter(store, settings)
.map(|s| s.name())
.unwrap_or_else(|| "Docker".into())
};
if let Some((_, name)) = &self.started {
return Offer::Starting(name.clone());
}
if let Some(error) = &self.error {
return Offer::Failed {
name: name(),
error: error.clone(),
};
}
match Self::starter(store, settings) {
Some(starter) => Offer::Start(starter.name()),
None => Offer::Nothing,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn failed_start_is_offered_again() {
let mut starter = EngineStarter {
builtin: vm::Engine::new(),
repaint: None,
started: Some((Instant::now(), "the Docker engine".into())),
error: None,
tried_at_launch: false,
provider_checked: false,
};
let store = Store::new();
let settings = Settings::default();
assert_eq!(
starter.offer(&store, &settings),
Offer::Starting("the Docker engine".into())
);
starter.finished(Err("no VM".into()));
assert!(matches!(
starter.offer(&store, &settings),
Offer::Failed { error, .. } if error == "no VM"
));
}
}