use super::*;
use arcbox_snapshot::SnapshotError;
const READY_PROBE_DEFAULT_TIMEOUT_SECS: u64 = 30;
const READY_PROBE_RETRY_MS: u64 = 500;
pub async fn run_ready_probe(
probe: &crate::template_catalog::ReadyProbeSpec,
agent: &dyn GuestAgent,
) -> Result<()> {
use crate::template_catalog::ReadyProbeSpec;
let effective = |timeout_seconds: u32| {
std::time::Duration::from_secs(if timeout_seconds == 0 {
READY_PROBE_DEFAULT_TIMEOUT_SECS
} else {
u64::from(timeout_seconds)
})
};
match probe {
ReadyProbeSpec::Port {
port,
timeout_seconds,
} => match agent
.wait_for_port(*port, effective(*timeout_seconds))
.await?
{
PortWait::Listening => Ok(()),
PortWait::Deadline => Err(ComputerError::DeadlineExceeded(format!(
"no listener on port {port} within the ready-probe deadline"
))),
},
ReadyProbeSpec::Command {
cmd,
timeout_seconds,
} => {
let deadline = tokio::time::Instant::now() + effective(*timeout_seconds);
let mut last_status: Option<String> = None;
loop {
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
return Err(ComputerError::DeadlineExceeded(format!(
"ready-probe command did not exit 0 within the deadline ({})",
last_status.as_deref().unwrap_or("no attempt completed")
)));
}
let budget_secs = u32::try_from(remaining.as_secs().max(1)).unwrap_or(u32::MAX);
match tokio::time::timeout(remaining, run_probe_command(cmd, agent, budget_secs))
.await
{
Ok(Ok(ExitStatus::Code(0))) => return Ok(()),
Ok(Ok(status)) => last_status = Some(format!("last exit: {status:?}")),
Ok(Err(error)) => last_status = Some(format!("last error: {error}")),
Err(_) => {
return Err(ComputerError::DeadlineExceeded(format!(
"ready-probe command did not exit within the deadline ({})",
last_status.as_deref().unwrap_or("no attempt completed")
)));
}
}
if tokio::time::Instant::now() >= deadline {
return Err(ComputerError::DeadlineExceeded(format!(
"ready-probe command did not exit 0 within the deadline ({})",
last_status.as_deref().unwrap_or("no attempt completed")
)));
}
let nap = std::time::Duration::from_millis(READY_PROBE_RETRY_MS)
.min(deadline.saturating_duration_since(tokio::time::Instant::now()));
tokio::time::sleep(nap).await;
}
}
}
}
async fn run_probe_command(
cmd: &[String],
agent: &dyn GuestAgent,
timeout_seconds: u32,
) -> Result<ExitStatus> {
let start = StartCommand {
cmd: cmd.to_vec(),
env: HashMap::new(),
working_dir: String::new(),
user: String::new(),
tty: false,
tty_width: 80,
tty_height: 24,
timeout_seconds,
};
let (_input, mut output) = agent.exec(start).await?;
while let Some(chunk) = output.recv().await {
if let OutputChunk::Exit(status) = chunk? {
return Ok(status);
}
}
Err(ComputerError::Vsock(
"probe command stream ended without an exit status".into(),
))
}
pub struct StageError {
pub error: ComputerError,
pub cow_handle: Option<CowHandle>,
}
pub struct StagedRootfs {
pub path: PathBuf,
pub cow_handle: Option<CowHandle>,
}
#[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 stage_rootfs_cow_or_copy(
cow_manager: &CowManager,
staging: &dyn Staging,
owner_id: &str,
rootfs: &str,
journal: &(dyn Fn(Option<&CowHandle>) -> Result<()> + Sync),
) -> std::result::Result<StagedRootfs, StageError> {
let fail =
|error: ComputerError, cow_handle: Option<CowHandle>| StageError { error, cow_handle };
let copy = async || {
staging
.stage_disk(ROOTFS_DISK_ID, DiskSource::Image(Path::new(rootfs)))
.await
.map_err(ComputerError::from)
};
match cow_manager.setup(owner_id, rootfs).await {
Ok(handle) => {
if let Err(error) = journal(Some(&handle)) {
return Err(fail(error, Some(handle)));
}
match staging
.stage_disk(
ROOTFS_DISK_ID,
DiskSource::Device(Path::new(&handle.dm_device)),
)
.await
{
Ok(path) => Ok(StagedRootfs {
path,
cow_handle: Some(handle),
}),
Err(e) => {
debug!(
owner_id,
error = %e,
"the overlay could not be brought into the vm's area, falling back to a rootfs copy"
);
if let Err(error) = cow_manager.teardown_checked(&handle).await {
return Err(fail(error.into(), Some(handle)));
}
journal(None).map_err(|error| fail(error, None))?;
Ok(StagedRootfs {
path: copy().await.map_err(|error| fail(error, None))?,
cow_handle: None,
})
}
}
}
Err(e) if matches!(e, SnapshotError::Unavailable(_)) => Err(fail(e.into(), None)),
Err(e) => {
debug!(
owner_id,
error = %e,
"dm-snapshot unavailable, staging a copy of the rootfs"
);
Ok(StagedRootfs {
path: copy().await.map_err(|error| fail(error, None))?,
cow_handle: None,
})
}
}
}
pub fn create_rootfs_symlink(vm_dir: &Path, dm_device: &str) -> Result<String> {
let link_path = vm_dir.join("rootfs.link");
let _ = std::fs::remove_file(&link_path);
std::os::unix::fs::symlink(dm_device, &link_path).map_err(ComputerError::Io)?;
link_path
.to_str()
.map(str::to_owned)
.ok_or_else(|| ComputerError::Config(format!("non-UTF-8 path: {}", link_path.display())))
}