use super::*;
fn claim_running(id: &SandboxId, instances: &InstanceMap) -> Result<()> {
let arc = instances
.read()
.unwrap()
.get(id)
.cloned()
.ok_or_else(|| VmmError::NotFound(id.clone()))?;
let mut inst = arc.lock().unwrap();
if inst.state != SandboxState::Ready {
return Err(VmmError::WrongState {
id: id.clone(),
expected: "Ready".into(),
actual: inst.state.to_string(),
});
}
inst.state = SandboxState::Running;
Ok(())
}
fn release_running(id: &SandboxId, instances: &InstanceMap) {
let arc = instances.read().unwrap().get(id).cloned();
if let Some(arc) = arc {
let mut inst = arc.lock().unwrap();
if inst.state == SandboxState::Running {
inst.state = SandboxState::Ready;
}
}
}
fn finish_workload(
id: &SandboxId,
exit_code: i32,
instances: &InstanceMap,
events_tx: &broadcast::Sender<SandboxEvent>,
) {
let arc = instances.read().unwrap().get(id).cloned();
if let Some(arc) = arc {
let mut inst = arc.lock().unwrap();
inst.last_exit_code = Some(exit_code);
inst.last_exited_at = Some(Utc::now());
if matches!(inst.state, SandboxState::Running | SandboxState::Stopping) {
inst.state = SandboxState::Ready;
}
}
let _ = events_tx
.send(SandboxEvent::new(id, "idle").with_attr("exit_code", &exit_code.to_string()));
}
fn spawn_exit_watcher(
id: &SandboxId,
inner_rx: tokio::sync::mpsc::Receiver<Result<OutputChunk>>,
instances: &InstanceMap,
events_tx: &broadcast::Sender<SandboxEvent>,
) -> tokio::sync::mpsc::Receiver<Result<OutputChunk>> {
let (wrapped_tx, wrapped_rx) = tokio::sync::mpsc::channel(64);
let instances = Arc::clone(instances);
let events_tx = events_tx.clone();
let sandbox_id = id.clone();
tokio::spawn(async move {
let mut inner_rx = inner_rx;
let mut consumer_gone = false;
while let Some(result) = inner_rx.recv().await {
if let Ok(chunk) = &result
&& chunk.stream == "exit"
{
finish_workload(&sandbox_id, chunk.exit_code, &instances, &events_tx);
}
if !consumer_gone && wrapped_tx.send(result).await.is_err() {
consumer_gone = true;
}
}
});
wrapped_rx
}
pub(super) async fn start_run_workload(
id: &SandboxId,
uds_path: &Path,
start: StartCommand,
instances: &super::InstanceMap,
events_tx: &broadcast::Sender<SandboxEvent>,
) -> Result<tokio::sync::mpsc::Receiver<Result<OutputChunk>>> {
claim_running(id, instances)?;
let inner_rx = match vsock::run(uds_path, start).await {
Ok(rx) => rx,
Err(e) => {
release_running(id, instances);
return Err(e);
}
};
let _ = events_tx.send(SandboxEvent::new(id, "running"));
Ok(spawn_exit_watcher(id, inner_rx, instances, events_tx))
}
impl SandboxManager {
#[allow(
clippy::too_many_arguments,
reason = "public API mirrors workload request"
)]
pub async fn run_in_sandbox(
&self,
id: &SandboxId,
cmd: Vec<String>,
env: HashMap<String, String>,
working_dir: String,
user: String,
tty: bool,
tty_size: Option<(u16, u16)>,
timeout_seconds: u32,
) -> Result<tokio::sync::mpsc::Receiver<Result<OutputChunk>>> {
let uds_path = self.require_ready_vsock(id)?;
let start = StartCommand {
cmd,
env,
working_dir,
user,
tty,
tty_width: tty_size.map_or(80, |(w, _)| w),
tty_height: tty_size.map_or(24, |(_, h)| h),
timeout_seconds,
};
start_run_workload(id, &uds_path, start, &self.instances, &self.events_tx).await
}
#[allow(clippy::too_many_arguments, reason = "public API mirrors exec request")]
pub async fn exec_in_sandbox(
&self,
id: &SandboxId,
cmd: Vec<String>,
env: HashMap<String, String>,
working_dir: String,
user: String,
tty: bool,
tty_size: Option<(u16, u16)>,
timeout_seconds: u32,
) -> Result<(
tokio::sync::mpsc::Sender<ExecInputMsg>,
tokio::sync::mpsc::Receiver<Result<OutputChunk>>,
)> {
let uds_path = self.require_ready_vsock(id)?;
let start = StartCommand {
cmd,
env,
working_dir,
user,
tty,
tty_width: tty_size.map_or(80, |(w, _)| w),
tty_height: tty_size.map_or(24, |(_, h)| h),
timeout_seconds,
};
claim_running(id, &self.instances)?;
let (in_tx, inner_rx) = match vsock::exec(&uds_path, start).await {
Ok(pair) => pair,
Err(e) => {
release_running(id, &self.instances);
return Err(e);
}
};
let _ = self.events_tx.send(SandboxEvent::new(id, "running"));
let wrapped_rx = spawn_exit_watcher(id, inner_rx, &self.instances, &self.events_tx);
Ok((in_tx, wrapped_rx))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn instance_map(id: &str, state: SandboxState) -> InstanceMap {
let mut inst = SandboxInstance::new(
id.to_owned(),
SandboxSpec::default(),
None,
PathBuf::from("/tmp/does-not-matter"),
);
inst.state = state;
let map = HashMap::from([(id.to_owned(), Arc::new(Mutex::new(inst)))]);
Arc::new(RwLock::new(map))
}
fn state_of(id: &str, instances: &InstanceMap) -> SandboxState {
instances.read().unwrap()[id].lock().unwrap().state
}
#[test]
fn claim_running_transitions_from_ready_only() {
let instances = instance_map("s", SandboxState::Ready);
claim_running(&"s".to_owned(), &instances).unwrap();
assert_eq!(state_of("s", &instances), SandboxState::Running);
let err = claim_running(&"s".to_owned(), &instances).unwrap_err();
assert!(matches!(err, VmmError::WrongState { .. }));
assert_eq!(state_of("s", &instances), SandboxState::Running);
}
#[test]
fn claim_running_rejects_missing_and_stopping() {
let instances = instance_map("s", SandboxState::Stopping);
assert!(matches!(
claim_running(&"s".to_owned(), &instances).unwrap_err(),
VmmError::WrongState { .. }
));
assert!(matches!(
claim_running(&"missing".to_owned(), &instances).unwrap_err(),
VmmError::NotFound(_)
));
}
#[test]
fn release_running_only_downgrades_running() {
let instances = instance_map("s", SandboxState::Running);
release_running(&"s".to_owned(), &instances);
assert_eq!(state_of("s", &instances), SandboxState::Ready);
instances.read().unwrap()["s"].lock().unwrap().state = SandboxState::Stopping;
release_running(&"s".to_owned(), &instances);
assert_eq!(state_of("s", &instances), SandboxState::Stopping);
}
#[tokio::test]
async fn watcher_restores_ready_even_when_consumer_disconnects() {
let instances = instance_map("s", SandboxState::Running);
let (events_tx, _events_rx) = broadcast::channel(16);
let (inner_tx, inner_rx) = tokio::sync::mpsc::channel(8);
let wrapped_rx = spawn_exit_watcher(&"s".to_owned(), inner_rx, &instances, &events_tx);
drop(wrapped_rx);
inner_tx
.send(Ok(OutputChunk {
stream: "stdout".into(),
data: b"noise".to_vec(),
exit_code: 0,
}))
.await
.unwrap();
inner_tx
.send(Ok(OutputChunk {
stream: "exit".into(),
data: vec![],
exit_code: 7,
}))
.await
.unwrap();
drop(inner_tx);
for _ in 0..50 {
if state_of("s", &instances) == SandboxState::Ready {
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
assert_eq!(state_of("s", &instances), SandboxState::Ready);
let last_exit_code = {
let guard = instances.read().unwrap();
let code = guard["s"].lock().unwrap().last_exit_code;
code
};
assert_eq!(last_exit_code, Some(7));
}
#[tokio::test]
async fn watcher_forwards_then_finishes_for_a_live_consumer() {
let instances = instance_map("s", SandboxState::Running);
let (events_tx, mut events_rx) = broadcast::channel(16);
let (inner_tx, inner_rx) = tokio::sync::mpsc::channel(8);
let mut wrapped_rx = spawn_exit_watcher(&"s".to_owned(), inner_rx, &instances, &events_tx);
inner_tx
.send(Ok(OutputChunk {
stream: "stdout".into(),
data: b"hello".to_vec(),
exit_code: 0,
}))
.await
.unwrap();
let first = wrapped_rx.recv().await.unwrap().unwrap();
assert_eq!(first.data, b"hello");
inner_tx
.send(Ok(OutputChunk {
stream: "exit".into(),
data: vec![],
exit_code: 0,
}))
.await
.unwrap();
drop(inner_tx);
let exit = wrapped_rx.recv().await.unwrap().unwrap();
assert_eq!(exit.stream, "exit");
assert!(wrapped_rx.recv().await.is_none());
assert_eq!(state_of("s", &instances), SandboxState::Ready);
let ev = events_rx.recv().await.unwrap();
assert_eq!(ev.action, "idle");
}
}