use std::{fs::File, sync::Mutex};
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>,
}
impl Machine {
pub(crate) const fn new(child: Child) -> Self {
Self {
child,
pauses: Mutex::new(0),
}
}
#[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 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()
}
}
#[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
}