use std::future::Future;
use std::sync::Arc;
use std::time::Instant;
use anyhow::Result;
use arcbox_api::SetupPhase;
use arcbox_core::Runtime;
#[cfg(target_os = "macos")]
use macos_resolver::FileResolver;
use tracing::{info, warn};
use crate::context::{ControlPlane, DaemonContext, EarlyContext, ServiceHandles, StartupHandles};
use crate::{DaemonArgs, recovery, services};
use super::{
acquire_lock, assets, host_mounts, init_early, init_runtime, prepare_assets, wait_for_resources,
};
pub struct Startup {
args: DaemonArgs,
handles: StartupHandles,
}
pub struct ReadyDaemon {
pub ctx: DaemonContext,
pub handles: ServiceHandles,
}
impl Startup {
pub fn from_args(args: DaemonArgs, handles: StartupHandles) -> Self {
Self { args, handles }
}
pub async fn prepare_host(self) -> Result<HostPrepared> {
info!("Starting ArcBox daemon...");
let early =
record_startup_phase("prepare_host", init_early(self.args, self.handles)).await?;
Ok(HostPrepared { early })
}
}
pub struct HostPrepared {
early: EarlyContext,
}
impl HostPrepared {
pub async fn acquire_daemon_lease(self) -> Result<DaemonLeased> {
let ctx = record_startup_phase("acquire_daemon_lease", acquire_lock(self.early)).await?;
Ok(DaemonLeased { ctx })
}
}
pub struct DaemonLeased {
ctx: DaemonContext,
}
impl DaemonLeased {
pub async fn start_control_plane(self) -> Result<ControlPlaneStarted> {
let control_plane = record_startup_phase(
"start_control_plane",
services::start_control_plane(&self.ctx),
)
.await?;
Ok(ControlPlaneStarted {
ctx: self.ctx,
control_plane,
})
}
}
pub struct ControlPlaneStarted {
ctx: DaemonContext,
control_plane: ControlPlane,
}
impl ControlPlaneStarted {
pub async fn release_stale_resources(self) -> Result<ResourcesReleased> {
record_startup_phase("release_stale_resources", async {
wait_for_resources(&self.ctx).await?;
host_mounts::prepare(&self.ctx).await
})
.await?;
Ok(ResourcesReleased {
ctx: self.ctx,
control_plane: self.control_plane,
})
}
}
pub struct ResourcesReleased {
ctx: DaemonContext,
control_plane: ControlPlane,
}
impl ResourcesReleased {
pub async fn prepare_assets(self) -> Result<AssetsPrepared> {
let assets = record_startup_phase("prepare_assets", prepare_assets(&self.ctx)).await?;
info!(agent = ?assets.agent(), "Startup assets prepared");
Ok(AssetsPrepared {
ctx: self.ctx,
control_plane: self.control_plane,
_assets: assets,
})
}
}
pub struct AssetsPrepared {
ctx: DaemonContext,
control_plane: ControlPlane,
_assets: assets::PreparedAssets,
}
impl AssetsPrepared {
pub async fn boot_runtime(self) -> Result<RuntimeBooted> {
let runtime = record_startup_phase("boot_runtime", init_runtime(&self.ctx)).await?;
Ok(RuntimeBooted {
ctx: self.ctx,
control_plane: self.control_plane,
runtime,
})
}
}
pub struct RuntimeBooted {
ctx: DaemonContext,
control_plane: ControlPlane,
runtime: Arc<Runtime>,
}
impl RuntimeBooted {
pub async fn start_runtime_services(self) -> Result<RuntimeServicesStarted> {
let linux_vm = self.runtime.config().vm.autostart;
let handles = record_startup_phase("start_runtime_services", async {
let handles =
services::start_services(&self.ctx, &self.runtime, self.control_plane).await?;
recovery::run(&self.ctx, &self.runtime, handles.dns_port).await?;
if linux_vm {
services::enable_docker_integration(&self.ctx);
crate::kubernetes_lb::spawn(&self.ctx, &self.runtime);
crate::sandbox_cleanup::spawn(&self.ctx, &self.runtime);
}
crate::disk_reclaim::spawn(&self.ctx, &self.runtime);
crate::nfs_mount::spawn(&self.ctx, &self.runtime);
crate::ssh_agent::spawn(&self.ctx, &self.runtime);
crate::machine_dns::spawn(&self.ctx, &self.runtime);
crate::machine_mount::spawn(&self.ctx, &self.runtime);
Ok(handles)
})
.await?;
self.ctx
.setup_state
.set_phase(SetupPhase::NetworkReady, "Network services ready");
Ok(RuntimeServicesStarted {
ctx: self.ctx,
handles,
linux_vm,
})
}
}
pub struct RuntimeServicesStarted {
ctx: DaemonContext,
handles: ServiceHandles,
linux_vm: bool,
}
impl RuntimeServicesStarted {
pub async fn mark_ready(self) -> Result<ReadyDaemon> {
record_startup_phase("mark_ready", async {
#[cfg(target_os = "macos")]
check_resolver_installed(&self.ctx.dns_domain);
self.ctx
.setup_state
.set_phase(SetupPhase::Ready, "Daemon ready");
println!("ArcBox daemon started");
if self.linux_vm {
println!(" Docker API: {}", self.ctx.layout.docker_socket.display());
}
println!(" gRPC API: {}", self.ctx.layout.grpc_socket.display());
println!(" DNS: 127.0.0.1:{}", self.handles.dns_port);
println!(" Data: {}", self.ctx.layout.data_dir.display());
println!();
if self.linux_vm {
println!("Use 'abctl docker enable' to configure Docker CLI integration.");
} else {
println!(
"Running as a VM host only (--no-linux-vm): Docker and Kubernetes are disabled."
);
}
println!("Press Ctrl+C to stop.");
Ok(())
})
.await?;
Ok(ReadyDaemon {
ctx: self.ctx,
handles: self.handles,
})
}
}
#[cfg(target_os = "macos")]
fn check_resolver_installed(domain: &str) {
let resolver = FileResolver::new("arcbox");
if !resolver.is_registered(domain) {
println!("Hint: Run 'sudo abctl dns install' to enable *.{domain} DNS resolution.");
}
}
async fn record_startup_phase<T, F>(phase: &'static str, future: F) -> Result<T>
where
F: Future<Output = Result<T>>,
{
let started = Instant::now();
let result = future.await;
let elapsed_ms = started.elapsed().as_millis() as u64;
match &result {
Ok(_) => info!(startup_phase = phase, elapsed_ms, "Startup phase complete"),
Err(error) => warn!(startup_phase = phase, elapsed_ms, %error, "Startup phase failed"),
}
result
}