Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 5 additions & 22 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions app/arcbox-daemon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ bytes.workspace = true
http-body-util = "0.1"
tokio-util = "0.7"
arcbox-docker-tools.workspace = true
filetime = "0.2.29"

[target.'cfg(target_os = "macos")'.dependencies]
macos-resolver = { workspace = true }
Expand All @@ -55,3 +56,6 @@ gic = ["arcbox-core/gic"]

[lints]
workspace = true

[dev-dependencies]
tempfile.workspace = true
12 changes: 6 additions & 6 deletions app/arcbox-daemon/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
//! The startup sequence produces progressively richer context types:
//!
//! ```text
//! init_early() → EarlyContext (no lock, no runtime)
//! acquire_lock() → DaemonContext (lock held, no runtime yet)
//! init_runtime() → Arc<Runtime> (also fills SharedRuntime for gRPC)
//! Startup::prepare_host() → EarlyContext (no lock, no runtime)
//! Startup::acquire_daemon_lease() → DaemonContext (lock held, no runtime yet)
//! Startup::boot_runtime() → Arc<Runtime> (also fills SharedRuntime for gRPC)
//! ```
//!
//! This encoding makes it a compile error to access the daemon lock
Expand All @@ -20,12 +20,12 @@ use tokio_util::sync::CancellationToken;

use crate::startup::DaemonLock;

/// Pre-lock context returned by [`startup::init_early`].
/// Pre-lock context produced by the startup pipeline.
///
/// Contains everything needed to start the gRPC SystemService (so
/// clients can observe progress), but the daemon lock has not been
/// acquired yet. Consumed by [`startup::acquire_lock`] to produce
/// a [`DaemonContext`].
/// acquired yet. Consumed by the daemon lease phase to produce a
/// [`DaemonContext`].
pub struct EarlyContext {
pub layout: HostLayout,
pub shared_runtime: SharedRuntime,
Expand Down
74 changes: 19 additions & 55 deletions app/arcbox-daemon/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@ mod shutdown;
mod startup;

use anyhow::Result;
use arcbox_api::SetupPhase;
use arcbox_logging::LogConfig;
use clap::Parser;
use macos_resolver::FileResolver;
use tracing::info;

#[derive(Debug, Parser)]
#[command(name = "arcbox-daemon")]
Expand Down Expand Up @@ -99,56 +96,23 @@ fn sentry_environment() -> Option<String> {
}

async fn run(args: DaemonArgs) -> Result<()> {
info!("Starting ArcBox daemon...");

// Phase 1: directories, config, sockets — no runtime yet.
let early = startup::init_early(args).await?;

// Acquire exclusive daemon lock. Terminates any stale daemon that
// still holds the lock (up to ~30 s SIGTERM wait). Must complete
// before start_grpc because the old daemon may be listening on the
// same socket paths. Consumes EarlyContext, producing a
// DaemonContext with a guaranteed lock.
let ctx = startup::acquire_lock(early).await?;

// Start gRPC with all services. Machine/Sandbox return UNAVAILABLE
// until runtime is ready. SystemService works immediately so clients
// can observe the full startup progression (CLEANING_UP →
// INITIALIZING → DOWNLOADING_ASSETS → ASSETS_READY → …).
let shared_runtime = ctx.shared_runtime.clone();
let grpc = services::start_grpc(&ctx, shared_runtime).await?;

// Wait for residual resource holders (e.g. orphaned
// Virtualization.framework XPC helpers holding docker.img).
// Visible to gRPC clients as the CLEANING_UP phase.
startup::wait_for_resources(&ctx).await?;

// Phase 2: seed/download boot assets, build runtime, start VM.
// Progress is visible to gRPC clients via WatchSetupStatus.
let runtime = startup::init_runtime(&ctx).await?;

// Phase 3: start remaining services that require the runtime.
let handles = services::start_services(&ctx, &runtime, grpc).await?;
recovery::run(&ctx, &runtime).await;

check_resolver_installed(&ctx.dns_domain);
ctx.setup_state.set_phase(SetupPhase::Ready, "Daemon ready");

println!("ArcBox daemon started");
println!(" Docker API: {}", ctx.layout.docker_socket.display());
println!(" gRPC API: {}", ctx.layout.grpc_socket.display());
println!(" DNS: 127.0.0.1:{}", ctx.dns_port);
println!(" Data: {}", ctx.layout.data_dir.display());
println!();
println!("Use 'arcbox docker enable' to configure Docker CLI integration.");
println!("Press Ctrl+C to stop.");

shutdown::run(ctx, handles).await
}

fn check_resolver_installed(domain: &str) {
let resolver = FileResolver::new("arcbox");
if !resolver.is_registered(domain) {
println!("Hint: Run 'sudo arcbox dns install' to enable *.{domain} DNS resolution.");
}
let ready = startup::Startup::from_args(args)
.prepare_host()
.await?
.acquire_daemon_lease()
.await?
.start_control_plane()
.await?
.release_stale_resources()
.await?
.prepare_assets()
.await?
.boot_runtime()
.await?
.start_runtime_services()
.await?
.mark_ready()
.await?;

shutdown::run(ready.ctx, ready.handles).await
}
64 changes: 32 additions & 32 deletions app/arcbox-daemon/src/recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,38 +143,6 @@ fn spawn_vmnet_route_reconcile(setup_state: Arc<arcbox_api::SetupState>, bridge:
}));
}

#[cfg(test)]
mod tests {
use super::*;

#[cfg(all(target_os = "macos", feature = "vmnet"))]
#[test]
fn cold_start_route_plan_uses_vmnet_bridge() {
let plan = cold_start_route_plan(Some("bridge100".to_string()), None);

assert_eq!(
plan,
Some(ColdStartRoutePlan::VmnetBridge("bridge100".to_string()))
);
}

#[cfg(all(target_os = "macos", feature = "vmnet"))]
#[test]
fn cold_start_route_plan_skips_when_vmnet_bridge_is_unknown() {
let plan = cold_start_route_plan(None, Some("fe:b2:14:4d:a4:64".to_string()));

assert_eq!(plan, None);
}

#[cfg(all(target_os = "macos", not(feature = "vmnet")))]
#[test]
fn cold_start_route_plan_uses_bridge_mac_without_vmnet() {
let plan = cold_start_route_plan(Some("bridge100".to_string()), Some("mac".to_string()));

assert_eq!(plan, Some(ColdStartRoutePlan::BridgeMac("mac".to_string())));
}
}

// =============================================================================
// Docker CLI tools
// =============================================================================
Expand Down Expand Up @@ -336,3 +304,35 @@ async fn recover_container_networking(
);
}
}

#[cfg(test)]
mod tests {
use super::*;

#[cfg(all(target_os = "macos", feature = "vmnet"))]
#[test]
fn cold_start_route_plan_uses_vmnet_bridge() {
let plan = cold_start_route_plan(Some("bridge100".to_string()), None);

assert_eq!(
plan,
Some(ColdStartRoutePlan::VmnetBridge("bridge100".to_string()))
);
}

#[cfg(all(target_os = "macos", feature = "vmnet"))]
#[test]
fn cold_start_route_plan_skips_when_vmnet_bridge_is_unknown() {
let plan = cold_start_route_plan(None, Some("fe:b2:14:4d:a4:64".to_string()));

assert_eq!(plan, None);
}

#[cfg(all(target_os = "macos", not(feature = "vmnet")))]
#[test]
fn cold_start_route_plan_uses_bridge_mac_without_vmnet() {
let plan = cold_start_route_plan(Some("bridge100".to_string()), Some("mac".to_string()));

assert_eq!(plan, Some(ColdStartRoutePlan::BridgeMac("mac".to_string())));
}
}
Loading
Loading