mod assets;
mod cleanup;
mod container_network_lease;
mod lock;
mod pipeline;
mod resource_cleanup;
pub use assets::find_bundle_contents;
pub use container_network_lease::ContainerNetworkLease;
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 arcbox_helper::validate::Domain;
use macos_resolver::to_env_prefix;
use tracing::info;
use crate::DaemonArgs;
use crate::context::{DaemonContext, EarlyContext, StartupHandles, VmArgs};
const DNS_PREFIX: &str = "arcbox";
pub 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;
}
let dns_domain = dns_domain(args.dns_domain)?;
validate_explicit_dns_resolver(profile, args.install_dns_resolver, &dns_domain)?;
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_port = dns_port(args.dns_port);
let kubernetes_context = kubernetes_context(profile, args.kubernetes_context)?;
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,
container_network_lease_slot: handles.container_network_lease,
dns_domain,
dns_port,
install_dns_resolver: args.install_dns_resolver,
kubernetes_port: args.kubernetes_port,
kubernetes_context,
docker_integration: args.docker_integration,
mount_nfs: !args.no_mount_nfs,
vm_args: VmArgs {
guest_docker_vsock_port: args.guest_docker_vsock_port,
container_cidr: args.container_cidr,
kernel: args.kernel,
no_linux_vm: args.no_linux_vm,
},
})
}
fn validate_explicit_dns_resolver(
profile: ArcboxProfile,
enabled: bool,
domain: &str,
) -> Result<()> {
anyhow::ensure!(
profile == ArcboxProfile::Production || !enabled || domain != DEFAULT_DNS_DOMAIN,
"development --install-dns-resolver requires a non-canonical --dns-domain"
);
Ok(())
}
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,
container_network_lease_slot: early.container_network_lease_slot,
dns_domain: early.dns_domain,
dns_port: early.dns_port,
install_dns_resolver: early.install_dns_resolver,
kubernetes_port: early.kubernetes_port,
kubernetes_context: early.kubernetes_context,
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 config = effective_config(ctx)?;
let prepared = assets::prepare(&config, &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 config = effective_config(ctx)?;
if config.vm.autostart
&& let Some(lease) = ContainerNetworkLease::acquire(config.container.cidr)?
{
ctx.container_network_lease_slot
.set(lease)
.map_err(|_| anyhow::anyhow!("container network lease already configured"))?;
}
let selected_guest_docker_port = config.container.guest_docker_vsock_port;
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);
crate::power::spawn_wake_clock_sync(&runtime, &ctx.shutdown);
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,
container_cidr = %runtime.config().container.cidr,
"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.as_ref())
.await
.context("Failed to initialize sandbox cleanup")?
} else {
false
};
if sandbox_cleanup_supported {
arcbox_api::spawn_sandbox_cleanup(Arc::clone(&runtime));
}
Ok(runtime)
}
fn effective_config(ctx: &DaemonContext) -> Result<Config> {
let mut config = Config::load_for_profile(ctx.profile).context("Failed to load config")?;
config.data_dir.clone_from(&ctx.layout.data_dir);
config
.docker
.socket_path
.clone_from(&ctx.layout.docker_socket);
if let Some(port) = ctx.vm_args.guest_docker_vsock_port {
config.container.guest_docker_vsock_port = port;
}
if let Some(cidr) = ctx.vm_args.container_cidr {
config.container.cidr = cidr;
}
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;
}
Ok(config)
}
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(cli_port: Option<u16>) -> u16 {
if let Some(port) = cli_port {
return port;
}
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(cli_domain: Option<Domain>) -> Result<String> {
if let Some(domain) = cli_domain {
return Ok(domain.to_string());
}
let key = format!("{}_DNS_DOMAIN", to_env_prefix(DNS_PREFIX));
std::env::var(key)
.map_or_else(
|_| Ok(DEFAULT_DNS_DOMAIN.to_string()),
|domain| domain.parse::<Domain>().map(|domain| domain.to_string()),
)
.map_err(anyhow::Error::msg)
.context("Invalid DNS domain")
}
fn kubernetes_context(profile: ArcboxProfile, requested: Option<String>) -> Result<String> {
let context = requested.unwrap_or_else(|| profile.docker_context_name().to_owned());
let bytes = context.as_bytes();
if bytes.is_empty()
|| bytes.len() > 253
|| !bytes.first().is_some_and(u8::is_ascii_alphanumeric)
|| !bytes.last().is_some_and(u8::is_ascii_alphanumeric)
|| !bytes
.iter()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
{
anyhow::bail!(
"Kubernetes context must be 1-253 ASCII letters, digits, '.', '_' or '-', with an alphanumeric first and last character"
);
}
Ok(context)
}
#[cfg(test)]
mod tests {
use super::{DEFAULT_DNS_DOMAIN, kubernetes_context, validate_explicit_dns_resolver};
use arcbox_constants::paths::ArcboxProfile;
#[test]
fn kubernetes_context_defaults_by_profile_and_rejects_yaml_syntax() {
assert_eq!(
kubernetes_context(ArcboxProfile::Production, None).unwrap(),
"arcbox"
);
assert_eq!(
kubernetes_context(
ArcboxProfile::Development,
Some("arcbox-dev-worktree-1".to_owned())
)
.unwrap(),
"arcbox-dev-worktree-1"
);
assert!(
kubernetes_context(ArcboxProfile::Development, Some("bad: value".to_owned())).is_err()
);
}
#[test]
fn development_cannot_claim_the_production_resolver_domain() {
assert!(
validate_explicit_dns_resolver(ArcboxProfile::Development, true, DEFAULT_DNS_DOMAIN)
.is_err()
);
assert!(
validate_explicit_dns_resolver(
ArcboxProfile::Development,
true,
"worktree.dev.arcbox.local"
)
.is_ok()
);
assert!(
validate_explicit_dns_resolver(ArcboxProfile::Production, true, DEFAULT_DNS_DOMAIN)
.is_ok()
);
}
}