Skip to content
Closed
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
11 changes: 10 additions & 1 deletion src/boxlite/src/litebox/box_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,7 @@ impl BoxImpl {
use super::BoxBuilder;
use std::sync::Arc;

let state = self.state.read().clone();
let mut state = self.state.read().clone();
let is_first_start = state.status == BoxStatus::Configured;

// Retrieve the lock (allocated in create())
Expand All @@ -653,6 +653,15 @@ impl BoxImpl {
// LockGuard acquires lock on creation and releases on drop.
let _guard = LockGuard::new(&*locker);

if state.status != BoxStatus::Running {
state.advance_lifecycle_generation()?;
let mut current_state = self.state.write();
current_state.lifecycle_generation = state.lifecycle_generation;
self.runtime
.box_manager
.save_box(&self.config.id, &current_state)?;
}

// Build the box (lock is held)
// The returned cleanup_guard stays armed until we disarm it after all
// operations succeed. If any operation fails, the guard's Drop will
Expand Down
9 changes: 8 additions & 1 deletion src/boxlite/src/litebox/init/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,15 @@ impl BoxBuilder {
let status = state.status;
let reuse_rootfs = status == BoxStatus::Stopped;
let skip_guest_wait = status == BoxStatus::Running;
let lifecycle_generation = state.lifecycle_generation;

let ctx = InitPipelineContext::new(config, runtime.clone(), reuse_rootfs, skip_guest_wait);
let ctx = InitPipelineContext::new(
config,
runtime.clone(),
reuse_rootfs,
skip_guest_wait,
lifecycle_generation,
);
let ctx = Arc::new(Mutex::new(ctx));
let ctx_for_cleanup = Arc::clone(&ctx);

Expand Down
5 changes: 5 additions & 0 deletions src/boxlite/src/litebox/init/tasks/guest_init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ impl PipelineTask<InitCtx> for GuestInitTask {
container_mounts,
network_spec,
ca_cert_pem,
lifecycle_generation,
) =
{
let mut ctx = ctx.lock().await;
Expand Down Expand Up @@ -63,6 +64,7 @@ impl PipelineTask<InitCtx> for GuestInitTask {
container_mounts,
network_spec,
ca_cert_pem,
ctx.lifecycle_generation,
)
};

Expand All @@ -75,6 +77,7 @@ impl PipelineTask<InitCtx> for GuestInitTask {
&container_mounts,
&network_spec,
ca_cert_pem.as_deref(),
lifecycle_generation,
)
.await
.inspect_err(|e| log_task_error(&box_id, task_name, e))?;
Expand Down Expand Up @@ -104,6 +107,7 @@ async fn run_guest_init(
container_mounts: &[ContainerMount],
network_spec: &NetworkSpec,
ca_cert_pem: Option<&str>,
lifecycle_generation: u64,
) -> BoxliteResult<()> {
let container_id_str = container_id.as_str();

Expand Down Expand Up @@ -141,6 +145,7 @@ async fn run_guest_init(
rootfs_init.clone(),
container_mounts.to_vec(),
ca_certs,
lifecycle_generation,
)
.await?;
tracing::info!(container_id = %returned_id, "Container initialized");
Expand Down
3 changes: 3 additions & 0 deletions src/boxlite/src/litebox/init/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ pub struct InitPipelineContext {
pub config: BoxConfig,
pub runtime: SharedRuntimeImpl,
pub guard: CleanupGuard,
pub lifecycle_generation: u64,
pub reuse_rootfs: bool,
/// Skip waiting for guest ready signal (for reattach to running box).
pub skip_guest_wait: bool,
Expand All @@ -312,12 +313,14 @@ impl InitPipelineContext {
runtime: SharedRuntimeImpl,
reuse_rootfs: bool,
skip_guest_wait: bool,
lifecycle_generation: u64,
) -> Self {
let guard = CleanupGuard::new(runtime.clone(), config.id.clone());
Self {
config,
runtime,
guard,
lifecycle_generation,
reuse_rootfs,
skip_guest_wait,
layout: None,
Expand Down
29 changes: 29 additions & 0 deletions src/boxlite/src/litebox/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@ pub struct BoxState {
/// Serde default keeps existing DB rows readable without migration.
#[serde(default)]
pub error_reason: Option<String>,
/// Monotonic identity for each VM run.
#[serde(default)]
pub lifecycle_generation: u64,
}

/// Health status of a box.
Expand Down Expand Up @@ -315,9 +318,18 @@ impl BoxState {
lock_id: None,
health_status: HealthStatus::new(),
error_reason: None,
lifecycle_generation: 0,
}
}

pub fn advance_lifecycle_generation(&mut self) -> BoxliteResult<u64> {
self.lifecycle_generation = self
.lifecycle_generation
.checked_add(1)
.ok_or_else(|| BoxliteError::Internal("lifecycle generation overflow".to_string()))?;
Ok(self.lifecycle_generation)
}

/// Set lock ID and update timestamp.
pub fn set_lock_id(&mut self, lock_id: LockId) {
self.lock_id = Some(lock_id);
Expand Down Expand Up @@ -990,5 +1002,22 @@ mod tests {
assert_eq!(state.health_status.state, HealthState::None);
assert_eq!(state.health_status.failures, 0);
assert!(state.health_status.last_check.is_none());
assert_eq!(state.lifecycle_generation, 0);
}

#[test]
fn lifecycle_generation_advances_monotonically() {
let mut state = BoxState::new();

assert_eq!(state.advance_lifecycle_generation().unwrap(), 1);
assert_eq!(state.advance_lifecycle_generation().unwrap(), 2);
}

#[test]
fn lifecycle_generation_rejects_overflow() {
let mut state = BoxState::new();
state.lifecycle_generation = u64::MAX;

assert!(state.advance_lifecycle_generation().is_err());
}
}
2 changes: 2 additions & 0 deletions src/boxlite/src/portal/interfaces/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ impl ContainerInterface {
rootfs: ContainerRootfsInitConfig,
mounts: Vec<ContainerMount>,
ca_certs: Vec<String>,
lifecycle_generation: u64,
) -> BoxliteResult<String> {
let proto_config = ProtoContainerConfig {
entrypoint: image_config.final_cmd(),
Expand Down Expand Up @@ -140,6 +141,7 @@ impl ContainerInterface {
rootfs: Some(rootfs.into_proto()),
mounts: proto_mounts,
ca_certs: ca_certs.into_iter().map(|pem| CaCert { pem }).collect(),
lifecycle_generation,
};

let response = self.client.init(request).await?.into_inner();
Expand Down
58 changes: 58 additions & 0 deletions src/boxlite/tests/init_stdio.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
mod common;

use boxlite::runtime::options::{BoxOptions, BoxliteOptions};
use boxlite::{BoxCommand, BoxliteRuntime};
use std::time::Duration;

#[tokio::test]
async fn init_output_larger_than_pipes_does_not_block_entrypoint() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let handle = runtime
.create(
BoxOptions {
entrypoint: Some(vec![
"sh".to_string(),
"-c".to_string(),
"dd if=/dev/zero bs=1024 count=1024; dd if=/dev/zero bs=1024 count=1024 >&2; touch /tmp/init-output-drained; exec sleep 300"
.to_string(),
]),
..common::alpine_opts_auto()
},
None,
)
.await
.expect("create box");
let box_id = handle.id().to_string();

handle.start().await.expect("start box");

let ready = tokio::time::timeout(Duration::from_secs(15), async {
loop {
let execution = handle
.exec(
BoxCommand::new("test")
.arg("-f")
.arg("/tmp/init-output-drained"),
)
.await
.expect("start readiness check");
let result = execution.wait().await.expect("wait for readiness check");
if result.exit_code == 0 {
return;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
})
.await;

let _ = handle.stop().await;
let _ = runtime.remove(&box_id, false).await;
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;

ready.expect("init stdout or stderr filled its pipe before the entrypoint could continue");
}
89 changes: 65 additions & 24 deletions src/guest/src/container/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@

use super::command::ContainerCommand;
use super::spec::UserMount;
use super::stdio::ContainerStdio;
use super::stdio::{ContainerStdio, InitOutputCompletion, InitStdioFds};
use super::zygote::{self, InitSpec};
use super::{kill, spec, start};
use crate::layout::GuestLayout;
use crate::service::exec::InitHealthCheck;
use boxlite_shared::errors::{BoxliteError, BoxliteResult};
use libcontainer::container::Container as LibContainer;
use libcontainer::signal::Signal;
use std::collections::HashMap;
use std::os::fd::AsRawFd;
use std::path::{Path, PathBuf};

/// OCI container
Expand Down Expand Up @@ -55,6 +57,12 @@ pub struct Container {
is_shutdown: std::sync::atomic::AtomicBool,
}

pub(crate) struct StartedContainer {
pub container: Container,
pub init_pid: nix::unistd::Pid,
pub output_completion: InitOutputCompletion,
}

impl Container {
/// Create and start an OCI container
///
Expand All @@ -80,16 +88,15 @@ impl Container {
/// - Empty rootfs or entrypoint
/// - Failed to create container directory
/// - Failed to create or start container
/// - Init process exited immediately
pub fn start(
pub async fn start(
container_id: &str,
rootfs: impl AsRef<Path>,
entrypoint: Vec<String>,
env: Vec<String>,
workdir: impl AsRef<Path>,
user: &str,
user_mounts: Vec<UserMount>,
) -> BoxliteResult<Self> {
) -> BoxliteResult<StartedContainer> {
let rootfs = rootfs.as_ref();
let workdir = workdir.as_ref();

Expand Down Expand Up @@ -167,20 +174,29 @@ impl Container {

// Create stdio pipes before container creation.
// These keep the init process alive by holding stdin open.
let (stdio, init_fds) = ContainerStdio::new()?;

// Create and start container with custom stdio
start::create_container_with_stdio(container_id, &state_root, &bundle_path, init_fds)?;
start::start_container(container_id, &state_root)?;

Ok(Self {
id: container_id.to_string(),
state_root,
bundle_path,
env: env_map,
user: (uid, gid),
stdio,
is_shutdown: std::sync::atomic::AtomicBool::new(false),
let (stdio, init_fds, output_completion) = ContainerStdio::new()?;

create_init_via_zygote(
container_id,
state_root.clone(),
bundle_path.clone(),
init_fds,
)
.await?;
let init_pid = start::start_container(container_id, &state_root)?;

Ok(StartedContainer {
container: Self {
id: container_id.to_string(),
state_root,
bundle_path,
env: env_map,
user: (uid, gid),
stdio,
is_shutdown: std::sync::atomic::AtomicBool::new(false),
},
init_pid,
output_completion,
})
}

Expand Down Expand Up @@ -269,15 +285,12 @@ impl Container {
)
}

/// Drain init process stdout and stderr.
///
/// Reads all available data from the init process pipes using non-blocking I/O.
/// Can only be called once — subsequent calls return empty strings.
/// Return the output tail captured from the init process.
///
/// # Returns
///
/// `(stdout, stderr)` — captured output from the init process.
pub fn drain_init_output(&mut self) -> (String, String) {
pub fn drain_init_output(&self) -> (String, String) {
self.stdio.drain_output()
}

Expand All @@ -304,7 +317,6 @@ impl Container {
pub fn diagnose_exit(&mut self) -> String {
let container_state_path = self.container_state_path();

// Drain init process output before building diagnostics
let (init_stdout, init_stderr) = self.drain_init_output();

// Try to load container state from libcontainer
Expand Down Expand Up @@ -429,6 +441,35 @@ impl Container {
}
}

async fn create_init_via_zygote(
container_id: &str,
state_root: PathBuf,
bundle_path: PathBuf,
init_fds: InitStdioFds,
) -> BoxliteResult<()> {
let spec = InitSpec {
container_id: container_id.to_string(),
state_root,
bundle_path,
};

tokio::task::spawn_blocking(move || {
let raw_fds = [
init_fds.stdin.as_raw_fd(),
init_fds.stdout.as_raw_fd(),
init_fds.stderr.as_raw_fd(),
];
let result = zygote::ZYGOTE
.get()
.expect("zygote not started")
.build_init(spec, raw_fds);
drop(init_fds);
result
})
.await
.map_err(|error| BoxliteError::Internal(format!("init build join error: {error}")))?
}

// ====================
// Init Health Check
// ====================
Expand Down
Loading
Loading