use command_fds::CommandFdExt;
use ic_host_fs::durable::{self, RegularFileLockError};
use std::{
fmt,
fs::{self, File, Metadata},
io,
os::{fd::AsRawFd, unix::fs::MetadataExt},
path::{Path, PathBuf},
process::{Child, Command},
thread,
time::{Duration, Instant},
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct FileIdentity {
pub device: u64,
pub inode: u64,
}
impl FileIdentity {
pub fn from_file(file: &File) -> io::Result<Self> {
let metadata = file.metadata()?;
if !metadata.is_file() {
return Err(io::Error::from(io::ErrorKind::InvalidInput));
}
Ok(Self::from_metadata(&metadata))
}
fn from_metadata(metadata: &Metadata) -> Self {
Self {
device: metadata.dev(),
inode: metadata.ino(),
}
}
fn require(self, metadata: &Metadata, path: &Path) -> Result<(), CustodyError> {
if !metadata.is_file() || Self::from_metadata(metadata) != self {
return Err(CustodyError::IdentityChanged {
path: path.to_path_buf(),
});
}
Ok(())
}
}
#[derive(Debug)]
pub struct CommandCustody {
file: File,
path: PathBuf,
identity: FileIdentity,
dispatched: bool,
}
impl CommandCustody {
pub fn acquire(path: &Path) -> Result<Self, CustodyError> {
let file = durable::open_private_regular_lock_file(path).map_err(CustodyError::Lock)?;
try_lock(&file, path)?;
Ok(Self {
identity: FileIdentity::from_file(&file)?,
file,
path: path.to_path_buf(),
dispatched: false,
})
}
#[must_use]
pub const fn identity(&self) -> FileIdentity {
self.identity
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
pub fn spawn(
&mut self,
mut command: Command,
descriptor_environment: &str,
) -> Result<Child, CustodyError> {
if self.dispatched {
return Err(CustodyError::AlreadyDispatched {
path: self.path.clone(),
});
}
self.identity
.require(&fs::symlink_metadata(&self.path)?, &self.path)?;
self.dispatched = true;
let descriptor = rustix::io::fcntl_dupfd_cloexec(&self.file, 3).map_err(io::Error::from)?;
command.env(descriptor_environment, descriptor.as_raw_fd().to_string());
command.preserved_fds(vec![descriptor]);
let child = command.spawn();
drop(command);
child.map_err(CustodyError::Io)
}
pub fn finish(
self,
grace: Duration,
poll_interval: Duration,
) -> Result<QuiescenceGuard, CustodyError> {
if poll_interval.is_zero() {
return Err(io::Error::from(io::ErrorKind::InvalidInput).into());
}
let deadline = Instant::now()
.checked_add(grace)
.ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
let Self {
file,
path,
identity,
..
} = self;
drop(file);
loop {
match QuiescenceGuard::acquire(&path, identity) {
Err(CustodyError::InFlight { .. }) if Instant::now() < deadline => {
thread::sleep(
poll_interval.min(deadline.saturating_duration_since(Instant::now())),
);
}
result => return result,
}
}
}
}
#[derive(Debug)]
pub struct QuiescenceGuard {
file: File,
identity: FileIdentity,
}
impl QuiescenceGuard {
pub fn acquire(path: &Path, expected: FileIdentity) -> Result<Self, CustodyError> {
let metadata = fs::symlink_metadata(path)?;
if metadata.is_file() {
expected.require(&metadata, path)?;
}
let file = durable::open_existing_regular_lock_file(path).map_err(CustodyError::Lock)?;
try_lock(&file, path)?;
expected.require(&file.metadata()?, path)?;
Ok(Self {
file,
identity: expected,
})
}
#[must_use]
pub const fn identity(&self) -> FileIdentity {
self.identity
}
}
impl Drop for QuiescenceGuard {
fn drop(&mut self) {
let _ = durable::unlock(&self.file);
}
}
fn try_lock(file: &File, path: &Path) -> Result<(), CustodyError> {
durable::try_lock_exclusive(file).map_err(|source| {
if source.kind() == io::ErrorKind::WouldBlock {
CustodyError::InFlight {
path: path.to_path_buf(),
}
} else {
CustodyError::Io(source)
}
})
}
#[derive(Debug)]
pub enum CustodyError {
InFlight {
path: PathBuf,
},
IdentityChanged {
path: PathBuf,
},
AlreadyDispatched {
path: PathBuf,
},
Lock(RegularFileLockError),
Io(io::Error),
}
impl fmt::Display for CustodyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::InFlight { .. } => "command custody remains in flight",
Self::IdentityChanged { .. } => "command custody identity changed",
Self::AlreadyDispatched { .. } => "command spawn already attempted",
Self::Lock(_) => "command sidecar admission failed",
Self::Io(_) => "command custody I/O failed",
})
}
}
impl std::error::Error for CustodyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Lock(e) => Some(e),
Self::Io(e) => Some(e),
_ => None,
}
}
}
impl From<io::Error> for CustodyError {
fn from(source: io::Error) -> Self {
Self::Io(source)
}
}
#[cfg(test)]
mod tests;