ic_host_process/custody/
mod.rs1use command_fds::CommandFdExt;
8use ic_host_fs::{
9 durable::{self, RegularFileLockError},
10 identity::FileIdentity,
11};
12use std::{
13 fmt,
14 fs::{self, File, Metadata},
15 io,
16 os::fd::AsRawFd,
17 path::{Path, PathBuf},
18 process::{Child, Command},
19 thread,
20 time::{Duration, Instant},
21};
22
23fn require_identity(
24 expected: FileIdentity,
25 metadata: &Metadata,
26 path: &Path,
27) -> Result<(), CustodyError> {
28 if !metadata.is_file() || FileIdentity::from_metadata(metadata) != expected {
29 return Err(CustodyError::IdentityChanged {
30 path: path.to_path_buf(),
31 });
32 }
33 Ok(())
34}
35
36#[derive(Debug)]
41pub struct CommandCustody {
42 file: File,
43 path: PathBuf,
44 identity: FileIdentity,
45 dispatched: bool,
46}
47
48impl CommandCustody {
49 pub fn acquire(path: &Path) -> Result<Self, CustodyError> {
56 let file = durable::open_private_regular_lock_file(path).map_err(CustodyError::Lock)?;
57 try_lock(&file, path)?;
58 Ok(Self {
59 identity: FileIdentity::from_file(&file)?,
60 file,
61 path: path.to_path_buf(),
62 dispatched: false,
63 })
64 }
65
66 #[must_use]
68 pub const fn identity(&self) -> FileIdentity {
69 self.identity
70 }
71
72 #[must_use]
74 pub fn path(&self) -> &Path {
75 &self.path
76 }
77
78 pub fn spawn(
87 &mut self,
88 mut command: Command,
89 descriptor_environment: &str,
90 ) -> Result<Child, CustodyError> {
91 if self.dispatched {
92 return Err(CustodyError::AlreadyDispatched {
93 path: self.path.clone(),
94 });
95 }
96 require_identity(
97 self.identity,
98 &fs::symlink_metadata(&self.path)?,
99 &self.path,
100 )?;
101 self.dispatched = true;
102 let descriptor = rustix::io::fcntl_dupfd_cloexec(&self.file, 3).map_err(io::Error::from)?;
103 command.env(descriptor_environment, descriptor.as_raw_fd().to_string());
104 command.preserved_fds(vec![descriptor]);
105 let child = command.spawn();
106 drop(command);
107 child.map_err(CustodyError::Io)
108 }
109
110 pub fn finish(
118 self,
119 grace: Duration,
120 poll_interval: Duration,
121 ) -> Result<QuiescenceGuard, CustodyError> {
122 if poll_interval.is_zero() {
123 return Err(io::Error::from(io::ErrorKind::InvalidInput).into());
124 }
125 let deadline = Instant::now()
126 .checked_add(grace)
127 .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
128 let Self {
129 file,
130 path,
131 identity,
132 ..
133 } = self;
134 drop(file);
135 loop {
136 match QuiescenceGuard::acquire(&path, identity) {
137 Err(CustodyError::InFlight { .. }) if Instant::now() < deadline => {
138 thread::sleep(
139 poll_interval.min(deadline.saturating_duration_since(Instant::now())),
140 );
141 }
142 result => return result,
143 }
144 }
145 }
146}
147
148#[derive(Debug)]
153pub struct QuiescenceGuard {
154 file: File,
155 identity: FileIdentity,
156}
157
158impl QuiescenceGuard {
159 pub fn acquire(path: &Path, expected: FileIdentity) -> Result<Self, CustodyError> {
166 let metadata = fs::symlink_metadata(path)?;
167 if metadata.is_file() {
168 require_identity(expected, &metadata, path)?;
169 }
170 let file = durable::open_existing_regular_lock_file(path).map_err(CustodyError::Lock)?;
171 try_lock(&file, path)?;
172 require_identity(expected, &file.metadata()?, path)?;
173 Ok(Self {
174 file,
175 identity: expected,
176 })
177 }
178
179 #[must_use]
181 pub const fn identity(&self) -> FileIdentity {
182 self.identity
183 }
184}
185
186impl Drop for QuiescenceGuard {
187 fn drop(&mut self) {
188 let _ = durable::unlock(&self.file);
189 }
190}
191
192fn try_lock(file: &File, path: &Path) -> Result<(), CustodyError> {
193 durable::try_lock_exclusive(file).map_err(|source| {
194 if source.kind() == io::ErrorKind::WouldBlock {
195 CustodyError::InFlight {
196 path: path.to_path_buf(),
197 }
198 } else {
199 CustodyError::Io(source)
200 }
201 })
202}
203
204#[derive(Debug)]
206pub enum CustodyError {
207 InFlight {
209 path: PathBuf,
211 },
212 IdentityChanged {
214 path: PathBuf,
216 },
217 AlreadyDispatched {
219 path: PathBuf,
221 },
222 Lock(RegularFileLockError),
224 Io(io::Error),
226}
227
228impl fmt::Display for CustodyError {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 f.write_str(match self {
231 Self::InFlight { .. } => "command custody remains in flight",
232 Self::IdentityChanged { .. } => "command custody identity changed",
233 Self::AlreadyDispatched { .. } => "command spawn already attempted",
234 Self::Lock(_) => "command sidecar admission failed",
235 Self::Io(_) => "command custody I/O failed",
236 })
237 }
238}
239
240impl std::error::Error for CustodyError {
241 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
242 match self {
243 Self::Lock(e) => Some(e),
244 Self::Io(e) => Some(e),
245 _ => None,
246 }
247 }
248}
249
250impl From<io::Error> for CustodyError {
251 fn from(source: io::Error) -> Self {
252 Self::Io(source)
253 }
254}
255
256#[cfg(test)]
257mod tests;