Skip to content

touch: use write-open + futimens on Unix to trigger IN_CLOSE_WRITE#9813

Closed
mattsu2020 wants to merge 4 commits intouutils:mainfrom
mattsu2020:bug_analystic
Closed

touch: use write-open + futimens on Unix to trigger IN_CLOSE_WRITE#9813
mattsu2020 wants to merge 4 commits intouutils:mainfrom
mattsu2020:bug_analystic

Conversation

@mattsu2020
Copy link
Copy Markdown
Contributor

Summary

Make touch open regular files for write and call futimens on the fd (Unix only). This matches GNU touch behavior and ensures inotify emits IN_CLOSE_WRITE when timestamps are updated.

Motivation

Some inotify-based reloaders watch only IN_CLOSE_WRITE. With the current path-based timestamp update, touch emits only IN_ATTRIB, so those watchers miss the change. This caused infinite loops in downstream tests (e.g., 0 A.D. hotload tests on Ubuntu Questing).

Changes

  • On Unix, attempt OpenOptions::new().write(true) and call futimens on the fd for regular files.
  • Fallback to the existing set_file_times path-based update when open/futimens fails or the target is not a regular file.
  • Keep existing symlink handling intact.

Testing

  • cargo test -p uu_touch

Notes

  • Behavior is Unix-only and preserves current semantics on non-Unix platforms.

related
#9812

Comment thread src/uu/touch/src/touch.rs Outdated
Comment thread src/uu/touch/src/touch.rs
@sylvestre
Copy link
Copy Markdown
Contributor

is it possible to add tests ? thanks

@mattsu2020
Copy link
Copy Markdown
Contributor Author

is it possible to add tests ? thanks

add

@sylvestre
Copy link
Copy Markdown
Contributor

sorry, it needs to be rebased

@mattsu2020 mattsu2020 requested a review from sylvestre January 5, 2026 10:16
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jan 5, 2026

GNU testsuite comparison:

Skipping an intermittent issue tests/timeout/timeout (passes in this run but fails in the 'main' branch)

@github-actions
Copy link
Copy Markdown

GNU testsuite comparison:

Congrats! The gnu test tests/stty/bad-speed is now passing!

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Jan 19, 2026

Merging this PR will not alter performance

✅ 288 untouched benchmarks
⏩ 42 skipped benchmarks1


Comparing mattsu2020:bug_analystic (7a73d07) with main (1d113b5)

Open in CodSpeed

Footnotes

  1. 42 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@github-actions
Copy link
Copy Markdown

GNU testsuite comparison:

Skipping an intermittent issue tests/shuf/shuf-reservoir (passes in this run but fails in the 'main' branch)
Skipping an intermittent issue tests/sort/sort-stale-thread-mem (passes in this run but fails in the 'main' branch)

Comment thread src/uu/touch/src/touch.rs Outdated
let mtime_spec = TimeSpec::new(mtime.unix_seconds() as _, mtime.nanoseconds() as _);

futimens(&file, &atime_spec, &mtime_spec)
.map_err(|err| std::io::Error::from_raw_os_error(err as i32))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

he conversion from nix::Error to std::io::Error is incorrect

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix

Comment thread src/uu/touch/src/touch.rs
#[cfg(unix)]
{
// Open write-only and use futimens to trigger IN_CLOSE_WRITE on Linux.
if !is_stdout && try_futimens_via_write_fd(path, atime, mtime).is_ok() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent error swallowing with is_ok() loses information about why futimens failed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix

Comment thread src/uu/touch/src/touch.rs Outdated
/// access and modification times on the open FD (not by path), which also
/// triggers `IN_CLOSE_WRITE` on Linux when the FD is closed.
fn try_futimens_via_write_fd(path: &Path, atime: FileTime, mtime: FileTime) -> std::io::Result<()> {
let metadata = fs::metadata(path)?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary metadata check before open. Opens file twice (metadata + open). Should check metadata after opening via file.metadata() to avoid TOCTOU and reduce syscalls.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix

Comment thread src/uu/touch/src/touch.rs Outdated
fn try_futimens_via_write_fd(path: &Path, atime: FileTime, mtime: FileTime) -> std::io::Result<()> {
let metadata = fs::metadata(path)?;
if !metadata.is_file() {
return Err(Error::other("not a regular file"));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use translate!()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix

Comment thread src/uu/touch/src/touch.rs Outdated
}

let file = OpenOptions::new().write(true).open(path)?;
let atime_spec = TimeSpec::new(atime.unix_seconds() as _, atime.nanoseconds() as _);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsafe type cast 'as _' for nanoseconds. Should explicitly cast to i32: atime.nanoseconds() as i32

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix

@github-actions
Copy link
Copy Markdown

GNU testsuite comparison:

Congrats! The gnu test tests/pr/bounded-memory is no longer failing!

Comment thread src/uu/touch/src/touch.rs Outdated
uu_app,
};

#[cfg(unix)]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant import - std::fs is already imported unconditionally at line 26

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix

@github-actions
Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/touch/not-owner. tests/touch/not-owner is passing on 'main'. Maybe you have to rebase?

@github-actions
Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/factor/t10. tests/factor/t10 is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/factor/t34. tests/factor/t34 is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/touch/not-owner. tests/touch/not-owner is passing on 'main'. Maybe you have to rebase?
Skipping an intermittent issue tests/tail/follow-name (passes in this run but fails in the 'main' branch)
Skipping an intermittent issue tests/tty/tty-eof (passes in this run but fails in the 'main' branch)
Congrats! The gnu test tests/cut/cut-huge-range is now passing!
Congrats! The gnu test tests/rm/many-dir-entries-vs-OOM is now passing!

@github-actions
Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/rm/isatty. tests/rm/isatty is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/touch/not-owner. tests/touch/not-owner is passing on 'main'. Maybe you have to rebase?
Congrats! The gnu test tests/cut/bounded-memory is no longer failing!
Note: The gnu test tests/cp/link-heap is now being skipped but was previously passing.

@github-actions
Copy link
Copy Markdown

GNU testsuite comparison:

Congrats! The gnu test tests/cut/bounded-memory is no longer failing!
Note: The gnu test tests/rm/many-dir-entries-vs-OOM is now being skipped but was previously passing.
Note: The gnu test tests/unexpand/bounded-memory is now being skipped but was previously passing.

On Unix, try futimens through a write-opened file descriptor to trigger IN_CLOSE_WRITE semantics, and fall back to set_file_times when needed. Also adds a unit test for the futimens path and wires unix-only dependencies.
@github-actions
Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/cut/bounded-memory. tests/cut/bounded-memory is passing on 'main'. Maybe you have to rebase?
Skip an intermittent issue tests/pr/bounded-memory (fails in this run but passes in the 'main' branch)
Skipping an intermittent issue tests/tail/follow-name (passes in this run but fails in the 'main' branch)
Congrats! The gnu test tests/date/date-locale-hour is no longer failing!

This commit removes a trailing newline at the end of the test module in src/uu/touch/src/touch.rs to maintain consistent code formatting and eliminate unnecessary whitespace.
Replace deprecated nix::libc::O_NONBLOCK with the standard libc::O_NONBLOCK constant to fix compilation warnings and ensure compatibility with the latest nix crate version.
@github-actions
Copy link
Copy Markdown

GNU testsuite comparison:

Skipping an intermittent issue tests/pr/bounded-memory (passes in this run but fails in the 'main' branch)
Congrats! The gnu test tests/date/date-locale-hour is no longer failing!

Replace direct libc::O_NONBLOCK usage with nix::libc::O_NONBLOCK to maintain consistency with the nix crate imports and avoid potential cross-platform compatibility issues.
@github-actions
Copy link
Copy Markdown

GNU testsuite comparison:

Skipping an intermittent issue tests/pr/bounded-memory (passes in this run but fails in the 'main' branch)

@sylvestre
Copy link
Copy Markdown
Contributor

@mattsu2020 sorry it took forever but it fails with:


error[E0308]: arguments to this function are incorrect
   --> src/uu/touch/src/touch.rs:625:22
    |
625 |     let atime_spec = TimeSpec::new(atime_sec, atime_nsec);
    |                      ^^^^^^^^^^^^^ ---------  ---------- expected `i32`, found `i64`
    |                                    |
    |                                    expected `i32`, found `i64`
    |
note: associated function defined here
   --> /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/nix-0.30.1/src/sys/time.rs:356:18
    |
356 |     pub const fn new(seconds: time_t, nanoseconds: timespec_tv_nsec_t) -> Self {
    |                  ^^^
help: you can convert an `i64` to an `i32` and panic if the converted value doesn't fit
    |
625 |     let atime_spec = TimeSpec::new(atime_sec.try_into().unwrap(), atime_nsec);
    |                                             ++++++++++++++++++++
help: you can convert an `i64` to an `i32` and panic if the converted value doesn't fit
    |
625 |     let atime_spec = TimeSpec::new(atime_sec, atime_nsec.try_into().unwrap());
    |                                                         ++++++++++++++++++++

error[E0308]: arguments to this function are incorrect
   --> src/uu/touch/src/touch.rs:626:22
    |
626 |     let mtime_spec = TimeSpec::new(mtime_sec, mtime_nsec);
    |                      ^^^^^^^^^^^^^ ---------  ---------- expected `i32`, found `i64`
    |                                    |
    |                                    expected `i32`, found `i64`
    |
note: associated function defined here
   --> /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/nix-0.30.1/src/sys/time.rs:356:18
    |
356 |     pub const fn new(seconds: time_t, nanoseconds: timespec_tv_nsec_t) -> Self {
    |                  ^^^
help: you can convert an `i64` to an `i32` and panic if the converted value doesn't fit
    |
626 |     let mtime_spec = TimeSpec::new(mtime_sec.try_into().unwrap(), mtime_nsec);
    |                                             ++++++++++++++++++++
help: you can convert an `i64` to an `i32` and panic if the converted value doesn't fit
    |
626 |     let mtime_spec = TimeSpec::new(mtime_sec, mtime_nsec.try_into().unwrap());
    |                                                         ++++++++++++++++++++

For more information about this error, try `rustc --explain E0308`.

@sylvestre
Copy link
Copy Markdown
Contributor

i merged it!

@sylvestre sylvestre closed this Mar 4, 2026
@mattsu2020
Copy link
Copy Markdown
Contributor Author

thanks

@mattsu2020 mattsu2020 deleted the bug_analystic branch March 4, 2026 22:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants