use crate::model::command_custody::{CommandCustodyRecord, CommandCustodyRecordError};
#[cfg(unix)]
use ic_host_fs::identity::FileIdentity;
#[cfg(unix)]
use ic_host_process::custody::{CommandCustody, CustodyError, QuiescenceGuard};
#[cfg(unix)]
use std::time::Duration;
use std::{
fs, io,
path::{Path, PathBuf},
process::{Child, Command},
};
use thiserror::Error;
pub const COMMAND_CUSTODY_DESCRIPTOR_ENV: &str = "IC_BACKUP_COMMAND_CUSTODY_FD";
#[derive(Debug)]
pub struct CommandLifetimeLock {
#[cfg(unix)]
custody: CommandCustody,
path: PathBuf,
record: CommandCustodyRecord,
}
impl CommandLifetimeLock {
pub fn acquire(
journal: &Path,
operation_sequence: u64,
) -> Result<Self, CommandLifetimeLockError> {
let journal = super::journal_path::resolve_journal(
journal,
|| CommandLifetimeLockError::InvalidJournal {
path: journal.to_path_buf(),
},
|path| CommandLifetimeLockError::InvalidJournal { path },
)?;
let path = lock_path(&journal, operation_sequence);
#[cfg(unix)]
{
let custody =
CommandCustody::acquire(&path).map_err(|error| project_error(&path, error))?;
let identity = custody.identity();
let record = CommandCustodyRecord::new(
journal,
operation_sequence,
identity.device,
identity.inode,
)?;
Ok(Self {
custody,
path,
record,
})
}
#[cfg(not(unix))]
{
let _ = path;
Err(io::Error::from(io::ErrorKind::Unsupported).into())
}
}
#[must_use]
pub fn record(&self) -> &CommandCustodyRecord {
&self.record
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
pub fn spawn(&mut self, command: Command) -> Result<Child, CommandLifetimeLockError> {
#[cfg(unix)]
{
self.custody
.spawn(command, COMMAND_CUSTODY_DESCRIPTOR_ENV)
.map_err(|error| project_error(&self.path, error))
}
#[cfg(not(unix))]
{
let _ = command;
Err(io::Error::from(io::ErrorKind::Unsupported).into())
}
}
pub fn finish(self) -> Result<CommandQuiescenceGuard, CommandLifetimeLockError> {
#[cfg(unix)]
{
let guard = self
.custody
.finish(Duration::from_millis(250), Duration::from_millis(5))
.map_err(|error| project_error(&self.path, error))?;
Ok(CommandQuiescenceGuard {
_guard: guard,
record: self.record,
})
}
#[cfg(not(unix))]
{
Err(io::Error::from(io::ErrorKind::Unsupported).into())
}
}
}
#[derive(Debug)]
pub struct CommandQuiescenceGuard {
#[cfg(unix)]
_guard: QuiescenceGuard,
record: CommandCustodyRecord,
}
impl CommandQuiescenceGuard {
pub fn acquire(expected: &CommandCustodyRecord) -> Result<Self, CommandLifetimeLockError> {
#[cfg(unix)]
{
let path = lock_path(expected.journal(), expected.operation_sequence());
let identity = FileIdentity {
device: expected.device(),
inode: expected.inode(),
};
let guard = QuiescenceGuard::acquire(&path, identity)
.map_err(|error| project_error(&path, error))?;
Ok(Self {
_guard: guard,
record: expected.clone(),
})
}
#[cfg(not(unix))]
{
let _ = expected;
Err(io::Error::from(io::ErrorKind::Unsupported).into())
}
}
#[must_use]
pub fn record(&self) -> &CommandCustodyRecord {
&self.record
}
}
fn lock_path(journal: &Path, operation_sequence: u64) -> PathBuf {
let mut path = journal.as_os_str().to_os_string();
path.push(format!(".command-{operation_sequence}.lock"));
PathBuf::from(path)
}
#[cfg(unix)]
fn project_error(path: &Path, error: CustodyError) -> CommandLifetimeLockError {
use ic_host_fs::durable::RegularFileLockError;
match error {
CustodyError::InFlight { path } => CommandLifetimeLockError::InFlight { path },
CustodyError::IdentityChanged { path } => {
CommandLifetimeLockError::IdentityChanged { path }
}
CustodyError::AlreadyDispatched { path } => {
CommandLifetimeLockError::AlreadyDispatched { path }
}
CustodyError::Io(error) => CommandLifetimeLockError::Io(error),
CustodyError::Lock(RegularFileLockError::NotRegular) => {
let kind = match fs::symlink_metadata(path) {
Ok(metadata) if metadata.file_type().is_symlink() => "Symlink",
Ok(metadata) if metadata.is_dir() => "Directory",
_ => "Special",
};
CommandLifetimeLockError::UnsafeEntry {
path: path.to_path_buf(),
kind: kind.into(),
}
}
CustodyError::Lock(error) => CommandLifetimeLockError::Io(error.into()),
}
}
#[derive(Debug, Error)]
pub enum CommandLifetimeLockError {
#[error("command custody remains in flight: {path:?}")]
InFlight {
path: PathBuf,
},
#[error("command custody identity changed: {path:?}")]
IdentityChanged {
path: PathBuf,
},
#[error("command spawn already attempted: {path:?}")]
AlreadyDispatched {
path: PathBuf,
},
#[error("unsafe command custody entry at {path:?}: {kind}")]
UnsafeEntry {
path: PathBuf,
kind: String,
},
#[error("invalid command journal location: {path:?}")]
InvalidJournal {
path: PathBuf,
},
#[error(transparent)]
Record(#[from] CommandCustodyRecordError),
#[error(transparent)]
Io(#[from] io::Error),
}
#[cfg(all(test, unix))]
mod tests;