use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use arcbox_snapshot::SnapshotError;
use arcbox_vm_driver::{IsolationSpec, PreparedVm, VmDriver, VmHandle, VmId};
use tracing::{debug, warn};
use crate::agent::{ClockSync, GuestAgent, GuestAgentFactory, ReadyGate};
use crate::config::RuntimeConfig;
use crate::error::{ComputerError, Result};
use crate::lifecycle::runtime::ComputerRuntime;
use crate::sandbox::boot::{StageError, create_rootfs_symlink, stage_rootfs_cow_or_copy};
use crate::sandbox::spec::build_vm_spec;
use crate::sandbox::{self, ComputerSpec, ComputerState, NetworkAttachment};
use crate::snapshot_cow::{CowHandle, CowManager};
pub type BootOutput = (Arc<dyn VmHandle>, Box<dyn ReadyGate>);
const AGENT_GATE_TIMEOUT: Duration = Duration::from_secs(35);
const CLOCK_SYNC_TIMEOUT: Duration = Duration::from_secs(10);
pub struct BootFailure {
pub error: ComputerError,
pub prepared: Option<Arc<dyn PreparedVm>>,
pub cow_handle: Option<CowHandle>,
}
pub async fn wait_for_agent(
id: &str,
handle: &Arc<dyn VmHandle>,
net: Option<&NetworkAttachment>,
mut ready_gate: Box<dyn ReadyGate>,
agents: &dyn GuestAgentFactory,
) -> Result<Arc<dyn GuestAgent>> {
let gated = match agents.connect(Arc::clone(handle), net.map(|n| &n.identity)) {
Ok(agent) => {
match tokio::time::timeout(AGENT_GATE_TIMEOUT, ready_gate.wait(handle, &agent)).await {
Ok(Ok(())) => Ok(agent),
Ok(Err(error)) => Err(ComputerError::Vsock(format!(
"agent readiness gate: {error}"
))),
Err(_) => Err(ComputerError::Vsock(format!(
"agent readiness gate: the guest agent did not answer within {}s",
AGENT_GATE_TIMEOUT.as_secs()
))),
}
}
Err(error) => Err(error),
};
drop(ready_gate);
let agent = gated?;
{
let id = id.to_owned();
let agent = Arc::clone(&agent);
tokio::spawn(async move {
match tokio::time::timeout(CLOCK_SYNC_TIMEOUT, agent.sync_clock()).await {
Ok(Ok(ClockSync::Synced)) => {}
Ok(Ok(ClockSync::AgentError(code))) => {
warn!(
sandbox_id = %id, code,
"agent could not set the clock; continuing with a possibly skewed clock"
);
}
Ok(Err(error)) => {
warn!(sandbox_id = %id, %error, "cold-boot clock sync failed; continuing");
}
Err(_) => {
warn!(sandbox_id = %id, "cold-boot clock sync timed out; continuing");
}
}
});
}
Ok(agent)
}
#[allow(
clippy::too_many_arguments,
reason = "boot owns one exact sandbox generation and its handoff signal"
)]
#[allow(
clippy::result_large_err,
reason = "the failure hands back the half-built sandbox's resources; boxing it is a refactor of its own"
)]
pub async fn do_boot(
id: &str,
spec: &ComputerSpec,
net: Option<&NetworkAttachment>,
vm_dir: &Path,
driver: &dyn VmDriver,
agents: &dyn GuestAgentFactory,
config: &RuntimeConfig,
cow_manager: &CowManager,
computer: &Arc<Mutex<ComputerRuntime>>,
resource_handoff: tokio::sync::oneshot::Sender<()>,
) -> std::result::Result<BootOutput, BootFailure> {
let mut resource_handoff = Some(resource_handoff);
let fc_cfg = &config.firecracker;
let prepared: Result<(Arc<dyn PreparedVm>, IsolationSpec)> = async {
let vm_id = VmId::new(id)?;
let isolation = sandbox::isolation_spec(config)?;
let prepared = sandbox::prepare_capability(driver)
.prepare(&vm_id, &isolation, vm_dir)
.await?;
Ok((Arc::from(prepared), isolation))
}
.await;
let (prepared, isolation) = match prepared {
Ok(prepared) => prepared,
Err(error) => {
complete_resource_handoff(&mut resource_handoff);
return Err(BootFailure {
error,
prepared: None,
cow_handle: None,
});
}
};
let process_pid = sandbox::journaled_pid(&*prepared);
let vmm = sandbox::journaled_vmm(&*prepared);
let journal_error = sandbox::reconcile::ComputerStateRecord::new(
id,
process_pid,
net.map(NetworkAttachment::journaled),
None,
config,
None,
)
.map(|record| record.with_vmm(vmm.clone()))
.and_then(|record| sandbox::reconcile::write_state_record(vm_dir, &record))
.err();
let state = {
let mut computer = computer.lock().unwrap();
computer.prepared = Some(Arc::clone(&prepared));
computer.state
};
if matches!(state, ComputerState::Stopping | ComputerState::Stopped) {
complete_resource_handoff(&mut resource_handoff);
return Err(BootFailure {
error: ComputerError::WrongState {
id: id.to_owned(),
expected: "a sandbox still booting".into(),
actual: state.to_string(),
},
prepared: None,
cow_handle: None,
});
}
if let Some(error) = journal_error {
complete_resource_handoff(&mut resource_handoff);
return Err(BootFailure {
error,
prepared: None,
cow_handle: None,
});
}
let mut cow_handle = None;
let paths: Result<(PathBuf, PathBuf)> = async {
if fc_cfg.jailer.is_some() {
let journal = |cow: Option<&CowHandle>| {
sandbox::reconcile::ComputerStateRecord::new(
id,
process_pid,
net.map(NetworkAttachment::journaled),
cow,
config,
None,
)
.map(|record| record.with_vmm(vmm.clone()))
.and_then(|record| sandbox::reconcile::write_state_record(vm_dir, &record))
};
let staged = stage_rootfs_cow_or_copy(
cow_manager,
sandbox::staging_capability(prepared.staging()),
id,
&spec.rootfs,
&journal,
)
.await;
let rootfs = match staged {
Ok(staged) => {
cow_handle = staged.cow_handle;
staged.path
}
Err(StageError {
error,
cow_handle: acquired,
}) => {
cow_handle = acquired;
return Err(error);
}
};
Ok((PathBuf::from(&spec.kernel), rootfs))
} else {
let rootfs = match cow_manager.setup(id, &spec.rootfs).await {
Ok(handle) => {
cow_handle = Some(handle);
let record = sandbox::reconcile::ComputerStateRecord::new(
id,
process_pid,
net.map(NetworkAttachment::journaled),
cow_handle.as_ref(),
config,
None,
)?
.with_vmm(vmm.clone());
sandbox::reconcile::write_state_record(vm_dir, &record)?;
create_rootfs_symlink(vm_dir, &cow_handle.as_ref().unwrap().dm_device)?
}
Err(e) => {
if matches!(e, SnapshotError::Unavailable(_)) {
return Err(e.into());
}
debug!(
sandbox_id = %id,
error = %e,
"dm-snapshot unavailable, using rootfs directly"
);
spec.rootfs.clone()
}
};
Ok((PathBuf::from(&spec.kernel), PathBuf::from(rootfs)))
}
}
.await;
let state = {
let mut computer = computer.lock().unwrap();
if cow_handle.is_some() {
debug_assert!(computer.cow_handle.is_none());
computer.cow_handle = cow_handle.take();
}
computer.state
};
complete_resource_handoff(&mut resource_handoff);
let (kernel_path, rootfs_path) = paths.map_err(|error| BootFailure {
error,
prepared: None,
cow_handle: None,
})?;
if matches!(state, ComputerState::Stopping | ComputerState::Stopped) {
return Err(BootFailure {
error: ComputerError::WrongState {
id: id.to_owned(),
expected: "a sandbox still booting".into(),
actual: state.to_string(),
},
prepared: None,
cow_handle: None,
});
}
let failed = |error: ComputerError| BootFailure {
error,
prepared: None,
cow_handle: None,
};
let ready_gate = agents
.arm_readiness(prepared.as_ref())
.await
.map_err(failed)?;
let vm_spec =
build_vm_spec(id, spec, net, kernel_path, rootfs_path, isolation).map_err(failed)?;
let handle = prepared
.boot(vm_spec)
.await
.map_err(|error| failed(error.into()))?;
Ok((Arc::from(handle), ready_gate))
}
fn complete_resource_handoff(signal: &mut Option<tokio::sync::oneshot::Sender<()>>) {
if let Some(signal) = signal.take() {
let _ = signal.send(());
}
}