/build/source/nativelink-util/src/fs.rs
Line | Count | Source |
1 | | // Copyright 2024 The NativeLink Authors. All rights reserved. |
2 | | // |
3 | | // Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License"); |
4 | | // you may not use this file except in compliance with the License. |
5 | | // You may obtain a copy of the License at |
6 | | // |
7 | | // See LICENSE file for details |
8 | | // |
9 | | // Unless required by applicable law or agreed to in writing, software |
10 | | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | | // See the License for the specific language governing permissions and |
13 | | // limitations under the License. |
14 | | |
15 | | use core::pin::Pin; |
16 | | use core::sync::atomic::{AtomicUsize, Ordering}; |
17 | | use core::task::{Context, Poll}; |
18 | | use std::fs::{Metadata, Permissions}; |
19 | | use std::io::{self, IoSlice, Seek}; |
20 | | #[cfg(target_os = "linux")] |
21 | | use std::os::unix::io::AsRawFd; |
22 | | use std::path::{Path, PathBuf}; |
23 | | |
24 | | use nativelink_error::{Code, Error, ResultExt, make_err}; |
25 | | use rlimit::increase_nofile_limit; |
26 | | /// We wrap all `tokio::fs` items in our own wrapper so we can limit the number of outstanding |
27 | | /// open files at any given time. This will greatly reduce the chance we'll hit open file limit |
28 | | /// issues. |
29 | | pub use tokio::fs::DirEntry; |
30 | | use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncWrite, ReadBuf, SeekFrom, Take}; |
31 | | use tokio::sync::{Semaphore, SemaphorePermit}; |
32 | | use tracing::{error, info, trace, warn}; |
33 | | |
34 | | use crate::spawn_blocking; |
35 | | |
36 | | /// Default read buffer size when reading to/from disk. |
37 | | pub const DEFAULT_READ_BUFF_SIZE: usize = 0x4000; |
38 | | |
39 | | #[derive(Debug)] |
40 | | pub struct FileSlot { |
41 | | // We hold the permit because once it is dropped it goes back into the queue. |
42 | | _permit: SemaphorePermit<'static>, |
43 | | inner: tokio::fs::File, |
44 | | } |
45 | | |
46 | | impl FileSlot { |
47 | | /// Advise the kernel to drop page cache for this file's contents. |
48 | | /// Only available on Linux; |
49 | | #[cfg(target_os = "linux")] |
50 | 2 | pub fn advise_dontneed(&self) { |
51 | 2 | let fd = self.inner.as_raw_fd(); |
52 | 2 | let ret = unsafe { libc::posix_fadvise(fd, 0, 0, libc::POSIX_FADV_DONTNEED) }; |
53 | 2 | if ret != 0 { |
54 | 0 | tracing::debug!( |
55 | | fd, |
56 | | ret, |
57 | | "posix_fadvise(DONTNEED) returned non-zero (best-effort, ignoring)", |
58 | | ); |
59 | 2 | } |
60 | 2 | } |
61 | | |
62 | | #[cfg(not(target_os = "linux"))] |
63 | | pub const fn advise_dontneed(&self) { |
64 | | // No-op: posix_fadvise is not available on Mac or Windows. |
65 | | } |
66 | | } |
67 | | |
68 | | /// Enable [`IP_FREEBIND`](`libc::IP_FREEBIND`) before binding a socket. |
69 | | #[cfg(target_os = "linux")] |
70 | 1 | pub fn set_freebind<F: AsRawFd>(socket: &F) -> Result<(), io::Error> { |
71 | 1 | let enable = 1; |
72 | 1 | let optlen = libc::socklen_t::try_from(size_of::<libc::c_int>()) |
73 | 1 | .expect("size of c_int always fits in socklen_t"); |
74 | | |
75 | | // SAFETY: we pass in a valid fd, initialized optval, and matching optlen. |
76 | 1 | let ret = unsafe { |
77 | 1 | libc::setsockopt( |
78 | 1 | socket.as_raw_fd(), |
79 | | libc::IPPROTO_IP, |
80 | | libc::IP_FREEBIND, |
81 | 1 | core::ptr::addr_of!(enable).cast(), |
82 | 1 | optlen, |
83 | | ) |
84 | | }; |
85 | 1 | if ret == 0 { |
86 | 1 | Ok(()) |
87 | | } else { |
88 | 0 | Err(io::Error::last_os_error()) |
89 | | } |
90 | 1 | } |
91 | | |
92 | | #[cfg(not(target_os = "linux"))] |
93 | | pub const fn set_freebind<F>(_socket: &F) -> Result<(), io::Error> { |
94 | | Ok(()) |
95 | | } |
96 | | |
97 | | impl AsRef<tokio::fs::File> for FileSlot { |
98 | 0 | fn as_ref(&self) -> &tokio::fs::File { |
99 | 0 | &self.inner |
100 | 0 | } |
101 | | } |
102 | | |
103 | | impl AsMut<tokio::fs::File> for FileSlot { |
104 | 0 | fn as_mut(&mut self) -> &mut tokio::fs::File { |
105 | 0 | &mut self.inner |
106 | 0 | } |
107 | | } |
108 | | |
109 | | impl AsyncRead for FileSlot { |
110 | 9.17k | fn poll_read( |
111 | 9.17k | mut self: Pin<&mut Self>, |
112 | 9.17k | cx: &mut Context<'_>, |
113 | 9.17k | buf: &mut ReadBuf<'_>, |
114 | 9.17k | ) -> Poll<Result<(), tokio::io::Error>> { |
115 | 9.17k | Pin::new(&mut self.inner).poll_read(cx, buf) |
116 | 9.17k | } |
117 | | } |
118 | | |
119 | | impl AsyncSeek for FileSlot { |
120 | 828 | fn start_seek(mut self: Pin<&mut Self>, position: SeekFrom) -> Result<(), tokio::io::Error> { |
121 | 828 | Pin::new(&mut self.inner).start_seek(position) |
122 | 828 | } |
123 | | |
124 | 2.21k | fn poll_complete( |
125 | 2.21k | mut self: Pin<&mut Self>, |
126 | 2.21k | cx: &mut Context<'_>, |
127 | 2.21k | ) -> Poll<Result<u64, tokio::io::Error>> { |
128 | 2.21k | Pin::new(&mut self.inner).poll_complete(cx) |
129 | 2.21k | } |
130 | | } |
131 | | |
132 | | impl AsyncWrite for FileSlot { |
133 | 299 | fn poll_write( |
134 | 299 | mut self: Pin<&mut Self>, |
135 | 299 | cx: &mut Context<'_>, |
136 | 299 | buf: &[u8], |
137 | 299 | ) -> Poll<Result<usize, tokio::io::Error>> { |
138 | 299 | Pin::new(&mut self.inner).poll_write(cx, buf) |
139 | 299 | } |
140 | | |
141 | 4.41k | fn poll_flush( |
142 | 4.41k | mut self: Pin<&mut Self>, |
143 | 4.41k | cx: &mut Context<'_>, |
144 | 4.41k | ) -> Poll<Result<(), tokio::io::Error>> { |
145 | 4.41k | Pin::new(&mut self.inner).poll_flush(cx) |
146 | 4.41k | } |
147 | | |
148 | 0 | fn poll_shutdown( |
149 | 0 | mut self: Pin<&mut Self>, |
150 | 0 | cx: &mut Context<'_>, |
151 | 0 | ) -> Poll<Result<(), tokio::io::Error>> { |
152 | 0 | Pin::new(&mut self.inner).poll_shutdown(cx) |
153 | 0 | } |
154 | | |
155 | 1.77k | fn poll_write_vectored( |
156 | 1.77k | mut self: Pin<&mut Self>, |
157 | 1.77k | cx: &mut Context<'_>, |
158 | 1.77k | bufs: &[IoSlice<'_>], |
159 | 1.77k | ) -> Poll<Result<usize, tokio::io::Error>> { |
160 | 1.77k | Pin::new(&mut self.inner).poll_write_vectored(cx, bufs) |
161 | 1.77k | } |
162 | | |
163 | 1.77k | fn is_write_vectored(&self) -> bool { |
164 | 1.77k | self.inner.is_write_vectored() |
165 | 1.77k | } |
166 | | } |
167 | | |
168 | | // Note: If the default changes make sure you update the documentation in |
169 | | // `config/cas_server.rs`. |
170 | | pub const DEFAULT_OPEN_FILE_LIMIT: usize = 24 * 1024; // 24k. |
171 | | static OPEN_FILE_LIMIT: AtomicUsize = AtomicUsize::new(DEFAULT_OPEN_FILE_LIMIT); |
172 | | pub static OPEN_FILE_SEMAPHORE: Semaphore = Semaphore::const_new(DEFAULT_OPEN_FILE_LIMIT); |
173 | | |
174 | | /// Try to acquire a permit from the open file semaphore. |
175 | | #[inline] |
176 | 35.4k | pub async fn get_permit() -> Result<SemaphorePermit<'static>, Error> { |
177 | 35.4k | trace!( |
178 | 35.4k | available_permits = OPEN_FILE_SEMAPHORE.available_permits(), |
179 | | "getting FS permit" |
180 | | ); |
181 | 35.4k | OPEN_FILE_SEMAPHORE |
182 | 35.4k | .acquire() |
183 | 35.4k | .await |
184 | 35.4k | .map_err(|e| make_err!0 (Code::Internal0 , "Open file semaphore closed {:?}", e)) |
185 | 35.4k | } |
186 | | /// Acquire a permit from the open file semaphore and call a raw function. |
187 | | /// |
188 | | /// `f` runs on the blocking pool, so it must be *purely* blocking `std::fs` |
189 | | /// work. It must never `block_on` an async operation: `tokio::fs` is itself |
190 | | /// implemented with `spawn_blocking`, so a nested `block_on` needs a second |
191 | | /// pool thread while already holding one. Enough concurrent callers then park |
192 | | /// every pool thread on inner tasks that can never be scheduled, freezing all |
193 | | /// `fs::` operations in the process. Use [`get_permit`] plus a direct `.await` |
194 | | /// for anything already async (see [`read_dir`], [`symlink`]). |
195 | | #[inline] |
196 | 8.01k | pub async fn call_with_permit<F, T>(f: F) -> Result<T, Error> |
197 | 8.01k | where |
198 | 8.01k | F: FnOnce(SemaphorePermit<'static>) -> Result<T, Error> + Send + 'static, |
199 | 8.01k | T: Send + 'static, |
200 | 8.01k | { |
201 | 8.01k | let permit = get_permit().await?0 ; |
202 | 8.01k | spawn_blocking!("fs_call_with_permit", move || f8.01k (permit8.01k )) |
203 | 8.01k | .await |
204 | 8.00k | .unwrap_or_else(|e| {0 |
205 | 0 | Err(Error::from_std_err(Code::Internal, &e).append("background task failed")) |
206 | 0 | }) |
207 | 8.00k | } |
208 | | |
209 | | /// Sets the soft nofile limit to `desired_open_file_limit` and adjusts |
210 | | /// `OPEN_FILE_SEMAPHORE` accordingly. |
211 | | /// |
212 | | /// # Panics |
213 | | /// |
214 | | /// If any type conversion fails. This can't happen if `usize` is smaller than |
215 | | /// `u64`. |
216 | 0 | pub fn set_open_file_limit(desired_open_file_limit: usize) { |
217 | | // Tokio semaphores have a max of 2^61 - 1 permits. On some platforms |
218 | | // (e.g. macOS) the kernel reports an "unlimited" file limit that exceeds |
219 | | // this, causing a panic when we try to add permits. Cap at a generous but |
220 | | // safe value. |
221 | | const MAX_SAFE_LIMIT: usize = 1 << 30; // ~1 billion |
222 | | |
223 | 0 | let new_open_file_limit = { |
224 | 0 | match increase_nofile_limit( |
225 | 0 | u64::try_from(desired_open_file_limit) |
226 | 0 | .expect("desired_open_file_limit is too large to convert to u64."), |
227 | 0 | ) { |
228 | 0 | Ok(open_file_limit) => { |
229 | 0 | info!("set_open_file_limit() assigns new open file limit {open_file_limit}.",); |
230 | 0 | usize::try_from(open_file_limit) |
231 | 0 | .expect("open_file_limit is too large to convert to usize.") |
232 | 0 | .min(MAX_SAFE_LIMIT) |
233 | | } |
234 | 0 | Err(e) => { |
235 | 0 | error!( |
236 | | "set_open_file_limit() failed to assign open file limit. Maybe system does not have ulimits, continuing anyway. - {e:?}", |
237 | | ); |
238 | 0 | DEFAULT_OPEN_FILE_LIMIT |
239 | | } |
240 | | } |
241 | | }; |
242 | | // TODO(jaroeichler): Can we give a better estimate? |
243 | 0 | if new_open_file_limit < DEFAULT_OPEN_FILE_LIMIT { |
244 | 0 | warn!( |
245 | | "The new open file limit ({new_open_file_limit}) is below the recommended value of {DEFAULT_OPEN_FILE_LIMIT}. Consider raising max_open_files.", |
246 | | ); |
247 | 0 | } |
248 | | |
249 | | // Use only 80% of the open file limit for permits from OPEN_FILE_SEMAPHORE |
250 | | // to give extra room for other file descriptors like sockets, pipes, and |
251 | | // other things. |
252 | 0 | let reduced_open_file_limit = new_open_file_limit.saturating_sub(new_open_file_limit / 5); |
253 | 0 | let previous_open_file_limit = OPEN_FILE_LIMIT.load(Ordering::Acquire); |
254 | | // No permit should be acquired yet, so this warning should not occur. |
255 | 0 | if (OPEN_FILE_SEMAPHORE.available_permits() + reduced_open_file_limit) |
256 | 0 | < previous_open_file_limit |
257 | | { |
258 | 0 | warn!( |
259 | | "There are not enough available permits to remove {previous_open_file_limit} - {reduced_open_file_limit} permits.", |
260 | | ); |
261 | 0 | } |
262 | 0 | if previous_open_file_limit <= reduced_open_file_limit { |
263 | 0 | OPEN_FILE_LIMIT.fetch_add( |
264 | 0 | reduced_open_file_limit - previous_open_file_limit, |
265 | 0 | Ordering::Release, |
266 | 0 | ); |
267 | 0 | OPEN_FILE_SEMAPHORE.add_permits(reduced_open_file_limit - previous_open_file_limit); |
268 | 0 | } else { |
269 | 0 | OPEN_FILE_LIMIT.fetch_sub( |
270 | 0 | previous_open_file_limit - reduced_open_file_limit, |
271 | 0 | Ordering::Release, |
272 | 0 | ); |
273 | 0 | OPEN_FILE_SEMAPHORE.forget_permits(previous_open_file_limit - reduced_open_file_limit); |
274 | 0 | } |
275 | 0 | } |
276 | | |
277 | 136 | pub fn get_open_files_for_test() -> usize { |
278 | 136 | OPEN_FILE_LIMIT.load(Ordering::Acquire) - OPEN_FILE_SEMAPHORE.available_permits() |
279 | 136 | } |
280 | | |
281 | 1.14k | pub async fn open_file( |
282 | 1.14k | path: impl AsRef<Path>, |
283 | 1.14k | start: u64, |
284 | 1.14k | limit: u64, |
285 | 1.14k | ) -> Result<Take<FileSlot>, Error> { |
286 | 1.14k | let path = path.as_ref().to_owned(); |
287 | 1.14k | let (permit1.13k , os_file1.13k ) = call_with_permit(move |permit| { |
288 | 1.13k | let mut os_file = |
289 | 1.14k | std::fs::File::open(&path).err_tip(|| format!10 ("Could not open {}", path.display()10 ))?10 ; |
290 | 1.13k | if start > 0 { |
291 | 0 | os_file |
292 | 0 | .seek(SeekFrom::Start(start)) |
293 | 0 | .err_tip(|| format!("Could not seek to {start} in {}", path.display()))?; |
294 | 1.13k | } |
295 | 1.13k | Ok((permit, os_file)) |
296 | 1.14k | }) |
297 | 1.14k | .await?10 ; |
298 | 1.13k | Ok(FileSlot { |
299 | 1.13k | _permit: permit, |
300 | 1.13k | inner: tokio::fs::File::from_std(os_file), |
301 | 1.13k | } |
302 | 1.13k | .take(limit)) |
303 | 1.14k | } |
304 | | |
305 | 2.08k | pub async fn create_file(path: impl AsRef<Path>) -> Result<FileSlot, Error> { |
306 | 2.08k | let path = path.as_ref().to_owned(); |
307 | 2.08k | let (permit2.08k , os_file2.08k ) = call_with_permit(move |permit| {2.08k |
308 | | Ok(( |
309 | 2.08k | permit, |
310 | 2.08k | std::fs::File::options() |
311 | 2.08k | .read(true) |
312 | 2.08k | .write(true) |
313 | 2.08k | .create(true) |
314 | 2.08k | .truncate(true) |
315 | 2.08k | .open(&path) |
316 | 2.08k | .err_tip(|| format!0 ("Could not open {}", path.display()0 ))?0 , |
317 | | )) |
318 | 2.08k | }) |
319 | 2.08k | .await?0 ; |
320 | 2.08k | Ok(FileSlot { |
321 | 2.08k | _permit: permit, |
322 | 2.08k | inner: tokio::fs::File::from_std(os_file), |
323 | 2.08k | }) |
324 | 2.08k | } |
325 | | |
326 | 3 | pub async fn hard_link(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<(), Error> { |
327 | 3 | let src = src.as_ref().to_owned(); |
328 | 3 | let dst = dst.as_ref().to_owned(); |
329 | 3 | call_with_permit(move |_| std::fs::hard_link(src, dst).map_err(Into::<Error>::into)).await |
330 | 3 | } |
331 | | |
332 | | /// Hardlinks many files under a single permit and a single `spawn_blocking` |
333 | | /// dispatch, returning one result per input in the same order. |
334 | | /// |
335 | | /// [`hard_link`] pays a semaphore acquire plus a `spawn_blocking` hop per call, |
336 | | /// and both dwarf the `hard_link(2)` syscall itself, which is a few microseconds |
337 | | /// on tmpfs. Staging an input tree of thousands of small files therefore spends |
338 | | /// most of its wall time in the tokio scheduler rather than in the kernel. |
339 | | /// Batching amortizes the acquire and the dispatch over the whole batch. |
340 | | /// |
341 | | /// A single permit covers the batch because `hard_link(2)` is a metadata |
342 | | /// operation that never leaves a file descriptor open, so it does not consume |
343 | | /// the open-file budget [`OPEN_FILE_SEMAPHORE`] exists to protect. |
344 | | /// |
345 | | /// This does change the backpressure profile; the permit no longer paces |
346 | | /// individual syscalls, only whole batches, and one blocking thread is held for |
347 | | /// the length of a batch rather than for one syscall. On Linux and tmpfs, where |
348 | | /// this was measured, that trades well because the per-call dispatch was the |
349 | | /// cost. A filesystem that serializes metadata operations internally, as APFS |
350 | | /// does on a per-volume lock, gets a different bargain: less parallel |
351 | | /// contention, but a long batch occupies its thread throughout. Size batches |
352 | | /// accordingly. |
353 | 0 | pub async fn hard_link_many( |
354 | 0 | links: Vec<(PathBuf, PathBuf)>, |
355 | 74 | ) -> Result<Vec<Result<(), Error>>, Error> { |
356 | 74 | call_with_permit(move |_| { |
357 | 74 | Ok(links |
358 | 74 | .into_iter() |
359 | 152 | .map74 (|(src, dst)| std::fs::hard_link(src, dst).map_err(Into::<Error>::into)) |
360 | 74 | .collect()) |
361 | 74 | }) |
362 | 74 | .await |
363 | 74 | } |
364 | | |
365 | 2.33k | pub async fn set_permissions(src: impl AsRef<Path>, perm: Permissions) -> Result<(), Error> { |
366 | 2.33k | let src = src.as_ref().to_owned(); |
367 | 2.33k | call_with_permit(move |_| std::fs::set_permissions(src, perm).map_err(Into::<Error>::into)) |
368 | 2.33k | .await |
369 | 2.33k | } |
370 | | |
371 | | /// Creates many directories under a single permit and a single `spawn_blocking` |
372 | | /// dispatch, each parent before its children. |
373 | | /// |
374 | | /// An existing directory is an error, exactly as [`create_dir`] reports it. |
375 | | /// |
376 | | /// See [`hard_link_many`] for why batching matters here: the `mkdir(2)` syscall |
377 | | /// is dwarfed by the semaphore acquire and `spawn_blocking` hop, and an input |
378 | | /// tree contributes hundreds of directories. |
379 | 75 | pub async fn create_dir_many(mut dirs: Vec<PathBuf>) -> Result<(), Error>0 {
|
380 | | // Sorting puts every parent ahead of its children, since a parent's path is |
381 | | // a prefix of theirs. We do this instead of using `create_dir_all` below |
382 | | // because we want to fail on existing directories: on case-insensitive |
383 | | // filesystems, that will ensure that we don't silently merge two trees in |
384 | | // the event of a different-in-case-only collision. |
385 | 75 | dirs.sort_unstable(); |
386 | 75 | call_with_permit(move |_| { |
387 | 75 | for dir31 in dirs { |
388 | 31 | std::fs::create_dir(&dir).map_err(|e| {2 |
389 | 2 | Error::from(e).append(format!("Could not create directory {}", dir.display())) |
390 | 2 | })?; |
391 | | } |
392 | 73 | Ok(()) |
393 | 75 | }) |
394 | 75 | .await |
395 | 75 | } |
396 | | |
397 | 364 | pub async fn create_dir(path: impl AsRef<Path>) -> Result<(), Error> { |
398 | 364 | let path = path.as_ref().to_owned(); |
399 | 364 | call_with_permit(move |_| std::fs::create_dir(path).map_err(Into::<Error>::into)).await |
400 | 364 | } |
401 | | |
402 | 147 | pub async fn create_dir_all(path: impl AsRef<Path>) -> Result<(), Error> { |
403 | 147 | let path = path.as_ref().to_owned(); |
404 | 147 | call_with_permit(move |_| std::fs::create_dir_all(path).map_err(Into::<Error>::into)).await |
405 | 147 | } |
406 | | |
407 | | #[cfg(target_family = "unix")] |
408 | 4 | pub async fn symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<(), Error> { |
409 | | // TODO: add a test for #2051: deadlock with large number of files |
410 | 4 | let _permit = get_permit().await?0 ; |
411 | 4 | tokio::fs::symlink(src, dst).await.map_err(Into::into) |
412 | 4 | } |
413 | | |
414 | 6 | pub async fn read_link(path: impl AsRef<Path>) -> Result<PathBuf, Error> { |
415 | 6 | let path = path.as_ref().to_owned(); |
416 | 6 | call_with_permit(move |_| std::fs::read_link(path).map_err(Into::<Error>::into)).await |
417 | 6 | } |
418 | | |
419 | | #[derive(Debug)] |
420 | | pub struct ReadDir { |
421 | | // We hold the permit because once it is dropped it goes back into the queue. |
422 | | permit: SemaphorePermit<'static>, |
423 | | inner: tokio::fs::ReadDir, |
424 | | } |
425 | | |
426 | | impl ReadDir { |
427 | 1.36k | pub fn into_inner(self) -> (SemaphorePermit<'static>, tokio::fs::ReadDir) { |
428 | 1.36k | (self.permit, self.inner) |
429 | 1.36k | } |
430 | | } |
431 | | |
432 | | impl AsRef<tokio::fs::ReadDir> for ReadDir { |
433 | 0 | fn as_ref(&self) -> &tokio::fs::ReadDir { |
434 | 0 | &self.inner |
435 | 0 | } |
436 | | } |
437 | | |
438 | | impl AsMut<tokio::fs::ReadDir> for ReadDir { |
439 | 0 | fn as_mut(&mut self) -> &mut tokio::fs::ReadDir { |
440 | 0 | &mut self.inner |
441 | 0 | } |
442 | | } |
443 | | |
444 | 2.60k | pub async fn read_dir(path: impl AsRef<Path>) -> Result<ReadDir, Error> { |
445 | 2.60k | let path = path.as_ref().to_owned(); |
446 | | // Deliberately NOT `call_with_permit`: `tokio::fs::read_dir` is already |
447 | | // async, so it must be awaited directly rather than `block_on`ed from a |
448 | | // blocking-pool thread. See `call_with_permit` for why. |
449 | 2.60k | let permit = get_permit().await?0 ; |
450 | 2.60k | let inner1.36k = tokio::fs::read_dir(path) |
451 | 2.60k | .await |
452 | 2.60k | .map_err(Into::<Error>::into)?1.23k ; |
453 | 1.36k | Ok(ReadDir { permit, inner }) |
454 | 2.60k | } |
455 | | |
456 | 53 | pub async fn rename(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<(), Error> { |
457 | 53 | let from = from.as_ref().to_owned(); |
458 | 53 | let to = to.as_ref().to_owned(); |
459 | 53 | call_with_permit(move |_| std::fs::rename(from, to).map_err(Into::<Error>::into)).await |
460 | 53 | } |
461 | | |
462 | 113 | pub async fn remove_file(path: impl AsRef<Path>) -> Result<(), Error> { |
463 | 113 | let path = path.as_ref().to_owned(); |
464 | 113 | call_with_permit(move |_| std::fs::remove_file(path).map_err(Into::<Error>::into)).await |
465 | 101 | } |
466 | | |
467 | | /// Removes an empty directory. Errors if the directory is not empty; use |
468 | | /// [`remove_dir_all`] when the contents should be removed too. |
469 | 3 | pub async fn remove_dir(path: impl AsRef<Path>) -> Result<(), Error> { |
470 | 3 | let path = path.as_ref().to_owned(); |
471 | 3 | call_with_permit(move |_| std::fs::remove_dir(path).map_err(Into::<Error>::into)).await |
472 | 3 | } |
473 | | |
474 | 2 | pub async fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf, Error> { |
475 | 2 | let path = path.as_ref().to_owned(); |
476 | 2 | call_with_permit(move |_| std::fs::canonicalize(path).map_err(Into::<Error>::into)).await |
477 | 2 | } |
478 | | |
479 | 335 | pub async fn metadata(path: impl AsRef<Path>) -> Result<Metadata, Error> { |
480 | 335 | let path = path.as_ref().to_owned(); |
481 | 335 | call_with_permit(move |_| std::fs::metadata(path).map_err(Into::<Error>::into)).await |
482 | 335 | } |
483 | | |
484 | | /// Tests many paths for existence under a single permit and a single |
485 | | /// `spawn_blocking` dispatch, returning one answer per input in the same order. |
486 | | /// |
487 | | /// A `stat(2)` on a warm tmpfs costs far less than the permit acquisition and |
488 | | /// thread hop needed to dispatch it, so callers checking a whole input tree pay |
489 | | /// almost entirely dispatch overhead when they ask one path at a time. |
490 | | /// |
491 | | /// Any error is reported as `false`, since every failure mode (absent, |
492 | | /// dangling, unreadable parent) answers "can I use this path" the same way. |
493 | 76 | pub async fn exists_many(paths: Vec<PathBuf>) -> Result<Vec<bool>, Error> { |
494 | 76 | call_with_permit(move |_| { |
495 | 76 | Ok(paths |
496 | 76 | .into_iter() |
497 | 76 | .map(|path| std::fs::metadata(path)14 .is_ok14 ()) |
498 | 76 | .collect()) |
499 | 76 | }) |
500 | 76 | .await |
501 | 76 | } |
502 | | |
503 | 18 | pub async fn read(path: impl AsRef<Path>) -> Result<Vec<u8>, Error> { |
504 | 18 | let path = path.as_ref().to_owned(); |
505 | 18 | call_with_permit(move |_| std::fs::read(path).map_err(Into::<Error>::into)).await |
506 | 18 | } |
507 | | |
508 | 31 | pub async fn symlink_metadata(path: impl AsRef<Path>) -> Result<Metadata, Error> { |
509 | 31 | let path = path.as_ref().to_owned(); |
510 | 31 | call_with_permit(move |_| std::fs::symlink_metadata(path).map_err(Into::<Error>::into)).await |
511 | 31 | } |
512 | | |
513 | | // We can't just use the stock remove_dir_all as it falls over if someone's set readonly |
514 | | // permissions. This version walks the directories and fixes the permissions where needed |
515 | | // before deleting everything. |
516 | | // |
517 | | // Only directories are touched. Unlinking a file needs write and search |
518 | | // permission on its directory, never on the file itself, and the files of |
519 | | // a worker's tree are hardlinks to CAS blobs and executable variants that |
520 | | // other actions share: a chmod on one of those changes the inode for every |
521 | | // tree it is linked into, and an executable variant would lose its execute |
522 | | // bit under the actions still running it. |
523 | | #[cfg(not(target_family = "windows"))] |
524 | 313 | fn internal_remove_dir_all(path: impl AsRef<Path>) -> Result<(), Error> { |
525 | | // Because otherwise Windows builds complain about these things not being used |
526 | | use std::io::ErrorKind; |
527 | | use std::os::unix::fs::PermissionsExt; |
528 | | |
529 | | use tracing::debug; |
530 | | use walkdir::WalkDir; |
531 | | |
532 | 317 | for entry in WalkDir::new313 (&path313 ) { |
533 | 317 | let Ok(entry313 ) = &entry else { |
534 | 4 | debug!(?entry, "Can't get entry, assuming already deleted"); |
535 | 4 | continue; |
536 | | }; |
537 | 313 | let metadata = entry.metadata()?0 ; |
538 | 313 | if metadata.is_dir() { |
539 | 311 | match std::fs::remove_dir_all(entry.path()) { |
540 | 308 | Ok(()) => {} |
541 | 3 | Err(e) if e.kind() == ErrorKind::PermissionDenied => { |
542 | 3 | std::fs::set_permissions(entry.path(), Permissions::from_mode(0o700)).err_tip( |
543 | 0 | || format!("Setting permissions for {}", entry.path().display()), |
544 | 0 | )?; |
545 | | } |
546 | 0 | e @ Err(_) => e.err_tip(|| format!("Removing {}", entry.path().display()))?, |
547 | | } |
548 | 2 | } |
549 | | } |
550 | | |
551 | | // should now be safe to delete after we fixed all the permissions in the walk loop |
552 | 313 | match std::fs::remove_dir_all(&path) { |
553 | 2 | Ok(()) => {} |
554 | 311 | Err(e) if e.kind() == ErrorKind::NotFound => {} |
555 | 0 | e @ Err(_) => e.err_tip(|| { |
556 | 0 | format!( |
557 | | "Removing {} after permissions fixes", |
558 | 0 | path.as_ref().display() |
559 | | ) |
560 | 0 | })?, |
561 | | } |
562 | 313 | Ok(()) |
563 | 313 | } |
564 | | |
565 | | // We can't set the permissions easily in Windows, so just fallback to |
566 | | // the stock Rust remove_dir_all |
567 | | #[cfg(target_family = "windows")] |
568 | | fn internal_remove_dir_all(path: impl AsRef<Path>) -> Result<(), Error> { |
569 | | std::fs::remove_dir_all(&path)?; |
570 | | Ok(()) |
571 | | } |
572 | | |
573 | 313 | pub async fn remove_dir_all(path: impl AsRef<Path>) -> Result<(), Error> { |
574 | 313 | let path = path.as_ref().to_owned(); |
575 | 313 | call_with_permit(move |_| internal_remove_dir_all(path)).await |
576 | 313 | } |