use std::{
fs::{File, OpenOptions},
io::{Read, Seek, SeekFrom, Write},
path::PathBuf,
sync::Mutex,
time::{Duration, Instant},
};
use crate::{
control::{
decrement, AttachRequest, Attachment, AttachmentKind, ControlError, EventStream,
ExtensionHandle, NetworkUpdate, PauseGuard, ProcessInfo, ResourceUpdate, ShutdownPolicy,
Signal, SignalTarget,
},
Child, Domain, Error, Exit, Terminal,
};
#[derive(Debug)]
pub struct Machine {
child: Child,
pauses: Mutex<usize>,
checkpoint_directory: Option<PathBuf>,
}
impl Machine {
pub(crate) const fn new(child: Child, checkpoint_directory: Option<PathBuf>) -> Self {
Self {
child,
pauses: Mutex::new(0),
checkpoint_directory,
}
}
#[must_use]
pub fn id(&self) -> u64 {
self.child.id()
}
#[must_use]
pub const fn domain(&self) -> Domain {
self.child.domain()
}
pub fn take_stdin(&mut self) -> Option<File> {
self.child.take_stdin()
}
pub fn take_stdout(&mut self) -> Option<File> {
self.child.take_stdout()
}
pub fn take_stderr(&mut self) -> Option<File> {
self.child.take_stderr()
}
pub fn take_terminal(&mut self) -> Option<Terminal> {
self.child.take_terminal()
}
pub fn try_wait(&mut self) -> Result<Option<Exit>, Error> {
self.child.try_wait()
}
pub fn initial_process(&self) -> Result<ProcessInfo, ControlError> {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
loop {
if let Some(process) = self
.processes()?
.into_iter()
.find(|process| process.initial)
{
return Ok(process);
}
if self.child.completed() || std::time::Instant::now() >= deadline {
return Err(ControlError::finished("initial_process"));
}
std::thread::yield_now();
}
}
pub fn signal(&self, target: SignalTarget, signal: Signal) -> Result<(), ControlError> {
if self.child.completed() {
return Err(ControlError::finished("signal"));
}
match target {
SignalTarget::InitialProcess => self
.child
.signal(signal.host_number())
.map_err(|error| ControlError::engine("signal", &error)),
}
}
pub fn pause(&self) -> Result<PauseGuard<'_>, ControlError> {
let mut pauses = self.pauses.lock().map_err(|_| ControlError {
category: crate::ControlErrorCategory::Host,
operation: "pause",
context: "pause state lock is poisoned".into(),
})?;
if *pauses == 0 {
self.child
.signal(stop_signal())
.map_err(|error| ControlError::engine("pause", &error))?;
}
*pauses = pauses.checked_add(1).ok_or_else(|| ControlError {
category: crate::ControlErrorCategory::Host,
operation: "pause",
context: "pause reference count is exhausted".into(),
})?;
Ok(PauseGuard {
machine: self,
active: true,
})
}
pub fn checkpoint(&self, timeout: Duration) -> Result<PathBuf, ControlError> {
let directory = self
.checkpoint_directory
.as_ref()
.ok_or_else(|| ControlError::unsupported("checkpoint"))?;
if directory.exists() {
let mut entries = std::fs::read_dir(directory)
.map_err(|error| checkpoint_error("inspect checkpoint directory", &error))?;
if entries.next().transpose().map_err(|error| {
checkpoint_error("inspect checkpoint directory", &error)
})?.is_some()
{
return Err(checkpoint_context(
"checkpoint destination already contains data",
));
}
} else {
std::fs::create_dir(directory)
.map_err(|error| checkpoint_error("create checkpoint directory", &error))?;
}
let trigger = PathBuf::from(format!("{}.trigger", directory.display()));
let mut file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&trigger)
.map_err(|error| checkpoint_error("open checkpoint trigger", &error))?;
let mut bytes = [0_u8; 4];
let read = file
.read(&mut bytes)
.map_err(|error| checkpoint_error("read checkpoint trigger", &error))?;
if read != 0 && read != bytes.len() {
return Err(checkpoint_context("checkpoint trigger is corrupt"));
}
let generation = u32::from_le_bytes(bytes).wrapping_add(1).max(1);
file.seek(SeekFrom::Start(0))
.and_then(|_| file.write_all(&generation.to_le_bytes()))
.and_then(|_| file.set_len(4))
.and_then(|_| file.sync_data())
.map_err(|error| checkpoint_error("publish checkpoint request", &error))?;
crate::ffi::signal(self.id(), checkpoint_interrupt_signal())
.map_err(|error| checkpoint_error("interrupt checkpoint target", &error))?;
let manifest = directory.join("MANIFEST");
let deadline = Instant::now() + timeout;
loop {
if manifest.is_file() {
return Ok(directory.clone());
}
if self.child.completed() {
return Err(checkpoint_context(
"engine exited without publishing a complete checkpoint manifest",
));
}
if Instant::now() >= deadline {
return Err(checkpoint_context(
"checkpoint deadline expired before manifest publication",
));
}
std::thread::sleep(Duration::from_millis(2));
}
}
pub(crate) fn release_pause(&self, report: bool) -> Result<(), ControlError> {
let pauses = self.pauses.lock().map_err(|_| ControlError {
category: crate::ControlErrorCategory::Host,
operation: "resume",
context: "pause state lock is poisoned".into(),
})?;
if !decrement(pauses) {
return Ok(());
}
self.child.signal(continue_signal()).map_err(|error| {
let error = ControlError::engine("resume", &error);
if report {
error
} else {
ControlError {
context: "automatic resume failed".into(),
..error
}
}
})
}
pub fn shutdown(&mut self, policy: ShutdownPolicy) -> Result<(), ControlError> {
match policy {
ShutdownPolicy::Signal(signal) => self.signal(SignalTarget::InitialProcess, signal),
ShutdownPolicy::Force => self
.force_stop()
.map_err(|error| ControlError::engine("shutdown", &error)),
}
}
pub fn processes(&self) -> Result<Vec<ProcessInfo>, ControlError> {
crate::ffi::domain_processes(self.domain().identity(), self.id(), 65_536)
.map(|processes| {
processes
.into_iter()
.map(|process| ProcessInfo {
host_id: process.host_id,
initial: process.initial != 0,
})
.collect()
})
.map_err(|status| {
ControlError::engine("processes", &Error::Engine { status, detail: 0 })
})
}
pub fn attach(&mut self, request: AttachRequest) -> Result<Attachment, ControlError> {
let AttachRequest { streams } = request;
let wants = |kind| streams.contains(&kind);
let missing = (wants(AttachmentKind::Stdin) && self.child.stdin.is_none())
|| (wants(AttachmentKind::Stdout) && self.child.stdout.is_none())
|| (wants(AttachmentKind::Stderr) && self.child.stderr.is_none())
|| (wants(AttachmentKind::Terminal) && self.child.terminal.is_none());
if missing {
return Err(ControlError {
category: crate::ControlErrorCategory::Invalid,
operation: "attach",
context: "a requested stream is absent or already attached".into(),
});
}
let attachment = Attachment {
stdin: wants(AttachmentKind::Stdin)
.then(|| self.take_stdin())
.flatten(),
stdout: wants(AttachmentKind::Stdout)
.then(|| self.take_stdout())
.flatten(),
stderr: wants(AttachmentKind::Stderr)
.then(|| self.take_stderr())
.flatten(),
terminal: wants(AttachmentKind::Terminal)
.then(|| self.take_terminal())
.flatten(),
};
Ok(attachment)
}
pub fn update_resources(&self, _update: ResourceUpdate) -> Result<(), ControlError> {
Err(ControlError::unsupported("update_resources"))
}
pub fn update_network(&self, _update: NetworkUpdate) -> Result<(), ControlError> {
Err(ControlError::unsupported("update_network"))
}
pub fn hotplug(
&self,
_extension: crate::extension::ExtensionSpec,
) -> Result<ExtensionHandle, ControlError> {
Err(ControlError::unsupported("hotplug"))
}
pub fn events(&self) -> Result<EventStream, ControlError> {
Err(ControlError::unsupported("events"))
}
pub fn force_stop(&mut self) -> Result<(), Error> {
self.child.force_stop()
}
pub fn wait(self) -> Result<Exit, Error> {
self.child.wait()
}
}
fn checkpoint_context(context: impl Into<String>) -> ControlError {
ControlError {
category: crate::ControlErrorCategory::Host,
operation: "checkpoint",
context: context.into(),
}
}
fn checkpoint_error(context: &str, error: &std::io::Error) -> ControlError {
checkpoint_context(format!("{context}: {error}"))
}
#[cfg(target_os = "linux")]
const fn checkpoint_interrupt_signal() -> i32 {
23 }
#[cfg(target_os = "macos")]
const fn checkpoint_interrupt_signal() -> i32 {
29 }
#[cfg(target_os = "linux")]
const fn stop_signal() -> i32 {
19
}
#[cfg(target_os = "macos")]
const fn stop_signal() -> i32 {
17
}
#[cfg(target_os = "linux")]
const fn continue_signal() -> i32 {
18
}
#[cfg(target_os = "macos")]
const fn continue_signal() -> i32 {
19
}