ic_backup/ops/persistence/command_lifetime_lock/
mod.rs1use crate::model::command_custody::{CommandCustodyRecord, CommandCustodyRecordError};
4#[cfg(unix)]
5use ic_host_fs::identity::FileIdentity;
6#[cfg(unix)]
7use ic_host_process::custody::{CommandCustody, CustodyError, QuiescenceGuard};
8#[cfg(unix)]
9use std::time::Duration;
10use std::{
11 fs, io,
12 path::{Path, PathBuf},
13 process::{Child, Command},
14};
15use thiserror::Error;
16
17pub const COMMAND_CUSTODY_DESCRIPTOR_ENV: &str = "IC_BACKUP_COMMAND_CUSTODY_FD";
19
20#[derive(Debug)]
25pub struct CommandLifetimeLock {
26 #[cfg(unix)]
27 custody: CommandCustody,
28 path: PathBuf,
29 record: CommandCustodyRecord,
30}
31
32impl CommandLifetimeLock {
33 pub fn acquire(
37 journal: &Path,
38 operation_sequence: u64,
39 ) -> Result<Self, CommandLifetimeLockError> {
40 let journal = super::journal_path::resolve_journal(
41 journal,
42 || CommandLifetimeLockError::InvalidJournal {
43 path: journal.to_path_buf(),
44 },
45 |path| CommandLifetimeLockError::InvalidJournal { path },
46 )?;
47 let path = lock_path(&journal, operation_sequence);
48 #[cfg(unix)]
49 {
50 let custody =
51 CommandCustody::acquire(&path).map_err(|error| project_error(&path, error))?;
52 let identity = custody.identity();
53 let record = CommandCustodyRecord::new(
54 journal,
55 operation_sequence,
56 identity.device,
57 identity.inode,
58 )?;
59 Ok(Self {
60 custody,
61 path,
62 record,
63 })
64 }
65 #[cfg(not(unix))]
66 {
67 let _ = path;
68 Err(io::Error::from(io::ErrorKind::Unsupported).into())
69 }
70 }
71
72 #[must_use]
74 pub fn record(&self) -> &CommandCustodyRecord {
75 &self.record
76 }
77
78 #[must_use]
80 pub fn path(&self) -> &Path {
81 &self.path
82 }
83
84 pub fn spawn(&mut self, command: Command) -> Result<Child, CommandLifetimeLockError> {
91 #[cfg(unix)]
92 {
93 self.custody
94 .spawn(command, COMMAND_CUSTODY_DESCRIPTOR_ENV)
95 .map_err(|error| project_error(&self.path, error))
96 }
97 #[cfg(not(unix))]
98 {
99 let _ = command;
100 Err(io::Error::from(io::ErrorKind::Unsupported).into())
101 }
102 }
103
104 pub fn finish(self) -> Result<CommandQuiescenceGuard, CommandLifetimeLockError> {
111 #[cfg(unix)]
112 {
113 let guard = self
114 .custody
115 .finish(Duration::from_millis(250), Duration::from_millis(5))
116 .map_err(|error| project_error(&self.path, error))?;
117 Ok(CommandQuiescenceGuard {
118 _guard: guard,
119 record: self.record,
120 })
121 }
122 #[cfg(not(unix))]
123 {
124 Err(io::Error::from(io::ErrorKind::Unsupported).into())
125 }
126 }
127}
128
129#[derive(Debug)]
134pub struct CommandQuiescenceGuard {
135 #[cfg(unix)]
136 _guard: QuiescenceGuard,
137 record: CommandCustodyRecord,
138}
139
140impl CommandQuiescenceGuard {
141 pub fn acquire(expected: &CommandCustodyRecord) -> Result<Self, CommandLifetimeLockError> {
145 #[cfg(unix)]
146 {
147 let path = lock_path(expected.journal(), expected.operation_sequence());
148 let identity = FileIdentity {
149 device: expected.device(),
150 inode: expected.inode(),
151 };
152 let guard = QuiescenceGuard::acquire(&path, identity)
153 .map_err(|error| project_error(&path, error))?;
154 Ok(Self {
155 _guard: guard,
156 record: expected.clone(),
157 })
158 }
159 #[cfg(not(unix))]
160 {
161 let _ = expected;
162 Err(io::Error::from(io::ErrorKind::Unsupported).into())
163 }
164 }
165
166 #[must_use]
168 pub fn record(&self) -> &CommandCustodyRecord {
169 &self.record
170 }
171}
172
173fn lock_path(journal: &Path, operation_sequence: u64) -> PathBuf {
174 let mut path = journal.as_os_str().to_os_string();
175 path.push(format!(".command-{operation_sequence}.lock"));
176 PathBuf::from(path)
177}
178
179#[cfg(unix)]
180fn project_error(path: &Path, error: CustodyError) -> CommandLifetimeLockError {
181 use ic_host_fs::durable::RegularFileLockError;
182 match error {
183 CustodyError::InFlight { path } => CommandLifetimeLockError::InFlight { path },
184 CustodyError::IdentityChanged { path } => {
185 CommandLifetimeLockError::IdentityChanged { path }
186 }
187 CustodyError::AlreadyDispatched { path } => {
188 CommandLifetimeLockError::AlreadyDispatched { path }
189 }
190 CustodyError::Io(error) => CommandLifetimeLockError::Io(error),
191 CustodyError::Lock(RegularFileLockError::NotRegular) => {
192 let kind = match fs::symlink_metadata(path) {
193 Ok(metadata) if metadata.file_type().is_symlink() => "Symlink",
194 Ok(metadata) if metadata.is_dir() => "Directory",
195 _ => "Special",
196 };
197 CommandLifetimeLockError::UnsafeEntry {
198 path: path.to_path_buf(),
199 kind: kind.into(),
200 }
201 }
202 CustodyError::Lock(error) => CommandLifetimeLockError::Io(error.into()),
203 }
204}
205
206#[derive(Debug, Error)]
208pub enum CommandLifetimeLockError {
209 #[error("command custody remains in flight: {path:?}")]
211 InFlight {
212 path: PathBuf,
214 },
215 #[error("command custody identity changed: {path:?}")]
217 IdentityChanged {
218 path: PathBuf,
220 },
221 #[error("command spawn already attempted: {path:?}")]
223 AlreadyDispatched {
224 path: PathBuf,
226 },
227 #[error("unsafe command custody entry at {path:?}: {kind}")]
229 UnsafeEntry {
230 path: PathBuf,
232 kind: String,
234 },
235 #[error("invalid command journal location: {path:?}")]
237 InvalidJournal {
238 path: PathBuf,
240 },
241 #[error(transparent)]
243 Record(#[from] CommandCustodyRecordError),
244 #[error(transparent)]
246 Io(#[from] io::Error),
247}
248
249#[cfg(all(test, unix))]
250mod tests;