Coverage Report

Created: 2026-10-29 17:20

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}