use std::fs::File;
use std::os::fd::{AsRawFd, OwnedFd};
use std::process::ExitStatus;
use nix::{
sys::signal::{self, Signal},
unistd::Pid,
};
use tempfile::TempDir;
use tokio::process::Child;
use microsandbox_metrics::MetricsRegistry;
use crate::MicrosandboxResult;
pub struct ProcessHandle {
pid: u32,
sandbox_name: String,
child: Child,
detached: bool,
parent_watchdog: Option<OwnedFd>,
metrics_reservation: Option<MetricsReservationCleanup>,
_file_mounts_staging: Option<TempDir>,
_disk_locks: Vec<File>,
}
#[derive(Clone, Debug)]
pub(crate) struct MetricsReservationCleanup {
shm_name: String,
slot: u32,
generation: u64,
}
impl ProcessHandle {
pub(crate) fn new(
pid: u32,
sandbox_name: String,
child: Child,
file_mounts_staging: Option<TempDir>,
disk_locks: Vec<File>,
parent_watchdog: Option<OwnedFd>,
metrics_reservation: Option<MetricsReservationCleanup>,
) -> Self {
Self {
pid,
sandbox_name,
child,
detached: false,
_file_mounts_staging: file_mounts_staging,
_disk_locks: disk_locks,
parent_watchdog,
metrics_reservation,
}
}
pub fn pid(&self) -> u32 {
self.pid
}
pub fn sandbox_name(&self) -> &str {
&self.sandbox_name
}
pub fn kill(&self) -> MicrosandboxResult<()> {
tracing::debug!(pid = self.pid, sandbox = %self.sandbox_name, "sending SIGKILL");
signal::kill(Pid::from_raw(self.pid as i32), Signal::SIGKILL)?;
Ok(())
}
pub fn drain(&self) -> MicrosandboxResult<()> {
tracing::debug!(pid = self.pid, sandbox = %self.sandbox_name, "sending SIGUSR1 (drain)");
signal::kill(Pid::from_raw(self.pid as i32), Signal::SIGUSR1)?;
Ok(())
}
pub async fn wait(&mut self) -> MicrosandboxResult<ExitStatus> {
tracing::debug!(pid = self.pid, sandbox = %self.sandbox_name, "waiting for exit");
let status = self.child.wait().await?;
tracing::debug!(pid = self.pid, ?status, "process exited");
self.cleanup_metrics_reservation();
Ok(status)
}
pub fn try_wait(&mut self) -> MicrosandboxResult<Option<ExitStatus>> {
Ok(self.child.try_wait()?)
}
pub fn disarm(&mut self) {
self.detached = true;
if let Some(parent_watchdog) = &self.parent_watchdog
&& let Err(err) = send_parent_watchdog_detach(parent_watchdog)
{
tracing::debug!(
error = %err,
sandbox = %self.sandbox_name,
"failed to send parent-watch detach"
);
}
if let Some(td) = self._file_mounts_staging.take() {
let _ = td.keep();
}
}
fn cleanup_metrics_reservation(&mut self) {
let Some(metrics_reservation) = self.metrics_reservation.take() else {
return;
};
metrics_reservation.release_reserved(&self.sandbox_name);
}
}
impl MetricsReservationCleanup {
pub(crate) fn new(shm_name: String, slot: u32, generation: u64) -> Self {
Self {
shm_name,
slot,
generation,
}
}
fn release_reserved(&self, sandbox_name: &str) {
let registry = match MetricsRegistry::open(&self.shm_name) {
Ok(registry) => registry,
Err(err) => {
tracing::debug!(
error = %err,
sandbox = %sandbox_name,
"metrics reservation cleanup: failed to open registry"
);
return;
}
};
if let Err(err) = registry.release_reserved(self.slot, self.generation) {
tracing::debug!(
error = %err,
sandbox = %sandbox_name,
slot = self.slot,
"metrics reservation cleanup: failed to release reserved slot"
);
}
}
}
impl Drop for ProcessHandle {
fn drop(&mut self) {
if self.detached {
return;
}
self.cleanup_metrics_reservation();
if self.parent_watchdog.is_some() {
tracing::debug!(
sandbox = %self.sandbox_name,
"drop: closing parent watchdog writer for attached sandbox cleanup"
);
return;
}
if let Ok(None) = self.child.try_wait()
&& let Some(pid) = self.child.id()
{
tracing::debug!(pid, sandbox = %self.sandbox_name, "drop: sending SIGTERM safety net");
let _ = signal::kill(Pid::from_raw(pid as i32), Signal::SIGTERM);
}
}
}
fn send_parent_watchdog_detach(fd: &OwnedFd) -> std::io::Result<()> {
let byte = [microsandbox_runtime::vm::PARENT_WATCH_DETACH];
loop {
let written = unsafe {
libc::write(
fd.as_raw_fd(),
byte.as_ptr().cast::<libc::c_void>(),
byte.len(),
)
};
if written == byte.len() as isize {
return Ok(());
}
if written < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
return Err(err);
}
return Err(std::io::Error::new(
std::io::ErrorKind::WriteZero,
"failed to write parent-watch detach byte",
));
}
}
#[cfg(test)]
mod tests {
use std::io::Read;
use std::os::fd::FromRawFd;
use super::*;
#[test]
fn test_send_parent_watchdog_detach_writes_detach_byte() {
let mut fds = [0; 2];
assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
let read_fd = unsafe { OwnedFd::from_raw_fd(fds[0]) };
let write_fd = unsafe { OwnedFd::from_raw_fd(fds[1]) };
send_parent_watchdog_detach(&write_fd).unwrap();
let mut reader = std::fs::File::from(read_fd);
let mut byte = [0_u8; 1];
reader.read_exact(&mut byte).unwrap();
assert_eq!(byte[0], microsandbox_runtime::vm::PARENT_WATCH_DETACH);
}
}