mod assets;
mod cleanup;
mod lock;
mod pipeline;
mod resource_cleanup;
pub use assets::find_bundle_contents;
pub use lock::DaemonLock;
pub use pipeline::{ReadyDaemon, Startup};
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{Context, Result};
use arcbox_api::SetupPhase;
use arcbox_constants::paths::{ArcboxProfile, HostLayout};
use arcbox_core::{Config, InitProgress, Runtime};
use macos_resolver::to_env_prefix;
use tracing::{info, warn};
use crate::DaemonArgs;
use crate::context::{DaemonContext, EarlyContext, StartupHandles, VmArgs};
const DNS_PREFIX: &str = "arcbox";
const DEFAULT_DNS_DOMAIN: &str = "arcbox.local";
pub const DEFAULT_DNS_PORT: u16 = 5553;
async fn init_early(args: DaemonArgs, handles: StartupHandles) -> Result<EarlyContext> {
let profile = args
.profile
.unwrap_or_else(ArcboxProfile::from_env_or_default);
let mut layout = HostLayout::resolve_for_profile_from_env(profile, args.data_dir.as_deref());
if let Some(socket) = args.socket {
layout.docker_socket = socket;
}
if let Some(grpc) = args.grpc_socket {
layout.grpc_socket = grpc;
}
std::fs::create_dir_all(&layout.data_dir).context("Failed to create data directory")?;
std::fs::create_dir_all(&layout.run_dir).context("Failed to create run directory")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&layout.run_dir, std::fs::Permissions::from_mode(0o700))
.context("Failed to restrict run directory permissions")?;
}
std::fs::create_dir_all(&layout.log_dir).context("Failed to create log directory")?;
std::fs::create_dir_all(&layout.data_subdir)
.context("Failed to create persistent data directory")?;
let dns_domain = dns_domain();
let dns_port = dns_port();
Ok(EarlyContext {
profile,
layout,
shared_runtime: handles.shared_runtime,
early_runtime: handles.early_runtime,
setup_state: handles.setup_state,
shutdown: handles.shutdown,
daemon_lock_slot: handles.daemon_lock,
dns_domain,
dns_port,
docker_integration: args.docker_integration,
mount_nfs: !args.no_mount_nfs,
vm_args: VmArgs {
guest_docker_vsock_port: args.guest_docker_vsock_port,
kernel: args.kernel,
no_linux_vm: args.no_linux_vm,
},
})
}
async fn acquire_lock(early: EarlyContext) -> Result<DaemonContext> {
let lock_file = early.layout.lock_file.clone();
let lock = tokio::task::spawn_blocking(move || DaemonLock::acquire(&lock_file))
.await
.context("lock task panicked")?
.context("failed to acquire daemon lock")?;
let lock = Arc::new(lock);
early
.daemon_lock_slot
.set(Arc::clone(&lock))
.map_err(|_| anyhow::anyhow!("acquire_lock called twice"))?;
Ok(DaemonContext {
profile: early.profile,
layout: early.layout,
daemon_lock: lock,
shared_runtime: early.shared_runtime,
early_runtime: early.early_runtime,
setup_state: early.setup_state,
shutdown: early.shutdown,
dns_domain: early.dns_domain,
dns_port: early.dns_port,
docker_integration: early.docker_integration,
mount_nfs: early.mount_nfs,
vm_args: early.vm_args,
})
}
#[cfg(target_os = "macos")]
async fn wait_for_resources(ctx: &DaemonContext) -> Result<()> {
let _ = std::fs::remove_file(&ctx.layout.docker_socket);
let docker_imgs = resource_cleanup::disk_image_paths(&ctx.layout.data_subdir);
let decision = resource_cleanup::decide(
ctx.daemon_lock.displaced_stale_daemon(),
resource_cleanup::has_interrupted_running_machine(&ctx.layout.data_dir),
!docker_imgs.is_empty(),
);
let resource_cleanup::ResourceCleanupDecision::ScanDiskImageHolders { reason } = decision
else {
info!(
decision = ?decision,
"Skipping startup resource-holder cleanup"
);
return Ok(());
};
info!(
reason = ?reason,
image_count = docker_imgs.len(),
"Scanning disk-image holders before runtime startup"
);
ctx.setup_state
.set_phase(SetupPhase::CleaningUp, "Waiting for resource release…");
tokio::task::spawn_blocking(move || {
for path in docker_imgs {
cleanup::wait_for_docker_img_holders(&path);
}
})
.await
.context("resource wait task panicked")?;
ctx.setup_state.set_phase(
SetupPhase::Initializing,
"Finished waiting for resource release (see logs for details)",
);
Ok(())
}
#[cfg(not(target_os = "macos"))]
async fn wait_for_resources(_ctx: &DaemonContext) -> Result<()> {
Ok(())
}
async fn prepare_assets(ctx: &DaemonContext) -> Result<assets::PreparedAssets> {
let prepared = assets::prepare(&ctx.layout.data_dir, &ctx.setup_state).await?;
ctx.setup_state
.set_phase(SetupPhase::AssetsReady, "Boot assets ready");
Ok(prepared)
}
async fn init_runtime(ctx: &DaemonContext) -> Result<Arc<Runtime>> {
let mut config = Config::load_for_profile(ctx.profile).unwrap_or_else(|err| {
warn!(error = %err, "Failed to load config file; falling back to defaults");
Config::for_profile(ctx.profile)
});
config.data_dir = ctx.layout.data_dir.clone();
config.docker.socket_path = ctx.layout.docker_socket.clone();
if let Some(port) = ctx.vm_args.guest_docker_vsock_port {
config.container.guest_docker_vsock_port = port;
}
let selected_guest_docker_port = config.container.guest_docker_vsock_port;
if let Some(ref kernel) = ctx.vm_args.kernel {
config.vm.kernel_path = Some(kernel.clone());
}
if ctx.vm_args.no_linux_vm {
config.vm.autostart = false;
}
let runtime = Arc::new(Runtime::new(config).context("Failed to create runtime")?);
ctx.early_runtime
.set(Arc::clone(&runtime))
.map_err(|_| anyhow::anyhow!("init_runtime called twice"))?;
crate::services::spawn_vm_running_mirror(ctx, &runtime);
runtime
.init(|milestone| match milestone {
InitProgress::SystemVmStarting => ctx
.setup_state
.set_phase(SetupPhase::VmStarting, "Starting the Linux VM…"),
InitProgress::SystemVmReady => ctx.setup_state.set_phase(
SetupPhase::VmReady,
"Linux VM ready; waiting for the container runtime…",
),
})
.await
.context("Failed to initialize runtime")?;
info!(
data_dir = %ctx.layout.data_dir.display(),
guest_docker_vsock_port = selected_guest_docker_port,
"Runtime initialized"
);
if ctx.dns_domain != DEFAULT_DNS_DOMAIN {
runtime.network_manager().set_dns_domain(&ctx.dns_domain);
}
let sandbox_cleanup_supported = if runtime.config().vm.autostart {
arcbox_api::initialize_sandbox_cleanup(&runtime)
.await
.context("Failed to initialize sandbox cleanup")?
} else {
false
};
ctx.shared_runtime
.set(Arc::clone(&runtime))
.map_err(|_| anyhow::anyhow!("init_runtime called twice"))?;
if sandbox_cleanup_supported {
arcbox_api::spawn_sandbox_cleanup(Arc::clone(&runtime));
}
Ok(runtime)
}
pub fn resolve_data_dir(profile: ArcboxProfile, data_dir: Option<&PathBuf>) -> PathBuf {
HostLayout::resolve_for_profile_from_env(profile, data_dir.map(PathBuf::as_path)).data_dir
}
fn dns_port() -> u16 {
let key = format!("{}_DNS_PORT", to_env_prefix(DNS_PREFIX));
std::env::var(key)
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(DEFAULT_DNS_PORT)
}
fn dns_domain() -> String {
let key = format!("{}_DNS_DOMAIN", to_env_prefix(DNS_PREFIX));
std::env::var(key).unwrap_or_else(|_| DEFAULT_DNS_DOMAIN.to_string())
}