ic_host_process/custody/
mod.rs1use command_fds::CommandFdExt;
8use ic_host_fs::durable::{self, RegularFileLockError};
9use std::{
10 fmt,
11 fs::{self, File, Metadata},
12 io,
13 os::{fd::AsRawFd, unix::fs::MetadataExt},
14 path::{Path, PathBuf},
15 process::{Child, Command},
16 thread,
17 time::{Duration, Instant},
18};
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub struct FileIdentity {
23 pub device: u64,
25 pub inode: u64,
27}
28
29impl FileIdentity {
30 pub fn from_file(file: &File) -> io::Result<Self> {
35 let metadata = file.metadata()?;
36 if !metadata.is_file() {
37 return Err(io::Error::from(io::ErrorKind::InvalidInput));
38 }
39 Ok(Self::from_metadata(&metadata))
40 }
41
42 fn from_metadata(metadata: &Metadata) -> Self {
43 Self {
44 device: metadata.dev(),
45 inode: metadata.ino(),
46 }
47 }
48
49 fn require(self, metadata: &Metadata, path: &Path) -> Result<(), CustodyError> {
50 if !metadata.is_file() || Self::from_metadata(metadata) != self {
51 return Err(CustodyError::IdentityChanged {
52 path: path.to_path_buf(),
53 });
54 }
55 Ok(())
56 }
57}
58
59#[derive(Debug)]
64pub struct CommandCustody {
65 file: File,
66 path: PathBuf,
67 identity: FileIdentity,
68 dispatched: bool,
69}
70
71impl CommandCustody {
72 pub fn acquire(path: &Path) -> Result<Self, CustodyError> {
79 let file = durable::open_private_regular_lock_file(path).map_err(CustodyError::Lock)?;
80 try_lock(&file, path)?;
81 Ok(Self {
82 identity: FileIdentity::from_file(&file)?,
83 file,
84 path: path.to_path_buf(),
85 dispatched: false,
86 })
87 }
88
89 #[must_use]
91 pub const fn identity(&self) -> FileIdentity {
92 self.identity
93 }
94
95 #[must_use]
97 pub fn path(&self) -> &Path {
98 &self.path
99 }
100
101 pub fn spawn(
110 &mut self,
111 mut command: Command,
112 descriptor_environment: &str,
113 ) -> Result<Child, CustodyError> {
114 if self.dispatched {
115 return Err(CustodyError::AlreadyDispatched {
116 path: self.path.clone(),
117 });
118 }
119 self.identity
120 .require(&fs::symlink_metadata(&self.path)?, &self.path)?;
121 self.dispatched = true;
122 let descriptor = rustix::io::fcntl_dupfd_cloexec(&self.file, 3).map_err(io::Error::from)?;
123 command.env(descriptor_environment, descriptor.as_raw_fd().to_string());
124 command.preserved_fds(vec![descriptor]);
125 let child = command.spawn();
126 drop(command);
127 child.map_err(CustodyError::Io)
128 }
129
130 pub fn finish(
138 self,
139 grace: Duration,
140 poll_interval: Duration,
141 ) -> Result<QuiescenceGuard, CustodyError> {
142 if poll_interval.is_zero() {
143 return Err(io::Error::from(io::ErrorKind::InvalidInput).into());
144 }
145 let deadline = Instant::now()
146 .checked_add(grace)
147 .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
148 let Self {
149 file,
150 path,
151 identity,
152 ..
153 } = self;
154 drop(file);
155 loop {
156 match QuiescenceGuard::acquire(&path, identity) {
157 Err(CustodyError::InFlight { .. }) if Instant::now() < deadline => {
158 thread::sleep(
159 poll_interval.min(deadline.saturating_duration_since(Instant::now())),
160 );
161 }
162 result => return result,
163 }
164 }
165 }
166}
167
168#[derive(Debug)]
173pub struct QuiescenceGuard {
174 file: File,
175 identity: FileIdentity,
176}
177
178impl QuiescenceGuard {
179 pub fn acquire(path: &Path, expected: FileIdentity) -> Result<Self, CustodyError> {
186 let metadata = fs::symlink_metadata(path)?;
187 if metadata.is_file() {
188 expected.require(&metadata, path)?;
189 }
190 let file = durable::open_existing_regular_lock_file(path).map_err(CustodyError::Lock)?;
191 try_lock(&file, path)?;
192 expected.require(&file.metadata()?, path)?;
193 Ok(Self {
194 file,
195 identity: expected,
196 })
197 }
198
199 #[must_use]
201 pub const fn identity(&self) -> FileIdentity {
202 self.identity
203 }
204}
205
206impl Drop for QuiescenceGuard {
207 fn drop(&mut self) {
208 let _ = durable::unlock(&self.file);
209 }
210}
211
212fn try_lock(file: &File, path: &Path) -> Result<(), CustodyError> {
213 durable::try_lock_exclusive(file).map_err(|source| {
214 if source.kind() == io::ErrorKind::WouldBlock {
215 CustodyError::InFlight {
216 path: path.to_path_buf(),
217 }
218 } else {
219 CustodyError::Io(source)
220 }
221 })
222}
223
224#[derive(Debug)]
226pub enum CustodyError {
227 InFlight {
229 path: PathBuf,
231 },
232 IdentityChanged {
234 path: PathBuf,
236 },
237 AlreadyDispatched {
239 path: PathBuf,
241 },
242 Lock(RegularFileLockError),
244 Io(io::Error),
246}
247
248impl fmt::Display for CustodyError {
249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250 f.write_str(match self {
251 Self::InFlight { .. } => "command custody remains in flight",
252 Self::IdentityChanged { .. } => "command custody identity changed",
253 Self::AlreadyDispatched { .. } => "command spawn already attempted",
254 Self::Lock(_) => "command sidecar admission failed",
255 Self::Io(_) => "command custody I/O failed",
256 })
257 }
258}
259
260impl std::error::Error for CustodyError {
261 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
262 match self {
263 Self::Lock(e) => Some(e),
264 Self::Io(e) => Some(e),
265 _ => None,
266 }
267 }
268}
269
270impl From<io::Error> for CustodyError {
271 fn from(source: io::Error) -> Self {
272 Self::Io(source)
273 }
274}
275
276#[cfg(test)]
277mod tests;