ic_backup/ops/persistence/command_lifetime_lock/
mod.rs1use crate::model::command_custody::{CommandCustodyRecord, CommandCustodyRecordError};
4#[cfg(unix)]
5use ic_host_process::custody::{CommandCustody, CustodyError, FileIdentity, QuiescenceGuard};
6#[cfg(unix)]
7use std::time::Duration;
8use std::{
9 fs, io,
10 path::{Path, PathBuf},
11 process::{Child, Command},
12};
13use thiserror::Error;
14
15pub const COMMAND_CUSTODY_DESCRIPTOR_ENV: &str = "IC_BACKUP_COMMAND_CUSTODY_FD";
17
18#[derive(Debug)]
23pub struct CommandLifetimeLock {
24 #[cfg(unix)]
25 custody: CommandCustody,
26 path: PathBuf,
27 record: CommandCustodyRecord,
28}
29
30impl CommandLifetimeLock {
31 pub fn acquire(
35 journal: &Path,
36 operation_sequence: u64,
37 ) -> Result<Self, CommandLifetimeLockError> {
38 let journal = resolve_journal(journal)?;
39 let path = lock_path(&journal, operation_sequence);
40 #[cfg(unix)]
41 {
42 let custody =
43 CommandCustody::acquire(&path).map_err(|error| project_error(&path, error))?;
44 let identity = custody.identity();
45 let record = CommandCustodyRecord::new(
46 journal,
47 operation_sequence,
48 identity.device,
49 identity.inode,
50 )?;
51 Ok(Self {
52 custody,
53 path,
54 record,
55 })
56 }
57 #[cfg(not(unix))]
58 {
59 let _ = path;
60 Err(io::Error::from(io::ErrorKind::Unsupported).into())
61 }
62 }
63
64 #[must_use]
66 pub fn record(&self) -> &CommandCustodyRecord {
67 &self.record
68 }
69
70 #[must_use]
72 pub fn path(&self) -> &Path {
73 &self.path
74 }
75
76 pub fn spawn(&mut self, command: Command) -> Result<Child, CommandLifetimeLockError> {
83 #[cfg(unix)]
84 {
85 self.custody
86 .spawn(command, COMMAND_CUSTODY_DESCRIPTOR_ENV)
87 .map_err(|error| project_error(&self.path, error))
88 }
89 #[cfg(not(unix))]
90 {
91 let _ = command;
92 Err(io::Error::from(io::ErrorKind::Unsupported).into())
93 }
94 }
95
96 pub fn finish(self) -> Result<CommandQuiescenceGuard, CommandLifetimeLockError> {
103 #[cfg(unix)]
104 {
105 let guard = self
106 .custody
107 .finish(Duration::from_millis(250), Duration::from_millis(5))
108 .map_err(|error| project_error(&self.path, error))?;
109 Ok(CommandQuiescenceGuard {
110 _guard: guard,
111 record: self.record,
112 })
113 }
114 #[cfg(not(unix))]
115 {
116 Err(io::Error::from(io::ErrorKind::Unsupported).into())
117 }
118 }
119}
120
121#[derive(Debug)]
126pub struct CommandQuiescenceGuard {
127 #[cfg(unix)]
128 _guard: QuiescenceGuard,
129 record: CommandCustodyRecord,
130}
131
132impl CommandQuiescenceGuard {
133 pub fn acquire(expected: &CommandCustodyRecord) -> Result<Self, CommandLifetimeLockError> {
137 #[cfg(unix)]
138 {
139 let path = lock_path(expected.journal(), expected.operation_sequence());
140 let identity = FileIdentity {
141 device: expected.device(),
142 inode: expected.inode(),
143 };
144 let guard = QuiescenceGuard::acquire(&path, identity)
145 .map_err(|error| project_error(&path, error))?;
146 Ok(Self {
147 _guard: guard,
148 record: expected.clone(),
149 })
150 }
151 #[cfg(not(unix))]
152 {
153 let _ = expected;
154 Err(io::Error::from(io::ErrorKind::Unsupported).into())
155 }
156 }
157
158 #[must_use]
160 pub fn record(&self) -> &CommandCustodyRecord {
161 &self.record
162 }
163}
164
165fn resolve_journal(path: &Path) -> Result<PathBuf, CommandLifetimeLockError> {
166 let name = path
167 .file_name()
168 .ok_or_else(|| CommandLifetimeLockError::InvalidJournal {
169 path: path.to_path_buf(),
170 })?;
171 let parent = path
172 .parent()
173 .filter(|parent| !parent.as_os_str().is_empty())
174 .unwrap_or_else(|| Path::new("."));
175 let parent = parent.canonicalize()?;
176 if !parent.is_dir() {
177 return Err(io::Error::from(io::ErrorKind::NotADirectory).into());
178 }
179 let journal = parent.join(name);
180 match fs::symlink_metadata(&journal) {
181 Ok(metadata) if !metadata.is_file() => {
182 return Err(CommandLifetimeLockError::InvalidJournal { path: journal });
183 }
184 Err(error) if error.kind() != io::ErrorKind::NotFound => return Err(error.into()),
185 _ => {}
186 }
187 Ok(journal)
188}
189
190fn lock_path(journal: &Path, operation_sequence: u64) -> PathBuf {
191 let mut path = journal.as_os_str().to_os_string();
192 path.push(format!(".command-{operation_sequence}.lock"));
193 PathBuf::from(path)
194}
195
196#[cfg(unix)]
197fn project_error(path: &Path, error: CustodyError) -> CommandLifetimeLockError {
198 use ic_host_fs::durable::RegularFileLockError;
199 match error {
200 CustodyError::InFlight { path } => CommandLifetimeLockError::InFlight { path },
201 CustodyError::IdentityChanged { path } => {
202 CommandLifetimeLockError::IdentityChanged { path }
203 }
204 CustodyError::AlreadyDispatched { path } => {
205 CommandLifetimeLockError::AlreadyDispatched { path }
206 }
207 CustodyError::Io(error) => CommandLifetimeLockError::Io(error),
208 CustodyError::Lock(RegularFileLockError::NotRegular) => {
209 let kind = match fs::symlink_metadata(path) {
210 Ok(metadata) if metadata.file_type().is_symlink() => "Symlink",
211 Ok(metadata) if metadata.is_dir() => "Directory",
212 _ => "Special",
213 };
214 CommandLifetimeLockError::UnsafeEntry {
215 path: path.to_path_buf(),
216 kind: kind.into(),
217 }
218 }
219 CustodyError::Lock(error) => CommandLifetimeLockError::Io(error.into()),
220 }
221}
222
223#[derive(Debug, Error)]
225pub enum CommandLifetimeLockError {
226 #[error("command custody remains in flight: {path:?}")]
228 InFlight {
229 path: PathBuf,
231 },
232 #[error("command custody identity changed: {path:?}")]
234 IdentityChanged {
235 path: PathBuf,
237 },
238 #[error("command spawn already attempted: {path:?}")]
240 AlreadyDispatched {
241 path: PathBuf,
243 },
244 #[error("unsafe command custody entry at {path:?}: {kind}")]
246 UnsafeEntry {
247 path: PathBuf,
249 kind: String,
251 },
252 #[error("invalid command journal location: {path:?}")]
254 InvalidJournal {
255 path: PathBuf,
257 },
258 #[error(transparent)]
260 Record(#[from] CommandCustodyRecordError),
261 #[error(transparent)]
263 Io(#[from] io::Error),
264}
265
266#[cfg(all(test, unix))]
267mod tests;