use std::{
fs::File,
sync::Mutex,
time::{Duration, Instant},
};
use crate::{
control::{
decrement, AttachRequest, Attachment, AttachmentKind, ControlError, PauseGuard,
ProcessInfo, ResourceUpdate, ShutdownPolicy, Signal, SignalTarget,
},
Child, Domain, Error, Exit, Terminal,
};
#[derive(Debug)]
pub struct Machine {
child: Child,
pauses: Mutex<usize>,
store: Option<StoreChannel>,
}
#[derive(Debug)]
struct StoreChannel {
server: std::sync::Arc<crate::checkpoint_stream::SinkServer>,
trigger: crate::ffi::Trigger,
acceptor: Option<std::thread::JoinHandle<()>>,
}
impl Drop for StoreChannel {
fn drop(&mut self) {
self.server.stop();
if let Some(acceptor) = self.acceptor.take() {
let _ = acceptor.join();
}
}
}
impl Machine {
pub(crate) const fn new(child: Child) -> Self {
Self {
child,
pauses: Mutex::new(0),
store: None,
}
}
pub(crate) fn with_store(
child: Child,
server: std::sync::Arc<crate::checkpoint_stream::SinkServer>,
trigger: crate::ffi::Trigger,
acceptor: std::thread::JoinHandle<()>,
) -> Self {
Self {
child,
pauses: Mutex::new(0),
store: Some(StoreChannel {
server,
trigger,
acceptor: Some(acceptor),
}),
}
}
pub fn checkpoint_into_store(&self, timeout: Duration) -> Result<(), ControlError> {
let channel = self
.store
.as_ref()
.ok_or_else(|| ControlError::unsupported("checkpoint"))?;
channel.trigger.bump();
crate::ffi::signal(self.id(), checkpoint_interrupt_signal())
.map_err(|error| checkpoint_error("interrupt checkpoint target", &error))?;
let deadline = Instant::now() + timeout;
loop {
if channel.server.committed() {
return Ok(());
}
if let Some(failure) = channel.server.failure() {
return Err(checkpoint_context(failure));
}
if self.child.completed() {
return Err(checkpoint_context(
"engine exited without committing a complete checkpoint image",
));
}
if Instant::now() >= deadline {
return Err(checkpoint_context(
"checkpoint deadline expired before the image was committed",
));
}
std::thread::sleep(Duration::from_millis(2));
}
}
#[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(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 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
}