#[cfg(any(engine, test))]
pub mod download;
#[cfg(all(engine, target_os = "linux"))]
mod linux;
#[cfg(all(engine, target_os = "linux"))]
use linux as platform;
#[cfg(all(engine, target_os = "macos"))]
mod macos;
#[cfg(all(engine, target_os = "macos"))]
use macos as platform;
#[cfg(any(all(engine, windows), test))]
mod mux;
#[cfg(any(all(engine, target_os = "macos"), test))]
mod ports;
#[cfg(all(engine, windows))]
mod windows;
#[cfg(any(all(engine, windows), test))]
mod wsl;
#[cfg(all(engine, windows))]
use windows as platform;
use std::sync::{Arc, Mutex};
pub use ferrodock_core::builtin::{NAME, Paths, SUPPORTED};
#[cfg(all(engine, target_os = "macos"))]
pub const SHARE_TAG: &str = "home";
#[cfg(all(engine, target_os = "macos"))]
pub const VSOCK_PORT: u32 = 2375;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Resources {
pub cpus: u32,
pub memory_mib: u64,
}
impl Default for Resources {
fn default() -> Self {
let cpus = std::thread::available_parallelism().map_or(4, |n| n.get() as u32);
Self {
cpus: (cpus / 2).clamp(2, 8),
memory_mib: 4096,
}
}
}
impl Resources {
pub fn most() -> Self {
let cpus = std::thread::available_parallelism().map_or(4, |n| n.get() as u32);
Self {
cpus,
memory_mib: physical_memory_mib().unwrap_or(16 * 1024),
}
}
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn physical_memory_mib() -> Option<u64> {
let mut bytes: u64 = 0;
let mut size = std::mem::size_of::<u64>();
let status = unsafe {
libc::sysctlbyname(
c"hw.memsize".as_ptr(),
(&raw mut bytes).cast(),
&raw mut size,
std::ptr::null_mut(),
0,
)
};
(status == 0).then_some(bytes / (1024 * 1024))
}
#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
fn physical_memory_mib() -> Option<u64> {
None
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(not(engine), allow(dead_code))]
pub enum State {
NotInstalled,
Downloading { done: u64, total: Option<u64> },
Stopped,
Starting,
Running,
Failed(String),
}
impl State {
pub fn busy(&self) -> bool {
matches!(self, State::Downloading { .. } | State::Starting)
}
}
pub struct Engine {
paths: Option<Paths>,
state: Arc<Mutex<State>>,
given: Arc<Mutex<Option<Resources>>>,
#[cfg(engine)]
running: Arc<Mutex<Option<platform::Running>>>,
}
impl Engine {
pub fn new() -> Self {
let paths = Paths::locate();
let state = match &paths {
Some(p) if p.installed() => State::Stopped,
_ => State::NotInstalled,
};
Self {
paths,
state: Arc::new(Mutex::new(state)),
given: Arc::default(),
#[cfg(engine)]
running: Arc::default(),
}
}
pub fn state(&self) -> State {
self.state
.lock()
.map(|s| s.clone())
.unwrap_or(State::Stopped)
}
fn set(state: &Mutex<State>, value: State) {
if let Ok(mut s) = state.lock() {
*s = value;
}
}
pub fn start(&self, resources: Resources, repaint: impl Fn() + Send + 'static) {
if self.state().busy() || self.state() == State::Running {
return;
}
let Some(boot) = self.boot(resources, repaint) else {
return;
};
std::thread::spawn(boot);
}
pub fn restart(&self, resources: Resources, repaint: impl Fn() + Send + 'static) {
if self.state().busy() || !Self::RESTARTS {
return;
}
let Some(boot) = self.boot(resources, repaint) else {
return;
};
#[cfg(engine)]
let engine = self.running.lock().ok().and_then(|mut slot| slot.take());
let state = self.state.clone();
Self::set(&state, State::Starting);
std::thread::spawn(move || {
#[cfg(engine)]
if let Some(engine) = engine {
engine.stop();
}
boot();
});
}
pub const RESTARTS: bool = cfg!(any(target_os = "macos", windows));
fn boot(
&self,
resources: Resources,
repaint: impl Fn() + Send + 'static,
) -> Option<impl FnOnce() + Send + 'static> {
let Some(paths) = self.paths.clone() else {
Self::set(&self.state, State::Failed("No home folder".into()));
return None;
};
if !SUPPORTED {
Self::set(
&self.state,
State::Failed(
"Ferrodock's engine needs a Mac with Apple Silicon, Windows or Linux".into(),
),
);
return None;
}
let state = self.state.clone();
let given = self.given.clone();
#[cfg(engine)]
let running = self.running.clone();
Some(move || {
let result = (|| {
std::fs::create_dir_all(&paths.dir).map_err(|e| e.to_string())?;
#[cfg(engine)]
{
if !paths.installed() {
let archive = download::fetch(|done, total| {
Self::set(&state, State::Downloading { done, total });
repaint();
})?;
Self::set(&state, State::Starting);
repaint();
platform::install(&paths, &archive)?;
}
Self::set(&state, State::Starting);
repaint();
let engine = platform::Running::start(&paths, &resources)?;
if let Ok(mut slot) = running.lock() {
*slot = Some(engine);
}
Ok::<(), String>(())
}
#[cfg(not(engine))]
{
let _ = resources;
Err("unsupported".to_owned())
}
})();
if let Ok(mut slot) = given.lock() {
*slot = result.is_ok().then_some(resources);
}
Self::set(
&state,
match result {
Ok(()) => State::Running,
Err(e) => State::Failed(e),
},
);
repaint();
})
}
pub fn resources(&self) -> Option<Resources> {
self.given.lock().ok().and_then(|g| *g)
}
pub fn log(&self) -> Option<std::path::PathBuf> {
let console = &self.paths.as_ref()?.console;
(Self::RESTARTS && console.is_file()).then(|| console.clone())
}
pub fn stop(&self) {
#[cfg(engine)]
if let Some(engine) = self.running.lock().ok().and_then(|mut slot| slot.take()) {
engine.stop();
}
let installed = self.paths.as_ref().is_some_and(Paths::installed);
Self::set(
&self.state,
if installed {
State::Stopped
} else {
State::NotInstalled
},
);
}
pub fn check(&self) {
#[cfg(engine)]
if self.state() == State::Running {
let alive = self
.running
.lock()
.ok()
.and_then(|slot| slot.as_ref().map(platform::Running::is_running))
.unwrap_or(false);
if !alive {
Self::set(&self.state, State::Failed("The engine stopped".into()));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
#[cfg(unix)]
fn keeps_everything_in_one_folder() {
let paths = Paths::under(PathBuf::from("/Users/me/.ferrodock"));
let host = if cfg!(target_os = "linux") {
"unix:///run/ferrodock/docker.sock"
} else {
"unix:///Users/me/.ferrodock/docker.sock"
};
assert_eq!(paths.host(), host);
assert_eq!(
paths.kernel,
PathBuf::from("/Users/me/.ferrodock/engine/kernel")
);
assert!(!paths.installed());
let r = Resources::default();
assert!((2..=8).contains(&r.cpus));
}
#[test]
#[cfg(windows)]
fn offers_the_engine_on_a_named_pipe() {
let paths = Paths::under(PathBuf::from(r"C:\Users\me\AppData\Local\Ferrodock"));
assert_eq!(paths.host(), ferrodock_core::builtin::PIPE);
assert!(paths.distro.ends_with("wsl"));
assert!(!paths.installed());
}
}